Search code examples
javajaxbeclipselinkmoxy

EclipseLink MOXy with recursive data structures / child elements of same type


I am using EclipseLink MOXy and have a data structure that has child elements of the same data type. Now I don't want to serialize the datastructure with infinite depth, but only the first level.

Here is some example code of the data structure:

package test;

import java.util.Collection;
import java.util.Vector;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlRootElement
public class MyClass {
    private int id;
    private String details;
    private Collection<MyClass> children = new Vector<MyClass>();

    public MyClass() {
    }

    public MyClass(int id, String details) {
        this.id = id;
        this.details = details;
    }

    @XmlElementWrapper
    @XmlElementRef
    public Collection<MyClass> getChildren() {
        return children;
    }

    public void addChild(MyClass child) {
        children.add(child);
    }

    public String getDetails() {
        return details;
    }

    @XmlAttribute
    public int getId() {
        return id;
    }

    public void setChildren(Collection<MyClass> children) {
        this.children = children;
    }

    public void setDetails(String details) {
        this.details = details;
    }

    public void setId(int id) {
        this.id = id;
    }
}

And my test program:

package test;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;

public class Test {
    public static void main(String[] args) throws Exception {
        MyClass l1 = new MyClass(1, "Level 1");
        MyClass l2 = new MyClass(2, "Level 2");
        l1.addChild(l2);
        MyClass l3 = new MyClass(3, "Level 3");
        l2.addChild(l3);

        JAXBContext jc = JAXBContext.newInstance(MyClass.class);
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(l1, System.out);
    }
}

The following XML is generated:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<myClass id="1">
    <children>
        <myClass id="2">
            <children>
                <myClass id="3">
                    <children/>
                    <details>Level 3</details>
                </myClass>
            </children>
            <details>Level 2</details>
        </myClass>
    </children>
    <details>Level 1</details>
</myClass>

However, I'd like the xml too look like:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<myClass id="1">
    <children>
        <myClass id="2">
            <details>Level 2</details>
        </myClass>
    </children>
    <details>Level 1</details>
</myClass>

Thanks.


Solution

  • To accomplish this use case we will leverage two concepts from JAXB: XmlAdapter and Marshaller.Listener.

    MyClassAdapter

    We will leverage the default JAXB behaviour of not marshalling an element for a null value. To do this we will implement an XmlAdapter that returns null after a specified level has been reached. To count the levels we will create a Marshaller.Listener.

    package forum11769758;
    
    import javax.xml.bind.Marshaller;
    import javax.xml.bind.annotation.adapters.XmlAdapter;
    
    public class MyClassAdapter extends XmlAdapter<MyClass, MyClass>{
    
        private int levels;
        private MyMarshallerListener marshallerListener;
    
        public MyClassAdapter() {
        }
    
        public MyClassAdapter(int levels) {
            this.levels = levels;
        }
    
        public Marshaller.Listener getMarshallerListener() {
            if(null == marshallerListener) {
                marshallerListener = new MyMarshallerListener();
            }
            return marshallerListener;
        }
    
        @Override
        public MyClass marshal(MyClass myClass) throws Exception {
            if(null == marshallerListener || marshallerListener.getLevel() < levels) {
                return myClass;
            }
            return null;
        }
    
        @Override
        public MyClass unmarshal(MyClass myClass) throws Exception {
            return myClass;
        }
    
        static class MyMarshallerListener extends Marshaller.Listener {
    
            private int level = 0;
    
            public int getLevel() {
                return level;
            }
    
            @Override
            public void afterMarshal(Object object) {
                if(object instanceof MyClass) {
                    level--;
                }
            }
    
            @Override
            public void beforeMarshal(Object object) {
                if(object instanceof MyClass) {
                    level++;
                }
            }
    
        }
    
    }
    

    MyClass

    The @XmlJavaTypeAdapter annotation is used to specify that an XmlAdapter should be used.

    package forum11769758;
    
    import java.util.*;
    import javax.xml.bind.annotation.*;
    import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
    
    @XmlAccessorType(XmlAccessType.PROPERTY)
    @XmlRootElement
    public class MyClass {
    
        private int id;
        private String details;
        private Collection<MyClass> children = new Vector<MyClass>();
    
        public MyClass() {
        }
    
        public MyClass(int id, String details) {
            this.id = id;
            this.details = details;
        }
    
        @XmlElementWrapper
        @XmlElementRef
        @XmlJavaTypeAdapter(MyClassAdapter.class)
        public Collection<MyClass> getChildren() {
            return children;
        }
    
        public void addChild(MyClass child) {
            children.add(child);
        }
    
        public String getDetails() {
            return details;
        }
    
        @XmlAttribute
        public int getId() {
            return id;
        }
    
        public void setChildren(Collection<MyClass> children) {
            this.children = children;
        }
    
        public void setDetails(String details) {
            this.details = details;
        }
    
        public void setId(int id) {
            this.id = id;
        }
    
    }
    

    Test

    Since we need to use the XmlAdapter in a stateful way, we will set an instance of it on the Marshaller, we will also set the instance of Marshaller.Listener we created on the Marshaller.

    package forum11769758;
    
    import javax.xml.bind.*;
    
    public class Test {
    
        public static void main(String[] args) throws Exception {
            MyClass l1 = new MyClass(1, "Level 1");
            MyClass l2 = new MyClass(2, "Level 2");
            l1.addChild(l2);
            MyClass l3 = new MyClass(3, "Level 3");
            l2.addChild(l3);
    
            JAXBContext jc = JAXBContext.newInstance(MyClass.class);
            Marshaller marshaller = jc.createMarshaller();
            MyClassAdapter myClassAdapter = new MyClassAdapter(2);
            marshaller.setAdapter(myClassAdapter);
            marshaller.setListener(myClassAdapter.getMarshallerListener());
            marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
            marshaller.marshal(l1, System.out);
        }
    
    }
    

    Output

    <?xml version="1.0" encoding="UTF-8"?>
    <myClass id="1">
       <children>
          <myClass id="2">
             <children/>
             <details>Level 2</details>
          </myClass>
       </children>
       <details>Level 1</details>
    </myClass>
    

    For More Information

    The following articles expand on the topics discussed in this answer: