Talking about my stuff

May 18, 2010

Serializing and Deserializing Groovy Beans with XML

Filed under: Uncategorized — agoodspeed @ 3:22 pm

I was looking for a way to serialize and deserialize some Groovy Beans to/from XML in a groovy way. Using a MarkupBuilder seemed like the groovy way to go, but it turned out to be far more painful than using a Java library – XStream in my implementation – to support this capability. This post recounts my effort.

This is the interface I wanted  to support.

public interface Xmlable {
  String toXml() {}
  Xmlable fromXml(String xml) {}
}

toXml should produce the XML form for the object, and fromXml should return a populated object such that:

assertEquals xmlable.toXml(), new Xmlable().fromXml(xmlable.toXml()).toXml()

I ended up with a fairly involved (and incomplete) abstract class supporting this interface for use in the targetted Groovy Beans.

public abstract class XmlValue implements Xmlable {
  String toXml() {
    return new StreamingMarkupBuilder().bind(getValue())
  }

  Xmlable fromXml(String xml) {
    return fromXml(new XmlParser().parseText(xml))
  }

  protected abstract Closure getValue() {}

  protected abstract Xmlable fromXml(Node node) {}

  protected Closure add(element) {
    if (getProperty(element) == null)
      return {}
    if (element.size() > 1)
      return {
        "${element[0].toUpperCase()}${element[1..-1]}"(getProperty(element))
      }
    return {"${element[0].toUpperCase()}"(getProperty(element))}
  }

  protected String addBean(Xmlable bean) {
    if (bean == null)
      return ''
    return bean.toXml()
  }

  protected static Xmlable asXmlable(NodeList list, Xmlable bean) {
    if (list == null || list.size() == 0) {
      return null
    }
    assert list.size() == 1
    assert bean != null
    return bean.fromXml(list[0])
  }

  protected static Xmlable asXmlable(Node node, Xmlable bean) {
    return asXmlable((NodeList) node.value(), bean)
  }

  protected static List asXmlableList(NodeList list, Xmlable bean) {
    def returnList = []
    if (list?.size()) {
      Xmlable x
      list.each {xmlable ->
        x = asXmlable(xmlable, bean)
        if (x)
          returnList << x
      }
    }
    return returnList
  }

  protected static Integer asInteger(NodeList attribute) {
    if (attribute == null || attribute.size() == 0) {
      return null
    }
    return new Integer("0${attribute.text()}")
  }

  protected static Short asShort(NodeList attribute) {
    if (attribute == null || attribute.size() == 0) {
      return null
    }
    return new Short("0${attribute.text()}")
  }

  protected static String asString(NodeList attribute) {
    if (attribute == null || attribute.size() == 0) {
      return null
    }
    return attribute.text()
  }

  protected static Date asDate(NodeList attribute) {
    if (attribute == null || attribute.size() == 0) {
      return null
    }
    return DateUtil.toDate(attribute.text(), 'EEE MMM d HH:mm:ss z yyyy')
  }
}

This works reasonably well, but it requires adding methods for deserializing whatever types get serialized into the body of the elements – Integer, Short, String, and Date so for in the above. I also wanted to differentiate between a null value (no element) and an empty element (like for an empty String). An implementation might look like this.

public class SomeValue extends XmlValue {
  Integer someInteger
  String someString
  Date someDate
  Short someShort

  SecondValue someXmlValue
  List<ThirdValue> someXmlValueList

  SomeValue() {}

  protected Closure getValue() {
    return {
      SomeValue() {
        unescaped << add('someInteger')
        unescaped << add('someString')
        unescaped << add('someDate')
        unescaped << add('someShort')
        unescaped << addBean(someXmlValue)
        SomeXmlValueList() {
          someXmlValueList?.each() {listValue ->
            unescaped << addBean(listValue)
          }
        }
      }
    }
  }

  protected Xmlable fromXml(Node node) {
    someInteger = asInteger(node.SomeInteger)
    someString = asString(node.SomeString)
    someDate = asDate(node.SomeDate)
    someShort = asShort(node.SomeShort)
    someXmlValue = asXmlable(node.SomeXmlValue, new SecondValue())
    someXmlValueList = asXmlableList(node.SomeXmlValueList, new ThirdValue())
    return this
  }
}

Beyond the sheer bulk of the implementation, disadvantages include the difficulty of applying it to an existing Groovy Bean that already extends some base class.

XStream handles most of the troublesome aspects of this job much more agreeably. The implementation becomes:

String toXml() {
  XStream xstream = new XStream()
  return xstream.toXML(this)
}

Xmlable fromXml(String xml) {
  XStream xstream = new XStream()
  return xstream.fromXML(xml)
}

The toXml implementation creates a pretty-printed String, but that is easily overcome by using a CompactWriter and the marshal method.

String toXml() {
  XStream xstream = new XStream()
  StringWriter sw = new StringWriter()
  xstream.marshal(this, new CompactWriter(sw))
  return sw.toString()
}

Last, you might want to impose your own mapping rules. My desire was to have element names as capitalized versions of the property such that the “someInteger” property above would be serialized to the “SomeInteger” element. Here is an example of a custom Mapper.

public class SomeMapper extends MapperWrapper {

  public SomeMapper(Mapper wrapped) {
    super(wrapped)
  }

  public String serializedMember(Class type, String memberName) {
    if (memberName.startsWith("_")) {
      // _blah -> blah
      assert memberName > 1
      memberName = memberName[1..-1]
    }
    memberName = memberName[0].toUpperCase() +
            (memberName.size() > 1 ? memberName[1..-1] : '')
    return super.serializedMember(type, memberName)
  }

  public String realMember(Class type, String serialized) {
    String fieldName = super.realMember(type, serialized);
    fieldName = fieldName[0].toLowerCase() +
            (fieldName.size() > 1 ? fieldName[1..-1] : '')
    // Luckily the CachingMapper will ensure this is only ever
    // called once per field per class.
    if (isDeclaredField(type, "_${fieldName}")) {
      fieldName = "_" + fieldName
    }
    return fieldName
  }

  private static boolean isDeclaredField(Class type, String fieldName) {
    // Not very efficient or elegant.
    try {
      type.getDeclaredField(fieldName)
      return true
    } catch (NoSuchFieldException e) {
      return false
    }
  }
}

To make use of it you now need to introduce your own XStream implementation, as below, and instantiate it instead of XStream.

public class SomeXStream extends XStream {
  protected MapperWrapper wrapMapper(MapperWrapper next) {
    return new SomeMapper(next)
  }
}

One frustration that I had, and did not find a palatable solution to given time constraints, was the ability to serialize a class as an unqualified (no package prepended) element and then to deserialize it naturally. My interface should support this since the fromXml method is not static and must therefore have an instance of the target class supplied and available for unmarshalling the XML into. I could not find a convenient hook into the code for accomplishing this, so I am making do with the element name of the outermost class being qualified by the package. Overriding methods in the custom Mapper is where something like this would be done.

public String serializedClass(Class type) {
  return type.getName().replaceFirst(".*\\.", "")
}

public Class realClass(String elementName) {
  try {
    if (elementName.charAt(0) != '[') {
      return classLoader.loadClass(elementName);
    } else if (elementName.endsWith(";")) {
      return Class.forName(elementName.toString(), false, classLoader);
    } else {
      return Class.forName(elementName.toString());
    }
  } catch (ClassNotFoundException e) {
    throw new CannotResolveClassException(elementName + " : " + e.getMessage());
  }
}

Advertisement

Leave a Comment »

No comments yet.

RSS feed for comments on this post. TrackBack URI

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Theme: Rubric. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.