March 13, 2012

MOXy as Your JAX-RS JSON Provider - Server Side

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
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:

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

We will implement a JAX-RS MessageBodyReader/MessageBodyWriter to plugin support for MOXy's JSON binding.  This implementation could easily be adapted to enable JSON binding for any JAX-RS service using MOXy as the JAXB provider.  Some interesting items to note:
  • 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:

19 comments:

  1. Will MOXy format the JSON output if Marshaller.JAXB_FORMATTED_OUTPUT is set?

    ReplyDelete
    Replies
    1. Hi Dmitry,

      Yes, 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

      Delete
  2. Hi Blaise,

    I 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

    ReplyDelete
    Replies
    1. Hi Russ,

      For 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

      Delete
  3. Hi Blaise,
    great 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?

    ReplyDelete
    Replies
    1. Hi Gregor,

      Yes, in my example the named query returns a ParaterizedType. Can you provide more information about the GenericEntity that you return?

      -Blaise

      Delete
    2. Hi Blais,
      let'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?

      Delete
    3. Hi Blaise,
      in 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

      Delete
    4. Hi Gregor,

      It probably isn't that my JPA query is returning something different from yours, but the return type on the findCustomersByCity method is parameterized.

      -Blaise

      Delete
  4. 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.

    ReplyDelete
    Replies
    1. Hi Gregor,

      You 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

      Delete
    2. Hi Blaise,
      In 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

      Delete
    3. Hi Gregor,

      Thank you for pointing out this aspect. I have updated my example to include it.

      -Blaise

      Delete
    4. Hi Blaise,

      The 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

      Delete
    5. Hi Suku,

      I 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

      Delete
  5. 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).

    ReplyDelete
    Replies
    1. This 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:
      - MOXy as Your JAX-RS JSON Provider - MOXyJsonProvider

      Delete
  6. Hello,
    Couldn't it be possible to download the example? I'm trying to use MOXy with Spring Hibernate on Apache Tomcat!
    Thanks

    ReplyDelete
    Replies
    1. I currently do not have the files for this example hosted anywhere.

      Delete

Note: Only a member of this blog may post a comment.