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 * <mapping name="Person" class="com.example.Person">
067 * <b><value name="id" style="attribute" ident="def"
068 * get-method="getJiBXId" set-method="setJiBXId"/></b>
069 * <value name="name" field="name"/>
070 * </mapping>
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 * <mapping name="Company" class="com.example.Company">
078 * <collection name="Employees" field="employees" create-type="java.util.ArrayList">
079 * <structure name="Person" type="com.example.Person"
080 * <b>marshaller="org.dellroad.stuff.jibx.IdMapper"
081 * unmarshaller="org.dellroad.stuff.jibx.IdMapper"</b>/>
082 * </collection>
083 * <structure name="EmployeeOfTheWeek">
084 * <structure name="Person" field="employeeOfTheWeek"
085 * <b>marshaller="org.dellroad.stuff.jibx.IdMapper"
086 * unmarshaller="org.dellroad.stuff.jibx.IdMapper"</b>/>
087 * </structure>
088 * </mapping>
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><Person idref="..."/></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 * <Company>
105 * <Employees>
106 * <Person id="N00001">
107 * <name>Aardvark, Annie</name>
108 * </Person>
109 * <Person id="N00002">
110 * <name>Appleby, Arnold</name>
111 * </Person>
112 * ...
113 * </Employees>
114 * <EmployeeOfTheWeek>
115 * <Person idref="N00001"/>
116 * </EmployeeOfTheWeek>
117 * </Company>
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 * <mapping abstract="true" type-name="person" class="com.example.Person">
169 * <b><value name="id" style="attribute" ident="def"
170 * get-method="getJiBXId" set-method="setJiBXId"/></b>
171 * <value name="name" field="name"/>
172 * </mapping>
173 *
174 * <mapping name="Company" class="com.example.Company">
175 * <collection name="Employees" field="employees" create-type="java.util.ArrayList">
176 * <structure name="Person" map-as="person"/> <!-- first occurences of all these objects -->
177 * </collection>
178 * <structure name="EmployeeOfTheWeek">
179 * <structure name="Person">
180 * <b><value name="idref" style="attribute" field="employeeOfTheWeek"
181 * serializer="org.dellroad.stuff.jibx.ParseUtil.serializeReference"
182 * deserializer="com.example.Employee.deserializeEmployeeReference"</b>/>
183 * </structure>
184 * </structure>
185 * </mapping>
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 * <structure name="EmployeeOfTheWeek" <b>usage="optional" test-method="hasEmployeeOfTheWeek"</b>>
197 * <structure name="Person">
198 * <value name="idref" style="attribute" field="employeeOfTheWeek"
199 * serializer="org.dellroad.stuff.jibx.ParseUtil.serializeReference"
200 * deserializer="com.example.Employee.deserializeEmployeeReference"/>
201 * </structure>
202 * </structure>
203 * </pre></blockquote>
204 * This approach causes the whole <code><EmployeeOfTheWeek></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