Labels

.NET Job Questions About Java Absract class Abstract class Abstract Class and Interface Aggregation ajax aop apache ofbiz Apache ofbiz tutrial Association authentication autocad basics batch Binary Tree bootstrap loader in java build Builder design pattern C++ Job Questions caching CallableStatement in java certifications Chain of responsibility Design pattern charts check parentheses in a string Classes classloader in java classloading concept code quality collage level java program Composition concurrency Concurrency Tutorial Converting InputStream to String Core Java core java concept core java interview questions Core Java Interview Questions Core Java Questions core java tutorial CyclicBarrier in Java data structures database Database Job Questions datetime in c# DB Db2 SQL Replication deserialization in java Design Patterns designpatterns Downloads dtd Eclipse ejb example/sample code exception handling in core java file handling injava File I/O vs Memory-Mapped Filter first program in spring flex Garbage Collection Generics concept in java grails groovy and grails Guice Heap hibernate Hibernate Interview Questions how-to IBM DB2 IBM DB2 Tutorial ide immutable Interceptor Interface interview Interview Questions for Advanced JAVA investment bank j2ee java JAVA Code Examples Java 7 java changes java class loading JAVA Classes and Objects Java Classloader concept Java classloading concept java cloning concept java collection Java collection interview questions Java Collections java concurrency Java CountDownLatch java definiton Java design pattern Java EE 5 Java EE 6 Java Exceptions Java file Java Garbage Collection Java generics Java Glossary java hot concept java immutable concept Java Interface Java interview Question java interview question 2012 java interview question answer Java Interview Questions Java Interview Questions and Answers java interview topic java investment bank Java Job Questions java multithreading java multithreading concept java new features Java Packages java proxy object java questions Java Serialization Java serialization concept java serialization interview question java session concept java string Java Swings Questions java synchronization java threading Java Threads Questions java tutorial java util; java collections; java questions java volatile java volatile interview question Java Wrapper Classes java.java1.5 java.lang.ClassCastException JavaNotes javascript JAX-WS jdbc JDBC JDBC Database connection jdk 1.5 features JDK 1.5 new features Concurrent HashMap JMS interview question JMS tutorial job JSESSIONID concept JSESSIONID interview Question JSF jsp JSP Interview Question JSP taglib JSTL with JSP Junit Junit Concept Junit interview question.Best Practices to write JUnit test cases in Java JVM Linux - Unix tutorial Marker Interfaces MD5 encryption and decryption messaging MNC software java interview question musix NCR java interview question Networking Job Questions news Object Serialization Objects ojdbc14.jar OOP Oracle Oracle SQL Query for two timestamp difference orm own JavaScript function call in Apache ofbiz Packages Palm Apps patterns pdf persistence Portal Portlet Spring Integration Prime number test in java programs Rails Reboot remote computers REST Ruby Sample application schema SCJP security Senior java developer interviews servlet3 servlets session tracking singleton design pattern Spring Spring 2.5 Framework spring ebook Spring framework concept spring MVC spring pdf Spring Security Spring Security interview questions SQL SQL performance SQL Query to create xml file Sql Query tuning ssis and ssrs StAX and XML string concept string immutable string in java strings struts Struts2 Struts2 integration synchronization works in java Technical Interview testing tips Tomcat top Tutorial Volatile in deep Volatile working concept web Web Developer Job Questions web services weblogic Weblogic Application Server websphere what is JSESSIONID xml XML parsing in java XML with Java xslt


Thursday 25 July 2013

jqGrid, REST, AJAX and Spring MVC Integration with example

More than two years back I wrote an article on how two implement elegant CRUD in Struts2. Actually I had to devote two articles on that subject because the topic was so broad. Today I have taken much more lightweight and modern approach with a set of popular and well established frameworks and libraries. Namely, we will use Spring MVC on the back-end to provide REST interface to our resources, fabulous jqGrid plugin for jQuery to render tabular grids (and much more!) and we will wire up everything with a pinch of JavaScript and AJAX.


Back-end is actually the least interesting part of this showcase, it could have been implemented using any server-side technology capable of handling RESTful requests, probably JAX-RS should now be considered standard in this field. I have chosen Spring MVC without any good reason, but it's also not a bad choice for this task. We will expose CRUD operations over REST interface; the list of best selling books in history will be our domain model (can you guess who is on the podium?)


01@Controller
02@RequestMapping(value = "/book")
03public class BookController {
04 
05  private final Map<Integer, Book> books = new ConcurrentSkipListMap<Integer, Book>();
06 
07  @RequestMapping(value = "/{id}", method = GET)
08  public @ResponseBody Book read(@PathVariable("id") int id) {
09    return books.get(id);
10  }
11 
12  @RequestMapping(method = GET)
13  public @ResponseBody Page<Book> listBooks(
14      @RequestParam(value = "page", required = false, defaultValue = "1") int page,
15      @RequestParam(value = "max", required = false, defaultValue = "20") int max) {
16    final ArrayList<Book> booksList = new ArrayList<Book>(books.values());
17    final int startIdx = (page - 1) * max;
18    final int endIdx = Math.min(startIdx + max, books.size());
19    return new Page<Book>(booksList.subList(startIdx, endIdx), page, max, books.size());
20  }
21 
22}

Few things need explanation. First of all for the purposes of this simple showcase I haven't used any database, all the books are stored in an in-memory map inside a controller. Forgive me. Second issue is more subtle. Since there seems to be no agreement on how to handle paging with RESTful web services, I used simple query parameters. You may find it ugly, but I find abusing Accept-Ranges and Range headers together with 206 HTTP response code even uglier.

Last notable detail is the Page wrapper class:


01@XmlRootElement
02public class Page<T> {
03 
04  private List<T> rows;
05 
06  private int page;
07  private int max;
08  private int total;
09 
10  //...
11 
12}

I could have return raw list (or, more precisely, requested part of the list), but I also need a way to provide convenient metadata like total number of records to the view layer, not to mention some difficulties while marshalling/unmarshalling raw lists.

We are now ready to start our application and do a little test drive with curl:

02 
03<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
04<page>
05  <total>43</total>
06  <page>1</page>
07  <max>3</max>
08  <rows xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="book">
09    <author>Charles Dickens</author>
10    <available>true</available>
11    <cover>PAPERBACK</cover>
12    <id>1</id>
13    <publishedYear>1859</publishedYear>
14    <title>A Tale of Two Cities</title>
15  </rows>
16  <rows xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="book">
17    <author>J. R. R. Tolkien</author>
18    <available>true</available>
19    <cover>HARDCOVER</cover>
20    <id>2</id>
21    <publishedYear>1954</publishedYear>
22    <title>The Lord of the Rings</title>
23  </rows>
24  <rows xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="book">
25    <author>J. R. R. Tolkien</author>
26    <available>true</available>
27    <cover>PAPERBACK</cover>
28    <id>3</id>
29    <publishedYear>1937</publishedYear>
30    <title>The Hobbit</title>
31  </rows>
32</page>

Response type defaults to XML if none is specified but if we add Jackson library to the CLASSPATH, Spring will pick it up and enable us to use JSON as well:

01// $ curl -v -H "Accept: application/json" "http://localhost:8080/books/rest/book?page=1&max=3"
02 
03{
04    "total":43,
05    "max":3,
06    "page":1,
07    "rows":[
08        {
09            "id":1,
10            "available":true,
11            "author":"Charles Dickens",
12            "title":"A Tale of Two Cities",
13            "publishedYear":1859,
14            "cover":"PAPERBACK",
15            "comments":null
16        },
17        {
18            "id":2,
19            "available":true,
20            "author":"J. R. R. Tolkien",
21            "title":"The Lord of the Rings",
22            "publishedYear":1954,
23            "cover":"HARDCOVER",
24            "comments":null
25        },
26        {
27            "id":3,
28            "available":true,
29            "author":"J. R. R. Tolkien",
30            "title":"The Hobbit",
31            "publishedYear":1937,
32            "cover":"PAPERBACK",
33            "comments":null
34        }
35    ]
36}

Nice, now we can work on the front-end, hopefully not making our hands too dirty. With regards to HTML markup, this is all we need, seriously:

1<table id="grid"></table>
2<div id="pager"></div>

Keep in mind that we will implement all CRUD operations, but still, this is all we need. No more HTML. Rest of the magic happens thanks to marvellous jqGrid library. Here is a basic setup:

01$("#grid")
02    .jqGrid({
03      url:'rest/book',
04      colModel:[
05        {name:'id', label: 'ID', formatter:'integer', width: 40},
06        {name:'title', label: 'Title', width: 300},
07        {name:'author', label: 'Author', width: 200},
08        {name:'publishedYear', label: 'Published year', width: 80, align: 'center'},
09        {name:'available', label: 'Available', formatter: 'checkbox', width: 46, align: 'center'}
10      ],
11      caption: "Books",
12      pager : '#pager',
13      height: 'auto'
14    })
15    .navGrid('#pager', {edit:false,add:false,del:false, search: false});

Technically, this is all we need. URL to fetch the data, pointing to our controller (jqGrid will perform all the AJAX magic for us) and the data model (you may recognize book fields and their descriptions). However, since jqGrid is highly customizable, I applied few tweaks to make the grid look a bit better. Also I didn't like suggested names of metadata, for instance total field returned from the server is suppose to be the total number of pages, not records – highly counter-intuitive. Here are my tweaked options:

01$.extend($.jgrid.defaults, {
02  datatype: 'json',
03  jsonReader : {
04    repeatitems:false,
05    total: function(result) {
06      //Total number of pages
07      return Math.ceil(result.total / result.max);
08    },
09    records: function(result) {
10      //Total number of records
11      return result.total;
12    }
13  },
14  prmNames: {rows: 'max', search: null},
15  height: 'auto',
16  viewrecords: true,
17  rowList: [10, 20, 50, 100],
18  altRows: true,
19  loadError: function(xhr, status, error) {
20    alert(error);
21  }
22  });

Eager to see the results? Here is a browser screenshot:



Good looking, with customizable paging, lightweight refreshing... And our hands are still relatively clean! But I promised CRUD... If you were careful, you have probably noticed few navGrid attributes, dying to be turned on:

01var URL = 'rest/book';
02var options = {
03  url: URL,
04  editurl: URL,
05  colModel:[
06    {
07      name:'id', label: 'ID',
08      formatter:'integer',
09      width: 40,
10      editable: true,
11      editoptions: {disabled: true, size:5}
12    },
13    {
14      name:'title',
15      label: 'Title',
16      width: 300,
17      editable: true,
18      editrules: {required: true}
19    },
20    {
21      name:'author',
22      label: 'Author',
23      width: 200,
24      editable: true,
25      editrules: {required: true}
26    },
27    {
28      name:'cover',
29      label: 'Cover',
30      hidden: true,
31      editable: true,
32      edittype: 'select',
33      editrules: {edithidden:true},
34      editoptions: {
35        value: {'PAPERBACK': 'paperback', 'HARDCOVER': 'hardcover', 'DUST_JACKET': 'dust jacket'}
36      }
37    },
38    {
39      name:'publishedYear',
40      label: 'Published year',
41      width: 80,
42      align: 'center',
43      editable: true,
44      editrules: {required: true, integer: true},
45      editoptions: {size:5, maxlength: 4}
46    },
47    {
48      name:'available',
49      label: 'Available',
50      formatter: 'checkbox',
51      width: 46,
52      align: 'center',
53      editable: true,
54      edittype: 'checkbox',
55      editoptions: {value:"true:false"}
56    },
57    {
58      name:'comments',
59      label: 'Comments',
60      hidden: true,
61      editable: true,
62      edittype: 'textarea',
63      editrules: {edithidden:true}
64    }
65  ],
66  caption: "Books",
67  pager : '#pager',
68  height: 'auto'
69};
70$("#grid")
71    .jqGrid(options)
72    .navGrid('#pager', {edit:true,add:true,del:true, search: false});

The configuration is getting dangerously verbose, but there's nothing complicated out there – for each field we have added few additional attributes controlling how this field should be treated in edit mode. This includes what type of HTML input should represent it, validation rules, visibility, etc. But honestly, I believe it was worth it:



This nicely looking edit window has been fully generated by jqGrid based on our edit options mentioned above, including validation logic. We can make some of the fields visible in the grid hidden/inactive in edit dialog (like id) and vice-versa (cover and comments are not present in the grid, however you can modify them). Also notice few new icons visible in bottom-left corner of the grid. Adding and deleting is possible as well – and we haven't written a single line of HTML/JSP/JavaScript (excluding jqGrid configuration object).



Of course we all know that The User Interface Is The Application, and our interface is pretty good, however sometimes we really want a beautiful and working application. And currently the latter requirement is our Achilles' heel. Not because the back-end isn't ready, this is rather trivial:

01@Controller
02@RequestMapping(value = "/book")
03public class BookController {
04 
05  private final Map<Integer, Book> books = new ConcurrentSkipListMap<Integer, Book>();
06 
07  @RequestMapping(value = "/{id}", method = GET)
08  public @ResponseBody Book read(@PathVariable("id") int id) {
09    //...
10  }
11 
12  @RequestMapping(method = GET)
13  public
14  @ResponseBody
15  Page<Book> listBooks(
16      @RequestParam(value = "page", required = false, defaultValue = "1") int page,
17      @RequestParam(value = "max", required = false, defaultValue = "20") int max) {
18    //...
19  }
20 
21  @RequestMapping(value = "/{id}", method = PUT)
22  @ResponseStatus(HttpStatus.NO_CONTENT)
23  public void updateBook(@PathVariable("id") int id, @RequestBody Book book) {
24    //...
25  }
26 
27  @RequestMapping(method = POST)
28  public ResponseEntity<String> createBook(HttpServletRequest request, @RequestBody Book book) {
29    //...
30  }
31 
32  @RequestMapping(value = "/{id}", method = DELETE)
33  @ResponseStatus(HttpStatus.NO_CONTENT)
34  public void deleteBook(@PathVariable("id") int id) {
35    //...
36  }
37 
38}

Server-side is ready, but when it comes to data manipulation on the client-side, jqGrid reveals its dirty secret – all the traffic to the server is sent using POST like this:

1Content-Type: application/x-www-form-urlencoded in the following format:
2id=&title=And+Then+There+Were+None&author=Agatha+Christie&cover=PAPERBACK&publishedYear=1939&available=true&comments=&oper=add

The last attribute (oper=add) is crucial. Not really idiomatic REST, don't you think? If we could only use POST/PUT/DELETE appropriately and serialize data using JSON or XML... Modifying my server so that it is compliant with some JavaScript library (no matter how cool it is), seems like a last resort. Thankfully, everything can be customized with a moderate amount of work.

01$.extend($.jgrid.edit, {
02      ajaxEditOptions: { contentType: "application/json" },
03      mtype: 'PUT',
04      serializeEditData: function(data) {
05        delete data.oper;
06        return JSON.stringify(data);
07      }
08    });
09$.extend($.jgrid.del, {
10      mtype: 'DELETE',
11      serializeDelData: function() {
12        return "";
13      }
14    });
15 
16var URL = 'rest/book';
17var options = {
18  url: URL,
19  //...
20}
21 
22var editOptions = {
23  onclickSubmit: function(params, postdata) {
24    params.url = URL + '/' + postdata.id;
25  }
26};
27var addOptions = {mtype: "POST"};
28var delOptions = {
29  onclickSubmit: function(params, postdata) {
30    params.url = URL + '/' + postdata;
31  }
32};
33 
34$("#grid")
35    .jqGrid(options)
36    .navGrid('#pager',
37    {}, //options
38    editOptions,
39    addOptions,
40    delOptions,
41    {} // search options
42);

We have customized HTTP method per operation, serialization is handled using JSON and finally URLs for edit and delete operations are now suffixed with /record_id. Now it not only looks, it works! Look at the browser interaction with the server (note different HTTP methods and URLs):



Here is an example of creating a new resource on browser side:



To follow REST principles as closely as possible I return 201 Created response code together with Location header pointing to newly created resource. As you can see data is now being sent to the server in JSON format.

To summarize, such an approach has plenty of advantages:
  • GUI is very responsive, page appears instantly (it can be a static resource served from CDN), while data is loaded asynchronously via AJAX in lightweight JSON format
  • We get CRUD operations for free
  • REST interface for other systems is also for free

Compare this with any web framework out there. And did I mention about this little cherry on our JavaScript frosting: jqGrid is fully compliant with jQuery UI themes and also supports internationalization. Here is the same application with changed theme and language:



Full source code is available on Tomek's GitHub account. The application is self contained, just build it and deploy it to some servlet container.

No comments:

Post a Comment

LinkWithin

Related Posts Plugin for WordPress, Blogger...