Friday, February 19, 2010

Using Mule with Xstream, tweaking the Map converter

In the Mule enterprise edition, we have the option of using jdbc:transports, and oh boy! does that make our lives easier. But like every technology, you have to jump through some hoops to make technologies talk to each other. Here is a little help for those who want to use Xstream to convert a JDBC transport generated map into xml using Xstream.


Xstream in itself provides various flavours of converters, the list of which is available here  http://xstream.codehaus.org/converters.html

The Map converter that come by default does a great job of generating XML out of maps but there are a couple of constraints
1. It will only convert maps of the type HashMap, Hashtable, java.util.LinkedHashMap, sun.font.AttributeMap (Used by java.awt.Font in JDK 6).
This causes a  problem because Mule jdbc:transport uses  a customized map which doesnt conform to any of the types that XStream Map converter supports.
 
2.The output XML will look like









However , we would like to get the output as,









mainly because Mule data integrator likes the input that way.

To address the above , I created a customconverter which takes a map and iterates through it generating tags with "Key" values and node values with "Value" fields . This solution is pretty easily available on the internet and looks like this

public class CustomMapConverter implements Converter {

public boolean canConvert(Class type) {
   return type.equals(HashMap.class) ||
             type.equals(Hashtable.class) ||
             type.getName().equals("java.util.LinkedHashMap") ||
             type.getName().equals("sun.font.AttributeMap") // Used by java.awt.Font in JDK 6
           ;
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
      Map map = (Map) source;
      for (Iterator iterator = map.entrySet().iterator(); iterator.hasNext();) {
         Map.Entry entry = (Map.Entry) iterator.next();
         writer.startNode(entry.getKey().toString());
         writer.setValue(entry.getValue().toString());
         writer.endNode();
       }
}
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
   //no implementation , not needed in my code
   Map map = new HashMap();
   return map;
} .........

My standalone test class looked like
public class XStreamStandalone {

    public static void main(String args[]){
        Map map = new HashMap();
        map.put("fname","Rupa");
        map.put("lname","Majumdar");
        map.put("age","90");
        map.put("addr","345");
        map.put("phn","567");
        XStream xs = new XStream();
       xs.alias("customer", Map.class);
       xs.registerConverter(new CustomMapConverter());
      String xml = xs.toXML(map);

      System.out.println("Result of tweaked XStream toXml()");
      System.out.println(xml);
    }
}
While this worked fine on the standalaone test program, the moment I started sending my converter the map generated by the mule JDBC transport (after a select query), the converter didnt get called at all.
The two reasons why the above customization doesnt work on the maps from mule jdbc transport select query are
1. The map is of type org.apache.commons.dbutils.BasicRowProcessor$CaseInsensitiveHashMap
2. The actual payload is nested.

To address the two issues, I had to modify my customConverter as follows
public class CustomMapConverter implements Converter {

public boolean canConvert(Class type) {
  return AbstractMap.class.isAssignableFrom(type);
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
  Set entrySet = (Set)((HashMap)source).entrySet();
  Iterator itr = (Iterator)entrySet.iterator();
  while(itr.hasNext()){
   Map.Entry entry = (Entry) itr.next();
   writer.startNode(String.valueOf(entry.getKey()));
   writer.setValue(String.valueOf(entry.getValue()));
   writer.endNode();
  }
}

And now it works ...

Hope this helps ...




2 comments:

  1. Very useful information, hard to find elsewhere.

    ReplyDelete
  2. You should use String.valueOf(entry.getValue()) instead of entry.getValue().toString() to avoid NPEs. ;-)

    ReplyDelete