Monday, September 11, 2006

Using Annotations to Build Object Representations

One of my co-workers came to me with a familiar problem. He had class that was being rendered as XML. The class itself could have different renderings, so the XML creation was done using a classic builder pattern. This was good, but he still fretted about how his class would probably change (new features, imagine that) and he would have to change the XML builders so they would output the new properties.

It's a classic model-view coupling problem. I had suggested the builder architecture, in part because I remembered a classic article by Holub on this kind of problem. I started thinking and came up with a possible solution. This solution may have its own design flaws, but it was interesting.

My idea was to use Java's annotations to decide what parts of an object need to be present in its representation. Thus the annotation was a clue to the builder saying "show me." A reusable builder could this be built to simply obey the hints provided by the object it was building a representation for. In this case, everything was XML.

So I built a simple annotation class:


import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;

@Documented
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface XmlView {
XmlViewType value();
}

I created this XmlViewType as an Enum. Basically there could be one value in the Enum for each representation needed. Actually value() should really return an array of XmlViewTypes, since you will often want to show the same data in different representations. Or for maximum reusability, just have it return an array of strings. You can pass in the value to match against later.

Now to create a reusable builder.




import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Type;

import org.apache.commons.lang.StringUtils;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;

public class AnnotationXmlBuilder{

public static Element buildXml(Object obj, XmlViewType type){
String objName = obj.getClass().getSimpleName();
return buildXml(obj, objName, type);
}
public static Element buildXml(Object object, String elementName, XmlViewType type){

if (isPrimitive(object.getClass())){
Element e = DocumentHelper.createElement(elementName);
e.addText(String.valueOf(object));
return e;
} else if (object instanceof Iterable){
Iterable iter = (Iterable) object;
String pluralName = "";
String singularName = "";
if (elementName.endsWith("s")){
pluralName = elementName;
singularName = elementName.substring(0, elementName.length()-1);
} else {
pluralName = elementName + 's';
singularName = elementName;
}
Element e = DocumentHelper.createElement(pluralName);
for (Object obj : iter){
e.add(buildXml(obj, singularName, type));
}
return e;
} else if (object.getClass().isArray()){
String pluralName = "";
String singularName = "";
if (elementName.endsWith("s")){
pluralName = elementName;
singularName = elementName.substring(0, elementName.length()-1);
} else {
pluralName = elementName + 's';
singularName = elementName;
}
Element e = DocumentHelper.createElement(pluralName);
int length = Array.getLength(object);
for (int i=0;i<length;i++){
Object obj = Array.get(object, i);
e.add(buildXml(obj, singularName, type));
}
return e;
} else {
// go through its methods
Element e = DocumentHelper.createElement(elementName);
e.addAttribute("kind", object.getClass().getSimpleName());
for (Method method : object.getClass().getMethods()){
XmlViewType xvt = getAnnotation(method);
boolean showMethod = ((type == null) ||
(xvt != null && xvt.equals(type))) &&
isProperty(method);
if (showMethod){
String propertyName = StringUtils.uncapitalize(method.getName().substring(3));
try {
Object val = method.invoke(object, (Object[])null);
e.add(buildXml(val, propertyName, null));
} catch (Exception exc) {
// eat it for now
}
}
}
return e;
}
}

private static boolean isPrimitive(Type type){
return (type.equals(String.class)) || (type.equals(Integer.class)) || (type.equals(Long.class))
|| (type.equals(Double.class)) || (type.equals(Float.class)) || (type.equals(Byte.class)) ||
(type.equals(Boolean.class)) || (type.equals(Short.class)) || (type.equals(Character.class))
|| (type.equals(Character[].class));
}

private static XmlViewType getAnnotation(Method method){
if (!method.isAnnotationPresent(XmlView.class)){
return null;
}
return method.getAnnotation(XmlView.class).value();
}

private static boolean isProperty(Method method){
return method.getName().startsWith("get") && !method.getName().startsWith("getClass");
}
}

This class returns a dom4j Element, but one could easily switch XML technologies for this. It would not be too hard to do it with just pure DOM. Anyways, what does this code do? It takes in a class and view type. It iterates over all methods in the class, looking for getters. If it finds a getter, then it looks to see if it has been annotated with the XmlView annotation. If so, it compares the value to the value passed into the method. If they match, then it uses the name of the property and invokes the getter. If the return type is a primitive or a String, then it crates a corresponding node for it. If it is a list or array, it creates a surrounding element, then creates a child element for each item in the array or list. If the return type is some other kind of object, then it descends the object graph using recursion. So if your class had a getFirstName method and an instance of this class happened to return "Michael" for this, then you would get something like <FirstName>Michael</FirstName> in your XML. If you object had a getCar() method that returned a Vehicel object, and Vehicle had a getMake() method you might get something like: <car><make>Volkswagen</make></car> Pretty straightforward.


No comments: