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
Differencebetween a Synchronized Method and Synchronized Block in Java
Difference Between an Unbound Wildcard and a Raw Type
How to Listen for Key Presses (Within Java Swing) Across All Components
How Are Servlet Url Mappings in Web.Xml Used
Any Character Including Newline - Java Regex
Spring Security's Securitycontextholder: Session or Request Bound
How to Append a Node to an Existing Xml File in Java
Using PDFbox to Write Utf-8 Encoded Strings to a PDF
Why Doesn't Java.Util.Collection Implement the New Stream Interface
Spring Boot Actuator Without Spring Boot
How to Extract Parameters from a Given Url
Why Does My Aes Encryption Throws an Invalidkeyexception
Java: Local Variable Mi Defined in an Enclosing Scope Must Be Final or Effectively Final