Binding JSON and XML to objects

After you have explored Play a little bit and written your first apps, you might have noticed that it works excellently when binding complex Java objects out of request parameters, as you can put complex objects as controller method parameters. This type of post request deserialization is the default in Play. This recipe shows how to convert JSON and XML data into objects without changing any of your controller code.

The source code of the example is available at examples/chapter4/mashup-json-xml.

Getting ready

Let's start with a controller, which will not change and does not yield any surprises, as shown in the following code snippet:

public class Application extends Controller {

  public static void thing(Thing thing) {
    renderText("foo:"+thing.foo+"|bar:"+thing.bar+"
");
  }
}

You should add a correct route for the controller as well in conf/routes

POST     /thing     Application.thing

Start with a test as usual:

public class ApplicationTest extends FunctionalTest {

  private String expectedResult = "foo:first|bar:second
";

  @Test
  public void testThatParametersWork() {
    String html = "/thing?thing.foo=first&thing.bar=second";
    Response response = POST(html);
    assertIsOk(response);
    assertContentType("text/plain", response);
    assertContentMatch(expectedResult, response);
  }

  @Test
  public void testThatXmlWorks() {
    String xml = "<thing><foo>first</foo><bar>second</bar></thing>";
    Response response = POST("/thing", "application/xml", xml);
    assertIsOk(response);
    assertContentType("text/plain", response);
    assertContentMatch(expectedResult, response);
  }

  @Test
  public void testThatJsonWorks() {
    String json = "{ thing : { "foo" : "first", "bar" : "second" } }";
    Response response = POST("/thing", "application/json", json);
    assertIsOk(response);
    assertContentType("text/plain", response);
    assertContentMatch(expectedResult, response);
  }

}

Three tests come with three different representations of the same data. The first one is the standard representation of an object and its properties come via HTTP parameters. Every parameter starting with "thing." is mapped as property to the thing object of the controller. The second example represents the thing object as XML entity, whereas the third does the same as JSON. In both cases, there is a thing root element. Inside of this element every property is mapped.

How to do it...

In order to get this to work, a small but very effective plugin is needed. In this case, the plugin will be put directly into the application. This should only be done for rapid prototyping but not in big production applications. The first step is to create an app/play.plugins file with the following content:

201:plugin.ApiPlugin

This ensures that the ApiPlugin class in the plugin package is loaded on application startup.

The next step is to modify the entity to support JAXB annotations:

@Entity
@XmlRootElement(name="thing")
@XmlAccessorType(XmlAccessType.FIELD)
public class Thing extends Model {
  @XmlElement public String foo;
  @XmlElement public String bar;
}

The last step is to write the plugin itself, as shown in the following code snippet:

public class ApiPlugin extends PlayPlugin {
  
  private JAXBContext jc;
  private Gson gson;

  public void onLoad() {
    Logger.info("ApiPlugin loaded");
    try {
      List<ApplicationClass>applicationClasses = Play.classes.getAnnotatedClasses(XmlRootElement.class);
      List<Class> classes = new ArrayList<Class>();
      for (ApplicationClassapplicationClass : applicationClasses) {
        classes.add(applicationClass.javaClass);
      }
      jc = JAXBContext.newInstance(classes.toArray(new Class[]{}));
    }
    catch (JAXBException e) {
      Logger.error(e, "Problem initializing jaxb context: %s", e.getMessage());
    }
    gson = new GsonBuilder().create();        

  }
  
  public Object bind(String name, Class clazz, Type type, Annotation[] 




    annotations, Map<String, String[]>params) {
    String contentType = Request.current().contentType;

    if ("application/json".equals(contentType)) {
      return getJson(clazz, name);
    }
    else if ("application/xml".equals(contentType)) {
      return getXml(clazz);
    }

    return null;
  }
  
  private Object getXml(Class clazz) {
    try {
      if (clazz.getAnnotation(XmlRootElement.class) != null) {
        Unmarshaller um = jc.createUnmarshaller();
        return um.unmarshal(Request.current().params.get("body"));
      }
    }
    catch (JAXBException e) {
      Logger.error("Problem rendering XML: %s", e.getMessage());
    }
    return null;
  }
  
  private Object getJson(Class clazz, String name) {
    try {
      String body = Request.current().params.get("body");
      JsonElement jsonElem = new JsonParser().parse(body);
      if (jsonElem.isJsonObject()) {
        JsonObject json = (JsonObject) jsonElem;
        
        if (json.has(name)) {
          JsonObject from = json.getAsJsonObject(name);
          Object result = gson.fromJson(from, clazz);
          return result;
        }
      }
    }
    catch (Exception e) {
      Logger.error("Problem rendering JSON: %s", e.getMessage());
    }
    return null;
  }

}

How it works...

Though the presented solution is pretty compact in lines of code and amount of files touched, lots of things happen here.

First, the created play.plugins file consists of two fields. The first field represents a priority. This priority defines the order of modules loaded. Take a look at framework/src/play.plugins of your Play installation. Check the number written before DBPlugin is mentioned. This number represents a loading priority. In any case, the ApiPlugin should have a lower priority than the DBPlugin, resulting in being loaded first. Otherwise, this recipe will not work because the bind() method of the DBPlugin gets executed in favor of our own written one.

The next step is to extend the entity. You need to add JAXB annotations. The @XmlRootElement annotation marks the name of the root element. You have to set this annotation, because it is also used by the plugin later on. The second annotation, @XmlAccessorType , is also mandatory with its value XmlAccessType.FIELD. This ensures field access instead of methods – this does not affect your model getters. If you use getters, they are called as in any other Play application. It is merely a JAXB issue. The @XmlElement annotations are optional and can be left out.

The core of this recipe is the ApiPlugin class. It consists of four methods, and more importantly, in order to work as a plugin it must extend PlayPlugin. The onLoad() method is called when the plugin is loaded, on the start of the application. It logs a small message, and creates a Google gson object for JSON serialization as well as a JAXB context. It searches for every class annotated with @XmlRootElement, and adds it to the list of classes to be parsed for this JAXB context. The bind() method is the one called on incoming requests. The method checks the Content-Type header. If it is either application/json or application/xml, it will call the appropriate method for the content type. If none is matched, null is returned, which means that this plugin could not create an object out of the request. The getXml() method tries to unmarshall the data in the request body to an object, in case the handed over class has an @XmlRootElement annotation. The getJson() method acts pretty similar. It converts the response string to a JSON object and then tries to find the appropriate property name. This property is then converted to a real object and returned if successful.

As you can see in the source, there is not much done about error handling. This is the main reason why the implementation is that short. You should return useful error messages to the user, instead of just catching and logging exceptions.

There's more...

This implementation is quite rudimentary and could be made more error proof. However, another major point is that it could be made even simpler for the developer using it.

Add the XML annotations via byte code enhancement

Adding the same annotations in every model class over and over again does not make much sense. This looks like an easy to automate job. However, adding annotations is not as smooth as expected with Java. You need to perform byte code enhancement. Read more about enhancing class annotations via bytecode enhancement at the following link:

http://www.csg.is.titech.ac.jp/~chiba/javassist/tutorial/tutorial3.html

Put plugins where they belong

Plugins never ever belong to your main application. They will clutter your code and make your applications less readable. Put them into their own module, always.

Change your render methods to use JAXB for rendering

You could add a renderXML(Object o) method, which is similar to its renderJSON(Object o) companion. By using JAXB, you get almost everything for free:

public class RenderXml extends Result {

  private Marshaller m;
  private Object o;
  
  public static void renderXML(Object o) {
    throw new RenderXml(o);
  }
  
  public RenderXml(Object o) {
    this.o = o;
  }
  
  @Override
  public void apply(Request request, Response response) {
    try {
      setContentTypeIfNotSet(response, "text/xml");
      m = ApiPlugin.jc.createMarshaller();
      m.marshal(o, response.out);
    }
    catch (JAXBException e) {
      Logger.error(e, "Error renderXml");
    }
  }
}

Your controller call may now look similar to the following code snippet:

public static void thingXml(Thing thing) {
  renderXML(thing);
}

Of course, you should not forget the static import in your controller, as shown in the following line of code:

import static render.RenderXml.*;

All of this is also included in the example source code.

See also

Learn how to create modules instead of putting code as shown in the preceding examples directly into your application in the next chapter.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.85.221