All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.dellroad.stuff.jibx.IdMapper Maven / Gradle / Ivy

There is a newer version: 3.0.8
Show newest version

/*
 * 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(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy