Skip to main content

Pluggable ID/IDREF handling in JAXB 2.0

Posted by kohsuke on August 15, 2005 at 4:44 PM PDT

ID/IDREF has been with XML since its very first day. It works nicely with databinding tools, because it's easy to do a type analysis with ID/IDREF In this regard, key/keyref in XML Schema is much worse --- in general it's not even possible to determine how a given key/keyref constraint maps to which field/method of an object.

ID/IDREF is great, but it is sometimes too simple. For example, people often want to:

  1. define distinctive symbol spaces; for example, in Java, methods and fields have different symbol spaces, so you can have a method "foo" and a field "foo" at the same time.
  2. define scoped symbol spaces; for example, in Java, fields are scoped to classes, so you can have a field "foo" in class "X" and field "foo" in class "Y" at the same time.

It's just so convenient if you can tell a databinding implementation to do those for you. That is what I implemented in the JAXB RI 2.0.

In JAXB 2.0, every ID and IDREF fields are marked with @XmlID and @XmlIDREF annotations. For example, if you have a schema that describes a document like this:

<br /><document><br />  <box><br />    <appleRef ref="a1" /><br />    <orangeRef ref="o1" /><br />    <apple id="a1" /><br />    <orange id="o1" /><br />  </box><br />  <box><br />    <apple id="a2" /><br />    <appleRef id="a2" /><br />  </box><br /></document><br />

Then you can have the schema compiler produce the following Java code:

@XmlRootElement
class Apple { @XmlID String id; }

@XmlRootElement
class AppleRef { @XmlIDREF Object ref; }

@XmlRootElement
class Orange { @XmlID String id; }

@XmlRootElement
class OrangeRef { @XmlIDREF Object ref; }

class Box {
  @XmlElementRef
  List fruits;
}

Supporting Distinctive Symbol Spaces

Suppose you want to define distinctive symbol spaces between apples and oranges. That is, you'd want to allow XML documents like this:

<br /><document><br />  <box><br />    <appleRef ref="a1" /><br />    <orangeRef ref="a1" /><br />    <apple id="a1" /><br />    <orange id="a1" /><br />  </box><br />  <box><br />    <apple id="a2" /><br />    <appleRef id="a2" /><br />  </box><br /></document><br />

Where orangeRefs would only refer to oranges and
appleRefs would only refer to apples.
(Note that this document no longer validates with the original schema.)

First, you'd have to modify the above classes to make it clear that AppleRef only refers to Apple:

@XmlRootElement
class AppleRef { @XmlIDREF Apple ref; }

@XmlRootElement
class OrangeRef { @XmlIDREF Orange ref; }

You can then implement a custom IDResolver, like this:

<br />class MyIDResolver extends IDResolver {<br />  Map<String,Apple> apples = new HashMap<String,Apple>();<br />  Map<String,Orange> oranges = new HashMap<String,Orange>();<br /><br />  void startDocument() {<br />    apples.clear();<br />    oranges.clear();<br />  }<br /><br />  public void bind(String id, Object obj) {<br />    if(obj instanceof Apple)<br />      apples.put(id,(Apple)obj);<br />    else<br />      oranges.put(id,(Orange)obj);<br />  }<br /><br />  Callable resolve(final String id, Class targetType) {<br />    return new Callable() {<br />      public Object call() {<br />        if(targetType==Apple.class)<br />          return apples.get(id);<br />        else<br />          return oranges.get(id);<br />      }<br />    };<br />  }<br />}<br />

and then set this to the Unmarshaller like this:

Unmarshaller u = context.createUnmarshaller();
u.setProperty(IDResolver.class.getName(),new MyIDResolver());
u.unmarshal(new File("document.xml"));

The JAXB RI unmarshaller will delegate all its ID/IDREF works to MyIDResolver. You can see that it's allowing the same ID for apples and oranges. The change we made ealier to AppleRef and OrangeRef allows the JAXB RI to call the resolve method with the right target type.

The Callable is used to support forward references.

Supporting Scoped Symbol Spaces

Let's extend this example so that we can handle documents like this:

<br /><document><br />  <box><br />    <appleRef ref="a1" /><br />    <orangeRef ref="a1" /><br />    <apple id="a1" /><br />    <orange id="a1" /><br />  </box><br />  <box><br />    <apple id="a1" /><br />    <appleRef id="a1" /><br />  </box><br /></document><br />

... where references are only allowed within the same box. This can be done by combining IDResolver with a recent addition to the JAXB 2.0 spec, Unmarshaller.Listener.

<br />class MyIDResolver extends IDResolver {<br />  Map<String,Apple> apples = new HashMap<String,Apple>();<br />  Map<String,Orange> oranges = new HashMap<String,Orange>();<br />  <br />  Unmarshaller.Listener createListener() {<br />    return new Unmarshaller.Listener() {<br />      void beforeUnmarshal(Object target, Object parent) {<br />        if(target instanceof Box) {<br />          apples = new HashMap<String,Apple>();<br />          oranges = new HashMap<String,Orange>();<br />        }<br />      }<br />    }<br />  }<br />  <br />  void startDocument() {<br />    apples.clear();<br />    oranges.clear();<br />  }<br /><br />  public void bind(String id, Object obj) {<br />    if(obj instanceof Apple)<br />      apples.put(id,(Apple)obj);<br />    else<br />      oranges.put(id,(Orange)obj);<br />  }<br /><br />  Callable resolve(final String id, Class targetType) {<br />    return new Callable() {<br />      public Object call() throws Exception {<br />        if(targetType==Apple.class)<br />          return apples.get(id);<br />        else<br />          return oranges.get(id);<br />      }<br />    };<br />  }<br />}<br />

When registered, a Listener gets notified for each object being unmarshalled. So the idea here is that every time the unmarshalling of a new Box object starts, we create a fresh symbol space. This guarantees that the resolve method always resolve within the same box. Note that we can't do Map.clear because that will cause the forward reference of orangeRef to fail (JAXB RI only process forward references at the very end of the document.)

You can then set up the unmarshaller like this:

Unmarshaller u = context.createUnmarshaller();
MyIDResolver resolver = new MyIDResolver();
u.setProperty(IDResolver.class.getName(),resolver);
u.setListener(resolver.createListener());
u.unmarshal(new File("document.xml"));

Conclusion

By combining IDResolver and Unmarshaller.Listener, you can do much sophisticated reference integrity processing in the JAXB RI. For more details about the interfaces, see this. You can build the JAXB RI by yourself today, or wait for next Monday's weekly build to play with this.

So Jeremy, I hope you like it, and sorry it took me a month to do this!

Related Topics >>

Comments

How to use refID to customize output??

Hi,
I‘ve been asked to customize an xml generation, using a refID
So, something that is generated through a Marshaller, out a bunch of classes
and looks like:
<Records>
<SDRecordRefId>ACD34DD567380043268A463589D34678</SDRecordRefId>
</Records>

Should end up looking like:

<SDRecord RefId=" ACD34DD567380043268A463589D34678">
<!-- additional custom data -->
</ SDRecord>

The first output, is the result of marshaling a bunch of JAXB generated classes.
Somehow I have to customize the marshaling process in such a way that the RefID can be used to look up some additional info
that will be used to generate the final output.

So the Resulting xml should look like:
<Records>
<SDRecord RefId=" ACD34DD567380043268A463589D34678 ">
<!-- additional custom data -->
</ SDRecord>
</Records>

Is it possible to use the IDResolver for such purpose, if not what other options do I have ?