//
// Copyright (c)1998-2011 Pearson Education, Inc. or its affiliate(s).
// All rights reserved.
//
package openadk.library;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.HashMap;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import openadk.library.impl.*;
/**
* The abstract base class for all SIF Elements.
* <p>
*
* Each object type and complex field element defined in the SIF Specification
* is encapsulated by a subclass of SIFElement. Objects include
* <StudentPersonal>, <StaffPersonal>, <BusInfo>, etc. while
* complex field elements include <Address>, <OtherId>,
* <PhoneNumber> and so on. Simple fields which have only a string value
* but which have no child elements are encapsulated by the SimpleField class
* instead of by SIFElement. An example of such a field is the <FirstName>
* child of the <Name> element.
* <p>
*
* SIFElements may have a single parent and zero or more children. Complex
* fields are always represented as child objects. The addChild, getChildren,
* removeChild, countChildren, and removeAllChildren methods are provided to
* manipulate the child list. Simple fields are stored in a dictionary keyed by
* the field's <code>ElementDef</code> constant as defined in the SIFDTD class.
* The value of a simple field is encapsulated by a SimpleField object, which
* stores not only the current string value of the field but also its change
* state and a reference to its ElementDef.
* <p>
*
* <b>Comparing SIFElement Graphs</b>
* </p>
*
* Agent developers do not typically work with SIFElement objects directly. The
* <code>compareGraphTo</code> method is one exception. Agents can use this
* method to compare the contents of two SIFElements in order to determine which
* elements and attributes are different. For an example of how this method can
* be used to assist in SIF_Event reporting, refer to the SchoolInfoProvider ADK
* Example program. Similarly, the SIFDiff example program demonstrates using
* the <code>compareGraphTo</code> method to display the differences between two
* SIF Data Objects read from disk.
* <p>
*
* <b>SIFVersion</b>
* <p>
*
* The abstract <code>getSIFVersion</code> method returns the SIFVersion that is
* currently in effect for this object. This is often used to determine the
* element tag name and sequence number when rendering XML because these may
* change from one version of SIF to the next. However, the SIFElement class
* does not itself keep track of version; it is up to the derived class to do
* so. Both SIFDataObject and SIFMessagePayload store the SIF version associated
* with their objects. By working up the object ancestry, it is possible to
* determine the SIFVersion currently associated with a SIFElement. The
* <code>effectiveSIFVersion</code> method performs this task.
* <p>
*
* @author Eric Petersen
* @version 1.0
*/
public abstract class SIFElement extends Element {
/**
* Field values. Simple fields (i.e. attributes and child elements that have
* only a text value and no attributes) are stored in the hashtable as
* SimpleField objects keyed by their associated ElementDef tag. ElementDef
* constants are defined by the SIFDTD class. Complex fields are stored in
* the fChildren vector.
*/
protected Map<String, SimpleField> fFields;
/**
* Used for storing a unique identifier for this element. Used in getID()
* and setID();
*/
private String fId;
/**
* Child elements.
*/
protected List<SIFElement> fChildren;
/**
* The object that should be synchronized on for any add or remove
* operations affecting the children elements
*/
protected transient Object fSyncLock = new Object();
/**
* Constructs a SIFElement
* <p>
*
* @param def
* The ElementDef constant from the <code>SIFDTD</code> class
* that provides metadata for this element
*/
public SIFElement(ElementDef def) {
super(def);
fParent = null;
}
/**
* Gets the SIFVersion associated with this element, if applicable.
* <p>
*
* The base class implementation of this method always returns null.
* <p>
*
* @return A SIFVersion object that identifies the version of SIF that
* should be used to render this object (or that was used to parse
* it). Not all implementation classes store a SIFVersion, so null
* may be returned.
*/
public SIFVersion getSIFVersion() {
return null;
}
/**
* Sets the SIFVersion associated with this element, if applicable.
* <p>
*
* The base class implementation of this method does nothing.
* <p>
*
* @param version
* A SIFVersion object that identifies the version of SIF that
* should be used to render this object (or that was used to
* parse it). Not all implementation classes store a SIFVersion,
* so calling this method may have no affect.
*/
public void setSIFVersion(SIFVersion version) {
// The base implementation does nothing
}
/**
* Gets the SIFVersion effective for this element by searching the ancestry
* until a valid SIFVersion is returned by one of the parent objects.
* <p>
*
* @return A SIFVersion object that identifies the version of SIF that
* should be used to render this object (or that was used to parse
* it).
*/
public SIFVersion effectiveSIFVersion() {
SIFVersion v = getSIFVersion();
SIFElement p = (SIFElement) fParent;
while (v == null && p != null) {
v = p.getSIFVersion();
p = (SIFElement) p.fParent;
}
if (v == null) {
v = ADK.getSIFVersion();
}
return v;
}
/**
* Gets the tag name for this element. The effective version of SIF is used
* to determine the exact tag name since tag names may change from one
* version of SIF to the next.
* <p>
*
* Note: In order for this method to return the proper tag name, it must
* know the version of SIF in use. The version is obtained by visiting the
* element ancestry and calling getSIFVersion on each parent until a
* non-null value is returned. Thus, this is a relatively expensive
* operation and should only be called when the SIFVersion is not known. If
* the SIFVersion is known, calling
* <code>getElementDef().tag(<i>version</i>)</code> directly is preferred.
* <p>
*
* @return The element tag name that should be used when rendering XML
*
* @see #effectiveSIFVersion
*/
public String tag() {
return fElementDef.tag(effectiveSIFVersion());
}
/**
* Gets the Vector of child objects.
*/
protected List<SIFElement> _childList() {
if (fChildren == null) {
synchronized (fSyncLock) {
if (fChildren == null) {
fChildren = new ArrayList<SIFElement>(1);
}
}
}
return fChildren;
}
/**
* Returns the unique identifier that was set to the
* {@link #setXmlId(String)} method.
* <p>
* This value is not used by the ADK and is reserved for use by the
* application.
*
* @return a string value that uniquely identifies this object to the
* application.
*/
public String getXmlId() {
return fId;
}
/**
* Sets an identifier that can be used to uniquely identify this SIFElement
* instance to an application.
* <p>
* This property is not used by the ADK and is reserved for use by the
* application.
*
* @param id
* a String value that uniquely identifies this object to the
* application that is using it.
*/
public void setXmlId(String id) {
fId = id;
}
/**
* Gets the key of this object. All SIFElements must be able to return a
* unique key that distinguishes the object from its peers in a child list.
* For SIF, the key is almost always the "RefId" field of an object. In some
* cases more than one attribute is combined to form a key; in this case the
* convention "attr1.attr2" should be used, where the key values are listed
* in sequential order.
* <p>
*
* @return The key value of this object
*/
public String getKey() {
return null;
}
/**
* Gets the child object with the matching element name and key
* <p>
*
* @param name
* The version-independent element name. Note the element name is
* not necessarily the same as the element tag, which is version
* dependent.
* @param key
* The key to match
* @return The SIFElement that has a matching element name and key, or null
* if no matches found
*/
public SIFElement getChild(String name, String key) {
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
for (SIFElement child : v) {
if (((ElementDefImpl) child.fElementDef).internalName().equals(name) && (key == null || (child.getKey().equals(key))))
return child;
}
}
return null;
}
/**
* Gets a child object identified by its ElementDef and composite key
* <p>
*
* @param id
* A ElementDef defined by the SIFDTD class to uniquely identify
* this field
* @param compKey
* The key values in sequential order
* @return The child that was requested, or null
*/
public SIFElement getChild(ElementDef id, String[] compKey) {
StringBuilder b = new StringBuilder(compKey[0]);
for (int i = 1; i < compKey.length; i++) {
b.append(".");
b.append(compKey[i]);
}
return getChild(id, b.toString());
}
/**
* Adds a child SIFElement
* <p>
*
* @param id
* The ElementDef for the child
* @param element
* The child element
* @throws IllegalArgumentException
* Thrown if the child being added is already a child of a
* different parent
*/
public void addChild(ElementDef id, SIFElement element) {
safeAddChild(id, element);
}
/**
* A safe, final implementation of AddChild that can be called from a
* constructor
*
* @param element
* The SIFElement to be added as a child to this SIFElement
* @throws IllegalArgumentException
* if the element is already a child of another element
* @return The SIFElement that was added as a child
*/
private final SIFElement safeAddChild(SIFElement element) {
if (element == null || element.fParent == this)
return element;
if (element.fParent != null) {
throw new IllegalStateException("Element \"" + element.fElementDef.name() + "\" is already a child of another element");
}
restoreImplementationDef(element);
element.fParent = this;
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
v.add(element);
}
return element;
}
/**
* A safe, final implementation of AddChild that can be called from a
* constructor
*
* @param element
* The SIFElement to be added as a child to this SIFElement
* @throws IllegalArgumentException
* if the element is already a child of another element
*/
protected final void safeAddChild(ElementDef def, SIFElement element) {
if (element == null || element.fParent == this)
return;
if (element.fParent != null) {
throw new IllegalStateException("Element \"" + element.fElementDef.name() + "\" is already a child of another element");
}
element.fParent = this;
element.fElementDef = def;
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
v.add(element);
}
}
/**
* Adds a child SIFElement.
*
* @param element
* @return The SIFElement that was actually added as a child to this object.
* @throws IllegalArgumentException
* Thrown if the child being added is already a child of a
* different parent
*/
@SuppressWarnings("unchecked")
public SIFElement addChild(SIFElement element) {
return safeAddChild(element);
}
/**
* Return the actual ElementRef of the child. For example, if this were a
* StudentPersonal and the child were a Name, we would reassign its
* ElementRef to be ADK.DTD.STUDENTPERSONAL_NAME
* <p>
* We also need to redefine the ElementRef to be
* ADK.DTD.STUDENTPERSONAL_NAME. This will allow it to be written in the
* proper sequence in versions of SIF in which it is collapsed
*
* @param candidate
*/
public void restoreImplementationDef(Element candidate) {
ElementDef candidateDef = candidate.fElementDef;
ElementDef parentDef = candidateDef.getParent();
SIFVersion adkVersion = ADK.getSIFVersion();
if (fElementDef != parentDef && candidateDef.isSupported(adkVersion)) {
// Fixup the ElementRef of the child. For example, if this were a
// StudentPersonal and the child were a Name, we would reassign its
// ElementRef to be ADK.DTD.STUDENTPERSONAL_NAME
// We also need to redefine the ElementRef to be
// ADK.DTD.STUDENTPERSONAL_NAME.
// This will allow it to be written in the proper sequence in
// versions of SIF
// in which it is collapsed
// NOTE: Eric Petersen: Prior to build 1.1.0.31, the following line
// used
// to use o.fElementDef.name(...) instead of o.fElementDef.tag(...)
// However, a customer discovered that it did not result in proper
// sequencing of StudentSchoolEnrollment/HomeRoom elements, because
// the tag name of this element is "Homeroom" but the name is
// "HomeRoom",
// with a capital R. Thus, the line below would never find
// "StudentSchoolEnrollment_Homeroom". So this line was changed to
// use the tag() instead of the name(). So far no side-effects have
// been noticed.
String tag = candidateDef.tag(adkVersion);
ElementDef implDef = ADK.DTD().lookupElementDef(fElementDef, tag);
if (implDef != null) {
candidate.fElementDef = implDef;
}
}
}
/**
* Adds the specified children as an array to the SIFElement object.
* <p>
* All existing children that are defined as the same type as the ElementDef
* parameter are removed and replaced with this list. Calling this method
* with the Element[] parameter set to null removes all children.
*
* @param id
* The type of element that is being added
* @param children
* The elements that are being added, all of which are defined
* with the same ElementDef
* @exception IllegalArgumentException
* thrown if either parameter is null
*/
public void setChildren(ElementDef id, SIFElement[] children) {
if (id == null) {
throw new IllegalArgumentException("Parameters cannot be null: id=" + id + ", children={1}" + children);
}
// First, remove all children of this type from the list
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
// Go through the vector in reverse order, removing any children of
// this type
for (int i = v.size() - 1; i >= 0; i--) {
SIFElement child = v.get(i);
if (((ElementDefImpl) child.fElementDef).internalName().equals(((ElementDefImpl) id).internalName())) {
v.remove(i);
}
}
// Successfully cleared the list
if (children == null) {
return;
}
// Add any children that were passed in
for (int i = 0; i < children.length; i++) {
if (children[i] == null)
continue;
if (children[i].fParent != null && children[i].fParent != this) {
throw new IllegalStateException("Element \"" + children[i].fElementDef.name() + "\" is already a child of another element");
}
children[i].fParent = this;
children[i].fElementDef = id;
v.add(children[i]);
}
}
}
// /**
// * Asserts that all specified parameter arguments are not null
// * @param params
// * @param paramNames
// */
// private void assertParametersNotNull( Object[] params, String[]
// paramNames ) {
// boolean hasNulls = false;
// // Do a quick check
// for( int a= 0; a< params.length; ){
// if( params[a] == null ){
// hasNulls = true;
// break;
// }
// }
// if( !hasNulls ){
// return;
// }
//
// // The code only reaches this point in exceptional cases. We're going to
// // throw an exception because one or more parameters is null;
// // Not sure if this helps Java optimize this method or not, but I'm
// putting
// // the work into an If block so that none of the variables are seen or
// initialized
// // unless this case is true;
// if( hasNulls ){
// StringBuilder errorMessage = new StringBuilder();
// errorMessage.append( "Parameter(s) cannot be null: " );
// for( int a= 0; a< params.length; ){
// if( params[a] == null ){
// // Guard against invalid paramNames argument
// if( paramNames != null && paramNames.length >= a+1 ){
//
// } else {
//
// }
// }
// }
// throw new IllegalArgumentException( "Parameters cannot be null: id=" + id
// + ", children={1}" + children );
// }
// }
/**
* Removes a child object
* <p>
*
* @param child
* The child element
* @return True if the child was removed from this element, otherwise false
*/
public Boolean removeChild(SIFElement child) {
if (child != null) {
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
if (v.remove(child)) {
child.fParent = null;
return true;
}
}
}
return false;
}
/**
* Removes a child object identified by its ElementDef and key
* <p>
* This method removes only the first element it finds that matches the
* ElementDef and key.
* <p>
*
* @param id
* The ElementDef constant that identifies the type of child to
* remove
* @param key
* The element key value that identifies the specific child to
* remove
* @return True if the child was removed from this element, otherwise false
*/
public boolean removeChild(ElementDef id, String key) {
if (id == null) {
return false;
}
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
for (SIFElement child : v) {
if (((ElementDefImpl) child.fElementDef).internalName().equals(((ElementDefImpl) id).internalName()) && (key == null || (child.getKey() != null && child.getKey().equals(key)))) {
boolean removed = v.remove(child);
if (removed) {
child.fParent = null;
}
return removed;
}
}
}
return false;
}
/**
* Removes a child object identified by its ElementDef
* <p>
* This method removes only the first element it finds that matches the
* ElementDef
* <p>
*
* @param id
* The ElementDef constant that identifies the type of child to
* remove
* @return True if the child was removed from this element, otherwise false
*/
public boolean removeChild(ElementDef id) {
if (id == null) {
return false;
}
return removeChild(id, (String) null);
}
/**
* Removes a child object identified by its ElementDef and complex key This
* method removes only the first element it finds that matches the
* ElementDef and key.
* <p>
*
* @param id
* The ElementDef constant that identifies the type of child to
* remove
* @param complexKey
* The complex key values that, taken together, identify the
* specific child to remove
* @return True if the child was removed from this element, otherwise false
*/
public Boolean removeChild(ElementDef id, String[] complexKey) {
if (id == null || complexKey == null) {
return false;
}
StringBuilder b = new StringBuilder(complexKey[0]);
for (int i = 1; i < complexKey.length; i++) {
b.append(".");
b.append(complexKey[i]);
}
return removeChild(id, b.toString());
}
/**
* Gets all child objects
* <p>
*
* @return An array of all SIFElement children
*/
public SIFElement[] getChildren() {
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
SIFElement[] arr = new SIFElement[v.size()];
return v.toArray(arr);
}
}
/**
* Gets all child objects as an unmodifiable list
* <p>
*
* @return An array of all SIFElement children
*/
public List<SIFElement> getChildList() {
return Collections.unmodifiableList(_childList());
}
/**
* Gets all child objects with a matching ElementDef
* <p>
*
* @param id
* An ElementDef defined by the SIFDTD class to uniquely identify
* this field
* @return An unsorted array of the SIFElements that have a matching
* ElementDef
*/
public SIFElement[] getChildren(ElementDef id) {
List<? extends SIFElement> match = getChildList(id);
SIFElement[] children = new SIFElement[match.size()];
match.toArray(children);
return children;
}
/**
* Gets all child objects with a matching ElementDef
* <p>
*
* @param id
* An ElementDef defined by the SIFDTD class to uniquely identify
* this field
* @return A Vector of the SIFElements that have a matching ElementDef
*/
public List<SIFElement> getChildList(ElementDef id) {
List<SIFElement> v = _childList();
List<SIFElement> match = new ArrayList<SIFElement>();
synchronized (fSyncLock) {
for (SIFElement child : v) {
if (((ElementDefImpl) child.fElementDef).internalName().equals(((ElementDefImpl) id).internalName()))
match.add(child);
}
}
return match;
}
/**
* Gets all child objects with a matching version-independent element name
* <p>
*
* @param name
* The version-independent name of an element. Note the name is
* not necessarily the same as the element tag
* @return A Vector of the SIFElements that have a matching element name
*/
public List<SIFElement> getChildList(String name) {
List<SIFElement> v = _childList();
List<SIFElement> match = new ArrayList<SIFElement>();
synchronized (fSyncLock) {
for (SIFElement o : v) {
if (((ElementDefImpl) o.fElementDef).internalName().equals(name))
match.add(o);
}
}
return match;
}
/**
* Gets the child object with the matching ElementDef
* <p>
*
* @param id
* A ElementDef defined by the SIFDTD class to uniquely identify
* this field
* @return The SIFElement that has a matching ElementDef, or null if none
* found
*/
public SIFElement getChild(ElementDef id) {
return getChild(id, (String) null);
}
/**
* Gets the child object with the matching ElementDef and key
* <p>
*
* @param id
* A ElementDef defined by the SIFDTD class to uniquely identify
* this field
* @param key
* The key to match
* @return The SIFElement that has a matching ElementDef and key, or null if
* no matches found
*/
public SIFElement getChild(ElementDef id, String key) {
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
for (SIFElement o : v) {
if (((ElementDefImpl) o.fElementDef).internalName().equals(((ElementDefImpl) id).internalName()) && (key == null || (key.equals(o.getKey()))))
return o;
}
}
return null;
}
/**
* Gets the child object with the matching ElementDef
* <p>
*
* @param tag
* The SIF 2.0 XML Tag name of the child
* @return The SIFElement that has a matching ElementDef, or null if none
* found
*/
public SIFElement getChild(String tag) {
List<SIFElement> v = _childList();
synchronized (fSyncLock) {
for (SIFElement o : v) {
// this is a special case. We're searching for the child element
// that has an version-independent internal name equal to "tag",
// which is a version-dependent string, so we need to compare on
// the ElementDef differently than usual. Note this method is
// currently only used by the SIFDTD._xpath method, which uses
// version-dependent tag names specified in the XPath query
// string to match SIFElements in memory.
//
String cmp = null;
ElementDef def = o.fElementDef;
if (def instanceof ElementDefAlias)
cmp = ((ElementDefAlias) def).internalName();
else
cmp = def.name();
if (cmp.equals(tag))
return o;
}
}
return null;
}
/**
* Returns the number of children
* <p>
*
* @return The count of all children
*/
public int getChildCount() {
List v = _childList();
synchronized (fSyncLock) {
return v.size();
}
}
/**
* Sets the text value of this element if applicable. The text value will be
* parsed into the native datatype of the the element.
* <p>
* The formatter used for parsing by default is the SIF 1.x formatter, which
* means this value must be able to be parsed using SIF 1.x formatting
* rules. To change the format used for Text values on elements, set the
* {@link ADK#setTextFormatter(SIFFormatter)} property
*
* @param value
* The text value of this element (e.g.
* <code><element><i>text</i><element></code>)
* @throws NumberFormatException
* if the value being set cannot be parsed into the datatype
* being stored for this field
*/
public void setTextValue(String value) {
if (value == null) {
removeField(this.fElementDef);
}
SIFTypeConverter converter = getTextTypeConverter();
SIFSimpleType typedValue = null;
try {
typedValue = converter.parse(ADK.getTextFormatter(), value);
} catch (ADKParsingException adkpe) {
throw new NumberFormatException(adkpe.getMessage());
}
setField(typedValue.createField(this, this.fElementDef));
}
/**
* Gets the TypeConverter to use for text values for this element
*
* @return The TypeConverter that is used for this SIFElement. If none is
* defined, the ADK automatically assumes it is a string type
*/
private SIFTypeConverter getTextTypeConverter() {
SIFTypeConverter converter = this.fElementDef.getTypeConverter();
if (converter == null) {
// TODO: Should we not allows this in "Strict" mode?
converter = SIFTypeConverters.STRING;
}
return converter;
}
/**
* Sets the SIF strongly-typed value of this element
*
* @param value
* The value of this element as a <c>SIFSimpleType</c>
*/
public void setSIFValue(SIFSimpleType value) {
setField(value.createField(this, this.fElementDef));
}
/**
* Gets the SIF strongly-typed value of this element
*
* @return The value of this element as a <c>SIFSimpleType</c>
*/
public SIFSimpleType getSIFValue() {
SimpleField value = getField(this.fElementDef);
if (value != null) {
return value.getSIFValue();
}
return null;
}
/**
* Gets the text value of this element if applicable. The default format of
* the text value is the SIF 1.x format
* <p>
* To change the format used for Text values on elements, set the
* {@link ADK#setTextFormatter(SIFFormatter)} property
*
* @return The text value of this element (e.g.
* <code><element><b><i>text</i></b></element></code>)
*/
public String getTextValue() {
return getFieldValue(this.fElementDef);
}
/**
* Does this element have a text value?
* <p>
*
* @return true if this element has a text value (e.g.
* <code><element><b><i>text</i></b></element></code>)
*/
public boolean hasTextValue() {
return getField(fElementDef) != null;
}
/**
* Gets a field's value and sequence number
* <p>
*
* @param id
* The field's ElementDef object defined by the SIFDTD class.
* @return A SimpleField object containing the field's value and sequence
* number, or null if the field has no value
* @see #getFieldValue
* @see #getField(String)
*/
public SimpleField getField(ElementDef id) {
if (fFields != null) {
synchronized (fFields) {
return fFields.get(id.name());
}
}
return null;
}
/**
* Gets a field's value and sequence number
* <p>
*
* @param name
* The name of the field's ElementDef object defined by the
* SIFDTD class.
* @return A SimpleField object containing the field's value and sequence
* number, or null if the field has no value
* @see #getFieldValue
* @see #getField(ElementDef)
*/
public SimpleField getField(String name) {
if (fFields != null) {
synchronized (fFields) {
return fFields.get(name);
}
}
return null;
}
/**
* Gets a field's value as a String
* <p>
*
* @param id
* The field's ElementDef object defined by the SIFDTD class.
* @return The current value of the field as a String, or null if the field
* has no value
* @see #getField(ElementDef)
*/
public String getFieldValue(ElementDef id) {
SimpleField v = getField(id);
if (v != null) {
return v.getTextValue();
}
return null;
}
protected Object getSIFSimpleFieldValue(ElementDef id) {
SimpleField v = getField(id);
if (v != null) {
return v.getValue();
}
return null;
}
/**
* Sets an integer field's value
* <p>
*
* @param id
* The field definition object
* @param value
* The value to assign to the field
* @return The internal field object, returned as a convenience so the
* caller can mark the field as dirty or empty by calling its
* setDirty and setEmpty methods.
* @deprecated Please call {@link #setField(ElementDef, SIFSimpleType)}
*/
public SimpleField setField(ElementDef id, int value) {
return setField(id, String.valueOf(value));
}
/**
* Sets a field's value
* <p>
*
* @param id
* The field definition object
* @param value
* The value to assign to the field
* @return The internal field object, returned as a convenience so the
* caller can mark the field as dirty or empty by calling its
* setDirty and setEmpty methods.
*/
public SimpleField setField(ElementDef id, SIFSimpleType value) {
assertElementDef(id);
if (value == null) {
removeField(id);
return null;
} else {
SimpleField field = value.createField(this, id);
setField(field);
return field;
}
}
/**
* Sets a field's value, after evaluating the raw data type
* <p>
* This method is a convenience method that can be used by property set
* methods.
*
* @param id
* The field definition object
* @param wrapped
* The SIFSimpleType value to assign to the field
* @param unwrappedValue
* The raw, java value that was set. If this value is null, the
* field will be removed, rather than added
* @return The internal field object, returned as a convenience so the
* caller can mark the field as dirty or empty by calling its
* setDirty and setEmpty methods.
*/
protected SimpleField setFieldValue(ElementDef id, SIFSimpleType wrappedValue, Object unwrappedValue) {
if (unwrappedValue == null) {
removeField(id);
return null;
}
return setField(id, wrappedValue);
}
/**
* Asserts the ElementDef argument for methods that accept it as a parameter
*
* @param id
* The ElementDef to assert
*/
protected void assertElementDef(ElementDef id) {
if (!ADK.isInitialized())
throw new InternalError("The ADK is not initialized");
if (id == null) {
throw new IllegalArgumentException("ElementDef cannot be null");
}
}
/**
* Sets a field's value
* <p>
*
* @param id
* The field definition object
* @param value
* The value to assign to the field
* @return The internal field object, returned as a convenience so the
* caller can mark the field as dirty or empty by calling its
* setDirty and setEmpty methods.
*/
public SimpleField setField(ElementDef id, String value) {
return setFieldValue(id, new SIFString(value), value);
}
/**
* Sets the value of an attribute or simple text element on this SIFElement
*
* @param field
*/
public void setField(SimpleField field) {
if (fFields == null) {
// TODO: Tune the size of the field hashmap, based on metadata
fFields = new HashMap<String, SimpleField>(2);
}
synchronized (fFields) {
fFields.put(field.getElementDef().name(), field);
field.setParent(this);
}
}
/**
* Removes the field with the specified ID
*
* @param id
*/
protected void removeField(ElementDef id) {
if (fFields == null) {
return;
}
synchronized (fFields) {
fFields.remove(id.name());
}
}
/**
* Gets all fields for this object. The returned list will be a copy of the
* underlying list of fields
* <p>
*
* @return An array of unsorted SimpleField objects
*/
public List<SimpleField> getFields() {
List<SimpleField> arr = new ArrayList<SimpleField>();
if (fFields != null) {
synchronized (fFields) {
arr.addAll(fFields.values());
}
}
return arr;
}
/**
* Gets the number of fields for this object
*
* @return The number of fields that are currently set on this object
*/
public int getFieldCount() {
if (fFields != null) {
synchronized (fFields) {
return fFields.size();
}
}
return 0;
}
/**
* Gets an ordered list of all child elements and fields that are in a
* changed state and do not have a null value. Attributes are not included.
* Elements are ordered by sequence number according to the version of SIF
* effective for this object.
* <p>
*
* @return An Element array comprised of all child SIFElement and
* SimpleField objects ordered according to sequence number. An
* empty array is returned if there are no child SIFElements or
* fields (e.g. if this SIFElement has a text value as its content.)
*
* @see #effectiveSIFVersion
*/
public List<Element> getContent() {
return getContent(effectiveSIFVersion());
}
/**
* Gets an ordered list of all child elements and fields that are in a
* changed state and do not have a null value. Attributes are not included.
* Elements are ordered by sequence number according to the specified
* version of SIF.
* <p>
*
* @param version
* The version of SIF to use when ordering the elements.
*
* @return An Element array comprised of all child SIFElement and
* SimpleField objects ordered according to sequence number. An
* empty array is returned if there are no child SIFElements or
* fields (e.g. if this SIFElement has a text value as its content.)
*/
public List<Element> getContent(SIFVersion version) {
return ADK.DTD().getFormatter(version).getContent(this, version);
}
/**
* Sets this element and each of its children to the specified empty state.
* An object in the empty state will be written to an XML stream as an empty
* element with no children.
* <p>
*
* @param empty
* true to set the empty state, false to clear it
*/
public void setEmpty(boolean empty) {
super.setEmpty(empty);
List<SIFElement> children = _childList();
synchronized (fSyncLock) {
for (SIFElement e : children) {
e.setEmpty(empty);
}
}
}
/**
* Sets this element and each of its children to the specified changed
* state. An object in the changed state will be written to the XML stream
* when a SIF message is rendered; objects that are not changed will be
* excluded.
* <p>
*
* @param changed
* true to set the changed state, false to clear it
*/
public void setChanged(boolean changed) {
super.setChanged(changed);
synchronized (fSyncLock) {
if (fChildren != null) {
for (SIFElement e : fChildren) {
e.setChanged(changed);
}
}
if (fFields != null) {
for (SimpleField field : fFields.values()) {
field.setChanged(changed);
}
}
}
}
/**
* Compares all child elements and attributes of this Element with that of
* another Element. Any attributes or elements of the target that have a
* different text value from the corresponding attribute or element in this
* object, or that appear in one graph but not in the other, are returned in
* an array. Repeatable elements are considered the same object only if
* their <i>keys</i> match.
* <p>
*
* The comparision is exclusive of the source and target objects; that is,
* their text values are not included in the comparision. This method is
* typically called on top-level SIF Data Objects such as StudentPersonal,
* LibraryPatronStatus, CircTx, BusInfo, etc.
* <p>
*
* The result of the comparision is returned as a dual-dimensioned array,
* where the first element in the array represents the source object and the
* second element represents the target object. Each of these arrays will
* contain the same number of entries. Within each array, each slot consists
* of a <code>SIFElement</code> or <code>SimpleField</code> object. In the
* ADK, the SIFElement class encapsulates complex elements such as Name,
* PhoneNumber, and Demographics while the SimpleField class encapsulates
* elements with no children such as Name/FirstName and Name/LastName, or
* attributes such as StudentPersonal/@RefId. If a given element or
* attribute appears in one graph but not in the other, its slot in the
* array will contain a null value.
* <p>
*
* For example, the arrays returned by this method might consist of the
* following:
* <p>
*
* <table>
*
* <tr>
* <td><code><b>Array[0][0..5]</b></td>
* <td><code><b>Array[1][0..5]</b></td>
*
* <tr>
* <td><code>@RefId='AB123...'</code></td>
* <td><code>@RefId='CCD91...'</code></td>
* </tr>
* <tr>
* <td><code><LastName>Johnson</LastName></code></td>
* <td><code><LastName>Johnsen</LastName></code></td>
* </tr>
* <tr>
* <td><code><OtherId Type='06'>1004</OtherId></code></td>
* <td><code>null</code></td>
* </tr>
* <tr>
* <td><code><OtherId Type='ZZ'>SCHOOL:997</OtherId></code></td>
* <td><code>null</code></td>
* </tr>
* <tr>
* <td><code>null</code></td>
* <td><code><OtherId Type='ZZ'>BARCODE:P12345</OtherId></code></td>
* </tr>
*
* </table>
* <p>
*
* In this example, the RefId attribute (a SimpleField instance) has a
* different value in the source than in the target, as does the LastName
* element (a SIFElement instance). Two OtherId elements of type '06' and
* 'ZZ' appeared in the source but not in the target, so they are included
* in Array[0][2] and Array[0][3] but have a corresponding null value in
* Array[1][2] and Array[1][3]. Finally, one OtherId element of type 'ZZ'
* appeared in the target but not in the source, so it appears in
* Array[1][5] but has a corresponding null value in Array[0][5].
* <p>
*
* For examples of how this method is used, please consult the SIFDiff and
* SchoolInfoProvider ADK Example agents.
* <p>
*
* @param target
* The Element to be compared
*
* @return An dual-dimensioned array of Elements constituting the
* differences between this Element and the comparision Element. The
* size of these arrays will always be equals. If there are no
* elements in either array, this Element and the comparision
* Element have identical content.
*
* @exception IllegalArgumentException
* is thrown if this Element and the target are not of the
* same type; that is, they do not have the same ElementDef
* value. For example, trying to compare a
* <code>SIFDTD.STUDENTPERSONAL</code> element with a
* <code>SIFDTD.BUSINFO</code> element will result in an
* exception.
*/
public Element[][] compareGraphTo(SIFElement target) {
if (target.fElementDef != fElementDef)
throw new IllegalArgumentException("Element types differ (cannot compare " + fElementDef.getSQPPath(getSIFVersion()) + " to " + target.fElementDef.getSQPPath(target.getSIFVersion()));
List<Element> srcDiffs = new ArrayList<Element>();
List<Element> dstDiffs = new ArrayList<Element>();
_compareGraphTo(effectiveSIFVersion(), target, srcDiffs, dstDiffs, false);
target._compareGraphTo(effectiveSIFVersion(), this, srcDiffs, dstDiffs, true);
Element[] src = new Element[srcDiffs.size()];
srcDiffs.toArray(src);
Element[] dst = new Element[dstDiffs.size()];
dstDiffs.toArray(dst);
return new Element[][] { src, dst };
}
private void _compareGraphTo(SIFVersion ver, SIFElement target, List<Element> srcMap, List<Element> dstMap, boolean isDst) {
List<Element> diffs = isDst ? dstMap : srcMap;
List<Element> odiffs = isDst ? srcMap : dstMap;
List<SIFElement> v = _childList();
if (fFields != null) {
synchronized (fFields) {
for (Iterator it = fFields.keySet().iterator(); it.hasNext();) {
String name = (String) it.next();
if (name.length() == 0)
continue;
SimpleField fld = fFields.get(name);
SimpleField fldComp = target.getField(name);
if (fldComp != null) {
if (fld.compareTo(fldComp) != 0) {
// System.out.println("Field differs: " +
// fld.fElementDef.getSDOPath() + " = " +
// fld.getTextValue() );
if (!diffs.contains(fld)) {
diffs.add(fld);
odiffs.add(fldComp);
}
}
} else if (!isDst) {
if (!diffs.contains(fld)) {
diffs.add(fld);
odiffs.add(null);
}
}
}
}
}
// Compare child elements
synchronized (fSyncLock) {
for (SIFElement xCh : v) {
SIFElement xComp = xCh.fElementDef.isRepeatable(ver) ? target.getChild(xCh.fElementDef, xCh.getKey()) : target.getChild(xCh.fElementDef);
if (xComp != null) {
if (xCh.compareTo(xComp) != 0) {
// System.out.println("Element differs: " +
// xCh.fElementDef.name() + " - " + xCh.getClass() );
if (!diffs.contains(xCh)) {
diffs.add(xCh);
odiffs.add(xComp);
}
}
// TODO: Is there anything that can be done with this
// warning?
xCh._compareGraphTo(ver, xComp, srcMap, dstMap, false);
} else if (!isDst) {
// System.out.println("Element in "+(isDst?"target":"source")+" but not in "+(isDst?"source":"target")+": "
// + xCh.fElementDef.name() + " (srcKey=" + xCh.getKey() +
// ")" );
if (!diffs.contains(xCh)) {
diffs.add(xCh);
odiffs.add(null);
}
}
}
}
}
/**
* Creates an instance of a SIFElement from its ID
*
* @param id
* The metadata element that identifies
* @param parent
* The parent Metadata element
* @return The newly-created child
* @throws ADKSchemaException
*/
public static SIFElement create(SIFElement parent, ElementDef id) throws ADKSchemaException {
SIFElement element = null;
try {
Class clazz = null;
if ( id == null ) {
//null pointer exceptions thrown in a catch statement don't seem to generate any stack trace. I'd rather do it here.
throw new NullPointerException("ElementDef in SIFElement.create cannot be null, parent: " + parent);
}
try {
clazz = Class.forName(id.getFQClassName());
} catch (Exception e) {
// ignore
}
if (clazz == null) {
// JEN added - try parent package
String packageName = parent.getClass().getPackage().getName();
String fullClassName = packageName + "." + id.getClassName();
clazz = Class.forName(fullClassName);
if (clazz == null) {
throw new ADKSchemaException("Could not create an instance of " + id.getFQClassName() + " to wrap a " + id.name() + " object because that class doesn't exist", null);
}
}
element = (SIFElement) clazz.newInstance();
} catch (Exception e) {
throw new ADKSchemaException("Could not create an instance of " + id.getFQClassName() + " to wrap a " + id.name() + ":" + e, null, e);
}
element.setElementDef(id);
return element;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#clone()
*/
@Override
public Object clone() throws CloneNotSupportedException {
try {
SIFElement elementCopy = null;
// Most SIFElement subclasses should have a constructor with no
// arguments that
// sets the ElementDef automatically. If not, we need to find the
// constructor with the ElementDef argument
for (Constructor c : this.getClass().getConstructors()) {
Class[] parameterTypes = c.getParameterTypes();
if (parameterTypes.length == 0) {
// A Zero-Parameter constructor
elementCopy = (SIFElement) c.newInstance();
break;
} else if (parameterTypes.length == 1 && parameterTypes[0] == ElementDef.class) {
elementCopy = (SIFElement) c.newInstance(fElementDef);
break;
}
}
if (elementCopy == null) {
throw new CloneNotSupportedException("Unable to find constructor suitable for cloning");
}
elementCopy.fId = this.fId;
if (fFields != null) {
for (Map.Entry<String, SimpleField> entry : fFields.entrySet()) {
SimpleField fieldCopy = (SimpleField) entry.getValue().clone();
elementCopy.setField(fieldCopy);
}
}
if (fChildren != null) {
for (SIFElement childElement : fChildren) {
elementCopy.addChild((SIFElement) childElement.clone());
}
}
return elementCopy;
} catch (CloneNotSupportedException re) {
throw re;
} catch (RuntimeException re) {
throw re;
} catch (Exception iae) {
throw new CloneNotSupportedException(iae.getMessage());
}
}
public String toString() {
String retval = "";
if (fElementDef != null)
retval = fElementDef.tag(ADK.getSIFVersion());
if (getTextValue() != null)
retval += ":" + getTextValue();
if (getFields() != null)
retval += appendFields(this.getFields());
// if (getChildren() != null)
// retval += appendElements(getChildren());
if (getChildCount() > 0)
retval += appendElements(getChildren());
return retval;
}
private String appendElements(SIFElement[] elements) {
String retval = "";
for (int i = 0; i < elements.length; ++i) {
SIFElement element = elements[i];
// retval += "-" + element.fElementDef.tag(ADK.getSIFVersion());
retval += "-" + element.fElementDef.name();
if (element.getFields() != null)
retval += appendFields(element.getFields());
// if (element.getChildren() != null)
// retval += appendElements(element.getChildren());
if (element.getChildCount() > 0)
retval += appendElements(element.getChildren());
}
return retval;
}
private String appendFields(List<SimpleField> fields) {
int count = 1;
String retval = "(";
for (SimpleField field : fields) {
retval += field.getElementDef().tag(ADK.getSIFVersion());
retval += "=";
retval += field.getTextValue();
if (count < fields.size())
retval += ",";
++count;
}
retval += ")";
return retval;
}
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
//
// Write the path to the ElementDef
//
String path = fElementDef.getSDOPath();
out.writeUTF(path);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Reset the transient fields ( They are not initialized )
fSyncLock = new Object();
//
// Read the path to the ElementDef
//
String path = in.readUTF();
ElementDef foundElementDef = null;
if (path.length() > 0) {
foundElementDef = ADK.DTD().lookupElementDef(path);
}
if (foundElementDef == null) {
// TODO: MLW - I consider this a hack. On deserialization, the
// no-arguments constructor is
// not called. Also, SIFElements that were serialized without a
// parent but normally do have a parent
// are not returned by the lookupElementDef() call above. To fix
// this, I instantiate
// a new object of this type, and then see what the elementdef of
// that object is.
try {
SIFElement instanceOfThisType = getClass().newInstance();
foundElementDef = instanceOfThisType.getElementDef();
} catch (InstantiationException ex) {
throw new RuntimeException("Deserialization failed: " + ex.getMessage(), ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException("Deserialization failed" + ex.getMessage(), ex);
}
}
fElementDef = foundElementDef;
}
}