See part 1 for my initial confused ramblings, and part 2 for a workaround. This third and final part will hopefully bring some clarity to both the initial problem and the solution.
- The use case
- The solution sketch
- What the documentation doesn’t tell you
- Is there a better way?
- Example code
- Help!
- The gory details
Firstly, a brief statement of how I went astray:
The use case
The requirement is to create a REST API for manipulating an existing domain model. While I do have access to the domain model source code, I’m reluctant to alter it for this specific scenario, so anything requiring annotating the domain model is out. Similarly, while it’s possible to create an intermediate model (e.g. a set of DTO’s) and annotate that, that approach has always seemed to me like excessive double handling. To my mind, XML is supposed to be the dumb DTO format. I know there are use cases which require all these layers, but I also know that most of the time YAGNI.
The solution sketch
- JAX-RS seems to be a nicely concise, annotation-driven way to create REST services on a servlet container
- Jersey is a JAX-RS implementation with some traction, and some useful integrations with various containers and format providers
- By using JAXB with Jersey, we remove the need to write the XML marshalling/unmarshalling layer
- JAXB by default is completely annotation-driven, but Moxy is a JAXB implementation that can be configured via an external mapping document. Happily, Jersey ships with Moxy integration.
- Finally, Moxy gives us JSON capability for free.
So that was the plan. Implement that stack of frameworks, create a mapping file, and serve up both XML and JSON based on the one unannotated domain model. There are lots of examples around to help get started:
- Jersey + Moxy + JSON
- Jersy + JAXB (although uses an older version of Jersey)
- Moxy with an external mapping file
plus many more linked from the above. There’s no example I could find that reproduces all the elements of my solution sketch – Jersey + JAXB + Moxy + external mapping file + unannotated file – but surely there’s enough to get started.
What the documentation doesn’t tell you
Spot the flaw in my thinking:
- Jersey integrates with Moxy out of the box – TRUE
- Moxy supports the use of an external mapping file – TRUE
- When you use Moxy with the external mapping, you don’t need any annotations on the model – TRUE
- The Jersey docs even show you how to configure Moxy with a mapping file – TRUE
- Therefore, you can use Jersey + Moxy without annotations on the model – FALSE
Stumbled at the last hurdle. To add to the bafflement, this all works flawlessly for JSON marshalling, but fails for XML. I’ll go into more detail below as to why this is so, but for now, here’s what you need to know:
Jersey’s Moxy integration does not expose all of Moxy’s functionality for the XML case. There is still some of the standard annotation-driven JAXB code in the mix, and to get past that your model must be annotated at least with @XmlRootElement.
Once you’ve satisified Jersey’s default JAXB provider by adding that one annotation, Jersey will happily hand you over to Moxy and all the external mapping goodness works just fine.
Is there a better way?
But wait, I started off by saying I didn’t want to annotate my model. If you really don’t want to or can’t annotate the model, there is another way. You need to register your own provider. This example gives the basic idea, although I needed to tweak it a bit to get it to work (more on that below). This is in fact what Jersey’s Moxy code does for the JSON case, which is why that works and the XML case doesn’t.
Example code
Most of this example code is identical to all the other examples out there, but I’ll collect it here as the full description of what finally worked for me. This is using Jersey 2.9 and EclipseLink Moxy 2.5 on Java 7. This code can also be found on bitbucket.
web.xml
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0"> <display-name>test</display-name> <servlet> <servlet-name>Jersey REST Service</servlet-name> <servlet-class>org.glassfish.jersey.servlet.ServletContainer</servlet-class> <init-param> <param-name>jersey.config.server.provider.packages</param-name> <param-value>testing</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Jersey REST Service</servlet-name> <url-pattern>/rest/*</url-pattern> </servlet-mapping> </web-app>
JAX-RS service definition
Without the custom provider described further down, this code works for the JSON case (/json) and the two endpoints that use the annotated model (/json2 and /xml2), but fails for the unannotated XML case (/xml).
package testing; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("/services") public class Services { @GET @Path("/json") @Produces(MediaType.APPLICATION_JSON) public Planets serviceListJSON () { return new Planets(); } @GET @Path("/xml") @Produces(MediaType.APPLICATION_XML) public Planets serviceListXML () { return new Planets(); } @GET @Path("/json2") @Produces(MediaType.APPLICATION_JSON) public PlanetsAnnotated serviceListJSON2 () { return new PlanetsAnnotated(); } @GET @Path("/xml2") @Produces(MediaType.APPLICATION_XML) public PlanetsAnnotated serviceListXML2 () { return new PlanetsAnnotated(); } }
Model classes
package testing; public class Planets { private int id = 1; private String name = "test"; private double radius = 3.0; }
package testing; import javax.xml.bind.annotation.XmlRootElement; @XmlRootElement public class PlanetsAnnotated { private int id = 1; private String name = "test"; private double radius = 3.0; }
Mapping file
<?xml version="1.0"?> <xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm" package-name="testing" xml-mapping-metadata-complete="true" xml-accessor-type="NONE"> <java-types> <java-type name="Planets"> <xml-root-element/> <java-attributes> <xml-element java-attribute="id"/> <xml-element java-attribute="name"/> <xml-element java-attribute="radius"/> </java-attributes> </java-type> <java-type name="PlanetsAnnotated"> <xml-root-element/> <java-attributes> <xml-element java-attribute="id"/> <xml-element java-attribute="name"/> <xml-element java-attribute="radius"/> </java-attributes> </java-type> </java-types> </xml-bindings>
jaxb.properties
javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory
Context resolver
This is one of several ways to give Moxy the mapping file.
package testing; import java.util.HashMap; import java.util.Map; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import org.eclipse.persistence.jaxb.JAXBContextProperties; @Provider @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public class MyMoxyContextResolver implements ContextResolver<JAXBContext> { private JAXBContext context = null; public JAXBContext getContext(Class<?> type) { System.out.println("Invoking MyMoxyContextResolver.getContext"); if (context == null) { try { Map<String, Object> properties = new HashMap<String, Object>(1); properties.put(JAXBContextProperties.OXM_METADATA_SOURCE, "testing/planets.oxm.xml"); context = JAXBContext.newInstance(new Class[] {Planets.class}, properties); } catch(JAXBException e) { throw new RuntimeException(e); } } return context; } }
Custom provider
This is the part that makes the XML case finally work without annotations. For a full solution this would implement MessageBodyReader as well.
package testing; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import javax.ws.rs.Produces; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.MessageBodyWriter; import javax.ws.rs.ext.Provider; import javax.ws.rs.ext.Providers; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; @Provider @Produces("application/xml") public class MyMessageBodyWriter implements MessageBodyWriter<Object> { @Context protected Providers providers; @Override public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return true; } @Override public long getSize(Object les, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return -1; } @Override public void writeTo(Object les, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { try { ContextResolver<JAXBContext> resolver = providers.getContextResolver(JAXBContext.class, mediaType); JAXBContext context = resolver.getContext(type); context.createMarshaller().marshal(les, entityStream); } catch (JAXBException e) { e.printStackTrace(); } } }
Source code layout
This is my example structure for a gradle project.
src/ main/ java/ jaxb.properties MyMessageBodyWriter.java MyMoxyContextResolver.java Planets.java planets.oxm.xml PlanetsAnnotated.java Services.java webapp/ WEB-INF/ web.xml
Help!
This is a super-simple proof-of-concept. No doubt it has all sorts of sub-optimal choices, and for all I know breaks all sorts of other use cases while solving mine. However, as far as I can tell there is no other example out there for this specific scenario. It would be great if this could be evolved into something more robust, so please let me know if you have any suggestions.
The gory details
This is a sketch of what’s happening in the innards of Jersey when using JAXB to marshal the results of a service call. I’m including this as background to any future discussion on what a supported solution might look like. For example, maybe MoxyXmlFeature should register a provider that relaxes the requirement for @XmlRootAnnotation.
At startup time:
- Jersey registers built-in providers
- Features (discovered on the classpath) register providers and context resolvers
- The jaxb.properties file defines the JAXBContextFactory to be used
At request processing time:
- The annotated JAX-RS service method returns a Java object
- Jersey’s interceptor fires (triggered by the @Produces annotation)
- Jersey searches its list of providers based on the class of the returned Java object and on the media type required
- The chosen provider searches for a context resolver that is configured to provide a context for the Java class
- The chosen context resolver obtains and configures a JAXBContext and returns it to the provider
- The JAXBContext returns a marshaller
- The provider invokes the marshaller, which emits the marshalled representation of the Java object
Each provider has a list of rules defining what it will handle. For example, there’s a built-in XmlRootElementProvider, which will only handle classes annotated with @XmlRootElement for media type application/xml. If no matching provider can be found, an exception is thrown.
So, on to Moxy. The jersey-media-moxy module, which ships with Jersey and provides Moxy integration, registers two features – MoxyJsonFeature and MoxyXmlFeature.
MoxyJsonFeature registers a provider, ConfigurableMoxyJsonProvider, which will handle marshalling any object where the media type is application/json. You can then use either the MoxyJsonConfig method or register your own ContextResolver to configure a mapping file if so desired.
MoxyXmlFeature, on the other hand, does not register a provider. For annotated models, one of Jersey’s built-in providers will work just fine, but for non-annotated models that means that no provider will be found and an exception will be thrown. MoxyXmlFeature does register a context resolver, thus providing a mechanism for configuring a mapping file, but without any annotations to get you past the Jersey provider that context resolver will never be invoked. Hence the need to create our own provider.