Saturday, 30 January 2010

(Almost) RESTful application with Grails and Dojo

Introduction
As REST techniques are getting more and more popular I wanted to give it a try. Of course I wanted to use Grails. For client side I have chosen Dojo javascript framework as I already had some experience with it.

Project setup
During my experiment I used Grails 1.2.0.
Let's start with generating simple project with Book domain object and controller - almost as in Grails tutorial:

grails create-app GrailsRestTest
cd GrailsRestTest
grails create-domain-class book

grails create-controller Book

Only one thing that surprised me was a message: "WARNING: You have not specified a package. It is good practise to place classes". I wanted to make it simple so I just ignored it by writing "y" and pushing enter;)

As in Grails tutorial, I added some sample data to /grails-app/conf/BootStrap.groovy file:
def init = { servletContext ->
new Book(author:"Stephen King",title:"The Shining").save()
new Book(author:"James Patterson",title:"Along Came a Spider").save()
}
Final project structure will be:



RESTful server
We need to create controller that will be performing base CRUD operations and map its methods to REST style URLs.

Edit /grails-app/conf/UrlMappings.groovy:
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?"{
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
"/book" (controller: "book") {
action = [GET:"list", POST: "create"]
}
"/book/$id" (resource: "book")
}
}
Entry with /book maps GET and POST HTTP requests to this URL to list and create controller methods. Below we map URLs like /book/$id using default grails REST mapping:
  • GET -> show
  • PUT -> update
  • POST -> save (used when you know ID of item you create - not our case;)
  • DELETE -> delete
Implementation of our controller will use GORM for data storage and will communicate with client application using JSON format. Edit /grails-app/controllers/BookController.groovy:

import grails.converters.*

class BookController {
def list = {
response.setHeader("Cache-Control", "no-store")
render Book.list(params) as JSON
}

def show = {
Book b=Book.get(params.id)
render b as JSON
}

def create = {
def json = JSON.parse(request);
def b = new Book(json)
b.save()
response.status = 201
response.setHeader('Location', '/book/'+b.id)
render b as JSON
}

def delete = {
Book b=Book.get(params.id)
b.delete()
render(status: 200)
}
}
I guess this code is quite easy to understand. In create operation we need to send 201 HTTP code and URL to newly created item in Location header. I did not implement save and update methods to simplify my tutorial.

Run this code with grails run-app and point your browser to http://localhost:8080/GrailsRestTest/book. You should get following response:
[{"class":"Book","id":1,"author":"Stephen King","title":"The Shining"},{"class":"Book","id":2,"author":"James Patterson","title":"Along Came a Spider"}]
To get particular item try http://localhost:8080/GrailsRestTest/book/1 - response should be:
{"class":"Book","id":1,"author":"Stephen King","title":"The Shining"}
Client interface with Dojo
Install Dojo with:
grails install-plugin dojo

Add following page to /grails-app/views/gui/books.gsp:
<html>
<head>
<script type="text/javascript" src="<g:createLinkTo file="/js/dojo/dojo-1.3.0/dojo/dojo.js" />" djConfig="parseOnLoad:true, isDebug:false">
<script type="text/javascript" src="<g:createLinkTo file="/js/dojo/dojo-1.3.0/dijit/dijit.js" />"></script>
<style type="text/css">
@import "<g:createLinkTo file="/js/dojo/dojo-1.3.0/dijit/themes/tundra/tundra.css" />";
@import "<g:createLinkTo file="/js/dojo/dojo-1.3.0/dojox/grid/resources/Grid.css" />";
@import "<g:createLinkTo file="/js/dojo/dojo-1.3.0/dojox/grid/resources/tundraGrid.css" />";
</style>
</head>
<body class="tundra">
<script type="text/javascript">
dojo.require("dojox.grid.DataGrid");
dojo.require("dojox.data.JsonRestStore");
dojo.require("dijit.Dialog");
dojo.require("dijit.form.TextBox");
dojo.require("dijit.form.Button");

dojo.addOnLoad(function() {
var restStore = new dojox.data.JsonRestStore({target:"<g:createLink controller="book" />"});

var layout = [
{
field: 'id',
name: 'Id',
width: '50px'
},
{
field: 'title',
name: 'Title',
width: '200px'
},
{
field: 'author',
name: 'Author',
width: '100px'
}];

var grid = new dojox.grid.DataGrid({
store: restStore,
structure: layout,
selectionMode: 'single'
},
document.createElement('div'));

dojo.byId("gridContainer").appendChild(grid.domNode);

grid.startup();

saveBook = function(book) {
restStore.newItem(book);
restStore.save();
// to force grid refresh
grid.sort();
}

removeBook = function() {
var book = grid.selection.getSelected()[0];
if (book !=null && book.id != null) {
restStore.deleteItem(book);
restStore.save();
}
}

showAddForm = function() {
document.getElementById('title').value="";
document.getElementById('author').value="";
dijit.byId('dialog1').show();
}
});
</script>

<div id="gridContainer" style="width: 375px; height: 200px;"></div>
<button dojoType="dijit.form.Button" onclick="showAddForm()">Add book</button>
<button dojoType="dijit.form.Button" onclick="removeBook()">Remove</button>

<div dojoType="dijit.Dialog" id="dialog1" title="Add book"
execute="saveBook(arguments[0]);">
<table>
<tr>
<td><label for="title">Title: </label></td>
<td><input dojoType="dijit.form.TextBox" type="text" name="title" id="title"></td>
</tr>
<tr>
<td><label for="author">Author: </label></td>
<td><input dojoType="dijit.form.TextBox" type="text" name="author" id="author"></td>
</tr>
<tr>
<td colspan="2" align="center">
<button dojoType="dijit.form.Button" type="submit">Save</button></td>
</tr>
</table>
</div>
</body>
</html>
Our client is based on three Dojo components:
  • JsonRestStore - responsible for transparent client-server communication
  • DataGrid - grid presenation for data
  • Dialog - simple popup for adding new book items
After starting application and pointing your browse to http://localhost:8080/GrailsRestTest/gui/books.gsp you should see:



DataGrid asked JsonRestStore for data and it made following request to server:
GET /GrailsRestTest/book/? HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)
Accept: application/json,application/javascript
Accept-Language: pl,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Range: items=0-24
Content-Type: application/json
X-Requested-With: XMLHttpRequest
Referer: http://localhost:8080/GrailsRestTest/gui/books.gsp
Response contains JSON data as presented earlier.

Clicking Add button will show popup that allows creating new items:



Submitting data will result in following request and response:
POST /GrailsRestTest/book/ HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; pl; rv:1.9.1.7) Gecko/20091221 Firefox/3.5.7 (.NET CLR 3.5.30729)
Accept: application/json,application/javascript
Accept-Language: pl,en-us;q=0.7,en;q=0.3
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-2,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Transaction: commit
Content-ID:
Content-Type: application/json; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://localhost:8080/GrailsRestTest/gui/books.gsp
Content-Length: 49
Pragma: no-cache
Cache-Control: no-cache
{"title":"Test author 1","author":"Test title 1"}

HTTP/1.x 201 Created
Server: Apache-Coyote/1.1
Location: /book/3
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 30 Jan 2010 13:59:06 GMT
At that is the moment when I had some problems. You probably noticed commented line with grid.sort() in gsp page. It forces grid and JsonRestStore to reload data from server after saving new item. When you remove this line, you will notice that new item is still added to store and even to the table (without reloading) - but ID of new item is missing. According to Dojo documentation JsonRestStore should pick this ID from Location http header of response. Unfortunatelly it does not. Am I doing something wrong? Or is this a bug? Hints are welcome!

To remove item select it and push Remove button. Dojo will make request similar to:
DELETE /GrailsRestTest/book/8 HTTP/1.1
Host: localhost:8080
This time item is automatically removed from store and grid and any data reloading is not needed.

Conclusion
The title of this article starts with "Almost" as it is not complete application. I did not implement any update operation. Also this ID issue with Dojo makes me a little worried. Application was tested only on Firefox... so to make it "production ready" you should also test (or get it working ;) on other browsers. Anyway, it was fun to try something new...