Monday, July 16, 2012

JAXB XmlAdapter – Customized Marshaling and Unmarshaling

JAXB (Java Architecture for XML Binding), is an easy to use and efficient framework for working with XML documents in Java. A developer can easily convert an XML into Java objects (Unmarshaling) and vice versa (Marshaling) using JAXB. Although simple Java types (String, int) can be directly maps to XML types, sometimes a customized representation is required. For e.g. consider an XML snippet below

<config>
   <properties>
      <property name="name 1" value="value 1"/>
      <property name="name 2" value="value 2"/>
   </properties>
</config>

By default JAXB will bind it to the following Java object structure

@XmlRootElement
public class Config {
       @XmlElement (name="properties" type=Properties.class)     
        public Properties properties;
       …….
}

@XmlType
public class Properties {
        @XmlElement
       public List property;
       …………
}

@XmlType
public class Property {
      @XmlAttribute
       public String name;
       @XmlAttribute
       public String value;
       ……….
}

But imagine a scenario where you want the domain object to look like this

public class Config {
      public HashMap properties; //Map of property name and value
}

This is a typical case where you want to read an XML configuration file and maintain the properties in a Map.

To achieve this, you need a mechanism to intercept the default marshaling and unmarshaling of JAXB. And this can be done using XMLAdapter.

JAXB provides interceptors, in the form of XMLAdapters, to customize the marshaling and unmarshaling during binding. The below screen explains the concept.

Let’s consider the above example and see how can we achieve the expected output using adapters.

First of all, note that XMLAdapter is a customization on the top of default binding and hence the default classes, mentioned above, i.e. Config, Properties and Property need to be there. Now write a custom adapter. Customer adapters are provided with 2 parameters, one which JAXB will bind and one which you need as output of binding. Within adapter, you need to implement the logic of converting one to another. Here is how our custom adapter would look like.

public class CustomAdapter extends XmlAdapter> {
       @Override
       public Properties marshal(HashMap map) throws Exception {
              Iterator> itr = map.entrySet().iterator();
              ArrayList list = new ArrayList(map.entrySet().size());
              while (itr.hasNext()) {
                     Entry entry = itr.next();
                     Property property = new Property();
                     property.setName(entry.getKey());
                     property.setValue(entry.getValue());
                     list.add(property);
              }
              Poperties properties = new Properties();
              properties.setProperties(list);
              return properties;
       }
       @Override
       public HashMap unmarshal(Properties properties) throws Exception {      
              List list = properties.getProperties();      
              HashMap map = new HashMap(list.size());
              for (Property property : list) {               
                  map.put(property.getName(), property.getValue());
              }
              return map;
       }
}

Along with this, now we need to change the root element class a bit, since we want the Hashmap as the value holder now.
@XmlRootElement
public class Config {      
       @XmlJavaTypeAdapter (CustomAdapter.class)
       @XmlElement (name="properties", type=Properties.class)
       private HashMap properties;
       ………………
}

Now write a method to test the unmarshaling.

public void testUnmarshal() {
       try {
              JAXBContext context = JAXBContext.newInstance(Config.class);
              Config config = (Config) context.createUnmarshaller().unmarshal(new File("config.xml"));
              Iterator> itr = config.getProperties().entrySet().iterator();
              while (itr.hasNext()) {
                     Entry entry = itr.next();
                     System.out.println(entry.getKey() + "  " + entry.getValue());
              }                   
       } catch (JAXBException e) {
              e.printStackTrace();
       }
}

The output of the above test method would be
name 1   value 1
name 2   value 2


Conclusion: JAXB provides XMLAdapters as a way to the developer to intercept the marshaling and unmarshaling process and write code to custom mappings.

No comments: