001    
002    /*
003     * Copyright (C) 2011 Archie L. Cobbs. All rights reserved.
004     *
005     * $Id: IdMapper.java 314 2012-03-26 22:10:24Z archie.cobbs $
006     */
007    
008    package org.dellroad.stuff.jibx;
009    
010    import java.util.Map;
011    
012    import org.dellroad.stuff.java.IdGenerator;
013    import org.jibx.extras.IdDefRefMapperBase;
014    import org.jibx.runtime.IMarshallable;
015    import org.jibx.runtime.IMarshallingContext;
016    import org.jibx.runtime.JiBXException;
017    import org.jibx.runtime.impl.MarshallingContext;
018    
019    /**
020     * JiBX Marshaller/Unmarshaller that assigns unique ID's to each object and
021     * replaces duplicate appearances of the same object with an IDREF reference.
022     *
023     * <p>
024     * This class allows for easy ID/IDREF handling for existing classes, with minimal
025     * modifications to those classes and no custom (un)marshaller subclasses.
026     *
027     * <h3>JiBX Mapping</h3>
028     *
029     * <p>
030     * Suppose you have a class {@code Person.java} with a single {@code name} property
031     * and you want to add ID/IDREF support to it.
032     *
033     * <p>
034     * First add the following two pseudo-bean property methods to the classes:
035     *
036     * <blockquote><pre>
037     *  import org.dellroad.stuff.jibx.IdMapper;
038     *
039     *  public class Person {
040     *
041     *      private String name;
042     *
043     *      public String getName() {
044     *          return this.name;
045     *      }
046     *      public void setName(String name) {
047     *          this.name = name;
048     *      }
049     *
050     *      <b>// JiBX methods
051     *      private String getJiBXId() {
052     *          return IdMapper.getId(this);
053     *      }
054     *      private void setJiBXId(String id) {
055     *          // do nothing
056     *      }</b>
057     *  }
058     * </pre></blockquote>
059     * Note: if you subclass {@code Person.java} from a different sub-package, you may need
060     * to change the access privileges of those methods from {@code private} to {@code protected}.
061     * </p>
062     *
063     * <p>
064     * Next, define a concrete mapping for {@code Person.java} and add the {@code id} attribute:
065     * <blockquote><pre>
066     *  &lt;mapping name="Person" class="com.example.Person"&gt;
067     *      <b>&lt;value name="id" style="attribute" ident="def"
068     *        get-method="getJiBXId" set-method="setJiBXId"/&gt;</b>
069     *      &lt;value name="name" field="name"/&gt;
070     *  &lt;/mapping&gt;
071     * </pre></blockquote>
072     * </p>
073     *
074     * <p>
075     * Finally, use {@link IdMapper} as the custom marshaller and unmarshaller wherever a {@code Person} appears, e.g.:
076     * <blockquote><pre>
077     *  &lt;mapping name="Company" class="com.example.Company"&gt;
078     *      &lt;collection name="Employees" field="employees" create-type="java.util.ArrayList"&gt;
079     *          &lt;structure name="Person" type="com.example.Person"
080     *            <b>marshaller="org.dellroad.stuff.jibx.IdMapper"
081     *            unmarshaller="org.dellroad.stuff.jibx.IdMapper"</b>/&gt;
082     *      &lt;/collection&gt;
083     *      &lt;structure name="EmployeeOfTheWeek"&gt;
084     *          &lt;structure name="Person" field="employeeOfTheWeek"
085     *            <b>marshaller="org.dellroad.stuff.jibx.IdMapper"
086     *            unmarshaller="org.dellroad.stuff.jibx.IdMapper"</b>/&gt;
087     *      &lt;/structure&gt;
088     *  &lt;/mapping&gt;
089     * </pre></blockquote>
090     * Note the {@code EmployeeOfTheWeek} "wrapper" element for the {@code employeeOfTheWeek} field; this is required
091     * in order to use an XML name for this field other than {@code Person} (see limitations below).
092     * </p>
093     *
094     * <p>
095     * Now the first appearance of any {@code Person} will contain the full XML structure with an additional <code>id="..."</code>
096     * attribute, while all subsequent appearances will contain just a reference of the form <code>&lt;Person idref="..."/&gt;</code>.
097     * Conversely, when unmarshalled all {@code Person} XML elements that refer to the same original {@code Person} will
098     * re-use the same unmarshalled {@code Person} object.
099     * </p>
100     *
101     * <p>
102     * So the resulting XML might look like:
103     * <blockquote><pre>
104     *  &lt;Company&gt;
105     *      &lt;Employees&gt;
106     *          &lt;Person id="N00001"&gt;
107     *              &lt;name&gt;Aardvark, Annie&lt;/name&gt;
108     *          &lt;/Person&gt;
109     *          &lt;Person id="N00002"&gt;
110     *              &lt;name&gt;Appleby, Arnold&lt;/name&gt;
111     *          &lt;/Person&gt;
112     *          ...
113     *      &lt;/Employees&gt;
114     *      &lt;EmployeeOfTheWeek&gt;
115     *          &lt;Person idref="N00001"/&gt;
116     *      &lt;/EmployeeOfTheWeek&gt;
117     *  &lt;/Company&gt;
118     * </pre></blockquote>
119     * </p>
120     *
121     * <h3>Limitations</h3>
122     *
123     * <p>
124     * JiBX and this class impose some limitations:
125     * <ul>
126     * <li>JiBX marshalling must be performed within an invocation of {@link IdGenerator#run IdGenerator.run()}
127     *      so that an {@link IdGenerator} is available to generate the unique IDs (when using Spring, consider using
128     *      {@link IdMappingMarshaller}; otherwise, the {@link JiBXUtil} methods all satisfy this requirement).</li>
129     * <li>Classes that use ID/IDREF must have concrete JiBX mappings.</li>
130     * <li>All occurences of the class must use the XML element name of the concrete mapping, so the use of
131     *      a "wrapper" element is required when a different element name is desired.</li>
132     * </ul>
133     *
134     * <h3>A Simpler Approach</h3>
135     *
136     * The above approach is useful when you don't want to keep track of which instance of an object will appear first
137     * in the XML encoding: the first one will always fully define the object, while subsequent ones will just reference it.
138     *
139     * <p>
140     * If this flexibility is not needed, i.e., if you can identify where in your mapping the first occurrence of an object
141     * will appear, then the following simpler approach works without the above approach's limitations (other than requiring
142     * that marshalling be peformed within an invocation of {@link IdGenerator#run IdGenerator.run()}):
143     *
144     * <p>
145     * First, replace the <code>// do nothing</code> in the example above with call to {@link IdMapper#setId IdMapper.setId()},
146     * and add a custom deserializer delegating to {@link ParseUtil#deserializeReference ParseUtil.deserializeReference()} to
147     * <blockquote><pre>
148     *      private void setJiBXId(String id) {
149     *          IdMapper.setId(this, id);
150     *      }
151     *
152     *      public static Employee deserializeEmployeeReference(String string) throws JiBXParseException {
153     *          return ParseUtil.deserializeReference(string, Employee.class);
154     *      }
155     * </pre></blockquote>
156     * </p>
157     *
158     * <p>
159     * Then, map the first occurrence of an object exactly as in the concrete mapping above, exposing the <code>JiBXId</code> property.
160     * In all subsequent occurrences of the object, expose the reference to the object as a simple property using the custom
161     * serializer/deserializer pair {@link ParseUtil#serializeReference ParseUtil.serializeReference()} and
162     * {@code Employee.deserializeEmployeeReference()}.
163     * </p>
164     *
165     * <p>
166     * For example, the following binding would yeild the same XML encoding as before:
167     * <blockquote><pre>
168     *  &lt;mapping abstract="true" type-name="person" class="com.example.Person"&gt;
169     *      <b>&lt;value name="id" style="attribute" ident="def"
170     *        get-method="getJiBXId" set-method="setJiBXId"/&gt;</b>
171     *      &lt;value name="name" field="name"/&gt;
172     *  &lt;/mapping&gt;
173     *
174     *  &lt;mapping name="Company" class="com.example.Company"&gt;
175     *      &lt;collection name="Employees" field="employees" create-type="java.util.ArrayList"&gt;
176     *          &lt;structure name="Person" map-as="person"/&gt;    &lt;!-- first occurences of all these objects --&gt;
177     *      &lt;/collection&gt;
178     *      &lt;structure name="EmployeeOfTheWeek"&gt;
179     *          &lt;structure name="Person"&gt;
180     *              <b>&lt;value name="idref" style="attribute" field="employeeOfTheWeek"
181     *                serializer="org.dellroad.stuff.jibx.ParseUtil.serializeReference"
182     *                deserializer="com.example.Employee.deserializeEmployeeReference"</b>/&gt;
183     *          &lt;/structure&gt;
184     *      &lt;/structure&gt;
185     *  &lt;/mapping&gt;
186     * </pre></blockquote>
187     * </p>
188     *
189     * <p>
190     * If you want the reference to be optionally <code>null</code>, then you'll also need to add a <code>test-method</code>:
191     * <blockquote><pre>
192     *      <b>private boolean hasEmployeeOfTheWeek() {
193     *          return this.getEmployeeOfTheWeek() != null;
194     *      }</b>
195     *
196     *      &lt;structure name="EmployeeOfTheWeek" <b>usage="optional" test-method="hasEmployeeOfTheWeek"</b>&gt;
197     *          &lt;structure name="Person"&gt;
198     *              &lt;value name="idref" style="attribute" field="employeeOfTheWeek"
199     *                serializer="org.dellroad.stuff.jibx.ParseUtil.serializeReference"
200     *                deserializer="com.example.Employee.deserializeEmployeeReference"/&gt;
201     *          &lt;/structure&gt;
202     *      &lt;/structure&gt;
203     * </pre></blockquote>
204     * This approach causes the whole <code>&lt;EmployeeOfTheWeek&gt;</code> element to disappear when there is no
205     * such employee. Alternately, you can avoid the need for the <code>test-method</code> if you want to allow
206     * just the attribute to disappear, or you could even change from <code>style="attribute"</code> to <code>style="element"</code>;
207     * in both cases you would be making the reference itself optional instead of the containing element.
208     * </p>
209     *
210     * @see IdMappingMarshaller
211     */
212    public class IdMapper extends IdDefRefMapperBase {
213    
214        private final String uri;
215        private final int index;
216        private final String name;
217        private final String className;
218    
219        // This is here to work around bogus JiBX binding error
220        private IdMapper() {
221            super(null, 0, null);
222            throw new UnsupportedOperationException();
223        }
224    
225        public IdMapper(String uri, int index, String name, String className) {
226            super(uri, index, name);
227            this.uri = uri;
228            this.index = index;
229            this.name = name;
230            this.className = className;
231        }
232    
233        /**
234         * Get the unique ID value for the given object.
235         *
236         * <p>
237         * The implementation in {@link IdMapper} formats an ID of the form <code>N012345</code>
238         * using the {@link IdGenerator} acquired from {@link IdGenerator#get}.
239         *
240         * @param obj any object
241         * @return unique ID for the object
242         */
243        public static String getId(Object obj) {
244            return IdMapper.formatId(IdGenerator.get().getId(obj));
245        }
246    
247        /**
248         * Set the unique ID value for the given object.
249         *
250         * <p>
251         * The implementation in {@link IdMapper} expects an ID of the form <code>N012345</code>,
252         * then associates the parsed {@code long} value with the given object
253         * using the {@link IdGenerator} acquired from {@link IdGenerator#get}.
254         *
255         * @param obj object to register
256         * @param idref string ID assigned to the object
257         * @throws IllegalArgumentException if {@code idref} is not of the form <code>N012345</code>
258         * @throws IllegalArgumentException if {@code idref} is already associated with a different object
259         */
260        public static void setId(Object obj, String idref) {
261            IdGenerator.get().setId(obj, IdMapper.parseId(idref));
262        }
263    
264        /**
265         * Format the unique ID.
266         *
267         * @param id ID value
268         * @return formatted idref
269         */
270        public static String formatId(long id) {
271            return String.format("N%05d", id);
272        }
273    
274        /**
275         * Parse the unique ID value assigned to the given object by {@link #getId getId()}.
276         *
277         * @param idref ID value assigned to the object
278         * @return parse ID number
279         * @throws IllegalArgumentException if {@code idref} is not of the form <code>N012345</code>
280         */
281        public static long parseId(String idref) {
282            if (idref == null || idref.length() == 0 || !idref.matches("N-?\\d+"))
283                throw new IllegalArgumentException("invalid id value `" + idref + "'");
284            long id;
285            try {
286                return Long.parseLong(idref.substring(1), 10);
287            } catch (NumberFormatException e) {
288                throw new IllegalArgumentException("invalid id value `" + idref + "'");
289            }
290        }
291    
292        /**
293         * Get the unique ID for the given object. Delegates to {@link #getId getId()}.
294         */
295        @Override
296        protected String getIdValue(Object obj) {
297            return IdMapper.getId(obj);
298        }
299    
300        /**
301         * Get the ID reference attribute name. Default is <code>"idref"</code>.
302         */
303        @Override
304        protected String getAttributeName() {
305            return "idref";
306        }
307    
308        /**
309         * Overrides superclass to use object equality instead of {@code Object.equals()} for sanity checking.
310         */
311        @Override
312        @SuppressWarnings("unchecked")
313        public void marshal(Object obj, IMarshallingContext ictx) throws JiBXException {
314    
315            // Sanity check
316            if (obj == null)
317                return;
318            if (!(ictx instanceof MarshallingContext))
319                throw new JiBXException("Invalid context type for marshaller");
320    
321            // Check if ID already defined
322            MarshallingContext ctx = (MarshallingContext)ictx;
323            Map<String, Object> map = (Map<String, Object>)ctx.getIdMap();
324            String id = this.getIdValue(obj);
325            Object value = map.get(id);
326    
327            // New object? Output normally
328            if (value == null) {
329                if (!(obj instanceof IMarshallable))
330                    throw new JiBXException("instance of " + obj.getClass() + " is not marshallable");
331                map.put(id, obj);
332                ((IMarshallable)obj).marshal(ctx);
333                return;
334            }
335    
336            // Sanity check what we got
337            if (value != obj)
338                throw new JiBXException("encountered two objects with the same ID " + id);
339    
340            // Emit a reference
341            ctx.startTagAttributes(this.index, this.name);
342            ctx.attribute(0, this.getAttributeName(), id);
343            ctx.closeStartEmpty();
344        }
345    }
346