
org.dellroad.stuff.jibx.IdMapper Maven / Gradle / Ivy
Show all versions of dellroad-stuff-jibx Show documentation
/*
* Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.stuff.jibx;
import java.util.Map;
import org.dellroad.stuff.java.IdGenerator;
import org.jibx.extras.IdDefRefMapperBase;
import org.jibx.runtime.IMarshallable;
import org.jibx.runtime.IMarshallingContext;
import org.jibx.runtime.JiBXException;
import org.jibx.runtime.impl.MarshallingContext;
/**
* 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 {@code Person.java} with a single {@code 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 {@code Person.java} from a different sub-package, you may need
* to change the access privileges of those methods from {@code private} to {@code protected}.
*
*
* Next, define a concrete mapping for {@code Person.java} and add the {@code 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 {@link IdMapper} as the custom marshaller and unmarshaller wherever a {@code 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 {@code EmployeeOfTheWeek} "wrapper" element for the {@code employeeOfTheWeek} field; this is required
* in order to use an XML name for this field other than {@code Person} (see limitations below).
*
*
* Now the first appearance of any {@code 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 {@code Person} XML elements that refer to the same original {@code Person} will
* re-use the same unmarshalled {@code 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:
*
* - JiBX marshalling must be performed within an invocation of {@link IdGenerator#run IdGenerator.run()}
* so that an {@link IdGenerator} is available to generate the unique IDs (when using Spring, consider using
* {@link IdMappingMarshaller}; otherwise, the {@link JiBXUtil} methods all satisfy this requirement).
* - Classes that use ID/IDREF must have concrete JiBX mappings.
* - All occurences of the class must use the XML element name of the concrete mapping, so the use of
* a "wrapper" element is required when a different element name is desired.
*
*
* 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 {@link IdGenerator#run IdGenerator.run()}):
*
*
* First, replace the // do nothing
in the example above with call to {@link IdMapper#setId IdMapper.setId()},
* and add a custom deserializer delegating to {@link ParseUtil#deserializeReference 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 {@link ParseUtil#serializeReference ParseUtil.serializeReference()} and
* {@code 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" 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 IdMappingMarshaller
*/
public class IdMapper extends IdDefRefMapperBase {
private final int index;
private final String name;
// This is here to work around bogus JiBX binding error
private IdMapper() {
super(null, 0, null);
throw new UnsupportedOperationException();
}
public IdMapper(String uri, int index, String name, String className) {
super(uri, index, name);
this.index = index;
this.name = name;
}
/**
* Get the unique ID value for the given object.
*
*
* The implementation in {@link IdMapper} formats an ID of the form N012345
* using the {@link IdGenerator} acquired from {@link IdGenerator#get}.
*
* @param obj any object
* @return unique ID for the object
*/
public static String getId(Object obj) {
return IdMapper.formatId(IdGenerator.get().getId(obj));
}
/**
* Set the unique ID value for the given object.
*
*
* The implementation in {@link IdMapper} expects an ID of the form N012345
,
* then associates the parsed {@code long} value with the given object
* using the {@link IdGenerator} acquired from {@link IdGenerator#get}.
*
* @param obj object to register
* @param idref string ID assigned to the object
* @throws IllegalArgumentException if {@code idref} is not of the form N012345
* @throws IllegalArgumentException if {@code idref} is already associated with a different object
*/
public static void setId(Object obj, String idref) {
IdGenerator.get().setId(obj, IdMapper.parseId(idref));
}
/**
* Format the unique ID.
*
* @param id ID value
* @return formatted idref
*/
public static String formatId(long id) {
return String.format("N%05d", id);
}
/**
* Parse the unique ID value assigned to the given object by {@link #getId getId()}.
*
* @param idref ID value assigned to the object
* @return parse ID number
* @throws IllegalArgumentException if {@code idref} is not of the form N012345
*/
public static long parseId(String idref) {
if (idref == null || idref.length() == 0 || !idref.matches("N-?\\d+"))
throw new IllegalArgumentException("invalid id value `" + idref + "'");
try {
return Long.parseLong(idref.substring(1), 10);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("invalid id value `" + idref + "'");
}
}
/**
* Get the unique ID for the given object. Delegates to {@link #getId getId()}.
*/
@Override
protected String getIdValue(Object obj) {
return IdMapper.getId(obj);
}
/**
* Get the ID reference attribute name. Default is "idref"
.
*/
@Override
protected String getAttributeName() {
return "idref";
}
/**
* Overrides superclass to use object equality instead of {@code Object.equals()} for sanity checking.
*/
@Override
@SuppressWarnings("unchecked")
public void marshal(Object obj, IMarshallingContext ictx) throws JiBXException {
// Sanity check
if (obj == null)
return;
if (!(ictx instanceof MarshallingContext))
throw new JiBXException("Invalid context type for marshaller");
// Check if ID already defined
MarshallingContext ctx = (MarshallingContext)ictx;
Map map = (Map)ctx.getIdMap();
String id = this.getIdValue(obj);
Object value = map.get(id);
// New object? Output normally
if (value == null) {
if (!(obj instanceof IMarshallable))
throw new JiBXException("instance of " + obj.getClass() + " is not marshallable");
map.put(id, obj);
((IMarshallable)obj).marshal(ctx);
return;
}
// Sanity check what we got
if (value != obj)
throw new JiBXException("encountered two objects with the same ID " + id);
// Emit a reference
ctx.startTagAttributes(this.index, this.name);
ctx.attribute(0, this.getAttributeName(), id);
ctx.closeStartEmpty();
}
}