2

I'm trying to adapt a Map<Integer,String> XmlAdapter to one that supports Map<Object,Object>. The approach is based on this article: XmlAdapter - JAXB's Secret Weapon

This line in the testing harness generates NullPointerException:

JAXBContext jc = JAXBContext.newInstance(Foo.class);

If I change the harness and MapAdapter/MapEntry to be T<Integer,String>, the code works as expected.

What am I missing? Can the Object type be serialized or does it need to be cast as another, less abstract class? If so, I would think that I would encounter this error in the marshal() method, but it never seems to reach this point (at least in Netbean's debugger).

MapEntryType:

public class MyMapEntryType {

    @XmlAttribute
    public Object key; 

    @XmlValue
    public Object value;

}

MapType:

public class MyMapType {

    public List<MyMapEntryType> entry = new ArrayList<MyMapEntryType>();

}

MapAdapter:

public final class MyMapAdapter extends

   XmlAdapter<MyMapType,Map<Object, Object>> {

   @Override
   public MyMapType marshal(Map<Object, Object> arg0) throws Exception {

      MyMapType myMapType = new MyMapType();

      for(Entry<Object, Object> entry : arg0.entrySet()) {

         MyMapEntryType myMapEntryType =  new MyMapEntryType();
         myMapEntryType.key = entry.getKey();
         myMapEntryType.value = entry.getValue();
         myMapType.entry.add(myMapEntryType);

      }

      return myMapType;
   }

   @Override
   public Map<Object, Object> unmarshal(MyMapType arg0) throws Exception {

      HashMap<Object, Object> hashMap = new HashMap<Object, Object>();

      for(MyMapEntryType myEntryType : arg0.entry) {
         hashMap.put(myEntryType.key, myEntryType.value);
      }

      return hashMap;
   }

}

Object to be marshalled/unmarshalled:

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Foo {

   @XmlJavaTypeAdapter(MyMapAdapter.class)
   Map<Object, Object> map = new HashMap<Object, Object>();

   public Map getMap() {
      return map;
   }

   public void setMap(Map map) {
      this.map = map;
   }

}

Testing harness:

Map<Object,Object> xyz = new HashMap<Object,Object>();
xyz.put("key0", "value0");
xyz.put("key1", "value1");

Foo foo = new Foo();
foo.setMap(xyz);

//generates NullPointerException                
JAXBContext jc = JAXBContext.newInstance(Foo.class);

Marshaller marshaller = jc.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
marshaller.marshal(foo, System.out);

Stack trace:

java.lang.NullPointerException
    at com.sun.xml.internal.bind.v2.runtime.reflect.TransducedAccessor.get(TransducedAccessor.java:154)
    at com.sun.xml.internal.bind.v2.runtime.property.ValueProperty.<init>(ValueProperty.java:66)
    at com.sun.xml.internal.bind.v2.runtime.property.PropertyFactory.create(PropertyFactory.java:95)
    at com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl.<init>(ClassBeanInfoImpl.java:145)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getOrCreate(JAXBContextImpl.java:479)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getOrCreate(JAXBContextImpl.java:498)
    at com.sun.xml.internal.bind.v2.runtime.property.ArrayElementProperty.<init>(ArrayElementProperty.java:97)
    at com.sun.xml.internal.bind.v2.runtime.property.ArrayElementNodeProperty.<init>(ArrayElementNodeProperty.java:47)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
    at com.sun.xml.internal.bind.v2.runtime.property.PropertyFactory.create(PropertyFactory.java:113)
    at com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl.<init>(ClassBeanInfoImpl.java:145)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getOrCreate(JAXBContextImpl.java:479)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getOrCreate(JAXBContextImpl.java:498)
    at com.sun.xml.internal.bind.v2.runtime.property.SingleElementNodeProperty.<init>(SingleElementNodeProperty.java:90)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
    at com.sun.xml.internal.bind.v2.runtime.property.PropertyFactory.create(PropertyFactory.java:113)
    at com.sun.xml.internal.bind.v2.runtime.ClassBeanInfoImpl.<init>(ClassBeanInfoImpl.java:145)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getOrCreate(JAXBContextImpl.java:479)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.<init>(JAXBContextImpl.java:305)
    at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$JAXBContextBuilder.build(JAXBContextImpl.java:1100)
    at com.sun.xml.internal.bind.v2.ContextFactory.createContext(ContextFactory.java:143)
    at com.sun.xml.internal.bind.v2.ContextFactory.createContext(ContextFactory.java:110)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:228)
    at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:215)
    at javax.xml.bind.ContextFinder.find(ContextFinder.java:414)
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:618)
    at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:565)

I changed the Map from Map<Object,Object> to Map<String,Object> to better represent my needs (I also changed all of the internal objects to match this). I added @XmlAnyElement and @XmlMixed annotations to the Object property of the MyMapEntryType class; I removed the @XmlValue annotation.

When I debug the code, this line does NOT produce an error (hurray):

JAXBContext jc = JAXBContext.newInstance(PropertyBag.class);

However, attempting to marshal this entry:

xyz.put("key0", 1);

Results in an error that reads:

unable to marshal type `java.lang.Integer` as an element because it is missing an `@XmlRootElement` annotation
skaffman
  • 398,947
  • 96
  • 818
  • 769
craig
  • 25,664
  • 27
  • 119
  • 205
  • I might be wrong, but I think that JAXB cannot do the type exploring. In your original case you have objects in `MyMapEntryType`. Array is object, map is an object, primitive wrapper is object, any other composite class instance is an object. How JAXB can determine what serializer to use just by looking at this object? It can be a build-in serializer for "common" types, but unknown types require yet another adapter in chain. – dma_k Jan 28 '12 at 11:45
  • So, I would need an XmlAdapter for each non-common type? – craig Jan 28 '12 at 23:23
  • Yes. Basic types like String, Date, List, all primitives are already supported, but for the rest you need to declare explicitly this way or another how to (de-)serialize them. The main point here is that JAXB builds the meta-model of all classes (and possible XML mappings) when context is created, not when marshalling is done. For type `Object` JAXB has no way to discover what are possible ways to marshall. If the piece of XML which you want to hide behind `Object` is varying, then you need to go [`@XmlAnyElement`](http://stackoverflow.com/questions/9040695) way. – dma_k Jan 29 '12 at 08:53

2 Answers2

1

I just have a look around and come across this reply. So what you can use is @XmlAnyElement with custom XML<->DOM converter or you need to explicitly specify what are the possible classes for this property with @XmlElementRefs.

Community
  • 1
  • 1
dma_k
  • 10,431
  • 16
  • 76
  • 128
1

I was able to solve this by using a Map internally:

MyMapEntryType:

public class MyMapEntryType {

    @XmlAttribute
    public String key; 

    @XmlValue
    public String value;

    private MyMapEntryType() {
        //Required by JAXB
    } 

    public MyMapEntryType(String key, String value) {
        this.key   = key;
        this.value = value;
    }

}

MyMapType class:

public class MyMapType {

    @XmlElement(name="Property")
    public List<MyMapEntryType> entry = new ArrayList<MyMapEntryType>();

}

public class MyMapAdapter extends XmlAdapter<MyMapType, Map<String,Object>> {

    @Override
    public MyMapType marshal(Map<String,Object> bt) throws Exception {

        MyMapType myMapType =  new MyMapType();

        for(Map.Entry<String,Object> entry : bt.entrySet()) {

            MyMapEntryType myMapEntryType = new MyMapEntryType(entry.getKey(), entry.getValue().toString());
            myMapType.entry.add(myMapEntryType);

        }

        return myMapType;

    }

    ...

}

PropertyBag (nee Foo) class:

@XmlRootElement(name="PropertyBag")
public class PropertyBag {

    private Map<String,Object> map;

    public PropertyBag() {
        map = new HashMap<String,Object>();
    }

    @XmlJavaTypeAdapter(MyMapAdapter.class)
    @XmlElement(name="Properties")
    public Map<String,Object> getMap() {
        return map;
    }

    public void setMap(Map<String,Object> map) {
        this.map = map;
    }

}

Harness:

        Map<String, Object> parent = new HashMap<String, Object>();
        parent.put("SI_EMAIL_ADDRESS", "foo@bar.edu");
        parent.put("SI_DATE", new Date());
        parent.put("SI_GUID", "ATQJj1RvgVlLqDqP_VOGltM");
        parent.put("SI_BOOLEAN", true);
        parent.put("SI_INT", 1318);
        parent.put("SI_LONG", new Long(123456789));
        parent.put("SI_INTEGER", new Integer(23456));


        PropertyBag bag = new PropertyBag();
        bag.setMap(parent);

        JAXBContext jc = JAXBContext.newInstance(PropertyBag.class);            
        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(bag, System.out);

Output:

<PropertyBag>
    <Properties>
        <Property key="SI_INT">1318</Property>
        <Property key="SI_DATE">Wed Feb 01 22:18:35 EST 2012</Property>
        <Property key="SI_BOOLEAN">true</Property>
        <Property key="SI_LONG">123456789</Property>
        <Property key="SI_INTEGER">23456</Property>
        <Property key="SI_GUID">ATQJj1RvgVlLqDqP_VOGltM</Property>
        <Property key="SI_EMAIL_ADDRESS">foo@bar.edu</Property>
    </Properties>
</PropertyBag>

Ideally, I'd like to have the XML represented like:

<Properties>
    <SI_INT>1318</SI_INT>
    <SI_DATE>Wed Feb 01 22:18:35 EST 2012</SI_DATE>
    <SI_BOOLEAN>true</SI_BOOLEAN>
    <SI_LONG>123456789</SI_LONG>
    <SI_INTEGER>23456</SI_INTEGER>
    <SI_GUID>ATQJj1RvgVlLqDqP_VOGltM</SI_GUID>
    <SI_EMAIL_ADDRESS>foo@bar.edu</SI_EMAIL_ADDRESS>
</Properties>

I'd also like to be able to serialize subproperties:

...

Map<String,Object> child = new HashMap<String,Object>();
child.put("1", 33217);
child.put("2", 36351);
child.put("SI_TOTAL", 2);
parent.put("SI_LIST", child);

Which would be represented:

<Properties>
    <SI_INT>1318</SI_INT>
    <SI_DATE>Wed Feb 01 22:18:35 EST 2012</SI_DATE>
    <SI_BOOLEAN>true</SI_BOOLEAN>
    <SI_LONG>123456789</SI_LONG>
    <SI_INTEGER>23456</SI_INTEGER>
    <SI_GUID>ATQJj1RvgVlLqDqP_VOGltM</SI_GUID>
    <SI_EMAIL_ADDRESS>foo@bar.edu</SI_EMAIL_ADDRESS>
    <SI_LIST>
        <!-- numeric elements are illegal; refactor -->
        <1>33217</1>
        <2>36351</2>
        <SI_TOTAL>2</SI_TOTAL>
    </SI_LIST>
</Properties>

But I'm not certain that I can use annotation to convert a key name to an element.

craig
  • 25,664
  • 27
  • 119
  • 205
  • Thanks a lot for this answer. Based on this I was able to complete my `XMLAdapter Marshalling` metho. However, I am a bit struck by the `Unmarshalling` method within the `XmlAdapter` class. Can you please provide some idea on how to `Unmarshal` the type `Map`? – BATMAN_2008 May 20 '21 at 06:18