In a previous series of posts I covered how EclipseLink JAXB (MOXy) can be leveraged to create a RESTful data access service. In this post I will cover how easy it is to leverage MOXy's JSON binding on the server side to add support for JSON messages based on JAXB mappings.
UPDATE
UPDATE
MOXy now includes an implementation of MessageBodyReader/MessageBodyWriter to make it even easier to leverage MOXy's JSON binding in a JAX-RS application.
Why EclipseLink JAXB (MOXy)?
Below are some of the advantages of using MOXy as your JSON binding provider:
- Widest support for JAXB annotations among JSON binding providers.
- Support for both XML and JSON: Binding to JSON & XML - Geocode Example.
- MOXy contains extensions such as @XmlInverseReference for mapping JPA entities to JSON and XML: Part 3 - Mapping JPA entities to XML (using JAXB).
- External mapping document as an alternative to annotations: MOXy's XML Metadata in a JAX-RS Service.
CustomerService
The message types that a JAX-RS service understands is controlled using the @Produces and @Consumes annotations. In this post I have specified that all the operations now support "application/json" in addition to "application/xml". A more detailed description of this service is available in the following post: Creating a RESTful Web Service - Part 4/5.
package org.example; import java.util.List; import javax.ejb.*; import javax.persistence.*; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; @Stateless @LocalBean @Path("/customers") public class CustomerService { @PersistenceContext(unitName="CustomerService", type=PersistenceContextType.TRANSACTION) EntityManager entityManager; @POST @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public void create(Customer customer) { entityManager.persist(customer); } @GET @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Path("{id}") public Customer read(@PathParam("id") long id) { return entityManager.find(Customer.class, id); } @PUT @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) public void update(Customer customer) { entityManager.merge(customer); } @DELETE @Path("{id}") public void delete(@PathParam("id") long id) { Customer customer = read(id); if(null != customer) { entityManager.remove(customer); } } @GET @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Path("findCustomersByCity/{city}") public List<Customer> findCustomersByCity(@PathParam("city") String city) { Query query = entityManager.createNamedQuery("findCustomersByCity"); query.setParameter("city", city); return query.getResultList(); } }
CustomerJsonProvider
- There are no compile time dependencies on MOXy.
- The eclipselink.media-type property is used to enable JSON binding on the unmarshaller (line 34) and marshaller (line 55).
- The eclipselink.json.include-root property is used to indicate that the @XmlRootElement annotation should be ignored in the JSON binding (lines 35 and 56).
- When creating the JAXBContext the code first checks to see if a JAXBContext has been registered for this type (lines 70 and 71). This is useful if you want to leverage MOXy's external mapping document: MOXy's XML Metadata in a JAX-RS Service.
package org.example; import java.io.*; import java.lang.annotation.Annotation; import java.lang.reflect.*; import java.util.Map; import javax.xml.transform.stream.StreamSource; import javax.ws.rs.*; import javax.ws.rs.core.*; import javax.ws.rs.ext.*; import javax.xml.bind.*; @Provider @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public class CustomerJsonProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object>{ private static final String CHARSET = "charset"; @Context protected Providers providers; public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return Customer.class == getDomainClass(genericType); } public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream) throws IOException, WebApplicationException { try { Class<?> domainClass = getDomainClass(genericType); Unmarshaller u = getJAXBContext(domainClass, mediaType).createUnmarshaller(); u.setProperty("eclipselink.media-type", MediaType.APPLICATION_JSON); u.setProperty("eclipselink.json.include-root", false); StreamSource jsonSource; Map<String, String> mediaTypeParameters = mediaType.getParameters(); if(mediaTypeParameters.containsKey(CHARSET)) { String charSet = mediaTypeParameters.get(CHARSET); Reader entityReader = new InputStreamReader(entityStream, charSet); jsonSource = new StreamSource(entityReader); } else { jsonSource = new StreamSource(entityStream); } return u.unmarshal(jsonSource, domainClass).getValue(); } catch(JAXBException jaxbException) { throw new WebApplicationException(jaxbException); } } public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return isReadable(type, genericType, annotations, mediaType); } public void writeTo(Object object, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, Object> httpHeaders, OutputStream entityStream) throws IOException, WebApplicationException { try { Class<?> domainClass = getDomainClass(genericType); Marshaller m = getJAXBContext(domainClass, mediaType).createMarshaller(); m.setProperty("eclipselink.media-type", MediaType.APPLICATION_JSON); m.setProperty("eclipselink.json.include-root", false); Map<String, String> mediaTypeParameters = mediaType.getParameters(); if(mediaTypeParameters.containsKey(CHARSET)) { String charSet = mediaTypeParameters.get(CHARSET); m.setProperty(Marshaller.JAXB_ENCODING, charSet); } m.marshal(object, entityStream); } catch(JAXBException jaxbException) { throw new WebApplicationException(jaxbException); } } public long getSize(Object t, Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) { return -1; } private JAXBContext getJAXBContext(Class<?> type, MediaType mediaType) throws JAXBException { ContextResolver<JAXBContext> resolver = providers.getContextResolver(JAXBContext.class, mediaType); JAXBContext jaxbContext; if(null == resolver || null == (jaxbContext = resolver.getContext(type))) { return JAXBContext.newInstance(type); } else { return jaxbContext; } } private Class<?> getDomainClass(Type genericType) { if(genericType instanceof Class) { return (Class<?>) genericType; } else if(genericType instanceof ParameterizedType) { return (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0]; } else { return null; } } }
Server Setup
As of version 4 GlassFish contains everything you need. If you are using a previous GlassFish release as your application server then you need to replace the following EclipseLink bundles with their counterpart from an EclipseLink 2.4 (or newer) install.
- org.eclipse.persistence.antlr.jar
- org.eclipse.persistence.asm.jar
- org.eclipse.persistence.core.jar
- org.eclipse.persistence.core.jpql.jar
- org.eclipse.persistence.jpa.jar
- org.eclipse.persistence.jpa.jpql.jar
- org.eclipse.persistence.jpa-modelgen.jar
- org.eclipse.persistence.moxy.jar
- org.eclipse.persistence.oracle.jar
Further Reading
If you enjoyed this post then you may also be interested in:
- RESTful Services
- MOXy as Your JAX-RS JSON Provider - MOXyJsonProvider
- MOXy as Your JAX-RS JSON Provider - Client Side
- Creating a RESTful Service
- Part 2 - Mapping the Database to JPA Entities
- MOXy's XML Metadata in a JAX-RS Service
- JSON Binding
- Application Server Integration
Will MOXy format the JSON output if Marshaller.JAXB_FORMATTED_OUTPUT is set?
ReplyDeleteHi Dmitry,
DeleteYes, MOXy will format the JSON output if the standard Marshaller.JAXB_FORMATTED_OUTPUT flag is set. For an example see:
- JSON Binding with EclipseLink MOXy - Twitter Example
-Blaise
Hi Blaise,
ReplyDeleteI tried using your example with version 2.3.2 and the setProperty method on the JAXBMrshaller doesn't have either of the properties "eclipselink.json.include-root" or "eclipselink.media-type". Is something new required for version 2.3.2?
Thanks
Hi Russ,
DeleteFor the JSON-binding you need to use EclipseLink 2.4.0. You can download a pre-release version from the following link:
- http://www.eclipse.org/eclipselink/downloads/milestones.php
-Blaise
Hi Blaise,
ReplyDeletegreat Blog. It's good to see that eclipselink is about to support JSON out of the box. I had no problems to transfer your example to my case. But there is one issue that makes me headaches already for a long time: How to marshal a collection correctly. In my case, if I do not wrap my collection with a "GenericEntity", the genericType is java.util.Vector, which is an instance of Class but not of ParameterizedType. Whereas the GenericEntity yields a ParameterizedTypeImpl. As a consequence the sun marshaller is used and not the moxy-one, which leads to an exception, when the eclipselink properties get set.
Do you have any idea how I could get rid of the GenericEntity?
Actually I wonder how it can work in your example. Does your named query return a ParameterizedType?
Hi Gregor,
DeleteYes, in my example the named query returns a ParaterizedType. Can you provide more information about the GenericEntity that you return?
-Blaise
Hi Blais,
Deletelet's assume I want to return a Vector of Customers from my Resource Implementation, as in the following example:
@Path("/customer")
public class CustomerResource {
@GET
@Path("/list")
@Produces(MediaType.APPLICATION_JSON)
public Response get() {
Vector customers = new Vector();
Customer customer = new Customer("Fridolin Fröhlich", "4711");
customers.add(customer);
return Response.ok(customers).build();
}
}
When I invoke this I get an MessageException with the message: Caused by: com.sun.jersey.api.MessageException: A message body writer for Java class java.util.Vector, and Java type class java.util.Vector, and MIME media type application/json was not found
If I encapsulate the Vector within a javax.ws.rs.core.GenericEntity everything is okay:
GenericEntity> entities = new GenericEntity>(customers) {};
return Response.ok(entities).build();
The difference is, that in the later case, with the GenericEntity, the genericType is a ParameterizedType, the getDomainClass returns the actual domain type (Customer) and the JAXBContext creates an org.eclipse.persistence.jaxb.JAXBMarshaller, whereas with the plain Vector the genricType is a Class, the getDomainClass returns the Vector class, the isReadable returns false and our readFrom method is not called at all.
As I want Moxy also to marshall and unmarshall any Collections of my domain classes I changed the getDomainClass method in the following way:
private Class getDomainClass(Object object, Type genericType) {
if (genericType instanceof ParameterizedType) {
return (Class) ((ParameterizedType) genericType)
.getActualTypeArguments()[0];
} else if (object instanceof Collection) {
Collection collection = (Collection) object;
if (collection.isEmpty()) {
return this.getClass();
} else {
return collection.toArray()[0].getClass();
}
} else if (genericType instanceof Class) {
return (Class) genericType;
} else {
return null;
}
}
This works quite well and allows me to get rid of the GenericEntity encapsulation.
So far so good. The only question that remains for me is, why the JPA query in your case returns a ParameterizedType, but in my case I get a Vector (which is what I expected). Do you have an idea?
Hi Blaise,
Deletein my last reply I mistook isReadable for isWritable and readFrom for writeTo. For the GET, of course, the writing case is relevant. Sorry!
And in addition to the change I did to the getDomainClass method I also changed the isWritable to always return true. This is not a real satisfying solution, just a workaround.
Ciao
Gregor
Hi Gregor,
DeleteIt probably isn't that my JPA query is returning something different from yours, but the return type on the findCustomersByCity method is parameterized.
-Blaise
Setting the property "eclipselink.media-type" to "application/json; charset=utf-8" yields an exception. Thus the characterset must be stripped off before setting the property.
ReplyDeleteHi Gregor,
DeleteYou are correct. I have updated the example to explicitly set the media type on the Marshaller/Unmarshaller as MediaType.APPLICATION_JSON. MOXy now includes an implementation of MessageBodyReader/MessageBodyWriter to make things even easier:
- MOXy as Your JAX-RS JSON Provider - MOXyJsonProvider
-Blaise
Hi Blaise,
DeleteIn my tests I found that I have to explicitly set the characterset when reading from the stream, otherwise the German umlauts get misinterpreted (the same with French accents, I think). So I changed the unmarshaling part in the readFrom like the following:
Reader reader;
if (mediaType.getParameters().containsKey("charset")) {
reader = new InputStreamReader(entityStream, mediaType
.getParameters().get("charset"));
} else {
reader = new InputStreamReader(entityStream);
}
return u.unmarshal(new StreamSource(reader), domainClass)
.getValue();
Ciao
Gregor
Hi Gregor,
DeleteThank you for pointing out this aspect. I have updated my example to include it.
-Blaise
Hi Blaise,
DeleteThe CustomerJSONProvider.java is working fine for single class ( return Customer.class == getDomainClass(genericType); ). Suppose if we want to apply for multiple classes (pacakge)how can we proceed.
Thanks in advance...
Kumar
Hi Suku,
DeleteI would recommend using the MOXyJsonProvider class. This way you can enable it for you entire application:
- MOXy as Your JAX-RS JSON Provider - MOXyJsonProvider
-Blaise
Question: Did MOXy already solve this problem? Null @XmlRootElement produces null instead of empty JSON I'm facing this problem with GlassFish 3.1.2.2 (embedded).
ReplyDeleteThis issue does not exist in MOXy's JSON binding. The easiest way to enable MOXy as your JSON provider is to use the MOXyJsonProvider class:
Delete- MOXy as Your JAX-RS JSON Provider - MOXyJsonProvider
Hello,
ReplyDeleteCouldn't it be possible to download the example? I'm trying to use MOXy with Spring Hibernate on Apache Tomcat!
Thanks
I currently do not have the files for this example hosted anywhere.
Delete