//
// Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s).
// All rights reserved.
//
package openadk.library;
import java.io.*;
import openadk.library.common.*;
import openadk.library.datamodel.DatamodelDTD;
import openadk.library.datamodel.SIF_Metadata;
import openadk.library.tools.mapping.FieldAdaptor;
import openadk.library.tools.xpath.SIFXPathContext;
import org.apache.commons.jxpath.Variables;
/**
* The abstract base class for all root-level SIF Data Object classes.
* SIFDataObject encapsulates top-level data objects defined by SIF Working
* Groups, including <code><StudentPersonal></code>,
* <code><LibraryPatronStatus></code>, <code><BusInfo></code>,
* and so on.
* <p>
*
* <b>Setting Elements & Attributes of a SIF Data Object</b><p>
*
* There are two general approaches to getting and setting the element/attribute
* values of a SIFDataObject. First, you can call the getXxx and setXxx methods
* of the subclass to manipulate the elements and attributes in an object-oriented
* fashion. For example, to assign a first and last name to a StudentPersonal
* object, create a Name object and attach it to the StudentPersonal with the
* setName method:<p>
*
* <code> // Build a StudentPersonal object<br/>
* StudentPersonal sp = new StudentPersonal();<br/>
* sp.setRefId( ADK.makeGUID() );<br/>
* sp.setName( new Name( "Davis", "Alan" ) );</br>
* </code><p>
*
* The second approach to getting and setting element/attribute values is to
* call the <code>setElementOrAttribute</code> and <code>getElementOrAttribute</code>
* methods, which accept an XPath-like query string that identifies a specific
* SIF element or attribute relative to the SIFDataObject. (See also the
* Mappings class for a higher-level mechanism that performs much of the work
* involved in dynamically mapping application fields to SIF elements and
* attributes).<p>
*
* <code> // Build a StudentPersonal object<br/>
* StudentPersonal sp = new StudentPersonal();<br/>
* sp.setRefId( ADK.makeGUID() );<br/>
* sp.setElementOrAttribute( "Name[@Type='02']/LastName", "Davis", null );<br/>
* sp.setElementOrAttribute( "Name[@Type='02']/FirstName", "Brian", null );<br/>
* </code><p>
*
* XPath-like query strings can include substitution tokens and can even call
* static Java methods. For example, the following uses name/value pairs defined
* in a Map to select the first and last name. The static <code>capitalize</code>
* method of the "MyFunctions" class is called to capitalize the last name:
* <p>
*
* <code> // Prepare a table with field values<br/>
* HashMap values = new HashMap();<br/>
* values.put( "LASTNAME", "Davis" );<br/>
* values.put( "FIRSTNAME", "Brian" );<br/><br/>
* // Build a StudentPersonal object<br/>
* StudentPersonal sp = new StudentPersonal();<br/>
* sp.setRefId( ADK.makeGUID() );<br/>
* sp.setElementOrAttribute( "Name[@Type='02']/LastName=@MyFunctions.capitalize( $(LASTNAME) )", null, values );<br/>
* sp.setElementOrAttribute( "Name[@Type='02']/FirstName=$(FIRSTNAME)", null, values );<br/>
* </code><p>
*
* <b>Object Type</b><p>
*
* The <code>getObjectType</code> method returns the an ElementDef constant
* from the SIFDTD class that identifies the SIF Data Object. The the
* <code>getObjectTag</code> convenience method returns the element tag name of
* the object for the version of SIF associated with the instance. For example,
* <p>
*
* <code>
* // Lookup a Topic instance<br/>
* SIFDataObject data = new SIFDataObject( ADK.DTD().STUDENTPERSONAL );<br/>
* TopicFactory factory = myAgent.getTopicFactory();<br/>
* Topic t = factory.getInstance( data.getObjectType() );<br/>
* </code>
* <p>
*
* <b>SIF Version</b><p>
*
* Each SIFDataObject is associated with a SIFVersion instance. The version
* is used by the SIFWriter class when rendering the object as XML. By default,
* it is assumed to be the version of SIF in effect for this agent; that is,
* the value passed to the <code>ADK.initialize</code> method. However, to
* support mixed environments where an agent may send and receive objects
* using different versions of SIF, the version may be changed by the ADK
* during message processing:<p>
*
* <ul>
* <li>
* When constructing a SIFDataObject instance from parsed XML content,
* the SIFParser class sets its SIFVersion to the version identified by
* the <i>xmlns</i> attribute of the <code><SIF_Message></code>
* envelope.<br/><br/>
* </li>
* <li>
* The version may be changed by the ADK prior to rendering a
* SIFDataObject as XML. For example, when your agent responds to a
* <SIF_Request> message that specifically identifies a version
* to use for the results, the ADK will change the version of the
* SIFDataObject when generating <SIF_Response> messages. Once
* messages have been generated, it restores the SIFVersion to its
* original setting.
* <br/><br/>
* </li>
* <li>
* An agent may manually change the SIFVersion associated with a
* SIFDataObject by calling the <code>setSIFVersion</code> method.
* </li>
* </ul>
* <p>
*
* @author Eric Petersen
* @version ADK 1.0
*/
public abstract class SIFDataObject extends SIFElement
{
/**
* The version of SIF that should be used to render this SIFDataObject and
* its child elements. If this SIFDataObject is the result of parsing a SIF
* message, this is the version of SIF identified by the message envelope.
* The version is initially set by the constructor but may be changed at
* any time by the <code>setVersion</code> method.
*/
protected transient SIFVersion fVersion;
/**
* Constructs a SIFDataObject
* @param version The version of SIF that should be used to render this
* SIFDataObject and its child elements. If this SIFDataObject is the
* result of parsing a SIF message, this is the version of SIF
* identified by the message envelope.
*
* @param def The ElementDef that provides metadata for this element
*/
public SIFDataObject( SIFVersion version, ElementDef def )
{
super(def);
fVersion = version;
}
/**
* Gets this object's <i>RefId</i>.<p>
*
* Most SIF Data Object elements define a RefId value to uniquely identify
* the object. However, some objects such as SIF_ZoneStatus and StudentMeal
* do not have a RefId. For these, a blank string will be returned.
* <p>
*
* @return The value of this object's <i>RefId</i> element
*/
public String getRefId() {
return "";
}
/**
* Returns the unique identifier that was set to the {@link #setXmlId(String)} method.<p>
* If the xmlId is not set, SIFDataObject will return the value of the
* @see #getRefId() property.
* @return a string value that uniquely identifies this object to the application.
*/
public String getXmlId()
{
String id = super.getXmlId();
if( id == null ){
return getRefId();
}
return id;
}
/**
* Gets the ElementDef that identifies this SIF Data Object type
* @return An ElementDef constant defined by the SIFDTD class
*/
public ElementDef getObjectType() {
return getElementDef();
}
/**
* Gets the element tag name of this object
* @return The element tag for the version of SIF associated with the object
*/
public String getObjectTag() {
return getElementDef().tag(fVersion);
}
/**
* Gets the version of SIF that should be used to render this SIFDataObject
* and its child elements
* @return A SIFVersion
*/
public SIFVersion getSIFVersion() {
return fVersion;
}
/**
* Changes the version of SIF that should be used to render this SIFDataObject
* and its children. The calling thread may change the way a SIFDataObject
* is rendered by calling this method. It is recommended the version be
* restored to its original value after rendering is completed.
* @param version The version of SIF that should be used
*/
public void setSIFVersion( SIFVersion version ) {
fVersion = version;
}
/**
* Sets an element or attribute value
*
* NOTE: This method makes calls to SIFXPathContext. If multiple calls to
* <code>setElementOrAttribute</code> are being done, it is much more efficient to create
* a new <code>SIFXPathContext</code> by calling <code>SIFXPathContext.newInstance(sdo)</code> and then
* calling <code>.setElementorAttributeon</code> on that SIFXPathContext instance
*
* @param xpath An XPath-like query string that identifies identifies
* the element or attribute to set. The string must reference elements
* and attributes by their <i>version-independent</i> names.
* @param value The value of the element or attribute
* @throws ADKSchemaException IF the xpath is not valid
*/
public void setElementOrAttribute( String xpath, String value )
throws ADKSchemaException
{
this.setSIFVersion( ADK.getSIFVersion() );
SIFXPathContext spc = SIFXPathContext.newSIFContext( this );
spc.setElementOrAttribute( xpath, value );
}
/**
* Sets an element or attribute value identified by an XPath-like query string.<p>
*
* NOTE: This method makes calls to SIFXPathContext. If multiple calls to
* <code>setElementOrAttribute</code> are being done, it is much more efficient to create
* a new <code>SIFXPathContext</code> by calling <code>SIFXPathContext.newInstance(sdo)</code> and then
* calling <code>.setElementorAttributeon</code> on that SIFXPathContext instance
*
* @param xpath An XPath-like query string that identifies the element or
* attribute to set. The string must reference elements and attributes
* by their <i>version-independent</i> names.
* @param value The value to assign to the element or attribute if the
* query string does not set a value; may be null
* @param adaptor A data source may be used for variable
* substitutions within the query string
* @throws ADKSchemaException If the xpath is not valid
*/
public void setElementOrAttribute( String xpath, String value, FieldAdaptor adaptor )
throws ADKSchemaException
{
this.setSIFVersion( ADK.getSIFVersion() );
SIFXPathContext spc = SIFXPathContext.newSIFContext( this );
if( adaptor instanceof Variables ){
spc.setVariables( (Variables)adaptor );
}
spc.setElementOrAttribute( xpath, value );
}
/**
* Sets an element or attribute value identified by an XPath-like query string.<p>
*
* NOTE: This method makes calls to SIFXPathContext. If multiple calls to
* <code>setElementOrAttribute</code> are being done, it is much more efficient to create
* a new <code>SIFXPathContext</code> by calling <code>SIFXPathContext.newInstance(sdo)</code> and then
* calling <code>.setElementorAttributeon</code> on that SIFXPathContext instance
*
* @param xpath An XPath-like query string that identifies the element or
* attribute to set. The string must reference elements and attributes
* by their <i>version-independent</i> names.
* @param value The value to assign to the element or attribute if the
* query string does not set a value; may be null
* @param valueBuilder a ValueBuilder implementation that evaluates
* expressions in XPath-like query strings using name/value pairs in
* the <i>variables</i> map
* @throws ADKSchemaException If the xpath is not valid
*/
public void setElementOrAttribute( String xpath, String value, ValueBuilder valueBuilder )
throws ADKSchemaException
{
value = valueBuilder == null ? value : valueBuilder.evaluate( value );
setElementOrAttribute( xpath, value );
}
/**
* Gets an element or attribute value identified by an XPath-like query string.<p>
*
* @param xpath An XPath-like query string that identifies the element or
* attribute to get. The string must reference elements and attributes
* by their <i>version-independent</i> names.
* @return An Element instance encapsulating the element or attribute if
* found. If not found, <code>null</code> is returned. To retrieve the
* value of the Element, call its <code>getTextValue</code> method.
* @throws ADKSchemaException If the xpath is not valid
*/
public Element getElementOrAttribute( String xpath )
throws ADKSchemaException
{
// XPath Navigation using this API causes the object to have to
// remember the SIF Version being evaluated
this.setSIFVersion( ADK.getSIFVersion() );
SIFXPathContext spc = SIFXPathContext.newSIFContext( this );
return spc.getElementOrAttribute( xpath );
}
/**
* Sets a SIF_ExtendedElement.<p>
* @param name The element name
* @param value The element value
*
* @since ADK 1.5
*/
public void addSIFExtendedElement( String name, String value )
{
if( CommonDTD.SIF_EXTENDEDELEMENTS == null || name == null || value == null )
return;
SIF_ExtendedElement ele = null;
// Lookup existing SIF_ExtendedElements container
SIF_ExtendedElements see = (SIF_ExtendedElements)getChild( CommonDTD.SIF_EXTENDEDELEMENTS );
if( see == null )
{
// Create a new SIF_ExtendedElements container
see = new SIF_ExtendedElements();
addChild( see );
}
else
{
// Lookup existing SIF_ExtendedElement with this name
ele = see.getSIF_ExtendedElement( name );
}
// Create/update SIF_ExtendedElement
if( ele == null ) {
ele = new SIF_ExtendedElement( name, value );
see.addChild( ele );
}
else
ele.setTextValue( value );
}
/**
* Gets all SIF_ExtendedElements/SIF_ExtendedElement children of this object.<p>
* @return An array of SIF_ExtendedElement instances. If no SIF_ExtendedElements
* child element was found, an empty array is returned
*
* @since ADK 1.5
*/
public SIF_ExtendedElement[] getSIFExtendedElements()
{
if( CommonDTD.SIF_EXTENDEDELEMENTS == null )
return new SIF_ExtendedElement[0];
SIF_ExtendedElements container = (SIF_ExtendedElements)getChild( CommonDTD.SIF_EXTENDEDELEMENTS );
if( container == null )
return new SIF_ExtendedElement[0];
return container.getSIF_ExtendedElements();
}
/**
* Sets an array of <code>SIF_ExtendedElement</code> objects. All existing
* <code>SIF_ExtendedElement</code> instances
* are removed and replaced with this list. Calling this method with the
* parameter value set to null removes all <code>SIF_ExtendedElements</code>.
* @param elements The SIF_Extended elements instances to set on this object
*
* @since ADK 1.5
*/
public void setSIFExtendedElements(SIF_ExtendedElement[] elements) {
getSIFExtendedElementsContainer().setSIF_ExtendedElements(elements);
}
/**
* Gets the SIF_ExtendedElements container in which all child SIF_ExtendedElement
* elements are placed by the {@link #addSIFExtendedElement(String, String)}
* method. Note if there is currently no container element, one is created and
* added as a child of the SIFDataObject.<p>
*
* This method is provided as a convenience to agents that wish to obtain the
* SIF_ExtendedElements container element in order to manually add extended
* elements to it. This is useful, for example, if you need to call methods on
* the extended element before adding it to the container (e.g. the <code>setDoNotEncode</code>
* method). The equivalent functionality is possible by making this call:<p>
*
* <code>
* SIF_ExtendedElements container = (SIF_ExtendedElements)getChild( SIFDTD.SIF_EXTENDEDELEMENTS );<br/>
* </code>
*
* @return The SIF_ExtendedElements container element, which is created and
* added as a child to this SIFDataObject if it does not currently exist.
*/
public SIF_ExtendedElements getSIFExtendedElementsContainer()
{
SIF_ExtendedElements container = (SIF_ExtendedElements)getChild( CommonDTD.SIF_EXTENDEDELEMENTS.name() );
if( container == null ) {
container = new SIF_ExtendedElements();
addChild( container );
}
return container;
}
/**
* Sets the SIF_ExtendedElements container for this object.<P>
* Normally, agents can just call {@link #addSIFExtendedElement(String, String)},
* which automatically creates a SIF_ExtendedElements container, if necessary and
* allows for easy addition of SIF_ExtendedElements.<p>
* This method is provided as a convenience to agents that need more control or
* wish to set or completely replace the existing SIF_ExtendedElements container.
* @param container The new SIF_ExtendedElements container
*/
public void setSIFExtendedElementsContainer( SIF_ExtendedElements container )
{
removeChild( CommonDTD.SIF_EXTENDEDELEMENTS );
addChild( container );
}
/**
* Gets the SIF_Metadata for this object.
* @return The SIF_Metadata element, which is created and
* added as a child to this SIFDataObject if it does not currently exist.
*/
public SIF_Metadata getSIFMetadata()
{
SIF_Metadata metadata = (SIF_Metadata)getChild( DatamodelDTD.SIF_METADATA.name() );
if( metadata == null ) {
metadata = new SIF_Metadata();
setSIFMetadata( metadata );
}
return metadata;
}
/**
* Sets the SIF_Metadata for this object.<P>
* @param metadata The new SIF_Metadata object
*/
public void setSIFMetadata( SIF_Metadata metadata )
{
removeChild( DatamodelDTD.SIF_METADATA );
addChild( metadata );
}
/**
* Gets the SIF_ExtendedElement with the specified Name attribute.<p>
* @param name The value of the SIF_ExtendedElement/@Name attribute to search for
* @return The SIF_ExtendedElement that has a Name attribute matching the
* <i>name</i> parameter, or null if no such element exists
*
* @since ADK 1.5
*/
public SIF_ExtendedElement getSIFExtendedElement( String name )
{
if( CommonDTD.SIF_EXTENDEDELEMENTS == null || name == null )
return null;
SIF_ExtendedElements container = (SIF_ExtendedElements)getChild( CommonDTD.SIF_EXTENDEDELEMENTS );
if( container == null )
return null;
return container.getSIF_ExtendedElement( name );
}
/**
* Returns the XML representation of this SIF Data Object
* @return The XML representation of this SIF Data Object
*/
public String toXML()
{
StringWriter out = null;
SIFWriter w = null;
try
{
out = new StringWriter();
w = new SIFWriter(out,false);
w.write(this);
w.flush();
return out.toString();
}
catch( Exception e )
{
// TODO: This shouldn't be eating the exception
System.out.println("Unlogged Exception: " + e.getLocalizedMessage());
return "";
}
finally
{
try {
if( out != null )
out.close();
if( w != null )
w.close();
} catch( Exception ignored ) {
// TODO: This shouldn't be eating the exception
System.out.println("Unlogged Exception: " + ignored.getLocalizedMessage());
}
}
}
private void writeObject( java.io.ObjectOutputStream out )
throws IOException
{
out.defaultWriteObject();
out.writeUTF( getSIFVersion().toString() );
}
private void readObject( java.io.ObjectInputStream in )
throws IOException, ClassNotFoundException
{
in.defaultReadObject();
String versionString = in.readUTF();
fVersion = SIFVersion.parse( versionString );
}
@Override
public Object clone() throws CloneNotSupportedException {
SIFDataObject clonedObject = (SIFDataObject) super.clone();
clonedObject.setSIFVersion( fVersion );
return clonedObject;
}
/**
* Changes the default behavior of SIFElement so that
* the root element and attributes of a SIFData object are always
* written, even if setChanged(false) is called. Callers can
* call this method to ensure that the root elements and attibutes
* are always rendered even if a previous call to setChanged(false) was done.
*
* The reason for this is that the ADK uses the change tracking to determine
* whether an element should be written out or not when a Query is received with
* conditions. No matter what, even if the object does not have element specified as
* a query condition, the root SIFDataObjectElement and its mandatory attributes
* should still b rendered.
*/
public void ensureRootElementRendered( )
{
// Make sure that the root element and attributes are written out
this.setChildChanged();
synchronized( fSyncLock ){
if( fFields != null ){
for( SimpleField field : fFields.values() ){
if( field.getElementDef().isAttribute( this.getSIFVersion() ) ){
field.setChanged( true );
}
}
}
}
}
}