org.dellroad.stuff.jibx
Class IdMapper

java.lang.Object
  extended by org.jibx.extras.IdDefRefMapperBase
      extended by org.dellroad.stuff.jibx.IdMapper
All Implemented Interfaces:
IAliasable, IMarshaller, IUnmarshaller

public class IdMapper
extends IdDefRefMapperBase

JiBX Marshaller/Unmarshaller that assigns unique ID's to each object and replaces duplicate appearances of the same object with an IDREF reference.

This class allows for easy ID/IDREF handling for existing classes, with minimal modifications to those classes and no custom (un)marshaller subclasses.

JiBX Mapping

Suppose you have a class Person.java with a single name property and you want to add ID/IDREF support to it.

First add the following two pseudo-bean property methods to the classes:

  import org.dellroad.stuff.jibx.IdMapper;

  public class Person {

      private String name;

      public String getName() {
          return this.name;
      }
      public void setName(String name) {
          this.name = name;
      }

      // JiBX methods
      private String getJiBXId() {
          return IdMapper.getId(this);
      }
      private void setJiBXId(String id) {
          // do nothing
      }
  }
 
Note: if you subclass Person.java from a different sub-package, you may need to change the access privileges of those methods from private to protected.

Next, define a concrete mapping for Person.java and add the id attribute:

  <mapping name="Person" class="com.example.Person">
      <value name="id" style="attribute" ident="def"
        get-method="getJiBXId" set-method="setJiBXId"/>
      <value name="name" field="name"/>
  </mapping>
 

Finally, use IdMapper as the custom marshaller and unmarshaller wherever a Person appears, e.g.:

  <mapping name="Company" class="com.example.Company">
      <collection name="Employees" field="employees" create-type="java.util.ArrayList">
          <structure name="Person" type="com.example.Person"
            marshaller="org.dellroad.stuff.jibx.IdMapper"
            unmarshaller="org.dellroad.stuff.jibx.IdMapper"/>
      </collection>
      <structure name="EmployeeOfTheWeek">
          <structure name="Person" field="employeeOfTheWeek"
            marshaller="org.dellroad.stuff.jibx.IdMapper"
            unmarshaller="org.dellroad.stuff.jibx.IdMapper"/>
      </structure>
  </mapping>
 
Note the EmployeeOfTheWeek "wrapper" element for the employeeOfTheWeek field; this is required in order to use an XML name for this field other than Person (see limitations below).

Now the first appearance of any Person will contain the full XML structure with an additional id="..." attribute, while all subsequent appearances will contain just a reference of the form <Person idref="..."/>. Conversely, when unmarshalled all Person XML elements that refer to the same original Person will re-use the same unmarshalled Person object.

So the resulting XML might look like:

  <Company>
      <Employees>
          <Person id="N00001">
              <name>Aardvark, Annie</name>
          </Person>
          <Person id="N00002">
              <name>Appleby, Arnold</name>
          </Person>
          ...
      </Employees>
      <EmployeeOfTheWeek>
          <Person idref="N00001"/>
      </EmployeeOfTheWeek>
  </Company>
 

Limitations

JiBX and this class impose some limitations:

A Simpler Approach

The above approach is useful when you don't want to keep track of which instance of an object will appear first in the XML encoding: the first one will always fully define the object, while subsequent ones will just reference it.

If this flexibility is not needed, i.e., if you can identify where in your mapping the first occurrence of an object will appear, then the following simpler approach works without the above approach's limitations (other than requiring that marshalling be peformed within an invocation of IdGenerator.run()):

First, replace the // do nothing in the example above with call to IdMapper.setId(), and add a custom deserializer delegating to ParseUtil.deserializeReference() to

      private void setJiBXId(String id) {
          IdMapper.setId(this, id);
      }

      public static Employee deserializeEmployeeReference(String string) throws JiBXParseException {
          return ParseUtil.deserializeReference(string, Employee.class);
      }
 

Then, map the first occurrence of an object exactly as in the concrete mapping above, exposing the JiBXId property. In all subsequent occurrences of the object, expose the reference to the object as a simple property using the custom serializer/deserializer pair ParseUtil.serializeReference() and Employee.deserializeEmployeeReference().

For example, the following binding would yeild the same XML encoding as before:

  <mapping abstract="true" type-name="person" class="com.example.Person">
      <value name="id" style="attribute" ident="def"
        get-method="getJiBXId" set-method="setJiBXId"/>
      <value name="name" field="name"/>
  </mapping>

  <mapping name="Company" class="com.example.Company">
      <collection name="Employees" field="employees" create-type="java.util.ArrayList">
          <structure name="Person" map-as="person"/>    <!-- first occurences of all these objects -->
      </collection>
      <structure name="EmployeeOfTheWeek">
          <structure name="Person">
              <value name="idref" style="attribute" field="employeeOfTheWeek"
                serializer="org.dellroad.stuff.jibx.ParseUtil.serializeReference"
                deserializer="com.example.Employee.deserializeEmployeeReference"/>
          </structure>
      </structure>
  </mapping>
 

If you want the reference to be optionally null, then you'll also need to add a test-method:

      private boolean hasEmployeeOfTheWeek() {
          return this.getEmployeeOfTheWeek() != null;
      }

      <structure name="EmployeeOfTheWeek" usage="optional" test-method="hasEmployeeOfTheWeek">
          <structure name="Person">
              <value name="idref" style="attribute" field="employeeOfTheWeek"
                serializer="org.dellroad.stuff.jibx.ParseUtil.serializeReference"
                deserializer="com.example.Employee.deserializeEmployeeReference"/>
          </structure>
      </structure>
 
This approach causes the whole <EmployeeOfTheWeek> element to disappear when there is no such employee. Alternately, you can avoid the need for the test-method if you want to allow just the attribute to disappear, or you could even change from style="attribute" to style="element"; in both cases you would be making the reference itself optional instead of the containing element.

See Also:
IdMappingMarshaller

Constructor Summary
IdMapper(String uri, int index, String name, String className)
           
 
Method Summary
static String formatId(long id)
          Format the unique ID.
protected  String getAttributeName()
          Get the ID reference attribute name.
static String getId(Object obj)
          Get the unique ID value for the given object.
protected  String getIdValue(Object obj)
          Get the unique ID for the given object.
 void marshal(Object obj, IMarshallingContext ictx)
          Overrides superclass to use object equality instead of Object.equals() for sanity checking.
static long parseId(String idref)
          Parse the unique ID value assigned to the given object by getId().
static void setId(Object obj, String idref)
          Set the unique ID value for the given object.
 
Methods inherited from class org.jibx.extras.IdDefRefMapperBase
isExtension, isPresent, unmarshal
 
Methods inherited from class java.lang.Object
clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
 

Constructor Detail

IdMapper

public IdMapper(String uri,
                int index,
                String name,
                String className)
Method Detail

getId

public static String getId(Object obj)
Get the unique ID value for the given object.

The implementation in IdMapper formats an ID of the form N012345 using the IdGenerator acquired from IdGenerator.get().

Parameters:
obj - any object
Returns:
unique ID for the object

setId

public static void setId(Object obj,
                         String idref)
Set the unique ID value for the given object.

The implementation in IdMapper expects an ID of the form N012345, then associates the parsed long value with the given object using the IdGenerator acquired from IdGenerator.get().

Parameters:
obj - object to register
idref - string ID assigned to the object
Throws:
IllegalArgumentException - if idref is not of the form N012345
IllegalArgumentException - if idref is already associated with a different object

formatId

public static String formatId(long id)
Format the unique ID.

Parameters:
id - ID value
Returns:
formatted idref

parseId

public static long parseId(String idref)
Parse the unique ID value assigned to the given object by getId().

Parameters:
idref - ID value assigned to the object
Returns:
parse ID number
Throws:
IllegalArgumentException - if idref is not of the form N012345

getIdValue

protected String getIdValue(Object obj)
Get the unique ID for the given object. Delegates to getId().

Specified by:
getIdValue in class IdDefRefMapperBase

getAttributeName

protected String getAttributeName()
Get the ID reference attribute name. Default is "idref".

Overrides:
getAttributeName in class IdDefRefMapperBase

marshal

public void marshal(Object obj,
                    IMarshallingContext ictx)
             throws JiBXException
Overrides superclass to use object equality instead of Object.equals() for sanity checking.

Specified by:
marshal in interface IMarshaller
Overrides:
marshal in class IdDefRefMapperBase
Throws:
JiBXException