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
12 commentaires:
Hello Loic,
Great post on API building. I wish Java had an xml parser as easy as Python ones. :)
Is it also possible to have a single "list"-Action in the controller that does the appropriate thing according to the http-headers sent by the client? In Rails it is done the following way:
respond_to do |format|
format.html # index.html.erb
format.xml { render ..... }
You can have only one method in the controller class if you need the same number of parameters and the same treatment for both of renderings
In my case in may add parameters to the html version,
e.g. GET /albums/{genre}/{first}/{count} Application.list
So i preferred a separation of the rendering in two distinct methods.
I'll edit the post to clarify that
You know that this API has nothing to do with REST? You can call it a XML API. But not REST and in no way RESTful.
Hi Anonymous, could you develop and explain your idea please?
This API is based on RESTful URL and on HTTP verbs (GET, POST etc.) and it respects the principles of REST architecture (thanks to play background)
Hi Loïc.
Only using the HTTP verbs does not make a service RESTful. Fielding describes in his dissertation (along with other principles), that you need to "think" in resources and representations with an uniform interface. In your example you have a resource album. Why do you not create an album by sending a post to /album instead of to /album/new. With the "method" "new" you are breaking the uniform interface.
Could you explain what the difference between RESTful and RESTless URL is? URL is defined in RFC 1738, which does not say anything about REST.
An other thing is the use of "xml" in the URL. That may be allowed according to the REST principles (I'm not sure). But would it not be nicer if we do this with content negotiation through HTTP Accept-Headers? This would look much cleaner.
I think you wrote a really nice tutorial. But with a little more focus on the REST principles it would be even nicer.
Hi, you can look a this document about restful URL (friendly and significant) conventions:
http://microformats.org/wiki/rest/urls
You can see that new keyword is often used to create a new resource. But if you don't want to use it you can follow this tutorial and write your own URL patterns.
For the "xml"in the URL, i just did it to separate the html and xml routes, it's true that it could be nicer, and i will modify the post
"it not be nicer if we do this with content negotiation through HTTP Accept-Headers? This would look much cleaner"
That's right, i'm going to look at this point (don't know if it's possible to configure that in the play routes file)
Only because Rails is doing that it is RESTful?
These conventions make sense in many point, other like the file format part do not.
"You can see that new keyword is often used to create a new resource."
If you have a second look at the link you've posted, you will see that they use the keyword "new" with the HTTP-method GET for getting a html-form to create a new resource. Not for really creating the resource.
Yes you're right i've seen that too and i've modified the URL in my post ;) There is no new anymore
Thanks a lot for your feedbacks
Loic
Hi, I'm Jitesh, and I work for Packt. I am looking for reviewers for our recently published book, Play Framework Cookbook written by Alexander Reelsen.
I found that your blog is informative, and has good articles on Play! If you are interested in reviewing the book, then I'd be glad to send you an e-copy of it.
More information about the book: http://www.packtpub.com/play-framework-cookbook/book
For more details, contact me on: jiteshg@packtpub.com
Hi,
thanks I would be very happy to review this book.
Regards
Loic
great blog post. very helpful. Thank you for sharing!
Enregistrer un commentaire