SIFElement & SIFDataObject Classes
All SIF elements modeled by the ADK are encapsulated by classes derived from a common abstract base class: com.edustructures.sifworks.SIFElement. These in-clude top-level data objects like StudentPersonal as well as complex child elements like Name, Demographics, Transaction, and so on. (A complex element is any element that has attributes or children. In contrast, a simple field element is any element that has only a text value. The ADK does not model field elements as Java classes, but it does supply classes to encapsulate each and every complex element found in the SIF data model.)
Classes that model top-level SIF Data Objects like StudentPersonal are derived from the SIFDataObject base class, which descends from SIFElement. There are over 115 such data objects in the SIF 2.0r1 Specification.
The full class hierarchy is as follows:
com.edustructures.sifworks.Element
com.edustructures.sifworks.SIFElement
Complex Element Classes
com.edustructures.sifworks.SIFDataObject
Data Object Classes
Class Constructors
Each SIFElement class offers two constructors: a default constructor that accepts no parameters, and another that accepts all required or mandatory elements defined by the SIF Specification. The following code illustrates how to use both forms of constructor. In the first example, a StudentPersonal object is created using that class’s default constructor. None of the object’s required attributes will have values (in this case the RefId attribute), so it is the responsibility of the agent to ensure they are assigned values before published to SIF.
In the second example, an Address instance is created using the constructor that accepts a value for each required attribute or element (in this case, AddressType, Street/Line1, City, StateProvice, Country, and PostalCode are all denoted as Mandatory in the SIF Specification.) Using this constructor is a shortcut to explicitly assigning values to required attributes and elements, and makes it easy to create a valid Transaction instance in a single line of code:
// Create a StudentPersonal object
StudentPersonal sp = new StudentPersonal();
sp.setRefId( ADK.makeGUID() );
// Create an Address
Address addr = new Address(
AddressType.MAILING, new Street( "321 Baker Dr" ),
"Metropolis", StatePrCode.IL, CountryCode.US, "98855" );
Dynamic SIFDataObject Construction
SIFDataObject instances may also be created dynamically by passing an ElementDef constant to the SIFDTD.createSIFDataObject method, identifying the kind of object to create. (SIFDTD and ElementDef—two classes central to the SIF Data Objects libraries—are discussed in subsequent sections.)
// Kinds of objects to create
ElementDef[] kinds = new ElementDef[] {
StudentDTD.STUDENTPERSONAL,
StudentDTD.STAFFPERSONAL,
LibraryDTD.LIBRARYPATRONSTATUS };
// Create a bunch of SIFDataObject instances...
SIFDTD metadata = ADK.DTD();
SIFDataObject[] objects = new SIFDataObject[ kinds.length ];
for( int i = 0; i < kinds.length; i++ )
objects[i] = metadata.createSIFDataObject( kinds[i] );
Creating & Manipulating SIFDataObjects
The ADK supplies two ways to create and manipulate SIFDataObject instances. The first is to programmatically construct objects and call the methods of the SDO classes to get and set element and attribute values. For example,
//Constructing a StudentPersonal object
StudentPersonal sp = new StudentPersonal();
sp.setRefId( ADK.makeGUID() );
sp.setName( NameType.LEGAL, "Johnson", "Clifford" );
// Examining a StudentPersonal object
Name n = sp.getName();
System.out.println( "Name: " + n.getLastName() + ", " + n.getFirstName() );
OtherIdList ids = sp.getOtherIdList();
System.out.println( "This student has " + ids.size() + " IDs" );
You can also use the setElementOrAttribute method:
// Dynamically constructing a StudentPersonal object
SIFDataObject sp = ADK.DTD().createSIFDataObject( StudentDTD.STUDENTPERSONAL );
sp.setElementOrAttribute( "@RefId", ADK.makeGUID() );
sp.setElementOrAttribute( "Name[@Type='01']/LastName", "Johnson" );
sp.setElementOrAttribute( "Name[@Type='01']/FirstName", "Clifford" );
// Dynamically examining a StudentPersonal object
Element refId = sp.getElementOrAttribute( "@RefId" );
Element studentId = sp.getElementOrAttribute( "OtherId[@Type='06'" );
An alternative and more data-driven approach to working with SIFDataObjects is to use the Mappings class from the com.edustructures.sifworks.tools.Mappings package to convert SIFDataObject instances to and from a data source such as a Map by applying a set of XPath-like mapping rules. These rules are usually stored in an ex-ternal configuration file where they can be modified by system integrators in the field. For example, the following rules for StudentPersonal objects define how to translate the elements and attributes of that object to a flat list of field values:
<object object="StudentPersonal">
<field name="STUDENT_ID">OtherId[@Type=’01’]</field>
<field name="LAST_NAME">Name[@Type=’01’]/LastName</field>
<field name="FIRST_NAME">Name[@Type=’01’]/FirstName</field>
</object>
You can then call upon the Mappings class to convert a StudentPersonal instance into a Map of field/value pairs, or to convert a Map into a StudentPersonal object.
In practice, most agents use a combination of these two approaches when working with SIF Data Objects. Refer to the chapter on SIF Data Objects for more information.
The SIFDTD Class
In order to support all versions of the SIF specification, the ADK has a built-in metadata dictionary. This metadata encompasses information about each element and attribute from all versions of SIF 1.0r1 and later. The class framework relies on this information to determine the versions of SIF each element or attribute supports, its tag name, its sequence number, and various flags such as whether an element is repeatable or not.
The metadata dictionary is comprised of the static constants defined by a DTD class located within each package in the ADK that contains objects representing SIF elements. Each DTD class defines static ElementDef constants representing each element and attribute of SIF. ElementDef constants identify elements and attributes in a version-independent way so that the class framework knows what your agent is referring to regardless of whether the task at hand involves SIF 1.1, SIF 1.5r1, SIF 2.0r1 or some future version of specification.
Whenever a method requires an ElementDef parameter, you must pass a constant from one of the package-specific DTD classes:
// Create a Topic instance for "BusInfo" and "StudentPersonal" objects
Topic businfo = getTopicFactory().getInstance( TransDTD.BUSINFO );
Topic students = getTopicFactory().getInstance( StudentDTD.STUDENTPERSONAL );
SIFDTD Constants for Elements & Attributes
For elements that are part of the SIF Common Objects group or that otherwise are used across more than one SIF Data Object, each DTD class uses a naming convention to distinguish between the objects a common element appears in.
For example, the common <Name> element is used in StudentPersonal, StaffPersonal, and StudentContact. However, the characteristics of <Name>— such as its sequential order relative to its parent—are different when used as a child of <StudentPersonal> than when used as a child of <StaffPersonal> and <StudentContact>. Thus, the StudentDTD class defines multiple ElementDef constants for the <Name> element:
StudentDTD.STUDENTPERSONAL_NAME
StudentDTD.STAFFPERSONAL_NAME
StudentDTD.STUDENTCONTACT_NAME
The naming convention employed is the name of the parent SIF Data Object in upper-case, followed by an underscore (“_”) and the name of the element or attribute, also in upper-case:
[DTDClass].PARENT_CHILD
For example:
// Query for all BusInfo objects
Query q = new Query( TransDTD.BUSINFO );
// Include only the RefId, VehicleNumber, and Contractor in the response
q.setFieldRestrictions(
new ElementDef[] {
TransDTD.BUSINFO_REFID,
TransDTD.BUSINFO_VEHICLENUMBER,
TransDTD.BUSINFO_CONTRACTOR
}
);
Inspecting the ElementDef of a SIFDataObject
You can obtain the ElementDef associated with any SIFDataObject instance by calling its getElementDef method. This is often used in Boolean comparisons to determine if an object is of a certain type:
SIFDataObject someObject = ...
if( someObject.getElementDef() == StudentDTD.STUDENTPERSONAL )
{
// This is a StudentPersonal object
StudentPersonal sp = (StudentPersonal)someObject;
System.out.println("StudentPersonal with RefId " + sp.getRefId() );
}
else
if( someObject.getElementDef() == SIFDTD.STUDENTCONTACT )
{
etc.
Getting an Element’s Tag Name
The tag name of an element or attribute is specific to each version of SIF. With the ADK, each ElementDef is associated with two names: a version-independent name, which is used to identify the element or attribute regardless of the version of SIF you’re working with; and a version-dependent tag name, which is used when rendering and parsing the element. Often times it is necessary to use the element tag name in your code; for example, to log a debug message or to lookup an entry in a table that is keyed by element name.
If you want to obtain the version-independent name of an element or attribute, call the ElementDef.name method. This method returns the string used to identify the element or attribute in the ADK’s metadata dictionary. By convention it is usually equivalent to the name of the element as it first appears in the SIF Specification as of SIF 2.0 or later. .
For example, calling the name method on the LibraryDtd.TRANSACTION constant re-turns the string “Transaction”:
// Create a Transaction element and show its version-independent name
Transaction trans = new Transaction();
System.out.println( trans.getElementDef().name() );
If you want to obtain the version-dependent name of an element or attribute—that is, the string used when parsing and rendering the element—call the ElementDef.tag method. This method requires that a SIFVersion instance be passed to it. Unlike the name method, it returns the tag name specific to the version of SIF. For example, calling tag on the LibraryDTD.TRANSACTION constant yields different results for SIF 1.1 than for SIF 2.0. In SIF 1.1, the element is known as “CircTx” and in SIF 2.0 it is known as “Transaction”.
// Create a Transaction element and show its version-dependent tag name
Transaction trans = new Transaction();
// Show the tag name for SIF 1.1: "CircTX"
System.out.println( trans.getElementDef().tag( SIFVersion.SIF11 ) );
// Show the tag name for SIF 2.0: "Transaction"
System.out.println( trans.getElementDef().tag( SIFVersion.SIF20 ) );
The above code uses SIFVersion constants but in practice you obtain the SIFVersion associated with a SIF Data Object by calling the SIFDataObject.getSIFVersion me-thod:
public void onEvent( Event event, Zone zone, MessageInfo info )
throws ADKException
{
SIFMessageInfo inf = (SIFMessageInfo)info;
// Print the version of the SIF_Message envelope
System.out.println("Received a SIF_Event from a SIF " +
inf.getSIFVersion() + " agent" );
// Print the version-dependent tag name of the data contained in the event
SIFDataObject data = event.getData().readDataObject();
System.out.println( "A " + data.getElementDef().tag( data.getSIFVersion() ) +
" object is contained in the event" );
System.out.println( "The " + data.getElementDef().name() + " object is a SIF " +
data.getSIFVersion() + " object" );
}
The above example might print the following to the Java console:
Received a SIF_Event from a SIF 1.1 agent
A StudentPersonal object is contained in the event
The StudentPersonal object is a SIF 1.1 object
Building SIF Data Objects Dynamically
Most agents retrieve their data from a local database or other data store with its own unique schema and programming interfaces. A common approach to interacting with SIF is to dynamically map elements and attributes from a SIF Data Object to fields in the local application’s database. The ADK provides a powerful facility for mapping: the Mappings class found in the com.edustructures.sifworks.tools.mapping package. You could also implement your own mapping routines by using functions such as SIFDataObject.setElementOrAttribute.
The following code creates a StudentPersonal object by enumerating the fields in a mapping table. This code looks much different from most of the code shown through-out the Developer’s Guide and ADK Example agents because it does not directly use the SIF Data Object classes like Name, PhoneNumber, and StudentAddress to build the StudentPersonal object. Instead, it uses XPath-like query strings and the setElemen-tOrAttribute method of the SIFDataObject class. With the ADK, you have the flex-ibility to work with SIF Data Objects however you like.
Refer to the SIFDataObject class in the Javadoc for more information on the setEle-mentOrAttribute method.
// Build a table that maps fields in the local application to SIF
// elements & attributes. The key of each entry is a name of your
// choosing that identifies a field in the local database; the value
// of each entry is an XPath-link query string that identifies
// the corresponding SIF element or attribute of the StudentPersonal
// object
HashMap m = new HashMap();
m.put( "ForeignId", "@RefId" );
m.put( "ID", "OtherId[@Type=’06’]" );
m.put( "L_Name", "Name[@Type=’01’]/LastName" );
m.put( "F_Name", "Name[@Type=’01’]/FirstName" );
m.put( "Addr_Line1", "StudentAddress/Address[@Type=’M’]/Street/Line1" );
m.put( "Addr_Line2", "StudentAddress/Address[@Type=’M’]/Street/Line2" );
m.put( "Addr_City", "StudentAddress/Address[@Type=’M’]/City" );
m.put( "Addr_State", "StudentAddress/Address[@Type=’M’]/StatePr/@Code" );
m.put( "Addr_Zip", "StudentAddress/Address[@Type=’M’]/PostalCode" );
m.put( "Pri_Email", "Email[@Type=’Primary’]" );
m.put( "Sec_Email", "Email[@Type=’Alternate1’]" );
// Now create a StudentPersonal from the mapping table. For each entry
// in the table, lookup the associated value in the local application
// database, then call the StudentPersonal.setElementOrAttribute method
// to assign that value to the corresponding element or attribute in the
// SIF Data Object
StudentPersonal student = new StudentPersonal();
for( Iterator it = m.keySet().iterator(); it.hasNext(); )
{
String localField = (String)it.next();
String sifField = (ElementDef)m.get( field );
// Lookup the value of this field from the local application’s database
String value = myDatabaseView.getFieldValue( localField );
// Assign the field to the StudentPersonal object
if( value != null )
student.setElementOrAttribute( sifField, value );
}
Enumerated Types
The ADK defines enumerated type classes whenever a set of pre-defined values is ex-pected for an object field. These classes are named the same as the attribute or element they’re associated with. For example, the EducationLevel/Code element of the Stu-dentContact data type is represented by the EducationLevelCode class.
As you might expect, the StudentContact.setEducationLevel method accepts a single parameter: an EducationLevelCode object. Valid codes for this object are defined as static constants of the enumerated type class: EducationLevelCode.EMC, EducationLevelCode.E4, etc.
Enumerated type classes are provided as a convenience to programmers, to enforce type safety, and to help ensure that valid values are used in the construction of SIF messages. There are many cases when referencing one of the pre-defined constants of an enumerated type class is not appropriate: for example, when reading values from a database resultset you will want to assign them directly to an element or attribute. For this reason, all enumerated type classes offer a wrap method that you can use to pass your own string in place of a pre-defined constant. The following code demonstrates:
// Construct a StudentContact object
StudentContact sc = new StudentContact();
// Use the enum class to set EducationLevel to "E4"
sc.setEducationLevel( EducationLevelCode.E4 );
// Set the code directly
sc.setEducationLevel( EducationLevelCode.wrap( "E4" ) );
// Can also set the code to an invalid value
sc.setEducationLevel( EducationLevelCode.wrap( "Zebra" ) );
Dates and Times
SIF 2.0 uses XSD data types to represent Dates, DateTimes, and Times. The ADK represents each of these fields using the Java Calendar object.
Creating RefIds for SIF Data Objects
When an agent publishes a data object to a zone for the first time, it must assign the object a globally-unique identifier—or RefId—that will forever identify that object in the zone. Both the ADK.makeGUID static function and Agent.makeGUID convenience method create Globally Unique Identifiers (GUIDs).
// Create a new StudentPersonal object with a new GUID generated
// by this agent
StudentPersonal sp = new StudentPersonal();
sp.setRefId( ADK.makeGUID() );
...
Managing RefIds
Because it is specific to your agent’s transaction layer and involves interaction with a database or other persistent data store, the ADK does not provide any classes to manage RefIds. You will need to create a mechanism to store RefIds created by your agent when publishing SIF Data Objects for the first time, or imported from other agents when receiving objects in SIF_Response and SIF_Event messages. Although there are a number of ways to go about this, one simple solution is to use a database table to record the RefIds known to your agent. The following schema will usually suffice:
|
Column
|
Data Type
|
Definition
|
|
RefId
|
Char[32]
|
The GUID
|
|
PrimaryKey
|
String or Numeric
|
The primary key
of the record in your application's database that is associated with this
RefId.
|
|
SecondaryKey
|
String or Numeric
|
The optional
secondary key of the record in your application's database that is associated
with this RefId.
|
|
ObjectType
|
Numeric
|
A constant that
identifies the type of data associated with this RefId. Consider defining
numeric values for each object type; for example, 1=Students or
StudentPersonal; 2=Teachers or StaffPersonal; etc.
|
When publishing an object to a zone for the first time, assign a RefId to it and record the association between that RefId and the local application database record. You must use this RefId to subsequently identify the object whenever it is included in a SIF message: for example, when reporting a SIF_Event to a zone.
When receiving an object in a SIF_Event or SIF_Response message, the agent should first lookup its RefId in the above table. Most SIF Data Objects have an attribute named “RefId” that you can obtain via the SIFDataObject.getRefId method. If the RefId exists in your table, take the appropriate action on the corresponding database record. For example, if you receive a SIF_Event with a Change action that identifies a student you have on file, update the student record appropriately. However, if you receive a Change event for a student that does not exist in your RefId repository, you cannot take action on the event because the agent doesn’t know which student record to update.
How does the RefId repository get created initially? Edustructures recommends that all agents incorporate some form of “synchronization procedure” that is run the first time the agent is installed and again at the beginning of each school year. This process retrieves all SIF Data Objects from the zone or zones the agent is connected to, and using an algorithm of your choosing, matches them up with corresponding records in the application database. The goal of synchronization is to establish an initial set of RefIds such that your agent knows which SIF Data Objects equate to the record that already exist in the application’s database. Once this process is complete, you can begin responding to SIF Events.
There are many ways to implement a synchronization procedure, some more involved than others. For example, you might have a user interface for the administrator to control the synchronization process, to choose the kinds of SIF Data Objects to query from the zone, and to provide a way for the administrator to visually match up application records with their corresponding SIF Data Objects. Or, your might opt for a more automated process where the agent requests objects from the zone and tries to match them with application records using a common field such as Student ID. Re-gardless of the approach you take, be sure to include some form of synchronization procedure in your agent’s design.