2

I experience a strange behavior while marshalling an object graph with bi-directional relationships.

The error message is:

Exception [EclipseLink-25037] (Eclipse Persistence Services - 2.5.2.v20140319-9ad6abd): org.eclipse.persistence.exceptions.XMLMarshalException

Exception Description: A cycle is detected in the object graph. This will cause an infinite loop: com.moxytest.Cycle$Doc@27f723 -> com.moxytest.Cycle$Pub@670b40af -> com.moxytest.Cycle$Agree@4923ab24 -> com.moxytest.Cycle$Agen@44c8afef -> com.moxytest.Cycle$Acc@7b69c6ba -> com.moxytest.Cycle$Med@46daef40 -> com.moxytest.Cycle$Pag@12f41634 -> com.moxytest.Cycle$Doc@27f723

The object graph in the exception message seems not in the order of processing. Debugging XPathObjectBuilder shows that on the cycleDetectionStack the objects are pushed in the following order:

com.moxytest.Cycle$Doc
com.moxytest.Cycle$Pag
com.moxytest.Cycle$Med
com.moxytest.Cycle$Acc
com.moxytest.Cycle$Agen
com.moxytest.Cycle$Agree
com.moxytest.Cycle$Pub 

I do not understand why the exception is being thrown because I thought I would be fine with @XmlInverseReference. The problem only occurs with a more complex object graph. Here is the code:

import java.io.StringReader;
import java.io.StringWriter;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import org.eclipse.persistence.jaxb.JAXBContext;
import org.eclipse.persistence.jaxb.MarshallerProperties;
import org.eclipse.persistence.oxm.MediaType;
import org.eclipse.persistence.oxm.annotations.XmlInverseReference;

public final class Cycle {

    private static JAXBContext JAXB_CONTEXT;

    static {
        try {
            JAXB_CONTEXT = (JAXBContext) JAXBContext.newInstance(Doc.class);
        } catch (JAXBException ex) {
            ex.printStackTrace();
        }
    }

    @XmlTransient
    @XmlAccessorType(XmlAccessType.PROPERTY)
    public static abstract class Entity {

        private String id;

        @XmlElement
        public String getId() {
            return id;
        }

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

    }

    @XmlRootElement
    public static class Doc extends Entity {

        private Pag pag;

        private Pub pub;

        @XmlElement
        @XmlInverseReference(mappedBy = "doc")
        public Pag getPag() {
            return pag;
        }

        public void setPag(Pag pag) {
            this.pag = pag;
        }

        @XmlElement
        @XmlInverseReference(mappedBy = "docs")
        public Pub getPub() {
            return pub;
        }

        public void setPub(Pub pub) {
            this.pub = pub;
        }

    }

    @XmlRootElement
    public static class Acc extends Entity {

        private Agen agen;

        @XmlElement
        public Agen getAgen() {
            return agen;
        }

        public void setAgen(Agen agen) {
            this.agen = agen;
        }

    }

    @XmlRootElement
    public static class Med extends Entity {

        private Pag pag;

        private Acc acc;

        @XmlElement
        @XmlInverseReference(mappedBy = "meds")
        public Pag getPag() {
            return pag;
        }

        public void setPag(Pag pag) {
            this.pag = pag;
        }

        @XmlElement
        public Acc getAcc() {
            return acc;
        }

        public void setAcc(Acc acc) {
            this.acc = acc;
        }

    }

    @XmlRootElement
    public static class Pag extends Entity {

        private Doc doc;

        private List<Med> meds = new ArrayList<>();

        public void setDoc(Doc doc) {
            this.doc = doc;
        }

        @XmlElement
        @XmlInverseReference(mappedBy = "pag")
        public Doc getDoc() {
            return doc;
        }

        @XmlElement
        @XmlInverseReference(mappedBy = "pag")
        public List<Med> getMeds() {
            return meds;
        }

        public void setMeds(List<Med> meds) {
            this.meds = meds;
        }

    }

    @XmlRootElement
    public static class Pub extends Entity {

        private List<Doc> docs;

        private Agree agree;

        public List<Doc> getDocs() {
            return docs;
        }

        @XmlElement
        @XmlInverseReference(mappedBy = "pub")
        public void setDocs(List<Doc> docs) {
            this.docs = docs;
        }

        @XmlElement
        @XmlInverseReference(mappedBy = "pub")
        public Agree getAgree() {
            return agree;
        }

        public void setAgree(Agree agree) {
            this.agree = agree;
        }

    }

    @XmlRootElement
    public static class Agree extends Entity {

        private Pub pub;

        private Agen agen;

        @XmlElement
        @XmlInverseReference(mappedBy = "agree")
        public Pub getPub() {
            return pub;
        }

        public void setPub(Pub pub) {
            this.pub = pub;
        }

        @XmlElement
        @XmlInverseReference(mappedBy = "agrees")
        public Agen getAgen() {
            return agen;
        }

        public void setAgen(Agen agen) {
            this.agen = agen;
        }

    }

    @XmlRootElement
    public static class Agen extends Entity {

        private List<Agree> agrees;

        @XmlElement
        @XmlInverseReference(mappedBy = "agen")
        public List<Agree> getAgrees() {
            return agrees;
        }

        public void setAgrees(List<Agree> agrees) {
            this.agrees = agrees;
        }

    }

    public static void main(String[] args) throws JAXBException {
        Pag pag = new Pag();

        Med med = new Med();
        med.setPag(pag);
        pag.getMeds().add(med);

        Doc doc = new Doc();
        pag.setDoc(doc);
        doc.setPag(pag);

        Pub pub = new Pub();
        pub.setDocs(Arrays.asList(doc));
        doc.setPub(pub);

        Agree agree = new Agree();
        agree.setPub(pub);
        pub.setAgree(agree);

        Agen agen = new Agen();
        agen.setAgrees(Arrays.asList(agree));
        agree.setAgen(agen);

        Acc acc = new Acc();
        acc.setAgen(agen);
        med.setAcc(acc);

        String marshal = marshal(doc);
        System.err.println(marshal);
        Doc ud = unmarshal(Doc.class, marshal);
        String marshal2 = marshal(ud);
        System.err.println("\n\n" + marshal2);
        System.err.println("Equals? " + marshal.equals(marshal2));
    }

    public static String marshal(Object toMarshal) throws JAXBException {
        Marshaller marshaller = JAXB_CONTEXT.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
//        marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, MediaType.APPLICATION_XML);
        marshaller.setProperty(MarshallerProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);
        StringWriter sw = new StringWriter();
        marshaller.marshal(toMarshal, sw);
        return sw.toString();
    }

    @SuppressWarnings("unchecked")
    public static <T> T unmarshal(Class<T> unmarshallingClass, String str) throws JAXBException {
        Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();
//        unmarshaller.setProperty(MarshallerProperties.MEDIA_TYPE, MediaType.APPLICATION_XML);
        unmarshaller.setProperty(MarshallerProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);
        return (T) unmarshaller.unmarshal(new StringReader(str));
    }

}

If I remove @XMLElement at setDocs() in class Pub it works. But then I loose the docs objects when marshalling Pub.

Can you help?

=============== UPDATE

EclipseLink's implementation of XMLInverseReference does not take transitive cycles into account. During marshalling it creates a cycleDetectionStack where it puts all marshalled objects. If it encounters XMLInverseReference it tries to find the owner by getting the object from the cycleDetectionStack - 2. This works for direct cycles between objects but not for transitive cycles like A --> B --> C --> A.

Looking into the source code I found a trivial way to make it work for transitive cycles. All I had to do was iterating over the complete cycleDetectionStack:

XMLCompositeCollectionMappingNodeValue:

if ((isInverseReference || xmlCompositeCollectionMapping.getInverseReferenceMapping() != null) && size >= 2) {
    //Object owner = marshalRecord.getCycleDetectionStack().get(size - 2);
    // Bugfix InverseRef has no effect on "transitive" references within object graph
    CycleDetectionStack cycleDetectionStack = marshalRecord.getCycleDetectionStack();
    for (Object stackedObj : cycleDetectionStack) {
        try {
            if (cp.contains(stackedObj, collection, session)) {
                return false;
            }
        } catch (ClassCastException e) {
            // For Bug #416875
        }
    }
}

The second class was XMLCompositeObjectMappingNodeValue with the exact same solution:

if ((isInverseReference || xmlCompositeObjectMapping.getInverseReferenceMapping() != null) && objectValue != null && size >= 2) {
    //Object owner = marshalRecord.getCycleDetectionStack().get(size - 2);
    // Bugfix InverseRef has no effect on "transitive" references within object graph
    CycleDetectionStack cycleDetectionStack = marshalRecord.getCycleDetectionStack();
    for (Object stackedObj : cycleDetectionStack) {
        if (objectValue.equals(stackedObj)) {
            return false;
        }
    }
}

Maybe the developer had good reasons for just looking at the owner. As for me I could not find out why I should live with this shortcoming. We have a large domain model where all entities are transcoded via json. We have not got any error since.

dima
  • 75
  • 1
  • 10
  • This page: http://www.ibm.com/developerworks/rational/library/resolve-jaxb-cycle-errors/index.html states that "[XmlInverseReferece] does not work when the relationship goes beyond two entities". Of course, it's not working for me either, and I only have two entities in my relationship. :( – FrustratedWithFormsDesigner Mar 17 '15 at 17:37
  • It is funny. The mentioned link exactly describes the pain I had to go through. The workaround with XMLIDREF and a container object was no solution for me. I ended up investigating the eclipse link source code and saw a shortcoming in its implementation which could be fixed in a way that transitive cycles are also taken into account. The fix is so trivial that I was wondering if the developer had a good reason for it. However I could not find any problem why my fix and the solution works in a large entity domain where the objects are transcoded in json format. See my update – dima Mar 19 '15 at 15:53

1 Answers1

0

In a JPA model layer, it is not very uncommon to have cyclic references and foreign key references that may turn a JSON serialisation into cyclic structure. This is called JSOG, where you have cycles in your JSON structure and EclipseLinks's default Moxy Json Provider is unable to serialize such POJOs.

You need Jackson Json Provider rather than modifying your model Pojo classes.

I assume you are using JaxRs REST api trying to serialize the Pojos. In that case you can use Jackson packages from maven quite easily. Simply create the providers for the jaxb provider and object mapper. More details can be found here:

https://stackoverflow.com/a/60319306/5076414

You need JSOG library along with the providers: https://github.com/jsog/jsog-jackson

Sacky San
  • 1,535
  • 21
  • 26