The Source for Java Technology Collaboration
User: Password:



Kohsuke Kawaguchi

Kohsuke Kawaguchi's Blog

XmlAdapter in JAXB RI EA

Posted by kohsuke on April 22, 2005 at 03:50 PM | Comments (8)

Many people seem to have trouble understanding XmlAdapter/XmlJavaTypeAdapter. I think it's at least partially because of the lack of documentation/samples, but it might be that there's a problem in the design, and if so that's not good.

So today, I'm going to talk about XmlAdapter/XmlJavaTypeAdapter so that people can make more informed discussions about it.

Motivation

Normally, JAXB tries to copy the in-memory representation straight to XML. If your object has fields a, b, and c, your XML will get elements like a, b, and c. This is a reasonable default, but it's also common for in-memory data representation to be vastly different from the on-the-wire representation.

A canonical example of this is a map. On XML, it's usually written like a list:


<brochure>
  <course id="cs501">
    <name>Software Engineering</name>
    ...
  </course>
  <course id="cs519">
    <name>Network Security</name>
    ...
  </course>
  ...
</brochure>

But in memory, it takes much more complex form that involves arrays, linked lists, and etc. XmlAdapter is a mechanism intended to bridge this gap.

How It Works

XmlAdapter takes two classes as type parameters. One is the class that corresponds to the XML representation. The other is the class that corresponds to the in-memory representation. The former has to be a class that's bindable by JAXB, but the latter can be any Java type. XmlAdapter defines two methods; the unmarshal method is used by unmarshallers to convert the former to the latter, and the marshal method is used by marshallers to convert the latter to the former.

For example, in the above example, suppose in memory you have:


class Brochure {
  Map<String,Course> courses;
}

class Course {
  String id;
  String name;
}

Instead of printing the map in its 'raw' form, you'd want to print them like a list, so you write the following adapters:


class CourseListAdapter extends XmlAdapter<Course[],Map<String,Course>> {
  public Map<String,Course> unmarshal( Course[] value ) {
    Map<String,Course> r = new HashMap<String,Course>();
    for( Course c : value )
      r.put(c.id,c);
    return r;
  }
  public Course[] unmarshal( Map<String,Course> value ) {
    return value.values().toArray(new Course[value.size()]);
  }
}

And then you annotate your Java class like this:

@XmlRootElement(name="brochure")
class Brochure {
  @XmlJavaTypeAdapter(CourseListAdapter.class);
  @XmlElement(name="course")
  Map courses;
}

class Course {
  @XmlAttribute
  String id;
  @XmlElement
  String name;
}

This adapter annotation makes JAXB believe that the Brochure class looks like the following:

class Brochure {
  @XmlElement(name="course")
  Course[] courses;
}

This applies to the unmarshaller, the marshaller, and the schema generator. When you run one of those like

marshaller.marshal( new Brochure(...), System.out );

..., adapters are used behind the scene to maintain this illusion for JAXB. For example, during the unmarshalling, JAXB happily unmarshals Course[], and when it think it set the value to the courses field, an adapter kicks in and it replaces it to Map<String,Course>. Similarly, inside the marshaller, when JAXB wants to get a value from the courses field, an adapter kicks in again and converts the value from a Map to Course[] before JAXB sees the value.

The equivalent of this in Java serialization would be readResolve and writeResolve.

Sharing State Between Application and Adapter

Sometimes it's convenient to maintain application states inside adapters; for example, if you have an adapter that converts string on XML into a java.lang.Class object, you might want to have a ClassLoader in an adapter.

In JAXB, this is done by allowing applications to set configured instances of XmlAdapters to the unmarshaller/marshaller. This is also an opportunity to pass in a sub-class of the declared adapter, if you so wish.

If the application doesn't provide a configured instance, JAXB will create one by calling the default constructor.

Issues

The approach illustrated so far requires that @XmlJavaTypeAdapter to be present on every reference to a map. If you have multiple Maps all over your code, it's often convenient to be able to apply an XmlAdapter globally, like "use CourseListAdapter every Map you'll see in my classes".

If this were for only marshaller and unmarshaller, this can be easily done by, say, adding those adapters to JAXBContext. But for the schema generator to work, it has to be done declaratively, meaning that the schema generator can figure out exactly what adapters apply without running any code.

Another issue is that you can't adapt the root object of the tree, because there's no place to put @XmlJavaTypeAdapter. This is a lesser issue though, because you can always invoke the adapter yourself before passing the value into JAXB.

(I think my blog is getting more and more like articles, which is probably a bad thing)

Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first)

  • Hey, nice post, thanks!

    This is the JAXB 2.0 stuff, right? Looking forward to using it. You should talk with Dan Diephouse over at xfire.codehaus.org. Xfire is doing some really interesting stuff with WebService binding. Right now they can automatically generate XmlBeans bindings for a WSDL document, but XmlBeans is so ugly no-one's using it. I know he's working on getting this working with JAXB 2.0 and it could be an interesting showcase for the technology.

    Posted by: jcarreira on April 23, 2005 at 03:07 PM

  • Thanks for the info. I'll see if he has any interest. We have tool-integration APIs and so on, which is intended for other tools like XFire.

    Posted by: kohsuke on April 24, 2005 at 08:02 AM

  • I think the problem is that this approach results in data replication, i.e. you have to maintain the same information twice, the key and the id attribute of your indexed objects.

    This is the approach taken in databases where an index is updated automatically when changing a row but the approach is rarely followed in programming maps as you have to update both yourself.

    Or can we improve the example?

    Posted by: ibusse on April 25, 2005 at 01:30 PM

  • I also didn't like the data replication, but it's easy to fix:

    public class MapAdapter extends XmlAdapter> {

    @Override
    public Map unmarshal(MapPair[] pairs) {
    Map map = new HashMap();
    for (MapPair pair : pairs) {
    map.put(pair.key, pair.value);
    }
    return map;
    }

    @Override
    public MapPair[] marshal(Map map) {
    MapPair[] pairs = new MapPair[map.size()];
    int i = 0;
    for(Map.Entry entry : map.entrySet()) {
    pairs[i++] = new MapPair(entry.getKey(), entry.getValue());
    }
    return pairs;
    }
    }


    where MapPair is just a simple helper class

    public class MapPair {
    @XmlAttribute
    public String key;
    @XmlElement
    public Object value;

    public MapPair() {} // Required for marshalling to work

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

    Posted by: michaljakob on May 19, 2005 at 03:15 AM

  • I tried the examples but failed in using a field of type Map (courses in the example) as jaxb always complains that it does not find any no-argument constructor. Of course changing it to HashMap does the trick but this will destroy the flexibility we gain by using interfaces.

    Posted by: ibusse on June 21, 2005 at 04:12 AM

  • I'm trying this but my adapter keeps getting the individual elements - not the array itself. Is there a way around this? What would cause this to happen?

    Posted by: msandoz on April 17, 2006 at 11:47 AM

  • Does this work the other way around too? Say I have an existing map-like XML data structure - can I annotate the XML schema in some way so I can get a Map of the values out of the JAXB generated classes?

    Posted by: msascha on January 23, 2007 at 12:31 AM

  • No, not yet. Nothing causes XJC to generate a map, although it's a nice feature.

    Posted by: kohsuke on January 23, 2007 at 11:47 AM





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds