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.
Your solution seems very clever and it's just what we were looking for.
OdpowiedzUsuń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.
I just can't test it now, but I think this:
OdpowiedzUsuńhttp://download.oracle.com/javaee/5/api/javax/xml/bind/annotation/XmlAnyElement.html#lax%28%29
might be relevant to my issue...
Anyone find the solution for deserializing the serialized graph??
OdpowiedzUsuń@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ń