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.