maandag 24 augustus 2009

XStream and annotations: the nasty details

Recently a colleague introduced XStream into our project. The use case was to read an XML dump of a database, parse it into an object graph and use these objects to transfer the content to another database. XStream was just the right package to easily convert an XML stream into objects without having to resort to JAXB or other complex solutions. However when I tried to adapt the code to support a similar scenario I bumped into a few unexpected problems even though the documentation of this project XStream is actually pretty good.

The first step to start using XStream is to include a reference to it in the maven pom file:

<dependency>
<groupid>com.thoughtworks.xstream</groupid>
<artifactid>xstream</artifact>
<version>1.3</version>
</dependency>

The idea of XStream is to annotate your domain classes with xstream annotations which map the attributes and references to XML. In our case, we were going in the other direction: from XML to objects and in that case, if the structure of the XML is reasonably small it makes sense to define the objects inside one 'mother' class. For example say we have an XML:

<?xml version="1.0" encoding="UTF-8"?>
<records>
<book id="13">
<author>Robert C. Martin</author>
<title>Clean Code</title>
</book>
</records>

This datastructure can be mapped by the following (inner) classes like so (omitting the imports)

public class XStreamDemonstrator {

public static void main(String[] args) throws Exception {

XStream stream = new XStream();
stream.processAnnotations(Records.class);

FileInputStream is = new FileInputStream("resources/books.xml");
InputStreamReader isr = new InputStreamReader(is, "UTF-8");

Records records = (Records) stream.fromXML(isr);

for (Book book : records.books) {

System.out.println("Book " + book.id + ": " + book.title + " by " + book.author);

}
}

@XStreamAlias("records")
public static class Records {

@XStreamImplicit
List<Book> books;
}

@XStreamAlias("book")
public static class Book {
@XStreamAlias("id")
@XStreamAsAttribute
String id;

String author;

String title;
}
}

Now how's that for simplicity? It's fast, easy and it works. That's right; but now suppose the book gets translated and we'd like to add an extra attribute to the XML:

<?xml version="1.0" encoding="UTF-8"?>
<records>
<book id="13">
<author>Robert C. Martin</author>
<title language="en">Clean Code</title>
</book>
</records>

The problem lies in the added attribute 'language'. There is no way to map this with XStream annotations as is, this is also confirmed in the newsgroup. The solution is to use a Converter implementation class which reads out both the attribute and the child text at once:

@XStreamAlias("book")
public static class Book {
@XStreamAlias("id")
@XStreamAsAttribute
String id;

String author;

@XStreamConverter(TitleLanguageConverter.class)
TitleLanguage title;
}

public static class TitleLanguage {
String title;
String language;
public TitleLanguage(String title, String language) {
this.title = title;
this.language = language;
}
}

public static class TitleLanguageConverter implements Converter {
public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
return new TitleLanguage(reader.getValue(), reader.getAttribute("language"));
}
public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) {
// not implemented
}
public boolean canConvert(Class type) {
return type.equals(TitleLanguage.class);
}
}

That wasn't too bad...that is...there is one small problem with the above code because it throws the following stack trace upon execution:

Exception in thread "main" com.thoughtworks.xstream.converters.ConversionException: only START_TAG can have attributes END_TAG seen ...Robert C. Martin... @4:43 : only START_TAG can have attributes END_TAG seen ...Robert C. Martin... @4:43
---- Debugging information ----
message : only START_TAG can have attributes END_TAG seen ...Robert C. Martin... @4:43
cause-exception : java.lang.IndexOutOfBoundsException
cause-message : only START_TAG can have attributes END_TAG seen ...Robert C. Martin... @4:43
class : XStreamDemonstrator$Records
required-type : XStreamDemonstrator$TitleLanguage
path : /records/book/author
line number : 4
-------------------------------
at com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:88)
at com.thoughtworks.xstream.core.AbstractReferenceUnmarshaller.convert(AbstractReferenceUnmarshaller.java:55)
at com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:75)
at com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter.unmarshallField(AbstractReflectionConverter.java:234)
at com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter.doUnmarshal(AbstractReflectionConverter.java:206)
at com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter.unmarshal(AbstractReflectionConverter.java:150)
at com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:81)
at com.thoughtworks.xstream.core.AbstractReferenceUnmarshaller.convert(AbstractReferenceUnmarshaller.java:55)
at com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:75)
at com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:59)
at com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter.doUnmarshal(AbstractReflectionConverter.java:213)
at com.thoughtworks.xstream.converters.reflection.AbstractReflectionConverter.unmarshal(AbstractReflectionConverter.java:150)
at com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:81)
at com.thoughtworks.xstream.core.AbstractReferenceUnmarshaller.convert(AbstractReferenceUnmarshaller.java:55)
at com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:75)
at com.thoughtworks.xstream.core.TreeUnmarshaller.convertAnother(TreeUnmarshaller.java:59)
at com.thoughtworks.xstream.core.TreeUnmarshaller.start(TreeUnmarshaller.java:142)
at com.thoughtworks.xstream.core.AbstractTreeMarshallingStrategy.unmarshal(AbstractTreeMarshallingStrategy.java:33)
at com.thoughtworks.xstream.XStream.unmarshal(XStream.java:931)
at com.thoughtworks.xstream.XStream.unmarshal(XStream.java:917)
at com.thoughtworks.xstream.XStream.fromXML(XStream.java:861)
at XStreamDemonstrator.main(XStreamDemonstrator.java:30)
Caused by: java.lang.IndexOutOfBoundsException: only START_TAG can have attributes END_TAG seen ...Robert C. Martin... @4:43
at org.xmlpull.mxp1.MXParser.getAttributeValue(MXParser.java:927)
at com.thoughtworks.xstream.io.xml.XppReader.getAttribute(XppReader.java:93)
at com.thoughtworks.xstream.io.ReaderWrapper.getAttribute(ReaderWrapper.java:52)
at XStreamDemonstrator$TitleLanguageConverter.unmarshal(XStreamDemonstrator.java:65)
at com.thoughtworks.xstream.core.TreeUnmarshaller.convert(TreeUnmarshaller.java:81)
... 21 more

It turns out that the order is important in TitleLanguageConverter.unmarshal: when we first read the book title text, the XML cursor has already moved past the language attribute and cannot go back to read it. The solution is to reverse the order:

return new TitleLanguage(reader.getAttribute("language"), reader.getValue());

And everything works as expected!

Geen opmerkingen: