Pages

Thursday, April 26, 2012

How to use jQuery grid with struts 2 without plugin ?

When using jQuery with struts 2, the developers are persuaded to use struts2-jQuery plug-in. Because most of the forums and other Internet resources support struts2 jQuery plug in. I have this experience. I wanted to use jQuery grid plug-in with struts 2,but without using struts2 jQuery plug-in. It was very hard for me to find a tutorial or any good resource to implement struts 2 action class to create the jQuery grid without using struts2 jQuery plug-in. Finally, I got through this by myself and intended to post for your convenience.
This tutorial explains, How to create jQuery grid with struts2 without using the plug-in. I filtered this code out from my existing project. The architecture of the project is based on strts2, spring and hibernate integrated environment. I am sure, You can customise these code so that it suits to your environment.

Step 01: Creating entity class for 'Province' master screen. 

I use JPA as a persistence technology and hibernate data access support given by spring (HibernateDaoSupport). I am not going to explain these stuff in detail. My major concern is, How to create the struts 2 action class that supports jQuery grid. Here is my entity class.

Province.java 

/**
 * 
 */
package com.shims.model.maintenance;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import org.hibernate.annotations.Cascade;

import com.shims.model.Audited;

/**
 * @author Semika Siriwardana
 *
 */
 @Entity
 @Table(name="PROVINCE") 
 public class Province extends Audited implements Serializable {

 private static final long serialVersionUID = -6842726343310595087L;
 
 @Id
 @SequenceGenerator(name="province_seq", sequenceName="province_seq")
 @GeneratedValue(strategy = GenerationType.AUTO, generator = "province_seq")
 private Long id;
 
 @Column(name="description", nullable = false)
 private String name;
 
 @Column(name="status", nullable = false)
 private char status;
 
 /**
  * 
  */
 public Province() {
      super();
 }

 /**
  * @param id
  */
 public Province(Long id) {
      super();
      this.id = id;
 }

 /**
  * @return the id
  */
 public Long getId() {
      return id;
 }

 /**
  * @param id the id to set
  */
 public void setId(Long id) {
      this.id = id;
 }

 /**
  * @return the name
  */
 public String getName() {
      return name;
 }

 /**
  * @param name the name to set
  */
 public void setName(String name) {
      this.name = name;
 }

 /**
  * @return the status
  */
 public char getStatus() {
      return status;
 }

 /**
  * @param status the status to set
  */
 public void setStatus(char status) {
      this.status = status;
 }
}

Step 02: Creating JSP file for 'Province' master screen grid.

Keep in mind that jQuery grid is a plug-in for jQuery. So You need to download the relevant CSS file and JS file for the jQuery grid plug-in. You may need to include following resources in the head part of the JSP file.

<link type="text/css" rel="stylesheet" media="screen" href="<%=request.getContextPath()%>/css/jquery/themes/redmond/jquery-ui-1.8.16.custom.css">
<link type="text/css" rel="stylesheet" media="screen" href="<%=request.getContextPath()%>/css/ui.jqgrid.css">
<script src="<%=request.getContextPath()%>/js/jquery-1.6.2.min.js" type="text/javascript"></script>
<script src="<%=request.getContextPath()%>/js/grid.locale-en.js" type="text/javascript"></script>
<script src="<%=request.getContextPath()%>/js/jquery.jqGrid.src.js" type="text/javascript"></script>
<script src="<%=request.getContextPath()%>/js/jquery-ui-1.8.16.custom.min.js" type="text/javascript"></script>

Then, We will create the required DOM contents in JSP file to render the grid. For this, You need only a simple TABLE and DIV elements placed with in the JSP file with the given ID's as follows.

<table id="list"></table> 
<div id="pager"></div>

The 'pager' DIV tag is needed to render the pagination bar of the jQuery grid.

Step 03: Creating JS file for 'Province' master screen grid. 

jQuery grid is needed to be initiated with javascript. I am going to initiate the grid with page on load. There are so many functionalities like adding new records, updating records, deleting records, searching supported by jQuery grid. I guess, You can familiar with those stuff, If You can create the initial grid. This javascript contains only the code that initiating the grid.

var SHIMS = {}
var SHIMS.Province = {
 
 onRowSelect: function(id) {
      //Handle event
 },
 
 onLoadComplete: function() {
      //Handle grid load complete event.
 },

 onLoadError: function() {
      //Handle when data loading into grid failed. 
 },

 /**
  * Initialize grid
  */
 initGrid: function(){
  
  jQuery("#list").jqGrid({ 
         url:CONTEXT_ROOT + '/secure/maintenance/province!search.action', 
         id:"gridtable", 
         caption:"SHIMS:Province Maintenance",
         datatype: "json", 
         pager: '#pager', 
         colNames:['Id','Name','Status'], 
         pagerButtons:true,
         navigator:true,
         jsonReader : {
                      root: "gridModel", 
                      page: "page",
                      total: "total",
                      records: "records",
                      repeatitems: false,      
                      id: "0"      
         }, 
         colModel:[ {
                     name:'id',
                     index:'id', 
                     width:200, 
                     sortable:true, 
                     editable:false, 
                     search:true
                    },{
                     name:'name',
                     index:'name', 
                     width:280, 
                     sortable:true, 
                     editable:true, 
                     search:true, 
                     formoptions:{elmprefix:'(*)'},
                     editrules :{required:true}
                    },{
                     name:'provinceStatus',
                     index:'provinceStatus', 
                     width:200, 
                     sortable:false, 
                     editable:true, 
                     search:false, 
                     editrules:{required:true}, 
                     edittype:'select', 
                     editoptions:{value:'A:A;D:D'}
                 }], 
         rowNum:30, 
         rowList:[10,20,30], 
         width:680,
         rownumbers:true,
         viewrecords:true, 
         sortname: 'id', 
         viewrecords: true, 
         sortorder: "desc", 
         onSelectRow:SHIMS.Province.onRowSelect, 
         loadComplete:SHIMS.Province.onLoadComplete,
         loadError:SHIMS.Province.onLoadError,
         editurl:CONTEXT_ROOT + "/secure/maintenance/province!edit.action" 
         });
 },
 
 /**
  * Invoke this method with page on load.
  */
        
 onLoad: function() {
     this.initGrid();
 }
};

I wish to highlight some code snips from the above code. The 'jsonReader' attribute of the grid initialisation object was the key point where it was difficult to find and spent plenty of times to make the grid work.

root - This should be list of objects.
page - Current page number.
total - This is the total number of pages. For example, if You have 1000 records and the page size is 10, the 'total' value will be 100.

records - The total number of records or count of records.

If You are creating grid with JSON data, the response of the specified 'url' should be JSON response in this format. Sample JSON response is shown bellow.

{"gridModel":[
          {"id":15001,"name":"Western","provinceStatus":"A"},
          {"id":14001,"name":"North","provinceStatus":"A"},
          {"id":13001,"name":"North Central","provinceStatus":"A"},
          {"id":12002,"name":"East","provinceStatus":"A"},
          {"id":12001,"name":"Southern","provinceStatus":"A"}
             ],
"page":1,
"records":11,
"rows":30,
"sidx":"id",
"sord":"desc",
"total":2}

Above fields should be declared in the action class and updated appropriately. I will come to that later. If You intend to use operations like add record, delete record, update record, search etc, You must specify 'editurl'.

With in the JSP file, just above the close body tag, I have placed the following script to call the grid initialisation code.

 var CONTEXT_ROOT = "<%=request.getContextPath()%>";
 jQuery(document).ready(function(){
      SHIMS.Province.onLoad();
 }); 
Step 04: Creating Struts2 action class for 'Province' master screen grid. 

This is the most important part of this tutorial.

ProvinceAction.java 

/**
 * 
 */
package com.shims.web.actions.maintenance.province;

import java.util.List;

import org.apache.log4j.Logger;
import org.apache.struts2.convention.annotation.Namespace;
import org.apache.struts2.convention.annotation.ParentPackage;
import org.apache.struts2.convention.annotation.Result;
import org.apache.struts2.convention.annotation.Results;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;

import com.opensymphony.xwork2.ModelDriven;
import com.shims.dto.Page;
import com.shims.dto.ProvinceDto;
import com.shims.model.maintenance.Province;
import com.shims.service.maintenance.api.ProvinceService;
import com.shims.support.SHIMSSoringSupport;
import com.shims.web.actions.common.BaseAction;
import com.shims.web.common.WebConstants;

/**
 * @author Semika Siriwardana
 *
 */
 @Controller
 @Namespace(WebConstants.MAINTENANCE_NAMESPACE) 
 @ParentPackage(WebConstants.MAINTENANCE_PACKAGE)
 @Results({@Result(name=ProvinceAction.SUCCESS, location="/jsp/maintenance/province.jsp"), 
       @Result(name = "json", type = "json")}) 
 public class ProvinceAction extends BaseAction<Province> implements ModelDriven<Province> {

 private static final long serialVersionUID = -3007855590220260696L;

 private static Logger logger = Logger.getLogger(ProvinceAction.class);
 
 @Autowired
 private ProvinceService provinceService;
 
 private List<Province> gridModel = null;
 
 private Province model = new Province();
 
 private Integer rows = 0;
 private Integer page = 0;
 private String sord;
 private String sidx;
 private Integer total = 0;
 private Integer records = 0;
 
 @Override
 public String execute() {
      return SUCCESS;
 }
 
 /**
  * Search provinces
  * @return
  */
 public String search() throws Exception {
  
      Page<Province> resultPage = provinceService.findByCriteria(model, getRequestedPage(page));
      List<Province> provinceList = resultPage.getResultList(); 
      setGridModel(provinceList); 
      setRecords(resultPage.getRecords());
      setTotal(resultPage.getTotals());
   
      return JSON;
 }

 /**
  * @return the gridModel
  */
 public List<Province> getGridModel() {
      return gridModel;
 }

 /**
  * @param gridModel the gridModel to set
  */
 public void setGridModel(List<Province> gridModel) {
      this.gridModel = gridModel;
 }

 /**
  * @return the page
  */
 public Integer getPage() {
      return page;
 }

 /**
  * @param page the page to set
  */
 public void setPage(Integer page) {
      this.page = page;
 }

 /**
  * @return the rows
  */
 public Integer getRows() {
      return rows;
 }

 /**
  * @param rows the rows to set
  */
 public void setRows(Integer rows) {
      this.rows = rows;
 }

 /**
  * @return the sidx
  */
 public String getSidx() {
      return sidx;
 }

 /**
  * @param sidx the sidx to set
  */
 public void setSidx(String sidx) {
      this.sidx = sidx;
 }

 /**
  * @return the sord
  */
 public String getSord() {
      return sord;
 }

 /**
  * @param sord the sord to set
  */
 public void setSord(String sord) {
      this.sord = sord;
 }

 /**
  * @return the total
  */
 public Integer getTotal() {
      return total;
 }

 /**
  * @param total the total to set
  */
 public void setTotal(Integer total) {
      this.total = total;
 }

 /**
  * @return the records
  */
 public Integer getRecords() {
      return records;
 }

 /**
  * @param records the records to set
  */
 public void setRecords(Integer records) {
      this.records = records;
 }

 @Override
 public Province getModel() {
      return model;
 }
 
}

The getRequestedPage() method is a generic method implemented with in the 'BaseAction' class which returns a 'Page' instance for a given type. 

BaseAction.java

/**
 * 
 */
package com.shims.web.actions.common;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.interceptor.ServletRequestAware;
import org.apache.struts2.interceptor.SessionAware;

import com.opensymphony.xwork2.ActionSupport;
import com.shims.dto.Page;
import com.shims.dto.security.UserDto;
import com.shims.web.common.WebConstants;

import flexjson.JSONSerializer;

/**
 * @author Semika Siriwardana
 *
 */
 public abstract class BaseAction<T> extends ActionSupport implements ServletRequestAware, SessionAware {

 private static final long serialVersionUID = -8209196735097293008L;
 
 protected static final Integer PAGE_SIZE = 10;

 protected HttpServletRequest request;
 
 protected Map<String, Object> session; 
 
 protected String JSON = "json";
 
 public abstract String execute(); 
 
 public HttpServletRequest getRequest() {
      return request;
 }

 @Override
 public void setServletRequest(HttpServletRequest request) {
      this.request = request;
 }

 protected void setRequestAttribute(String key, Object obj) {
      request.setAttribute(key, obj);
 }
 
 /**
  * Returns generic Page instance.
  * @param domain
  * @param employeeDto
  * @return
  */
 protected Page<T> getRequestedPage(Integer page){
      Page<T> requestedPage = new Page<T>(); 
      requestedPage.setPage(page);
      requestedPage.setRows(PAGE_SIZE);
      return requestedPage;
 }
 
 /**
  * @return the session
  */
 public Map<String, Object> getSession() { 
      return session;
 }

 /**
  * @param session the session to set
  */
 public void setSession(Map<String, Object> session) { 
      this.session = session;
 }
 
}

I already explained 'gridModel', 'page', 'total' and 'records'. 'sord' and 'sidx' which are referenced as 'sorting order' and 'sorting index', are passed by the jQuery grid when We sort the data in the grid with some column. To fetch those tow fields, We should declare it with in the action class and provide setter and getter methods. Later, We can sort our data list based on those tow parameters.

Step 05: Implementing service methods. 

From here onwards, most of the techniques are specific to my current project framework. I will explain those so that You can understand, How I developed the related service and DAO methods.
Since, jQuery grid supports for paging, It was need to have a proper way of exchanging grid information from front-end to back-end and then back-end to front-end. I implemented generic 'Page' class for this purpose.

/**
 * 
 */
package com.shims.dto;

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

/**
 * @author semikas
 *
 */
 public class Page<T> {

 /**
  * Query result list.
  */
 private List<T> resultList = new ArrayList<T>(); 
 
 /**
  * Requested page number.
  */
 private Integer page = 1;
 
 /**
  * Number of rows displayed in a single page.
  */
 private Integer rows = 10;
 
 /**
  * Total number of records return from the query.
  */
 private Integer records;
 
 /**
  * Total number of pages.
  */
 private Integer totals;

 /**
  * @return the resultDtoList
  */
 public List<T> getResultList() {
      return resultList;
 }

 /**
  * @param resultDtoList the resultDtoList to set
  */
 public void setResultList(List<T> resultList) {
      this.resultList = resultList;
 }

 /**
  * @return the page
  */
 public Integer getPage() {
      return page;
 }

 /**
  * @param page the page to set
  */
 public void setPage(Integer page) {
      this.page = page;
 }

 /**
  * @return the rows
  */
 public Integer getRows() {
      return rows;
 }

 /**
  * @param rows the rows to set
  */
 public void setRows(Integer rows) {
      this.rows = rows;
 }

 /**
  * @return the records
  */
 public Integer getRecords() {
      return records;
 }

 /**
  * @param records the records to set
  */
 public void setRecords(Integer records) {
      this.records = records;
 }

 /**
  * @return the totals
  */
 public Integer getTotals() {
      return totals;
 }

 /**
  * @param totals the totals to set
  */
 public void setTotals(Integer totals) {
      this.totals = totals;
 }
 
}

Also, with some search criteria, We can fetch the data and update the grid accordingly.

My service interface and implementation classes are as follows.

ProvinceService.java 

/**
 * 
 */
package com.shims.service.maintenance.api;

import java.util.List;

import com.shims.dto.Page;
import com.shims.exceptions.ServiceException;
import com.shims.model.maintenance.Province;

/**
 * @author Semika Siriwardana
 *
 */
public interface ProvinceService {

 /**
  * Returns list of provinces for a given search criteria.
  * @return
  * @throws ServiceException
  */
  public Page<Province> findByCriteria(Province searchCriteria, Page<Province> page) throws ServiceException;
 
}

ProvinceServiceImpl.java 

/**
 * 
 */
package com.shims.service.maintenance.impl;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import com.shims.dto.Page;
import com.shims.exceptions.ServiceException;
import com.shims.model.maintenance.Province;
import com.shims.persist.maintenance.api.ProvinceDao;
import com.shims.service.maintenance.api.ProvinceService;


/**
 * @author Semika Siriwardana
 *
 */
 @Service
 public class ProvinceServiceImpl implements ProvinceService {

 @Autowired
 private ProvinceDao provinceDao; 
 
 /**
  * {@inheritDoc} 
  */
 @Override
 public Page<Province> findByCriteria(Province searchCriteria, Page<Province> page) throws ServiceException {
  
      Page<Province> resultPage = provinceDao.findByCriteria(searchCriteria, page); 
  
      return resultPage;
 }

}

I am using 'page' instance to exchange the grid information in between front end and back end.

Next, I will explain the other important part of this tutorial.

Step 06: Implementing data access method. 

In the DAO method, We should filtered only the records of the requested page by the user and also should be updated the 'page' instance attributes so that those should be reflected to the grid. Since, I am using hibernated, I used 'Criteria' to retrieve the required data from the database. You implement this in your way, But it should update the grid information properly.

ProvinceDao.java 
 
/**
 * 
 */
package com.shims.persist.maintenance.api;

import com.shims.dto.Page;
import com.shims.exceptions.DataAccessException;
import com.shims.model.maintenance.Province;
import com.shims.persist.common.GenericDAO;

/**
 * @author Semika Siriwardana
 *
 */
public interface ProvinceDao extends GenericDAO<Province, Long> { 

 /**
  * Returns search results for a given search criteria.
  * @param searchCriteria
  * @param page
  * @return
  * @throws DataAccessException
  */
  public Page<Province> findByCriteria(Province searchCriteria, Page<Province> page) throws DataAccessException;
 
}

ProvinceDaoImpl.java

/**
 * 
 */
package com.shims.persist.maintenance.impl;

import java.util.List;

import org.hibernate.Criteria;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.MatchMode;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.shims.dto.Page;
import com.shims.exceptions.DataAccessException;
import com.shims.model.maintenance.Province;
import com.shims.persist.maintenance.api.ProvinceDao;

/**
 * @author Semika Siriwardana
 *
 */
 @Repository
 public class ProvinceDaoImpl extends AbstractMaintenanceDaoSupport<Province, Long> implements ProvinceDao {

 @Autowired
 public ProvinceDaoImpl(SessionFactory sessionFactory) {
      setSessionFactory(sessionFactory);
 }

 /**
  * {@inheritDoc} 
  */
 @SuppressWarnings("unchecked")
 @Override
 public Page<Province> findByCriteria(ProvinceDto searchCriteria, Page<Province> page) throws SHIMSDataAccessException {
  
      Criteria criteria = getSession().createCriteria(Province.class);
  
      if (searchCriteria.getName() != null && searchCriteria.getName().trim().length() != 0) {
          criteria.add(Restrictions.ilike("name", searchCriteria.getName(), MatchMode.ANYWHERE)); 
   
      }
  
      //get total number of records first
      criteria.setProjection(Projections.rowCount());
      Integer rowCount = ((Integer)criteria.list().get(0)).intValue();
  
      //reset projection to null
      criteria.setProjection(null);
  
      Integer to = page.getPage() * page.getRows();
      Integer from = to - page.getRows();
  
      criteria.setFirstResult(from);
      criteria.setMaxResults(to); 
  
      //calculate the total pages for the query
      Integer totNumOfPages =(int) Math.ceil((double)rowCount / (double)page.getRows());
  
      List<Province> privinces = (List<Province>)criteria.list(); 
  
      //Update 'page' instance.
      page.setRecords(rowCount); //Total number of records
      page.setTotals(totNumOfPages); //Total number of pages
      page.setResultList(privinces);
  
      return page; 
 }
}

I think, This will be a good help for the developers who wish to use pure jQuery with struts2. If You find more related to this topic, please post bellow.
Share

Widgets