czwartek, 16 września 2010

Mapping cyclic object references to XML using JAXB

With JAXB being de facto standard for XML to Java data binding it is hard not to use it in enterprise application. Because of this as a java developer sooner or later you will notice some serious limitations of it. One of the most painful is a lack of support for object cycles and repetitions.

Lets start with a simple java class which we want to serialize to XML using JAXB.

@XmlRootElement
public class Node {
 private List<Node> nodes;
 @XmlAttribute
 private String name;

 public Node() {
 }

 public Node(String name) {
  this.name = name;
 }

 @XmlElementRef 
 public List<Node> getNodes() {
  return nodes;
 }

 public void setNodes(List<Node> nodes) {
  this.nodes = nodes;
 }

 public void addNode(Node node) {
  if (nodes == null) {
   nodes = new ArrayList<Node>();
  }
  nodes.add(node);
 }
}

If we run following test code we will get self explanatory error.

public class Test {
 public static void main(String[] args) throws Exception {
  Node node1 = new Node("node1");
  Node node2 = new Node("node2");
  Node node3 = new Node("node3");
  node1.addNode(node2);
  node1.addNode(node3);
  node2.addNode(node1);
  node2.addNode(node3);
  node3.addNode(node1);
  node3.addNode(node2);
  
  JAXBContext context = JAXBContext.newInstance(Node.class);
  Marshaller marshaller = context.createMarshaller();
  marshaller.marshal(node1, System.out);
 }
}

Exception in thread "main" javax.xml.bind.MarshalException
 - with linked exception:
[com.sun.istack.internal.SAXException2: 
A cycle is detected in the object graph.
This will cause infinitely deep XML: 
node.Node@1e9cb75 -> node.Node@c5c3ac -> node.Node@1e9cb75]

After spending a few hours on searching web for solution for this problem I haven't found any but I was able to came up with my own idea how to fix it.

First of all we need simple dummy XML adapter.

public class AnyTypeAdapter extends XmlAdapter<Object, Object> {
 public Object marshal(Object value) {
  return value;
 }

 public Object unmarshal(Object value) {
  return value;
 }
}

We need also to create wrapper class which will enable us to break cycles.

@XmlRootElement
public class ObjectWrapper {
 private static AtomicInteger nextId = new AtomicInteger(0);
 
 @XmlAttribute
 @XmlID
 private String id = String.valueOf(nextId.addAndGet(1));

 @XmlAttribute
 @XmlIDREF
 private ObjectWrapper ref;

 @XmlJavaTypeAdapter(AnyTypeAdapter.class)
 @XmlAnyElement(lax = true)
 private Object value;

 public ObjectWrapper() {
 }

 public ObjectWrapper(Object value) {
  this.value = value;
 }

 public Object getValue() {
  return value;
 }
 
 public void update(ObjectWrapper ref) {
  this.ref = ref;
  this.id = null;
  this.value = null;
 }

 public Object resolve() {
  return value == null ? 
    (ref == null ? null : ref.value) : 
    value;
 }
}

In order to make its usage easier we need to create another XML adapter.

public class ObjectWrapperAdapter extends 
  XmlAdapter<ObjectWrapper, Object> {
 public ObjectWrapper marshal(Object value) {
  return value == null ? 
    null : new ObjectWrapper(value);
 }

 public Object unmarshal(ObjectWrapper wrapper) {
  return wrapper == null ? 
    null : wrapper.resolve();
 }
}

Last part of the solution is a marshaller listener which will find repetitions in the object graph and transform them to XML id references (I'm using IdentityHashMap here but in some cases HashMap does make sense as well).

public class CycleListener extends Marshaller.Listener {
 private Map<Object, ObjectWrapper> objectMap = 
   new IdentityHashMap<Object, ObjectWrapper>();
 
 public void beforeMarshal(Object source) {
  if (source instanceof ObjectWrapper) {
   ObjectWrapper wrapper = (ObjectWrapper) source;
   if (objectMap.containsKey(wrapper.getValue())) {
    if (wrapper != objectMap.get(wrapper.getValue())) {
     wrapper.update(objectMap.get(wrapper.getValue()));
    }
   } else {
    objectMap.put(wrapper.getValue(), wrapper);
   }
  }
 }
}

With those four classes we need to change the Node class a little bit by applying ObjectWrapperAdapter to it. No other changes to the serialized class are needed.

@XmlJavaTypeAdapter(ObjectWrapperAdapter.class)
@XmlRootElement
public class Node {
 private List<Node> nodes;
 @XmlAttribute
 private String name;

 public Node() {
 }

 public Node(String name) {
  this.name = name;
 }

 @XmlElementRef 
 public List<Node> getNodes() {
  return nodes;
 }

 public void setNodes(List<Node> nodes) {
  this.nodes = nodes;
 }

 public void addNode(Node node) {
  if (nodes == null) {
   nodes = new ArrayList<Node>();
  }
  nodes.add(node);
 }
}

Finally we can rerun our test code. The only change we have to make is to register marshaller listener and wrap Node instance with ObjectWrapper (this is not needed if we know that the root object does not occur in any cycle).

public class Test {
 public static void main(String[] args) throws Exception {
  Node node1 = new Node("node1");
  Node node2 = new Node("node2");
  Node node3 = new Node("node3");
  node1.addNode(node2);
  node1.addNode(node3);
  node2.addNode(node1);
  node2.addNode(node3);
  node3.addNode(node1);
  node3.addNode(node2);

  JAXBContext context = JAXBContext.newInstance(
    Node.class, ObjectWrapper.class);
  Marshaller marshaller = context.createMarshaller();
  marshaller.setListener(new CycleListener());
  marshaller.marshal(new ObjectWrapper(node1), System.out);
 }
}

As a result we will get following XML.

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<objectWrapper id="1">
 <node name="node1">
  <objectWrapper id="2">
   <node name="node2">
    <objectWrapper ref="1" />
    <objectWrapper id="4">
     <node name="node3">
      <objectWrapper ref="1" />
      <objectWrapper ref="2" />
     </node>
    </objectWrapper>
   </node>
  </objectWrapper>
  <objectWrapper ref="4" />
 </node>
</objectWrapper>

As we can see each node element is wrapped with objectWrapper element which has an id and a node or a reference to other objectWrapper's id.

This approach works also with more complex object structures. Simply add @XmlJavaTypeAdapter(ObjectWrapperAdapter.class) annotation to all classes which can cause cycles or you want to remain unique after deserialization.

4 komentarze:

  1. Your solution seems very clever and it's just what we were looking for.

    We tested it and we had problems when deserializing the serialized graph. The AnyTypeAdapter would get its unmarshall() method called with a DOM Element when deserializing the value of the root ObjectWrapper, so we ended up with a root ObjectWrapper that just contained a DOM Element as its value.

    Does this solution depend on a specific version of the JAXB ri?

    Best regards, and thanks in advance.

    OdpowiedzUsuń
  2. I just can't test it now, but I think this:

    http://download.oracle.com/javaee/5/api/javax/xml/bind/annotation/XmlAnyElement.html#lax%28%29

    might be relevant to my issue...

    OdpowiedzUsuń
  3. Anyone find the solution for deserializing the serialized graph??

    OdpowiedzUsuń
  4. @P.V: As suggested by Skandalfo this problem seems to be related to the version of the JAXB RI. Setting lax to true on @XmlAnyElement should resolve it. I've updated my example to reflect this solution.

    OdpowiedzUsuń