Jaxb: How to Marshal Complex Nested Data Structures

JAXB: How should I marshal complex nested data structures?

I've solved the problem without XmlAdapter's.

I've written JAXB-annotated objects for Map, Map.Entry and Collection.

The main idea is inside the method xmlizeNestedStructure(...):

Take a look at the code:

public final class Adapters {

private Adapters() {
}

public static Class<?>[] getXmlClasses() {
return new Class<?>[]{
XMap.class, XEntry.class, XCollection.class, XCount.class
};
}

public static Object xmlizeNestedStructure(Object input) {
if (input instanceof Map<?, ?>) {
return xmlizeNestedMap((Map<?, ?>) input);
}
if (input instanceof Collection<?>) {
return xmlizeNestedCollection((Collection<?>) input);
}

return input; // non-special object, return as is
}

public static XMap<?, ?> xmlizeNestedMap(Map<?, ?> input) {
XMap<Object, Object> ret = new XMap<Object, Object>();

for (Map.Entry<?, ?> e : input.entrySet()) {
ret.add(xmlizeNestedStructure(e.getKey()),
xmlizeNestedStructure(e.getValue()));
}

return ret;
}

public static XCollection<?> xmlizeNestedCollection(Collection<?> input) {
XCollection<Object> ret = new XCollection<Object>();

for (Object entry : input) {
ret.add(xmlizeNestedStructure(entry));
}

return ret;
}

@XmlType
@XmlRootElement
public final static class XMap<K, V> {

@XmlElementWrapper(name = "map")
@XmlElement(name = "entry")
private List<XEntry<K, V>> list = new LinkedList<XEntry<K, V>>();

public XMap() {
}

public void add(K key, V value) {
list.add(new XEntry<K, V>(key, value));
}

}

@XmlType
@XmlRootElement
public final static class XEntry<K, V> {

@XmlElement
private K key;

@XmlElement
private V value;

private XEntry() {
}

public XEntry(K key, V value) {
this.key = key;
this.value = value;
}

}

@XmlType
@XmlRootElement
public final static class XCollection<V> {

@XmlElementWrapper(name = "list")
@XmlElement(name = "entry")
private List<V> list = new LinkedList<V>();

public XCollection() {
}

public void add(V obj) {
list.add(obj);
}

}

}

It works!

Let's look at a demo output:

<xMap>
<map>
<entry>
<key xsi:type="xCount" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<count>1</count>
<content xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">a</content>
</key>
<value xsi:type="xCollection" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<list>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">a1</entry>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">a2</entry>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">a3</entry>
</list>
</value>
</entry>
<entry>
<key xsi:type="xCount" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<count>2</count>
<content xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">b</content>
</key>
<value xsi:type="xCollection" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<list>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">b1</entry>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">b3</entry>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">b2</entry>
</list>
</value>
</entry>
<entry>
<key xsi:type="xCount" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<count>3</count>
<content xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">c</content>
</key>
<value xsi:type="xCollection" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<list>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">c1</entry>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">c2</entry>
<entry xsi:type="xs:string" xmlns:xs="http://www.w3.org/2001/XMLSchema">c3</entry>
</list>
</value>
</entry>
</map>
</xMap>

Sorry, the demo output uses also a data structure called "count"
which is not mentioned in the Adapter's source code.

BTW: does anyone know how to remove all these annoying
and (in my case) unnecessary xsi:type attributes?

Marshalling nested objects to a flat XML structure

I solved this using an XStream Converter. It checks for the @XmlType annotation to determine if a JAXB bean is being converted. All other types go through the default converters.

Though a JAXB-centric solution would have been nice, XStream provided a compellingly straightforward solution.

public class FlatXmlConverter implements Converter {

private static final Logger log =
LoggerFactory.getLogger(NvpConverter.class);

@Override
public void marshal(Object source, HierarchicalStreamWriter writer,
MarshallingContext context) {
Class<? extends Object> sourceClass = source.getClass();
String prefix = (String) context.get("prefix");
for (Field field : sourceClass.getDeclaredFields()) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
String name = field.getName();
Class<?> type = field.getType();

try {
Object value = field.get(source);
if (value != null) {
if (type.isAnnotationPresent(XmlType.class)) {
context.put("prefix", name);
context.convertAnother(value);
context.put("prefix", null);
} else {
String nodeName;
if (prefix == null) {
nodeName = name;
} else {
nodeName = prefix + "_" + name;
}

writer.startNode(nodeName);
context.convertAnother(value);
writer.endNode();
}
}
} catch (IllegalArgumentException ex) {
log.error("IllegalArgumentException", ex);
} catch (IllegalAccessException ex) {
log.error("IllegalAccessException", ex);
}
}
}

@Override
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
throw new UnsupportedOperationException("Not supported yet.");
}

@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public boolean canConvert(Class type) {
log.debug("canConvert({})", type);
return type.isAnnotationPresent(XmlType.class);
}
}

JAXB and complex maps

I think the main problem is the interface as JAXB ought to be able to marshal Map<String, ConcreteType>. The problem with interfaces is that JAXB demarshalling does not know what concrete type to use to implement the interface. The marshalling stream may not have come from Java code, so the stream can't contain the concrete type information. JAXB would have to choose an implementation, and it needs help to do that.

JAXB: How should I marshall complex nested data structures

Mapping your favorite class

JBoss Built-in JAXB Providers

This is a common problem with Web Services marshalling. One robust method is to use Data Transfer Objects containing concrete types that can be precisely defined in WSDL for the data transfer. You have to map your domain objects into and out of these DTOs in your application code, which is a disadvantage. One benefit of this approach is that your application is loosely coupled to the data transfer format.

Unmarshalling nested list of xml items using JAXB

You will need to define a custom XmlAdapter. The complicated part in your case is that you want to map one XML element into multiple Java Element objects. This means that, in Java., your XmlAdapter needs to be configured for collection of Element objects. Assuming your example XML fragment is part of a document:

<document>
<elements>
<element>
....
</element>
<elements>
</document>

Then you will need to configure the XmlAdapter for the List<Element> field in the Java Document class:

class Document {
@XmlJavaTypeAdapter(CustomAdapter.class)
List<Element> elements;
}

Then you your CustomAdapter class can receive a list of Element objects (corresponding to the actual XML structure with the nested items) and produce a list of Element with the structure you want.

For an example, check JAXB XmlAdapter – Customized Marshaling and Unmarshaling

JAXB Marshalling and Generics

You could write a custom adapter (not using JAXB's XmlAdapter) by doing the following:

1) declare a class which accepts all kinds of elements and has JAXB annotations
and handles them as you wish (in my example I convert everything to String)

@YourJAXBAnnotationsGoHere
public class MyAdapter{

@XmlElement // or @XmlAttribute if you wish
private String content;

public MyAdapter(Object input){
if(input instanceof String){
content = (String)input;
}else if(input instanceof YourFavoriteClass){
content = ((YourFavoriteClass)input).convertSomehowToString();
}else if(input instanceof .....){
content = ((.....)input).convertSomehowToString();
// and so on
}else{
content = input.toString();
}
}
}

// I would suggest to use a Map<Class<?>,IMyObjToStringConverter> ...
// to avoid nasty if-else-instanceof things

2) use this class instead of E in your to-be-marshalled class

NOTES

  • Of course this would not work for complex (nested) data structures.
  • You have to think how to unmarshall this back again, could be more tricky. If
    it's too tricky, wait for a better proposal than mine ;)

Unmarshal nested Map with Jaxb

I would recommend that you structure your XML like:

<exchangerate>
<date>2015-05-04</date>
<currency code="EUR">
<rate code="EUR">1</rate >
<rate code="GBP">0.73788</rate >
<rate code="USD">1.1152</rate >
</currency>
<currency code="GBP">
<rate code="EUR">1.35523</rate >
<rate code="GBP">1</rate >
<rate code="USD">1.51136</rate >
</currency>
<currency code="USD">
<rate code="EUR">0.8967</rate >
<rate code="GBP">0.66166</rate >
<rate code="USD">1</rate >
</currency>
</exchangerate>

and you have multiple Classes:

@XmlAccessorType(XmlAccessType.FIELD)
public class ExchangeRates {
@XmlJavaTypeAdapter(DateAdapter.class)
private Date date;

@XmlElement(name="currency")
private List<Currency> currencies = new ArrayList<>();

....
}

@XmlAccessorType(XmlAccessType.FIELD)
public class Currency {
@XmlAttribute
private String code;

@XmlElement(name="rate")
private List<Rate> rates= new ArrayList<>();

....
}

@XmlAccessorType(XmlAccessType.FIELD)
public class Rate {
@XmlAttribute
private String code;

@XmlValue
private Double value;

....
}


Related Topics



Leave a reply



Submit