Friday, September 28, 2007

Annotated JAXB Classes

Over the last week or so, I've started to use JAXB along with the Restlet framework. We are actively developing RESTful web services here at Overstock.com. So being new to the Restlet framework, I was eager to get started. One type of representation supported by Restlet is of course XML. To generate XML representations we are using JAXB 2. Being an advocate of annotations, I thought I'd start with annotated POJO's and let the JAXB provider do the rest (I assumed this would be a lot like JPA). I ran into a problem however, trying to create a JAXBContext for my package, I got this error:
WARNING: Problem creating Marshaller
javax.xml.bind.JAXBException: "com.overstock" doesnt contain ObjectFactory.class or jaxb.index
It took me a while to figure out what went wrong. So now that I've got things working correctly, I thought I'd post this example and solution to hopefully save you some time. Given this class:
 1 package com.overstock;
 2
 3 import javax.xml.bind.annotation.XmlAccessType;
 4 import javax.xml.bind.annotation.XmlAccessorType;
 5 import javax.xml.bind.annotation.XmlElement;
 6 import javax.xml.bind.annotation.XmlRootElement;
 7
 8 @XmlRootElement(name="example", namespace="http://overstock.com/example")
 9 @XmlAccessorType(XmlAccessType.FIELD)
10 public class ExampleJaxbClass {
11
12   @XmlElement(required=true)
13   private String elementOne;
14   private String elementTwo;
15
16   protected ExampleJaxbClass() {
17     super();
18   }
19
20   public String getElementOne() {
21     return elementOne;
22   }
23   public void setElementOne(String elementOne) {
24     this.elementOne = elementOne;
25   }
26   public String getElementTwo() {
27     return elementTwo;
28   }
29   public void setElementTwo(String elementTwo) {
30     this.elementTwo = elementTwo;
31   }
32 }
You can easily convert it to XML via the javax.xml.bind.Marshaller class, like this:
 1 public class ExampleTest {
 2
 3   @Test
 4   public void generateXml() throws JAXBException {
 5     ExampleJaxbClass ex = new ExampleJaxbClass();
 6     ex.setElementOne("first Element Value");
 7     ex.setElementTwo("second Element Value");
 8
 9     // Get a JAXB Context for the object we created above
10     JAXBContext context = JAXBContext.newInstance(ex.getClass());
11
12     // To convert ex to XML, I need a JAXB Marshaller
13     Marshaller marshaller = context.createMarshaller();
14
15     // Make the output pretty
16     marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
17     StringWriter sw = new StringWriter();
18
19     // marshall the object to XML
20     marshaller.marshal(ex, sw);
21
22     // print it out for this example
23     System.out.println(sw.toString());
24   }
25 }
Here is the XML generated by the annotations above:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<ns2:example xmlns:ns2="http://overstock.com/example">
<elementOne>first Element Value</elementOne>
<elementTwo>second Element Value</elementTwo>
</ns2:example>
Now for the problem. To create a marshaller, you first need to create a JAXBContext via its newInstance() factory method. You can create a context for a specific JAXB class, as in the example above, or you can create a context for a list of packages (check out the javadoc page for more). When using the Restlet class JaxbRepresentation (only available in Restlet 1.1m1), it uses the package version of newInstance(), that's when I got my error above. I didn't want to create an ObjectFactory (apparently this is another way to get around the above error), at least not yet if I could help it, so I wanted to get some more info on the jaxb.index file. I couldn't find out much, I even looked at the JSR-222 spec. Well, it turns out that all you need to do is add class names to the file and place the file in the package (directory) where your JAXB annotated classes reside (it's similar in one way to a jpa persistence.xml file but without the xml). Here is the content of my jaxb.index file for the example class above:
ExampleJaxbClass
As you can see, its just the class name, not the fully qualified name (the package name is determined by the directory you placed the file in) or the .class name. If you want to test this out, we need to slightly change the test above. Modify line 10 in the unit test above to look like this:
JAXBContext context = JAXBContext.newInstance(ex.getClass().getPackage().getName());
If the package com.overstock does not have jaxb.index file, this change will cause the test to throw the JAXBException. Add the file and everything works great. If you know where there is good documentation on this let me know I couldn't find any :-)
Code formatting courtesy of Code2HTML.

21 comments:

Nino said...
This comment has been removed by the author.
Nick said...

That's a fantastic example, thanks for putting it together! Can't believe that Sun come up with these small little annoying things like jaxb.index files.

Alejandro said...

Thanks dude, it was really helpfull.

rathbone1200cc said...

helped me as well. Thanks!

ktcorby said...

Thanks, very helpful for me as well. Too bad there's not a maven plugin to generate the jaxb.index files automatically.

ktcorby said...

Here's a bit of maven pom.xml code to generate these files using apt-jelly:

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>apt-maven-plugin</artifactId>
<version>1.0-alpha-2</version>
<configuration>
<factory>net.sf.jelly.apt.freemarker.FreemarkerProcessorFactory</factory>
<fork>true</fork>
<options>
<value>template=${basedir}/src/main/freemarker/gotm-jar-apt-jelly.fmt</value>
</options>
</configuration>
<executions>
<execution>
<goals>
<goal>process</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>net.sf.apt-jelly</groupId>
<artifactId>apt-jelly</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.12</version>
</dependency>
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.6.0_06</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
</dependencies>
</plugin>

And the jaxb-index.fmt file:
<@forAllPackages var="package">
<#assign printpackage=false>
<#list package.classes as class>
<@ifHasAnnotation declaration=class annotation="javax.xml.bind.annotation.XmlRootElement">
<#assign printpackage=true>
</@ifHasAnnotation>
</#list>
<#if printpackage>
<@file package="${package.qualifiedName}" name="jaxb.index">
<@forAllTypes var="type" annotation="javax.xml.bind.annotation.XmlRootElement">
${type.qualifiedName}
</@forAllTypes>
</@file>
</#if>
</@forAllPackages>

ktcorby said...

Actually in that freemarker file ${type.qualifiedName} should be ${type.simpleName}

Chris Maki said...

Hi ktcorby

Very nice, I was thinking it would be better to just automatically generate the jaxb.index file and there was your comment.

Thanks

Chris

Pavel said...

Hi everyone,
I've run into the same problem using axis2 for web services environment.

If I try to load schema-derived classes directly with JAXBContext newInstance(Class... classesToBeBound) everything is correct.

Then I tried to create jaxb.index with only the names (without .class extension) of JAXB annotated classes in the directory org/jetel/ws/provider/def/ where org.jetel.ws.provider.def is the package name and context path used by JAXBContext newInstance(String contextPath).
This example finished with JAXBException.

Is there any solution? I'll appreciate your answers.

All the compiled code with jaxb.index is packaged in jar file and deployed by axis2 JAXWSDeployer.

Should there be any manifest header to force classloader load jaxb.index resource?

Thanks

Pavel said...

Hi there,
I've found out some new stuff. If I place org/jetel/ws/provider/def/jaxb.index on the classpath of thread's systemClassLoader the resource can be received at runtime, but the exception is thrown:
javax.xml.bind.JAXBException: error loading class "EMPLOYEEReadPortTargets" listed in org/jetel/ws/provider/def/jaxb.index, make sure that entries are accessable on CLASSPATH and of the form "ClassName" or "OuterClass.InnerClass", not "ClassName.class" or "fully.qualified.ClassName"

where EMPLOYEEReadPortTargets.class is in package org.jetel.ws.provider.def

Any idea?

Thanks

Pavel said...

It's me again,

actually I've found the solution, so for anyone who don't want to spend over 8 hours googling and looking at JIRA issues:

CHANGE THE CLASSLOADER for JAXBContext initialization:
JAXBContext jc = JAXBContext.newInstance(contextPath,this.getClass().getClassLoader());

I hope that can help someone.

Have a nice day, Pavel

Vernon said...

Excellent! Very helpful.

sunspot said...

Thanks, this helped me!

V said...

You are da man!!!
I've kill couple hours before I've found your solution! Thank you :)

gsus23 said...

Thanks for this Article ! Solved my JAXB problems and saved me lots of time !

byF said...

Saved me a lot of time! Thanks!

jpoa said...

Thank you.

Really, thank you.

Netbeans has issues running projects with the profiler if some Jaxb classes are present, this saved my day.

Going to link back to here.

adilbaber said...

Hi Chris,
I am using JAXB and RESTlet. I am getting the below error when trying to convert from xml to java.

Problem creating Unmarshaller
javax.xml.bind.JAXBException: "org.restlet.ext.jaxb" doesnt contain ObjectFactory.class or jaxb.index.

Could you please let me know how could I resolve this exception.

Guilherme said...

Guy fucking awesome this tutorial!!!

You r helping brazilians to develop too. Tks

Roger Wegner said...

If somebody is still looking for a way to generate jaxb.index files I have this for you: http://jmdablog.blogspot.de/p/sample-keep-jaxbindex-files-and-package.html

Kasav Bere said...

Do you have a very simple Restlet web service that uses JPA with Java EE6? I have been trying to learn/use the following technology to create a webservice with Dao layer: Restlet 2.0, Spring, JPA, Hibernate, Glassfish 3.x.

Thanks for any help.