Friday, January 7, 2011

Spring 3: Dynamic MVC using jqGrid and MongoDB

In this tutorial we will create a simple Spring MVC 3 application that uses a document-oriented database: MongoDB, for its persistence layer and a JQuery plugin: jqGrid, for its presentation layer. We will explore and discover the dynamic relationship of both technlogies, along with Spring MVC 3. Our application is a simple CRUD service for managing a list of Users. We will provide facilities for adding, deleting, editing, and viewing of all registered users.

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 how our jqGrid-powered application would look like:


In a nutshell jqGrid is a table for manipulating data. It's an AJAX application that's built on top of JQuery. It communicates via JSON.

What is MongoDB?
MongoDB (from "humongous") is a scalable, high-performance, open source, document-oriented database. Written in C++, MongoDB features:
  • Document-oriented storage
  • Full Index Support
  • Replication & High Availability
  • Scale horizontally without compromising functionality.
  • Rich, document-based queries.
  • Atomic modifiers for contention-free performance.
  • Flexible aggregation and data processing.
  • Store files of any size without complicating your stack.
  • Enterprise class support, training, and consulting available.

Source: http://www.mongodb.org/
Here's our database schema:
{  
   id:'',
   firstName:'',
   lastName:'',
   money:''
}
In a nutshell MongoDB is a database that uses JSON instead of SQL There's no static schema to create. All schemas are dynamic, meaning you create them on-the-fly. You can try a real-time online shell for MongoDB at http://try.mongodb.org/. Visit the official MongoDB site for a through discussion.

In order to complete this tutorial, you will be required to install a copy of MongoDB. If you don't have a MongoDB yet, grabe one now by visiting this link http://www.mongodb.org/display/DOCS/Quickstart. The installation is really easy.

Let's begin by defining our MongoDBFactory.

MongoDBFactory

MongoDBFactory is simply a factory for retrieving a single instance of your database via getDB() and a single instance of your collection via getCollection(). This is a custom class we created to simplify the retrieval of these items. If you prefer to retrieve them manually instead of using this MongoDBFactory, you're free to do so. Here's an example on how you may retrieve them manually:

Remember our database and collections will be created on-the-fly. We will not create a domain object here because the fields of our jqGrid exactly matches our dynamic schema in MongoDB. This is intentional. What happens if we change the fields in jqGrid? Then we can simply create a new schema in MongoDB. What if we change the schema in MongoDB, then we can simply change the fields in jqGrid. This is what makes them dynamic.

Let's start.

First, we declare our service interface.

IUserService

Here's the service implementation:

UserService

This service defines our basic CRUD system. We have the following methods:
getAll() - for retrieving all persons
edit() - for editing
delete() - for deleting
add() - for adding
get() - for retrieving single person

The database is initialized once in the UserService' constructor through the init() method:


Notice we're creating a dynamic JSON schema here with the following format:
{  
   id:'',
   firstName:'',
   lastName:''
}

We have declared our service. Let's now declare a controller.

MediatorController

This controller declares a single mapping:
/main/users
This loads a JSP page containing our jqGrid.


Here's the JSP page:

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">

<c:url value="spring-jqgrid-mongo" var="baseUrl"/>

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

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

<body >

<script type="text/javascript">
 jq(function() {
  jq("#grid").jqGrid({
      url:'/${baseUrl}/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: "/${baseUrl}/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: "/${baseUrl}/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: '/${baseUrl}/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>Spring MVC 3: jqGrid and MongoDB 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>

Notice for each of the major jqGrid functions, there's a corresponding URL:
/${baseUrl}/krams/crud - retrieves all users in JSON format
/${baseUrl}/krams/crud/add" - adds a new user
/${baseUrl}/krams/crud/edit - edits an existing user
/${baseUrl}/krams/crud/delete - deletes an existing user
These are used by JQuery when performing AJAX calls. A separate controller handles the calls.

UserController

Each URLs are mapped to a specific handler method in the controller. When the controller receives the request, it delegates processing to the service. If the service returns a successful message, a success message is returned. Likewise, if the service returns a failure message, a failure message is returned.


CustomGenericResponse is a simple POJO where can assign our custom responses. When the object is returned, Spring will automatically convert it to a JSON object, which the jqGrid understands

CustomGenericResponse

However, when retrieving all users, we wrapped the response in a CustomUserResponse object instead


CustomUserResponse

Let's finalize our Spring MVC application by declaring the required XML configurations.

To enable Spring MVC we need to add it in the web.xml

web.xml

Take note of the URL pattern. When accessing any pages in our MVC application, the host name must be appended with
/krams
In the web.xml we declared a servlet-name spring. By convention, we must declare a spring-servlet.xml as well.

spring-servlet.xml

By convention, we must declare an applicationContext.xml as well.

applicationContext.xml

That's it. We've managed to create a simple Spring MVC 3 application that uses MongoDB for its database and jqGrid for its presentation layer. We've seen how we can map our schema from MongoDB to jqGrid easily.

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 download the project as a Maven build. Look for the spring-jqgrid-mongo.zip in the Download sections.

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

If you want to learn more about Spring MVC and integration with other technologies, feel free to read my other tutorials in the Tutorials section.
StumpleUpon DiggIt! Del.icio.us Blinklist Yahoo Furl Technorati Simpy Spurl Reddit Google I'm reading: Spring 3: Dynamic MVC using jqGrid and MongoDB ~ Twitter FaceBook

Subscribe by reader Subscribe by email Share

15 comments:

  1. great tutorial.. but the paging is not working yet

    ReplyDelete
  2. Thanks. That was intentional. The paging and search feature of jqGrid is another topic :) We'll make one next time.

    ReplyDelete
  3. Hi,

    Got the response

    {"total":"10","page":"1","records":"3","rows":[{"id":1,"firstName":"John","lastName":"Smith"},{"id":2,"firstName":"Jane","lastName":"Adams"},{"id":3,"firstName":"Jeff","lastName":"Mayer"}]}

    in the browser instead of jqgrid, any idea what's wrong here?

    ReplyDelete
  4. Seems like mime type set to application/json and only Chrome could display it.
    Looks like users.jsp won't get called, I am using the Maven project(and new to Spring MVC), any Idea how to fix this?

    ReplyDelete
  5. In case is of help to anyone using this example - I was getting responses 406 not acceptable for grid upates until I put jackson-mapper-asl in my classpath. Maybe this is pulled in autmatically by Maven - I was updating jars manually due to repo issues.

    ReplyDelete
  6. plz give me netbeans src for the same

    ReplyDelete
  7. could you please do the same with mysql? thank you

    ReplyDelete
  8. krams, do you have any ideas how records with nested records could be displayed? Let's say I have user table (id, username, password, enabled) and authorities table(id, userId, authority). User entity has
    @OneToMany(mappedBy="crdUser", fetch=FetchType.EAGER)
    private List crdAuthorities;
    and Authority has
    @ManyToOne(fetch=FetchType.EAGER)
    @JoinColumn(name="USER_ID")
    private CrdUser crdUser;
    When I used your tutorial, I have tried to display users without displaying authorities. When I'm requesting page, I'm getting request to save file with continues empty records:
    {"records":"3","total":"10","rows":[{"username":"admin","enabled":"y","userId":1,"crdAuthorities":[{"authorityId":1,"crdUser":{"username":"admin","enabled":"y","userId":1,"crdAuthorities":[{"authorityId":1,"crdUser":
    {"username":"admin"....

    and error in the logs:

    at org.codehaus.jackson.map.ser.ContainerSerializers$CollectionSerializer.serializeContents(ContainerSerializers.java:363)
    at org.codehaus.jackson.map.ser.ContainerSerializers$CollectionSerializer.serializeContents(ContainerSerializers.java:314)
    at org.codehaus.jackson.map.ser.ContainerSerializers$AsArraySerializer.serialize(ContainerSerializers.java:112)
    at org.codehaus.jackson.map.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:268)
    at org.codehaus.jackson.map.ser.BeanSerializer.serializeFields(BeanSerializer.java:160)

    Same records works fine for authorization and authentication (based on spring security). Any ideas what should I be doing in order to display mapped records properly? Ideally, I need to display list of users with their information and authorities (might be in a form of drop down list) that they have. Could you help me to find any information that can help to solve this problem?

    ReplyDelete
  9. If you're using JPA and Jackson, you have to convert your domain objects to dtos before presenting them to the presentation layer. If you have bidrectional associations between objects, i.e User and Role, Jackson will create an endless loop of mapping. Usually I create a UserDto (for example) that contains the exact role instead of a reference to the Role class.

    ReplyDelete
  10. hi i could not able fill grid the data.please help regarding this.

    ReplyDelete
  11. i want replace type="text" to type="checkbox" but how ? please help

    ReplyDelete
  12. I have read your blog its very attractive and impressive. I like it your blog.

    Spring online training Spring online training Spring Hibernate online training Spring Hibernate online training Java online training

    spring training in chennai spring hibernate training in chennai

    ReplyDelete
  13. Very Interesting information shared than other blogs
    Thanks for Sharing and Keep updating us

    ReplyDelete