Sunday, December 12, 2010

jqGrid and Spring 3 MVC Integration Tutorial

In this tutorial we will build a Spring 3 MVC application that utilizes annotations in the business layer and jqGrid in the presentation layer. A working knowledge of Spring MVC, JQuery, and jqGrid is assumed for this tutorial. If you need guidance regarding jqGrid, feel free to visit the jqGrid Documentation and also search the www.stackoverflow.com for questions about jqGrid configuration. For Spring and JQuery, a simple Google search will provide numerous tutorials.

Note: An updated version of this tutorial is now accessible at Spring MVC 3.1, jqGrid, and Spring Data JPA Integration Guide

What is jqGrid?
jqGrid is an Ajax-enabled JavaScript control that provides solutions for representing and manipulating tabular data on the web. Since the grid is a client-side solution loading data dynamically through Ajax callbacks, it can be integrated with any server-side technology, including PHP, ASP, Java Servlets, JSP, ColdFusion, and Perl.

jqGrid uses a jQuery Java Script Library and is written as plugin for that package.

Source: http://www.trirand.com/jqgridwiki/doku.php
Here's a screenshot of what we will be doing:



Our application is a simple CRUD system for managing a list of users. The presentation layer uses a jqGrid to display the data which is built on top of the JQuery, a great JavaScript framework. The business layer is composed of simple Spring beans annotated with @Controller and @Service. Our data is provided by an in-memory dummy list of users. Switching to a real data provider, like a database, should be easy enough. Notice in this tutorial I strive
to stick with simple POJO development. This makes the application easy to comprehend as well to debug. Comments are provided within the code to facilitate in the comprehension of this material.

Let's start by defining our domain.

Our domain is quite simple. It only contains a single object. We have a User class with three fields: id, firstName, lastName
package org.krams.tutorial.domain;

/**
 * A simple POJO to represent our user domain  
 *
 */
public class User {

 private Long id;
 private String firstName;
 private String lastName;
 
 public Long getId() {
  return id;
 }
 public void setId(Long id) {
  this.id = id;
 }
 
 public String getFirstName() {
  return firstName;
 }
 public void setFirstName(String firstName) {
  this.firstName = firstName;
 }
 
 public String getLastName() {
  return lastName;
 }
 public void setLastName(String lastName) {
  this.lastName = lastName;
 }

}
Now we define our service layer.

The service handles the business logic of our application. It's provides CRUD operations for the User object, like adding, editing, deleting, and retrieving all users. Following good OOP practice we declare an interface first:

Our service implementation is a simple class annotated with the @Service to make it service, and @Transactional to make it transactional as well. Our service manipulates an in-memory array of users. Then init() method is the one responsible for initializing our dummy users.

Here's the service implementation:
/**
 * 
 */
package org.krams.tutorial.service;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Handles CRUD services for users
 * 
 */
@Service("userService")
@Transactional
public class UserService implements IUserService   {
  
 private List dummyUsersList = new ArrayList();
 
 protected static Logger logger = Logger.getLogger("service");

 public UserService() {
  // Initialize our in-memory list of users
  init();
 }
 
 public List getAll() {
  logger.debug("Retrieving all users");

  return dummyUsersList;
 }
 
 public User get( String id ) {
  logger.debug("Retrieving an existing user");
  
  return dummyUsersList.get( Integer.valueOf(id) );
  
 }

 public Boolean add( User user ) {
  logger.debug("Adding a new user");
  
  try {
   // Assign a new id
   user.setId( Long.valueOf(dummyUsersList.size()) );
   
   dummyUsersList.add(user);
   return true;
  } catch (Exception e) {
   return false;
  }
 }
 
 public Boolean delete( User user ) {
  logger.debug("Deleting an existing user");
  
  try {
   // Retrieve id to delete
   Long id =  Long.valueOf( user.getId().toString() );
   
   // Loop array
   for ( User dummyUser: dummyUsersList) {
    if ( dummyUser.getId().compareTo(id) == 0 ) {
     dummyUsersList.remove(dummyUser); 
     break;
    }
   }
   
   return true;
  } catch (Exception e) {
   return false;
  }

 }
  
 public Boolean edit( User user ) {
  logger.debug("Editing an existing user");
  
  try {
   // Retrieve id to edit
   Long id =  Long.valueOf( user.getId().toString() );
   
   // Loop array
   for ( User dummyUser: dummyUsersList) {
    if ( dummyUser.getId().compareTo(id) == 0 ) {
     dummyUser.setFirstName( user.getFirstName());
     dummyUser.setLastName( user.getLastName());
     break;
    }
   }
   return true;
  } catch (Exception e) {
   return false;
  }
  
 }
 
 private void init() {
  // Populate our in-memory, dummy list of users
  // Normally, the data should come from your DAOs or your persitence layer

  logger.debug("Init in-memory users");
  
  User user = new User();
  user.setId(Long.valueOf("1"));
  user.setFirstName("John");
  user.setLastName("Smith");
  dummyUsersList.add(user);
  
  user = new User();
  user.setId(Long.valueOf("2"));
  user.setFirstName("Jane");
  user.setLastName("Adams");
  dummyUsersList.add(user);
  
  user = new User();
  user.setId(Long.valueOf("3"));
  user.setFirstName("Jeff");
  user.setLastName("Mayer");
  dummyUsersList.add(user);
 }
}
Let's move on to the controllers.

In this application we declare two controllers:
1. MediatorController
2. UserController

The purpose of the MediatorController is to handle and show the JSP page that contains the jqGrid itself. The JSP page is mapped to the URI template /main/users. To access the JSP page, you type the root path of your web application then followed by /main/users

Notice our controllers are simple classes annotated with @Controller and @RequestMapping. Same with the @Service annotation, this class becomes a Spring Controller that's capable to handle URI-template-based requests.

Here's the MediatorController:
package org.krams.tutorial.controller;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

/**
 * Handles CRUD requests for users
 * 
 */
@Controller
@RequestMapping("/main")
public class MediatorController {

 protected static Logger logger = Logger.getLogger("controller");
 
 /**
     * Retrieves the JSP page that contains our JqGrid
     */
    @RequestMapping(value = "/users", method = RequestMethod.GET)
    public String getUsersPage() {
     logger.debug("Received request to show users page");
    
     // This will resolve to /WEB-INF/jsp/users.jsp page
     return "users";
 }
}
On the other hand the UserController does not display any JSP page. It's job is to handle JSON request from the jqGrid and respond with JSON as well. The UserController receives the CRUD operations which it delegates to the UserService. When the UserService is done processing, the UserController responds back to the jqGrid.

Here's the UserController:
/**
 * 
 */
package org.krams.tutorial.controller;

import java.util.List;

import javax.annotation.Resource;

import org.apache.log4j.Logger;
import org.krams.tutorial.domain.User;
import org.krams.tutorial.json.CustomGenericResponse;
import org.krams.tutorial.json.CustomUserResponse;
import org.krams.tutorial.service.IUserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * Handles CRUD requests for users
 * 
 */
@Controller
@RequestMapping("/crud")
public class UserController {
 
  protected static Logger logger = Logger.getLogger("controller");
 
   @Resource(name="userService")
   private IUserService userService;
   
   /**
    * The default method when a request to /users is made.
    * This essentially retrieves all users, which are wrapped inside a CustomUserResponse object.
    * The object is automatically converted to JSON when returning back the response.
    * The @ResponseBody is responsible for this behavior.
    */
        @RequestMapping(method = RequestMethod.GET)
        public @ResponseBody  CustomUserResponse getAll(
        ) {
         logger.debug("Received request to get all users");

         // Retrieve all users from the service
         List users = userService.getAll();
         
         // Initialize our custom user response wrapper
         CustomUserResponse response = new CustomUserResponse();
         
         // Assign the result from the service to this response
         response.setRows(users);

         // Assign the total number of records found. This is used for paging
         response.setRecords( String.valueOf(users.size()) );
         
         // Since our service is just a simple service for teaching purposes
         // We didn't really do any paging. But normally your DAOs or your persistence layer should support this
         // Assign a dummy page
         response.setPage( "1" );
         
         // Same. Assign a dummy total pages
         response.setTotal( "10" );
         
         // Return the response
         // Spring will automatically convert our CustomUserResponse as JSON object. 
         // This is triggered by the @ResponseBody annotation. 
         // It knows this because the JqGrid has set the headers to accept JSON format when it made a request
         // Spring by default uses Jackson to convert the object to JSON
         return response;
     }
        
        /**
         * Edit the current user.
         */
        @RequestMapping(value = "/edit", method = RequestMethod.POST)
        public @ResponseBody CustomGenericResponse edit(
          @RequestParam("id") String id,
          @RequestParam("firstName") String firstName,
          @RequestParam("lastName") String lastName
        ) {
         logger.debug("Received request to edit user");
        
         // Construct our user object
         // Assign the values from the parameters
         User user = new User();
         user.setId( Long.valueOf(id) );
         user.setFirstName(firstName);
         user.setLastName(lastName);
         
         // Do custom validation here or in your service
         
         // Call service to edit
         Boolean success = userService.edit(user);
         
         // Check if successful
         if ( success == true ) {
          // Success. Return a custom response
          CustomGenericResponse response = new CustomGenericResponse();
       response.setSuccess(true);
       response.setMessage("Action successful!");
          return response;
          
         } else {
          // A failure. Return a custom response as well
          CustomGenericResponse response = new CustomGenericResponse();
       response.setSuccess(false);
       response.setMessage("Action failure!");
          return response;
         }
 
     }
        
        /**
         * Add a new user
         */
        @RequestMapping(value = "/add", method = RequestMethod.POST)
        public @ResponseBody CustomGenericResponse add(
          @RequestParam("firstName") String firstName,
          @RequestParam("lastName") String lastName
        ) {
         logger.debug("Received request to add a new user");
         
         // Construct our new user object. Take note the id is not required.
         // Assign the values from the parameters
         User user = new User();
         user.setFirstName(firstName);
         user.setLastName(lastName);
         
         // Do custom validation here or in your service
         
         // Call service to add
         Boolean success = userService.add(user);
         
         // Check if successful
         if ( success == true ) {
          // Success. Return a custom response
          CustomGenericResponse response = new CustomGenericResponse();
       response.setSuccess(true);
       response.setMessage("Action successful!");
          return response;
          
         } else {
          // A failure. Return a custom response as well
          CustomGenericResponse response = new CustomGenericResponse();
       response.setSuccess(false);
       response.setMessage("Action failure!");
          return response;
         }
         
     }
        
        /**
         * Delete an existing user
         */
        @RequestMapping(value = "/delete", method = RequestMethod.POST)
        public @ResponseBody CustomGenericResponse delete(
          @RequestParam("id") String id
        ) {
         
         logger.debug("Received request to delete an existing user");
         
         // Construct our user object. We just need the id for deletion.
         // Assign the values from the parameters
         User user = new User();
         user.setId( Long.valueOf(id) );
         
         // Do custom validation here or in your service
         
         // Call service to add
         Boolean success = userService.delete(user);
         
         // Check if successful
         if ( success == true ) {
          // Success. Return a custom response
          CustomGenericResponse response = new CustomGenericResponse();
       response.setSuccess(true);
       response.setMessage("Action successful!");
          return response;
          
         } else {
          // A failure. Return a custom response as well
          CustomGenericResponse response = new CustomGenericResponse();
       response.setSuccess(false);
       response.setMessage("Action failure!");
          return response;
         }
     }
        
}
The UserController has four mappings:
/crud/add
/crud/edit
/crud/delete
/crud

To make an add request, a jqGrid should call the /crud/add URI template. To make an edit request, call the /crud/edit URI template. And so forth.

When you call a get request from the Spring Controller, here's the exact JSON data that's being transported:


You can easily retrieve this data by calling directly the JSON controller via a third-party tool. I use RESTClient by WizTools to verify the response. Here's a screenshot of the tool:


You can download the tool at RESTClient

The UserController has two types of response:
1. CustomGenericResponse
2. CustomUserResponse

These responses are custom POJO classes that I created. The idea of these classes is to contain JSON object within a Java class. It's easier to manipulate a Java object than a JSON object inside Java. The conversion from JSON to Java is done automatically by Spring! It uses Jackson to do the conversion.

Here's the CustomGenericResponse:

The CustomUserResponse is a wrapper containing an array of users, along with some extra data used by a jqGrid, like paging, total records, and alike. The reasoning for the structure of this class is dictated by the standard parameters that a jqGrid expects. Check the following jqGrid doc jqGrid JSON format

Here's CustomUserResponse:


Let's move on to the JSP page.

The JSP page contains the jqGrid. I added custom buttons on the grid itself. The jqGrid declaration below has some extra features that we will not be using for this tutorial. I put them here anyway just in case I write another tutorial utilizing these extra features.

Here's a screenshot of these custom buttons:

On the body of the JSP, we declare the following:

users.jsp
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
 <link rel="stylesheet" type="text/css" media="screen" href="/spring-jqgrid-integration/resources/css/jquery/ui-lightness/jquery-ui-1.8.6.custom.css" />
 <link rel="stylesheet" type="text/css" media="screen" href="/spring-jqgrid-integration/resources/css/jqgrid/ui.jqgrid.css" />

 <script type="text/javascript" src="/spring-jqgrid-integration/resources/js/jquery/jquery-1.4.4.min.js"></script>
 <script type="text/javascript">
     var jq = jQuery.noConflict();
 </script>
 <script type="text/javascript" src="/spring-jqgrid-integration/resources/js/jquery/jquery-ui-1.8.6.custom.min.js"></script> 
 <script type="text/javascript" src="/spring-jqgrid-integration/resources/js/jqgrid/grid.locale-en.js" ></script>
 <script type="text/javascript" src="/spring-jqgrid-integration/resources/js/jqgrid/jquery.jqGrid.min.js"></script>
 
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
 <title>JqGrid - Spring 3 MVC Integration Tutorial</title>
 
</head>

<body >

<script type="text/javascript">
 jq(function() {
  jq("#grid").jqGrid({
      url:'/spring-jqgrid-integration/krams/crud',
   datatype: 'json',
   mtype: 'GET',
      colNames:['Id', 'First Name', 'Last Name'],
      colModel:[
       {name:'id',index:'id', width:55,editable:false,editoptions:{readonly:true,size:10},hidden:true},
       {name:'firstName',index:'lastName', width:100,editable:true, editrules:{required:true}, editoptions:{size:10}},
       {name:'lastName',index:'firstName', width:100,editable:true, editrules:{required:true}, editoptions:{size:10}}
      ],
      postData: { 
   },
   rowNum:20,
      rowList:[20,40,60],
      height: 200,
      autowidth: true,
   rownumbers: true,
      pager: '#pager',
      sortname: 'id',
      viewrecords: true,
      sortorder: "asc",
      caption:"Users",
      emptyrecords: "Empty records",
      loadonce: false,
      loadComplete: function() {
   },
      jsonReader : {
          root: "rows",
          page: "page",
          total: "total",
          records: "records",
          repeatitems: false,
          cell: "cell",
          id: "id"
      }
  });
  jq("#grid").jqGrid('navGrid','#pager',
    {edit:false,add:false,del:false,search:true},
    { },
          { },
          { }, 
    { 
        sopt:['eq', 'ne', 'lt', 'gt', 'cn', 'bw', 'ew'],
           closeOnEscape: true, 
            multipleSearch: true, 
             closeAfterSearch: true }
  );


  
  jq("#grid").navButtonAdd('#pager',
    {  caption:"Add", 
     buttonicon:"ui-icon-plus", 
     onClickButton: addRow,
     position: "last", 
     title:"", 
     cursor: "pointer"
    } 
  );
  
  jq("#grid").navButtonAdd('#pager',
    {  caption:"Edit", 
     buttonicon:"ui-icon-pencil", 
     onClickButton: editRow,
     position: "last", 
     title:"", 
     cursor: "pointer"
    } 
  );
  
  jq("#grid").navButtonAdd('#pager',
   {  caption:"Delete", 
    buttonicon:"ui-icon-trash", 
    onClickButton: deleteRow,
    position: "last", 
    title:"", 
    cursor: "pointer"
   } 
  );

  jq("#btnFilter").click(function(){
   jq("#grid").jqGrid('searchGrid',
     {multipleSearch: false, 
      sopt:['eq']}
   );
  });

  // Toolbar Search
  jq("#grid").jqGrid('filterToolbar',{stringResult: true,searchOnEnter : true, defaultSearch:"cn"});

 });
</script>
  

<script type="text/javascript">

function addRow() {

 // Get the currently selected row
    jq("#grid").jqGrid('editGridRow','new',
      {  url: "/spring-jqgrid-integration/krams/crud/add", 
     editData: {
       },
       recreateForm: true,
       beforeShowForm: function(form) {
       },
    closeAfterAdd: true,
    reloadAfterSubmit:false,
    afterSubmit : function(response, postdata) 
    { 
           var result = eval('(' + response.responseText + ')');
     var errors = "";
     
           if (result.success == false) {
      for (var i = 0; i < result.message.length; i++) {
       errors +=  result.message[i] + "<br/>";
      }
           }  else {
            jq("#dialog").text('Entry has been added successfully');
      jq("#dialog").dialog( 
        { title: 'Success',
         modal: true,
         buttons: {"Ok": function()  {
          jq(this).dialog("close");} 
         }
        });
                 }
        // only used for adding new records
        var new_id = null;
        
     return [result.success, errors, new_id];
    }
      });

}

function editRow() {
 // Get the currently selected row
 var row = jq("#grid").jqGrid('getGridParam','selrow');
 
 if( row != null ) 
  jq("#grid").jqGrid('editGridRow',row,
   { url: "/spring-jqgrid-integration/krams/crud/edit", 
    editData: {
          },
          recreateForm: true,
          beforeShowForm: function(form) {
          },
    closeAfterEdit: true,
    reloadAfterSubmit:false,
    afterSubmit : function(response, postdata) 
    { 
              var result = eval('(' + response.responseText + ')');
     var errors = "";
     
              if (result.success == false) {
      for (var i = 0; i < result.message.length; i++) {
       errors +=  result.message[i] + "<br/>";
      }
              }  else {
               jq("#dialog").text('Entry has been edited successfully');
      jq("#dialog").dialog( 
        { title: 'Success',
         modal: true,
         buttons: {"Ok": function()  {
          jq(this).dialog("close");} 
         }
        });
                 }
           
     return [result.success, errors, null];
    }
   });
 else jq( "#dialogSelectRow" ).dialog();
}

function deleteRow() {
 // Get the currently selected row
    var row = jq("#grid").jqGrid('getGridParam','selrow');

    // A pop-up dialog will appear to confirm the selected action
 if( row != null ) 
  jq("#grid").jqGrid( 'delGridRow', row,
           { url: '/spring-jqgrid-integration/krams/crud/delete', 
      recreateForm: true,
               beforeShowForm: function(form) {
                 //change title
                 jq(".delmsg").replaceWith('<span style="white-space: pre;">' +
                   'Delete selected record?' + '</span>');
                 
        //hide arrows
                 jq('#pData').hide();  
                 jq('#nData').hide();  
               },
              reloadAfterSubmit:false,
              closeAfterDelete: true,
              afterSubmit : function(response, postdata) 
      { 
                   var result = eval('(' + response.responseText + ')');
       var errors = "";
       
                   if (result.success == false) {
        for (var i = 0; i < result.message.length; i++) {
         errors +=  result.message[i] + "<br/>";
        }
                   }  else {
                    jq("#dialog").text('Entry has been deleted successfully');
        jq("#dialog").dialog( 
          { title: 'Success',
           modal: true,
           buttons: {"Ok": function()  {
            jq(this).dialog("close");} 
           }
          });
                   }
                   // only used for adding new records
                   var new_id = null;
                   
       return [result.success, errors, new_id];
      }
           });
  else jq( "#dialogSelectRow" ).dialog();
}

</script>  
  
<p>JqGrid - Spring 3 MVC Integration Tutorial</p>
<div id="jqgrid">
 <table id="grid"></table>
 <div id="pager"></div>
</div>

<div id="dialog" title="Feature not supported" style="display:none">
 <p>That feature is not supported.</p>
</div>

<div id="dialogSelectRow" title="Warning" style="display:none">
 <p>Please select row</p>
</div>

</body>

</html>

For the Javascript functions, addRow, editRow, deleteRow, each declares a custom URL. For example, the deleteRow declares the following URL:


This URL corresponds the controller delete URI template: /crud/delete.

Another important setting you must look at is the jsonReader we declared inside the jqGrid:
jsonReader : {
root: "rows",
page: "page",
total: "total",
records: "records",
repeatitems: false,
cell: "cell",
id: "id"
}
It turns out all you need to declare among those properties is the repeatitems: false. You can remove the root, page, total, records, cell, and id. The application will still run. I believe jqGrid has an internal naming convention that it expects. By default if your JSON data has the following format:

Then it's following convention. Try changing the rows to something like arbirtrary, i.e mydog. Your grid will not show any data.

If that's the case, make sure you set the root property in your jsonReader like the following:
jsonReader : {
root: "mydog",
repeatitems: false
}
Another important setting you must be wary of is the colModel names.

Our jqGrid expects the following colModel names:
colModel:[
{name:'id', ...}
{name:'firstName', ...},
{name:'lastName, ...}
],
These names must exactly match the properties you're passing in your JSON data. It's case sensitive! FIRSTNAME and firstName are not the same names!

Thanks to Oleg for pointing these nuances. He's the guy I consider expert when it comes to jqGrid. He's an active guy in www.stackoverflow.com. Ask a jqGrid question there and he'll more than likely answer it.

Of course, you need to add the JQuery and jqGrid library on the head section of your JSP. Otherwise, you won't see any grids. Make sure the path is correct.

I need to point one critical JQuery configuration, however. If you notice on standard JQuery examples, most use the $, but in this tutorial you don't see that sign! Instead you see the jq. The jq and $ are equivalent. To force JQuery to use a different identifier, declare the following on the head section of your JSP:
Let's return to Spring.

Typical with any Spring application, we must declare a few required beans in the xml configuration.

applicationContext.xml


spring-servlet.xml


web.xml

  spring
  org.springframework.web.servlet.DispatcherServlet
  1
 
 
  spring
  /krams/*
 

 
  org.springframework.web.context.ContextLoaderListener
 
Take note of the URL pattern. When accessing any pages in our MVC application, the host name must be appended with
/krams

Run the Application

To run the application, please use the following URL:
http://localhost:8080/spring-jqgrid-integration/krams/main/users

Some reminders.
1. Download the Jackson library and put it in your classpath.
2. Download the JQuery framework and put it in your resources folder
3. Download the jqGrid plugin .
4. You may need to customize the URLs declared in the JSP

The best way to learn further is to try the actual application.

Download the project
You can access the project site at Google's Project Hosting at http://code.google.com/p/jqgrid-spring3mvc-integration-tutorial/

You can run the project directly using an embedded server via Maven.
For Tomcat: mvn tomcat:run
For Jetty: mvn jetty:run

You can download the project as a Maven build. Look for the spring-jqgrid-integration.zip in the Download sections.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: jqGrid and Spring 3 MVC Integration Tutorial ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share