Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JAXB: How should I marshal complex nested data structures?

I have several complex data structures like

Map< A, Set< B > > Set< Map< A, B > > Set< Map< A, Set< B > > > Map< A, Map< B, Set< C > > > and so on (more complex data structures) 

Note: In my case it doesn't really matter if I use Set or List.

Now I know that JAXB let me define XmlAdapter's, that's fine, but I don't want to define an XmlAdapter for every of the given data structures (it would be just too much copy-and-paste code).

I tried to achieve my goal by declaring two generalizing XmlAdapters:

  • one for Map: MapAdapter<K,V>
  • one for Set: SetAdapter<V>

The problem:
JAXB complains as following:

javax.xml.bind.JAXBException: class java.util.Collections$UnmodifiableMap nor any of its   super class is known to this context. 

Here is my adapter class:

import java.util.*; import javax.xml.bind.annotation.*; import javax.xml.bind.annotation.adapters.*;  public class Adapters {  public final static class MapAdapter<K, V>         extends XmlAdapter<MapAdapter.Adapter<K, V>, Map<K, V>> {      @XmlType     @XmlRootElement     public final static class Adapter<K, V> {          @XmlElement         protected List<MyEntry<K, V>> key = new LinkedList<MyEntry<K, V>>();          private Adapter() {         }          public Adapter(Map<K, V> original) {             for (Map.Entry<K, V> entry : original.entrySet()) {                 key.add(new MyEntry<K, V>(entry));             }         }      }      @XmlType     @XmlRootElement     public final static class MyEntry<K, V> {          @XmlElement         protected K key;          @XmlElement         protected V value;          private MyEntry() {         }          public MyEntry(Map.Entry<K, V> original) {             key = original.getKey();             value = original.getValue();         }      }      @Override     public Adapter<K, V> marshal(Map<K, V> obj) {         return new Adapter<K, V>(obj);     }      @Override     public Map<K, V> unmarshal(Adapter<K, V> obj) {         throw new UnsupportedOperationException("unmarshalling is never performed");     }  }  } 

Here is my JUnit test case:

import java.io.*; import java.util.*; import javax.xml.bind.*; import javax.xml.bind.annotation.*; import javax.xml.bind.annotation.adapters.*; import org.junit.*; import static java.lang.System.*;  public class SomeTest {  @Test public void _map2()         throws Exception {      Map<String, Map<String, String>> dataStructure =             new HashMap<String, Map<String, String>>();      Map<String, String> inner1 = new HashMap<String, String>();     Map<String, String> inner2 = new HashMap<String, String>();      dataStructure.put("a", inner1);     dataStructure.put("b", inner1);      inner1.put("a1", "1");     inner1.put("a2", "2");     inner2.put("b1", "1");     inner2.put("b2", "2");      JAXBContext context = JAXBContext.newInstance(Adapters.XMap.class,             Adapters.XCount.class, Adapters.XEntry.class);      Marshaller marshaller = context.createMarshaller();     marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);     marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);      marshaller.setAdapter(new Adapters.MapAdapter());      StringWriter sw = new StringWriter();      marshaller.marshal(dataStructure, sw);     out.println(sw.toString()); }  } 
like image 481
ivan_ivanovich_ivanoff Avatar asked May 03 '09 23:05

ivan_ivanovich_ivanoff


People also ask

How does JAXB marshalling work?

In JAXB, marshalling involves parsing an XML content object tree and writing out an XML document that is an accurate representation of the original XML document, and is valid with respect the source schema. JAXB can marshal XML data to XML documents, SAX content handlers, and DOM nodes.

What is@ XmlElement in Java?

Guideline: The @XmlElement annotation maps a property or field to an XML element. This is also the default mapping in the absence of any other JAXB 2.0 annotations. The annotation parameters in @XmlElement can be used to specify whether the element is optional or required, nillable or not.


2 Answers

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?

like image 63
ivan_ivanovich_ivanoff Avatar answered Sep 23 '22 00:09

ivan_ivanovich_ivanoff


I had the same requirement to use a Map< String,Map< String,Integer>>. I used the XMLAdapter and it worked fine. Using XMLAdaptor is the cleanest solution I think. Below is the code of the adaptor. This is the jaXb class code snippet.

    @XmlJavaTypeAdapter(MapAdapter.class)     Map<String, Map<String, Integer>> mapOfMap = new HashMap<String,Map<String, Integer>>(); 

MapType Class :

public class MapType {  public List<MapEntryType> host = new ArrayList<MapEntryType>();  } 

MapEntry Type Class:

public class MapEntryType {  @XmlAttribute public String ip;  @XmlElement public List<LinkCountMapType> request_limit = new ArrayList<LinkCountMapType>();  } 

LinkCountMapType Class:

public class LinkCountMapType { @XmlAttribute public String service;  @XmlValue public Integer count; } 

Finally the MapAdaptor Class:

    public final class MapAdapter extends XmlAdapter<MapType, Map<String, Map<String, Integer>>> {  @Override public Map<String, Map<String, Integer>> unmarshal(MapType v) throws Exception {     Map<String, Map<String, Integer>> mainMap = new HashMap<String, Map<String, Integer>>();      List<MapEntryType> myMapEntryTypes = v.host;     for (MapEntryType myMapEntryType : myMapEntryTypes) {         Map<String, Integer> linkCountMap = new HashMap<String, Integer>();         for (LinkCountMapType myLinkCountMapType : myMapEntryType.request_limit) {             linkCountMap.put(myLinkCountMapType.service, myLinkCountMapType.count);         }         mainMap.put(myMapEntryType.ip, linkCountMap);     }     return mainMap; }  @Override public MapType marshal(Map<String, Map<String, Integer>> v) throws Exception {     MapType myMapType = new MapType();      List<MapEntryType> entry = new ArrayList<MapEntryType>();      for (String ip : v.keySet()) {         MapEntryType myMapEntryType = new MapEntryType();         Map<String, Integer> linkCountMap = v.get(ip);         List<LinkCountMapType> linkCountList = new ArrayList<LinkCountMapType>();         for (String link : linkCountMap.keySet()) {             LinkCountMapType myLinkCountMapType = new LinkCountMapType();             Integer count = linkCountMap.get(link);             myLinkCountMapType.count = count;             myLinkCountMapType.service = link;             linkCountList.add(myLinkCountMapType);         }         myMapEntryType.ip = ip;         myMapEntryType.request_limit = linkCountList;         entry.add(myMapEntryType);     }     myMapType.host = entry;     return myMapType; } 

}

Marshalling a Jaxb Object will give the below XML

     <mapOfmap>     <host ip="127.0.0.1">         <request_limit service="service1">7</request_limit>         <request_limit service="service2">8</request_limit>     </host> </mapOfmap> 
like image 26
Hunaid Husain Avatar answered Sep 24 '22 00:09

Hunaid Husain