Tuesday, March 22, 2011

Dealing with cyclic data in JAXB

JAXB is a great tool for converting Java data into XML. It does have some shortcomings, one of these is cyclic references. XML is great for tree structures, but any cycles will cause problems as your XML would end up infinitely nesting. There are some tricks explained here, but I was still having trouble marshaling my node data. I ended up needing to use a combination of XmlID + XmlIDREF, XmlTransient, and a wrapping object that transforms the cyclic tree into a list of all nodes in the tree and a reference to the root node.

This method is great for SOAP calls where you have might have a cyclic tree structure, as JAXWS allows for custom wrapper beans.

For this method to work, your nodes must have a unique id that is not only unique for the Node, but unique for the XML document. You will also need to be a bit more flexible in your XML schema, as cyclic data structures might look fine in your xsd, but in practice it does not work.

Here is the Node POJO:
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class BidirectionalNode {

 @XmlAttribute
 @XmlID
 private String id;

 @XmlTransient
 private BidirectionalNode parent;

 @XmlElement(name = "child")
 @XmlIDREF
 private List<BidirectionalNode> children;

 public BidirectionalNode() {
  super();
 }

 public BidirectionalNode(String name) {
  this();
  this.id = name;
 }

 public void addChild(BidirectionalNode child) {
  if (child != null) {
   child.parent = this;
   this.getChildren().add(child);
  }
 }

 public String getName() {
  return id;
 }

 public void setName(String name) {
  this.id = name;
 }

 public BidirectionalNode getParent() {
  return parent;
 }

 public void setParent(BidirectionalNode parent) {
  this.parent = parent;
 }

 public List<BidirectionalNode> getChildren() {
  if (children == null) {
   children = new ArrayList<BidirectionalNode>();
  }
  return children;
 }

 public void setChildren(List<BidirectionalNode> children) {
  this.children = children;
 }
}

and here is the wrapper bean with a wrap method that transforms the tree into a list:
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class NodeWrapper {
 
 @XmlAttribute
 @XmlIDREF
 private BidirectionalNode root;

 @XmlElement(name = "node")
 private List<BidirectionalNode> nodes;

 public BidirectionalNode getRoot() {
  return root;
 }

 public void setRoot(BidirectionalNode root) {
  this.root = root;
 }

 public List<BidirectionalNode> getNodes() {
  return nodes;
 }

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

 public void wrap(BidirectionalNode root) {
  this.root = root;
  this.nodes = parseNode(root, new HashSet<String>());
 }

 private List<BidirectionalNode> parseNode(BidirectionalNode node,
   HashSet<String> visited) {
  if (!visited.contains(node.getName())) {
   ArrayList<BidirectionalNode> results = new ArrayList<BidirectionalNode>();
   visited.add(node.getName());
   results.add(node);
   for (BidirectionalNode child : node.getChildren()) {
    results.addAll(parseNode(child, visited));
   }
   return results;
  }
  return Collections.emptyList();
 }

}
This will result in the following XML:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<nodeWrapper root="rootNode">
 <node id="rootNode">
  <child>child1</child>
 </node>
 <node id="child1">
  <child>child2</child>
 </node>
 <node id="child2">
  <child>child3</child>
  <child>child1</child>
 </node>
 <node id="child3">
  <child>child1</child>
 </node>
</nodeWrapper>

No comments:

Post a Comment