lundi 17 mai 2010

How to write a REST/XML API with Play framework

This is the english version of my post about REST/XML and Play! framework, for the planet <3 play! community.

Read this post in french : part 1 - part 2

Today we will see how to simply expose a REST/XML (or JSON, or another format) API with the Play! framework.
URL of Play! are RESTful in essence, so it becomes very easy to create a small REST API / XML beside the Web interface of Play! application.
Let's see how to do it.

Let's take the example of a music library. Our model includes albums, artists and genres.
The Album class looks like this:
@Entity
public class Album extends Model {
 public String name;
 @ManyToOne(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
 public Artist artist;
 public Date releaseDate;
 @Enumerated(EnumType.STRING)
 public Genre 
}

Genre is a simple Enum defined as:
public enum Genre {
    ROCK, METAL, JAZZ, BLUES, POP, WORLD, HIP_HOP, OTHER
}

We want to define an URL that returns a list of the albums in XML for a given genre, on a GET call.
To do this we must change the routes file:

GET /website/albums/{genre}       Application.list
GET /albums/{genre}   Application.listXml(format:'xml')

The first line corresponds to the HTML page (not shown in this post) that displays a list of available albums: the format is not specified, so render will be made with an HTML page.
In the second line, the parameter (format: 'xml') indicates that the render() method of the controller will look for a file named listXml.xml. The{genre} parameter will be recovered in the URL and passed to the controller.

NB : You could use only one method in the controller class for HTML and XML if you need the same number of parameters and the same treatment for both of renderings.
In this case we may add parameters to the html version later, without wanting to impact the XML rendering,
e.g. GET /albums/{genre}/{first}/{count} Application.list
So i preferred a separation of the rendering in two distinct methods.

See the code of the Application.listXml() method :
public static void listXml(String genre) {
        Genre genreEnum = Genre.valueOf(genre.toString().toUpperCase());
        List<Album> albums= Album.find("byGenre",genreEnum).fetch();
        render(albums);
    }

We're just looking for the album corresponding to the genre parameter, and we request the rendering of the list. By the way we see how using JPA with Play! is simple. The report will be made in the file corresponding to the pattern {name of the controller method} + {.xml}.
In this case it will be listXml.xml.
This template, placed in app/views directory, is defined as follows:
   
#{list albums, as:'album'}
    <album>
        <artist>${album.artist.name}</artist>
        <name>${album.name}</name>
        <release-date>${album.releaseDate.format('yyyy')}</release-date>
        <genre>${album.genre.toString()}</genre>
    </album>
#{/list}
</albums>

That is enough to expose our albums in XML. By following the URL pattern defined in the routes file, for example by calling http://localhost:9000/albums/rock, we obtain the following result:

<albums>
   <album>
      <artist>Nirvana</artist>
      <name>Nevermind</name>
      <release-date>1991</release-date>
      <genre>ROCK</genre>
   </album>
   <album>
      <artist>Muse</artist>
      <name>Origin of Symmetry</name>
      <release-date>2001</release-date>
      <genre>ROCK</genre>
      </album>
   <album>
      <artist>Muse</artist>
      <name>Black Holes and Revelations</name>
      <release-date>2006</release-date>
      <genre>ROCK</genre>
   </album>
</albums>

Now let's see how to send XML content to add albums to our music library.

We want to send the following content, using POST with application/xml content type :
<album>
      <artist>Metallica</artist>
      <name>Death Magnetic</name>
      <release-date>2008</release-date>
      <genre>METAL</genre>
   </album>

We add this line to the routes file to allow POST on /album URL :

POST /album  Application.saveXml

SaveXML method retrieves the content of the request in request.body variable.
Then it parses the content to create an album and save it in the database.
We use a class named play.libs.XPath to browse the XML document :

public static void saveXML(){
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        Document document = null;
        try {
            //create xml document
            DocumentBuilder builder = factory.newDocumentBuilder();
            document = builder.parse(requestBody);
        } catch (Exception e) {
            Logger.error(e.getMessage());
        }
        Element albumNode = document.getDocumentElement();
        //get the artist
        String artistName = XPath.selectText("artist",albumNode);
        Artist artist = new Artist(artistName);
        //get the name
        String albumName = XPath.selectText("name", albumNode);
        Album album = new Album(albumName);
        //get the date
        String date = XPath.selectText("release-date",albumNode);
        DateFormat dateFormat = new SimpleDateFormat("yyyy");
        try {
            album.releaseDate = dateFormat.parse(date);
        } catch (ParseException e) {
            Logger.error(e.getMessage());
        }
        //get the genre
        String genre = XPath.selectText("genre", albumNode);
        Genre genreEnum = Genre.valueOf(genre.toString().toUpperCase());
        album.genre = genreEnum;

        //save in db
        album.artist = artist;
        album.save();
    }

NB: Of course it is possible to obtain a less verbose code by de-serializing the request object using a tool like JAXB or XStream, but this is not the subject of this post.

When you write album.artist=artist, setArtist(Artist artist) method is automatically called by Play! (code is modified on runtime).
We can confirm that the artist exists or not in the database, and determine if we should create a new instance of the artist or retrieve the existing one. The save() method of Album class persists the album in the database, and the artist if it is unknown (using JPA cascade).

public void setArtist(Artist artist){
        List<artist> existingArtists = Artist.find("byName", artist.name).fetch();
        if(existingArtists.size()>0){
            //Artist name is unique
            this.artist=existingArtists.get(0);
        }
        else{
            this.artist=artist;
        }
    }

Our REST API / XML allows now to list and to add albums.
You can test sending XML content with Poster plugin for Firefox or with the rest-client application.

Keep Reading : How to export objects as JSON with Play framework