Tuesday, January 28, 2014

Selecting level of detail returned by varying the content type

So I have been playing with the various ways of generating JSON in Jersey this week and I have been looking at solutions to the problem of returning different levels of detail depending on the client requirements. Here is one solution that uses the MoXY JAX-B provider in Jersey 2.x.

Consider this very simple hello world class:

@Path("hello")
public class SelectableHello {
    
    @GET
    @Produces({"application/json; level=detailed", "application/json; level=summary", "application/json; level=normal"})
    public Message hello() {
        return new Message();
    }
}

The Message class is annotated with some special annotations from MOXy that allows you to specify different partial object graphs. So my very simple message class looks like the following with three different levels defined:

import javax.xml.bind.annotation.XmlRootElement;

import org.eclipse.persistence.oxm.annotations.XmlNamedAttributeNode;
import org.eclipse.persistence.oxm.annotations.XmlNamedObjectGraph;
import org.eclipse.persistence.oxm.annotations.XmlNamedObjectGraphs;


@XmlNamedObjectGraphs({
  @XmlNamedObjectGraph(name = "summary", 
    attributeNodes = { 
      @XmlNamedAttributeNode("summary") }),
  @XmlNamedObjectGraph(name = "normal",
    attributeNodes =
    { @XmlNamedAttributeNode("summary"), 
      @XmlNamedAttributeNode("message") }),
  @XmlNamedObjectGraph(name = "detailed",
    attributeNodes =
    { @XmlNamedAttributeNode("summary"), 
      @XmlNamedAttributeNode("message"),
      @XmlNamedAttributeNode("subtext") })
  })
@XmlRootElement
public class Message {

  private String summary, message, subtext;

  public Message() {
    summary = "Some simple summary";
    message = "This is indeed the message";
    subtext = "This is the deep and meaningful subtext";
  }

  public void setSummary(String summary) {
    this.summary = summary;
  }

  public String getSummary() {
    return summary;
  }

  public void setMessage(String message) {
    this.message = message;
  }

  public String getMessage() {
    return message;
  }

  public void setSubtext(String subtext) {
    this.subtext = subtext;
  }

  public String getSubtext() {
    return subtext;
  }


}

Then it is a relatively easy step to define a JAX-RS ContextResolver that returns a suitably configured MoxyJsonConfig depending on the mime type. The code in here is a little bit ropey as it assumes that the first item in the accept list is the correct one.

import javax.ws.rs.core.Context;

import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import org.eclipse.persistence.jaxb.MarshallerProperties;

import org.glassfish.jersey.moxy.json.MoxyJsonConfig;

@Provider
public class MoxyJsonConfigResolver implements ContextResolver<MoxyJsonConfig> {

    HttpHeaders httpHeaders;


    @Context
    public void setHeaders(HttpHeaders httpHeaders) {
        this.httpHeaders = httpHeaders;
    }

    @Override
    public MoxyJsonConfig getContext(Class<?> c) {

        // Assume we are going to match the first accept
        //
        MediaType responseType = httpHeaders.getAcceptableMediaTypes().get(0);

        MoxyJsonConfig config = new MoxyJsonConfig();
        String level = responseType.getParameters().get("level");
        config.getMarshallerProperties().put(MarshallerProperties.OBJECT_GRAPH, level != null ? level : "detailed");
        return config;
    }
}

So once this little application is running, you can see the output for different mime types. In our case the default for "application/json" is "detailed" but I can see the argument in many cases for "normal" to be the default.

GET .../hello Accept application/json; level=detailed or application/json

{
    "message" : "This is indeed the message",
    "subtext" : "This is the deep and meaningful subtext",
    "summary" : "Some simple summary"
}

GET .../hello Accept application/json; level=normal

{
    "message" : "This is indeed the message",
    "summary" : "Some simple summary"
}

GET .../hello Accept application/json; level=summary

{
    "summary" : "Some simple summary"
}


This does work, but as you can see the annotations could be prettier, the next blog post will be an example using EntityFilteringFeature of Jersey. This feature builds on top of the MOXy functionality above while allowing a custom annotation scheme. This to my mind is a lot easier to work with.

No comments: