I am trying to use MOXy JAXB to serialize a class A which looks like:
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement
public class A {
private Map<Foo, Bar> fooBar = new HashMap<Foo, Bar>();
private Set<Foo> foos = new HashSet<Foo>();
@XmlJavaTypeAdapter(FooBarMapAdapter.class)
public Map<Foo, Bar> getFooBar() {
return fooBar;
}
public void setFooBar(Map<Foo, Bar> fooBar) {
this.fooBar = fooBar;
}
@XmlElement
public Set<Foo> getFoos() {
return foos;
}
public void setFoos(Set<Foo> foos) {
this.foos = foos;
}
}
The point is that the Foo objects in the "foos" fields are a superset of those in the fooBar map. Therefore I would like to "link" the key elements for the "fooBar" map to the corresponding elements in the "foos" list. I have tried this using the XmlID and XmlIDREF annotations:
@XmlAccessorType(XmlAccessType.NONE)
public class Foo {
private String xmlId;
@XmlID
@XmlAttribute
public String getXmlId() {
return xmlId;
}
public void setXmlId(String xmlId) {
this.xmlId = xmlId;
}
}
@XmlAccessorType(XmlAccessType.NONE)
public class Bar {
// Some code...
}
Then in my XmlAdapter I have tried to use a XmlIDREF annotation on the adapted map entries' foo object:
public class FooBarMapAdapter extends
XmlAdapter<FooBarMapAdapter.FooBarMapType, Map<Foo, Bar>> {
public static class FooBarMapType {
public List<FooBarMapEntry> entries = new ArrayList<FooBarMapEntry>();
}
@XmlAccessorType(XmlAccessType.NONE)
public static class FooBarMapEntry {
private Foo foo;
private Bar bar;
@XmlIDREF
@XmlAttribute
public Foo getFoo() {
return foo;
}
public void setFoo(Foo foo) {
this.foo = foo;
}
@XmlElement
public Bar getBar() {
return bar;
}
public void setBar(Bar bar) {
this.bar = bar;
}
}
@Override
public FooBarMapType marshal(Map<Foo, Bar> map) throws Exception {
FooBarMapType fbmt = new FooBarMapType();
for (Map.Entry<Foo, Bar> e : map.entrySet()) {
FooBarMapEntry entry = new FooBarMapEntry();
entry.setFoo(e.getKey());
entry.setBar(e.getValue());
fbmt.entries.add(entry);
}
return fbmt;
}
@Override
public Map<Foo, Bar> unmarshal(FooBarMapType fbmt) throws Exception {
Map<Foo, Bar> map = new HashMap<Foo, Bar>();
for (FooBarMapEntry entry : fbmt.entries) {
map.put(entry.getFoo(), entry.getBar());
}
return map;
}
}
When marshaling the code above is working as expected and produces the following XML:
<?xml version="1.0" encoding="UTF-8"?>
<a>
<fooBar>
<entries foo="nr1">
<bar/>
</entries>
</fooBar>
<foos xmlId="nr1"/>
</a>
For testing unmarshal, I am using the following test-code:
public class Test {
public static void main(String[] args) throws Exception {
A a = new A();
Map<Foo, Bar> map = new HashMap<Foo, Bar>();
Foo foo = new Foo();
foo.setXmlId("nr1");
Bar bar = new Bar();
map.put(foo, bar);
a.setFooBar(map);
a.setFoos(map.keySet());
final File file = new File("test.xml");
if (!file.exists())
file.createNewFile();
FileOutputStream fos = new FileOutputStream(file);
JAXBContext jc = JAXBContext.newInstance(A.class);
Marshaller m = jc.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
m.marshal(a, fos);
FileInputStream fis = new FileInputStream(file);
Unmarshaller um = jc.createUnmarshaller();
A newA = (A) um.unmarshal(fis);
System.out.println(newA.getFooBar());
}
}
This code produces the (for me) unexpected result:
{null=test.moxy.Bar@373c0b53}
That is, the Foo object used as key in the map is null. If I change the map adapter and marshal the Foo object twice, instead of using an ID reference, I do not get this null pointer.
I have been able to find some posts about this on google using the JAXB-RI, where the problem could be solved writing an IDResolver as described at http://weblogs.java.net/blog/2005/08/15/pluggable-ididref-handling-jaxb-20. Unfortunately I have not been able to find any information about such a class in the MOXy JAXB JavaDoc.
Suggestion for workaround From Blaise Doughan answer, I have realized that this is a bug in the MOXy implementation of JAXB. I have been able to make a (ugly) workaround for this bug. The idea is that instead of using a XMLAdapter the map is "converted" inside its defining class. The class A now looks like:
@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement
public class A {
private Map<Foo, Bar> fooBar = new HashMap<Foo, Bar>();
private Set<Foo> foos = new HashSet<Foo>();
// Due to a bug a XMLAdapter approch is not possible when using XmlIDREF.
// The map is mapped by the wrapper method getXmlableFooBarMap.
// @XmlJavaTypeAdapter(FooBarMapAdapter.class)
public Map<Foo, Bar> getFooBar() {
return fooBar;
}
public void setFooBar(Map<Foo, Bar> fooBar) {
this.fooBar = fooBar;
}
@XmlElement
public Set<Foo> getFoos() {
return foos;
}
public void setFoos(Set<Foo> foos) {
this.foos = foos;
}
// // WORKAROUND FOR JAXB BUG /////
private List<FooBarMapEntry> mapEntries;
@XmlElement(name = "entry")
public List<FooBarMapEntry> getXmlableFooBarMap() {
this.mapEntries = new LinkedList<FooBarMapEntry>();
if (getFooBar() == null)
return mapEntries;
for (Map.Entry<Foo, Bar> e : getFooBar().entrySet()) {
FooBarMapEntry entry = new FooBarMapEntry();
entry.setFoo(e.getKey());
entry.setBar(e.getValue());
mapEntries.add(entry);
}
return mapEntries;
}
public void setXmlableFooBarMap(List<FooBarMapEntry> entries) {
this.mapEntries = entries;
}
public void transferFromListToMap() {
fooBar = new HashMap<Foo, Bar>();
for (FooBarMapEntry entry : mapEntries) {
fooBar.put(entry.getFoo(), entry.getBar());
}
}
}
After the unmarshal, the transferFromListToMap-method now needs to be called. So the following line should be added immediately after the reference to newA is obtained:
newA.transferFromListToMap();
Any suggestions for a nicer workaround / bug fix will be appreciated :).
Note: I'm the EclipseLink JAXB (MOXy) lead.
I have been able to confirm the issue that you are seeing:
Why the Issue is Happening
The issue is due to MOXy processing the XmlAdapter
logic before it has processed the @XmlIDREF
logic. MOXy does a single pass of the XML document, and the @XmlIDREF
relationships are processed at the end to ensure that all of the referenced objects have been built (as the reference may precede the referenced object, as in this case).
I will try to post a workaround to this issue, and you can track our progress on this issue using the above bug.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With