Friday, August 05, 2005

xstream & jdk1.5 annotations

I wanted to use XStream for persisting a small ammount of configuration data in one of my pet projects. XStream is a java to XML object serializer. It comes very close to perfection: it's small, fast, free, and the learning courve is of aproximately 20 seconds. To use it, all we have to write is: new XStream().toXml( yourObject , new FileWriter( "your.file.name" ) ) if we want serialize an object to disk and new XStream().fromXml(new FileReader("your.file.name")) if we want to deserialize one

The only (slight) problem with it is that if we do just that, we'll probably end up with an ugly and difficult to read xml file, because XStream uses java fully qualified class names and attribute names as XML tags. In order to address this, XStream enables us to create aliases for class and attribute names. These aliases will be then used to form XML tags, resulting in much readable XML content. This feature is also very simple to use: we just invoke XStream.alias(...) and XStream.aliasField(...) before writing / loading your object. So you'd end up writing a few static utility methods that would configure your XStream object with required aliases. Problem, however, is that that's a pain to maintain: each time you update your data model (add / remove a class, add / remove a field), you will have to update the aliases. It would be really nice if the aliases for classes and fields would be stored along with the data model itself. This way we have all information related to a class in one file, we don't have to remember to go and modify anothe one. That much easier to maintain, as all the information regarding a class and its attributes is under your eyes when you open that class.

The annotations feature in JDK 1.5 enables us to do just that: be able to decorate classes and fields with whatever information we need. In this case we'll decorate them to specify xstream aliases. To achieve this I have created these four annotations:


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface xstreamAlias {
public String alias();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface xstreamAliasField {
public String alias();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface xstreamContainedType {
public Class type();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface xstreamFileName {
public String name();
}


These annotations serve each a different goal:


  • xstreamAlias can decorate classes and can be used to specify class aliases.
  • xstreamAlias can decorate fields and can be used to specify field aliases.
  • xstreamContainedType can decorate fields and can be used to specify what classes are stored in collection fields.
  • xstreamFileName can decorate classes and can be optionally used to indicate the name of the file in which instances of that class must be saved.


This is what a couple of decorated classes look like:


@xstreamFileName (name = "parties")
@xstreamAlias(alias = "parties-container")
public class PartyContainer {
@xstreamAliasField(alias = "parties")
@xstreamContainedType(type = PartyPersistent.class)
public List parties;

public PartyContainer() {
parties = new ArrayList();
}
}

@xstreamAlias(alias = "party")
public class PartyPersistent implements Serializable {
@xstreamAliasField(alias = "pk")
private String pk;

@xstreamAliasField(alias = "name")
private String name;

@xstreamAliasField(alias = "last-name")
private String lastName;

@xstreamAliasField(alias = "is-owner")
private boolean isOwner;

@xstreamAliasField(alias = "is-organization")
private boolean isOrganization;
}


And finally, this is the class that contains the static methods used to configure the XStream object:

public class XStreamPersister {
private static final Set configuredTypes = new HashSet();

public static synchronized String configureAliases(Class topLevelClass, XStream xstream) {
Class crtClass = topLevelClass;
String fileName;
xstreamFileName xstreamFileName = (xstreamFileName) crtClass.getAnnotation(xstreamFileName.class);
if (xstreamFileName != null)
fileName = xstreamFileName.name();
else
fileName = topLevelClass.getName();

configuredTypes.clear();
configureClass(crtClass, xstream);

return fileName;
}

private static synchronized void configureClass(Class crtClass, XStream xstream) {
if (configuredTypes.contains(crtClass))
return;

xstreamAlias xstreamAlias = (xstreamAlias)crtClass.getAnnotation(xstreamAlias.class);
if (xstreamAlias != null)
xstream.alias(xstreamAlias.alias(), crtClass);

configuredTypes.add(crtClass);

Field[] fields = crtClass.getDeclaredFields();
for (Field field : fields) {
xstreamAliasField xstreamAliasField = (xstreamAliasField) field.getAnnotation(xstreamAliasField.class);
if (xstreamAliasField != null)
xstream.aliasField(xstreamAliasField.alias(), crtClass, field.getName());
Class fieldType = field.getType();
if (Collection.class.isAssignableFrom(fieldType)) {
xstreamContainedType xstreamContainedType = (xstreamContainedType)field.getAnnotation(xstreamContainedType.class);
if (xstreamContainedType != null) {
Class containedClass = xstreamContainedType.type();
configureClass(containedClass, xstream);
}
} else if (!field.getType().isPrimitive()) {
configureClass(field.getType(), xstream);
}
}
}


Once you have configured you xstream object, this is an example of what you get if you serialize an instance of the PartyContainer class from up the page:


<parties-container>
<parties>
<party>
<pk>c1ab52418523b250:-3c9bdb8b:1057839414b:-7ffe</pk>
<name>aaaaaaaa</name>
<last-name>aaaaaaaaa</last-name>
<is-owner>false</is-owner>
<is-organization>false</is-organization>
</party>
<party>
<pk>c1ab52418523b250:-3c9bdb8b:1057839414b:-7ffd</pk>
<name>bbbbb</name>
<last-name>bbbbbbb</last-name>
<is-owner>false</is-owner>
<is-organization>false</is-organization>
</party>
</parties>
</parties-container>

I think we all agree that's more readable then this:


<com.thekirschners.docmanager.localimpl.datamodel.v1.PartyContainer>
<parties>
<com.thekirschners.docmanager.localimpl.datamodel.v1.PartyPersistent>
<pk>c1ab52418523b250:55c36e5f:105783da309:-7fff</pk>
<name>aaaaaaaa</name>
<lastName>aaaaaa</lastName>
<isOwner>false</isOwner>
<isOrganization>false</isOrganization>
</com.thekirschners.docmanager.localimpl.datamodel.v1.PartyPersistent>
<com.thekirschners.docmanager.localimpl.datamodel.v1.PartyPersistent>
<pk>c1ab52418523b250:55c36e5f:105783da309:-7ffe</pk>
<name>bbbbb</name>
<lastName>bbbbb</lastName>
<isOwner>false</isOwner>
<isOrganization>false</isOrganization>
</com.thekirschners.docmanager.localimpl.datamodel.v1.PartyPersistent>
</parties>
</com.thekirschners.docmanager.localimpl.datamodel.v1.PartyContainer>

Hope this post will help people make better use of xstream and also get an idea of what annotations could be used for. Since this is my second hack arround annotations (see autoui) I think everybody notices a really like this feature. I think it's one of the most usefull one in jdk 1.5 as opposed to generics which, in my opinion, are the worst implementation of a new concept we've ever seen in the history of Java.

If you know what templates can do in C++ and you expect generics to provide comparable features, just forget about it. My generics rant will be online soon :-)