Custom Map<Object,Object> XmlAdapter

2019-07-21 12:06发布

问题:

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

回答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.



回答2:

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.