woensdag 26 augustus 2009

Strange javamail behaviour inside tomcat

In the series: 'weird problems you'd rather not spend your valuable time on' today I present a strange javamail/tomcat related problem and its solution.
While preparing the next version of our java web app I noticed that emails sent by our app were missing the mail subject. Moreover the message appeared to get sent as plain text instead of HTML so the message body was displaying ugly html.
While debugging everything seemed OK and the javamail API (invoked via Spring) was invoked with the correct parameters and a non-null subject.
So then I wrote a jUnit test to further isolate the problem and of course the unit test, invoking the same server-side java code as before, worked like a charm: the subject was present and the message body was interpreted as HTML.
I was now faced with a configuration problem because the exact same code was working fine from a unit test but was failing when executing from within Tomcat. After some googling I found the advice to check the classpath for duplicate or conflicting javamail implementations. I use the very handy maven command:

mvn dependency:tree

which shows the full dependency tree of your referenced libraries including implicit references, i.e. a jar required for one of my own dependencies. Then I noticed that axis-2 uses a geronimo-javamail implementation; in addition to the 'standard' javax.mail javamail. Sure enough when I excluded this implicit dependency like so:

<dependency>
<groupId>org.apache.axis2</groupId>
<artifactId>axis2-kernel</artifactId>
<version>1.4.1</version>
<exclusions>
<exclusion>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-activation_1.1_spec</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.geronimo.specs</groupId>
<artifactId>geronimo-javamail_1.4_spec</artifactId>
</exclusion>
</exclusions>
</dependency>

the mail got sent correctly.

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!