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.