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 ->Final project structure will be:
new Book(author:"Stephen King",title:"The Shining").save()
new Book(author:"James Patterson",title:"Along Came a Spider").save()
}
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 {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:
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")
}
}
- GET -> show
- PUT -> update
- POST -> save (used when you know ID of item you create - not our case;)
- DELETE -> delete
import grails.converters.*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.
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)
}
}
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>Our client is based on three Dojo components:
<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>
- JsonRestStore - responsible for transparent client-server communication
- DataGrid - grid presenation for data
- Dialog - simple popup for adding new book items
DataGrid asked JsonRestStore for data and it made following request to server:
GET /GrailsRestTest/book/? HTTP/1.1Response contains JSON data as presented earlier.
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
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.1At 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!
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
To remove item select it and push Remove button. Dojo will make request similar to:
DELETE /GrailsRestTest/book/8 HTTP/1.1This time item is automatically removed from store and grid and any data reloading is not needed.
Host: localhost:8080
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...