Search code examples
javaxmldata-bindingjaxbjibx

How to marshall an objectgraph with Java XML binding to a specific depth?


being a newbie to Java XML binding i am facing a challenge.

Let say i have a scenario where my domain model is constructed and i want to marshall this domain to an xml structure.

Now i want to provide different unmarshall path's:

  1. Marshall the whole object graph [no problem here]
  2. Marshall an objectgraph until a specific depth!!! [challenge]

I cannot figure out a good way on how to tackle this without introducing to much complexity. One can make a copy of the domain and manually later that, but that does not feel right. Any other solutions available?


Solution

  • You could leverage XmlAdapter and Marshal.Listener to get this behaviour:

    Demo

    A Marshal.Listener will be set to keep track of the depth of the tree we are marshalling. Also we will set runtime level XmlAdapters that are aware of the depth listener. These adapters will start returning null when the desired depth has been reached.

    import javax.xml.bind.JAXBContext;
    import javax.xml.bind.Marshaller;
    
    public class Demo {
    
        public static void main(String[] args) throws Exception {
            JAXBContext jc = JAXBContext.newInstance(Root.class);
    
            Root rootA = new Root();
            rootA.setName("A");
    
            Root rootB = new Root();
            rootB.setName("B");
            rootA.setChild(rootB);
    
            Root rootC = new Root();
            rootC.setName("C");
            rootB.setChild(rootC);
    
            Root rootD = new Root();
            rootD.setName("D");
            rootC.setChild(rootD);
    
            Root rootE = new Root();
            rootE.setName("E");
            rootD.setChild(rootE);
    
            Marshaller marshaller = jc.createMarshaller();
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
    
            DepthListener depthListener = new DepthListener(3);
            marshaller.setListener(depthListener);
            marshaller.setAdapter(new RootAdapter(depthListener));
            marshaller.marshal(rootA, System.out);
        }
    
    }
    

    DepthListener

    The purpose of this class is to to keep track of the current depth.

    import javax.xml.bind.Marshaller;
    
    public class DepthListener extends Marshaller.Listener {
    
        private int targetDepth;
        private int currentDepth = 0;
    
        public DepthListener(int depth) {
            this.targetDepth = depth;
        }
    
        @Override
        public void beforeMarshal(Object source) {
            currentDepth++;
        }
    
        @Override
        public void afterMarshal(Object source) {
            currentDepth--;
        }
    
        public boolean isMarshalDepth() {
            return currentDepth <= targetDepth; 
        }
    
    }
    

    RootAdapter

    The purpose of the XmlAdapter is to start returning null when the desired depth has been reached to stop the marshalling process.

    import javax.xml.bind.annotation.adapters.XmlAdapter;
    
    public class RootAdapter extends XmlAdapter<Root, Root> {
    
        private DepthListener depthListener;
    
        public RootAdapter() {
        }
    
        public RootAdapter(DepthListener depthListener) {
            this.depthListener = depthListener;
        }
    
        @Override
        public Root unmarshal(Root root) throws Exception {
            return root;
        }
    
        @Override
        public Root marshal(Root root) throws Exception {
            if(depthListener != null && !depthListener.isMarshalDepth()) {
                return null;
            }
            return root;
        }
    
    }
    

    Root

    The following demonstrates how to specify the XmlAdapter on the domain object via the @XmlJavaTypeAdapter annotation:

    import javax.xml.bind.annotation.XmlRootElement;
    import javax.xml.bind.annotation.XmlType;
    import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
    
    @XmlRootElement
    @XmlJavaTypeAdapter(RootAdapter.class)
    @XmlType(propOrder={"name", "child"})
    public class Root {
    
        private String name;
        private Root child;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Root getChild() {
            return child;
        }
    
        public void setChild(Root report) {
            this.child = report;
        }
    
    }
    

    Output

    The following is the output from the demo code:

    <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <root>
        <name>A</name>
        <child>
            <name>B</name>
            <child>
                <name>C</name>
            </child>
        </child>
    </root>