Himu\’s Attempt at Blogging

Tidbits from my thoughts

Spring MVC Tutorial – Paging Through Hibernate and Selection Handling

with 14 comments

In this installment, I’m expanding the project done in Spring MVC Tutorial – Hibernate Integration to include:

  1. Browsing/paging through a table with a navigation bar found in various forum sites and ASP.NET GridView (e.g. First 3 4 5 6 7 Last)
  2. Showing checkboxes against each row of the table for selective action

The related project code download is ibank-v2.zip

Handling Navigation/Pagination/Paging

There are free libraries out there like Google’s Jmesa, DisplayTag, etc. and also Spring has support through PagedListHolder and such. But I was in a different mood and decided to go for my own implementation.

Also, this helped me understand the logics of displaying a navigation bar. Maybe I’ll learn about the libraries later and share with you.

NavigationInfo

This is the workhorse for the navigation bar:

package org.himu.ibank.service.vo;

public class NavigationInfo {

    private int currentPage;
    private int pageSize;
    private int rowCount;
    private int maxIndices;

    public NavigationInfo() {
        currentPage = 0;
        rowCount = 0;
        maxIndices= 5;
        pageSize = 5;
    }

    public int getCurrentPage() {
        return currentPage;
    }

    public void setCurrentPage(int currentPage) {
        if (currentPage < 0)
            this.currentPage = 0;
        else if (currentPage > getPageCount() – 1)
            this.currentPage = getPageCount() – 1;
        else
            this.currentPage = currentPage;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public int getRowCount() {
        return rowCount;
    }

    public void setRowCount(int rowCount) {
        this.rowCount = rowCount;
    }

    public int getMaxIndices() {
        return maxIndices;
    }

    public void setMaxIndices(int maxIndices) {
        this.maxIndices = maxIndices;
    }

    public int getPageCount() {
        return (int) Math.ceil((double) rowCount / pageSize);
    }

    public int getPrevIndex() {
        int prev = currentPage – 1;
        return prev < 0 ? 0 : prev;
    }

    public int getNextIndex() {
        int lastIndex = getPageCount() – 1;
        int next = currentPage + 1;
        return next > lastIndex ? lastIndex : next;
    }
    public boolean isFirstPage() {
        return 0 == currentPage;
    }
    public boolean isLastPage() {
        return (getPageCount() – 1) == currentPage;
    }

    public int[] getIndexList() {
        int[] range = getIndexRange();
        int[] ilist = new int[range[1] – range[0] + 1];
        for (int i = 0; i < ilist.length; i++) {
            ilist[i] = range[0] + i;
        }
        return ilist;
    }
    public int[] getIndexRange() {
        // determine the standard window
        int start = currentPage – maxIndices / 2;
        int end = start + maxIndices – 1;
        // shift to right if start underflows 0
        if (start < 0) {
            end -= start; // end – -start = end + start = shift right
            start = 0;
        }
        // now maybe the window overflows pageCount – so shift to left again
        int lastIndex = getPageCount() – 1;
        if (end > (lastIndex)) {
            start -= (end – lastIndex);
            end = lastIndex;
        }
        // we have finalized end, now if start < 0 then truncate it
        if (start < 0)
            start = 0;
        return new int[] {start, end};
    }
}

The page indices are 0-based. The class has some defaults which can be overridden if required.

The starting point for this class is setRowCount(). Once the number of rows is determined you can use getPageCount() which in turn allows all other navigation methods to work.

setCurrentPage(), getPrevIndex() and getNextIndex() make sure you don’t fall out of valid page indices.

getIndexRange() is where all the muscle is. It uses currentPage and maxIndices (maximum number of links to be displayed in the navigation bar) to determine the start and end of page links in the navigation bar.

getIndexList() is a convenience method for generating the entire list of navigation indices from the range calculated by getIndexRange().

PagedCustView

This class is used to send the navigation information and the current page of customers to the view.

package org.himu.ibank.service.vo;

import java.util.List;

import org.himu.ibank.domain.Customer;

/**
* Hold necessary information for paged view of customer list
*/
public class PagedCustView {

    private NavigationInfo navInfo = new NavigationInfo();
    private List<Customer> customers;

    public NavigationInfo getNavInfo() {
        return navInfo;
    }

    public void setNavInfo(NavigationInfo navInfo) {
        this.navInfo = navInfo;
    }

    public List<Customer> getCustomers() {
        return customers;
    }

    public void setCustomers(List<Customer> customers) {
        this.customers = customers;
    }

    public Customer getCustomer(int i) {
        return (Customer) customers.get(i);
    }

    public void setCustomer(int i, Customer customer) {
        this.customers.add(i, customer);
    }
}

The actual page of customers is fetched in the controller and the navigation information is set accordingly.

Writing the Controller

You’re going to browse all registered but unauthorized customers.

The NewCustListController expects a page parameter in the query string of its calling URL. If this is not found then page 0 (the first page) is assumed.

package org.himu.ibank.controller;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.himu.ibank.service.CustomerRegistrationService;
import org.himu.ibank.service.vo.PagedCustView;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;

public class NewCustListController extends AbstractController {

    private CustomerRegistrationService custRegService;
    public void setCustRegService(CustomerRegistrationService custRegService) {
        this.custRegService = custRegService;
    }

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        Map<Object, Object> model = new HashMap<Object, Object>();
        PagedCustView pcustv = new PagedCustView();

        pcustv.getNavInfo().setRowCount(custRegService.getUnauthorizedCustomerCount());

        String page = (String)request.getParameter("page");
        if (null == page)
            pcustv.getNavInfo().setCurrentPage(0);
        else
            pcustv.getNavInfo().setCurrentPage(Integer.parseInt(page));

        pcustv.setCustomers(custRegService.getUnauthorizedCustomers(
                pcustv.getNavInfo().getCurrentPage(), pcustv.getNavInfo().getPageSize()));

        request.getSession().setAttribute("pagedcust", pcustv);
        model.put("pagedcust", pcustv);

        return new ModelAndView("admin/newcustlist", model);

    }
}

  1. First you determine the customer count and tell it to NavigationInfo.
  2. Then you set the current page.
  3. Based on the current page and page size as specified in NavigationInfo, you then fetch the only the specific page of customers through Hibernate. This is the reason I opted for my own implementation.
  4. Return the model and view name as usual.

The Service and DAO Classes

I’ve refactored CustomerRegistrationService and CustomerDao interfaces and their implementations a bit. Also note that I’ve corrected a mistake in HibernateCustomerDao’s paging code.

CustomerRegistrationService

public interface CustomerRegistrationService {

    Long registerCustomer(Customer c);
    void authorizeCustomer(Customer c);
    Customer getCustomer(Long id);
    List<Customer> getUnauthorizedCustomers(int startPage, int pageSize);
    List<Customer> getCustomers(int startPage, int pageSize);
    List<Customer> getAllCustomers(int startPage, int pageSize);
    public int getUnauthorizedCustomerCount();
}

StandardCustomerRegistrationService

public class StandardCustomerRegistrationService implements
        CustomerRegistrationService {

    private CustomerDao custDao;
    public void setCustDao(CustomerDao custDao) {
        this.custDao = custDao;
    }

    @Override
    public Long registerCustomer(Customer c) {
        return custDao.addNew(c);
    }

    @Override
    public void authorizeCustomer(Customer c) {
        if (c.getStatus() == Customer.STATUS_REGISTERED) {
            c.setStatus(Customer.STATUS_ACTIVE);
            custDao.update(c);
            // … mailer code for notifying customer …
        }
    }

    @Override
    public Customer getCustomer(Long id) {
        return custDao.findById(id);
    }

    /**
     * Get registered but unauthorized customers using optional paging.
     *
     * @param startPage Starting page number, first page is 0
     * @param pageSize Size of a single page, <=0 means no paging
     */
    @Override
    public List<Customer> getUnauthorizedCustomers(int startPage, int pageSize) {
        Customer c = new Customer();
        c.setStatus(Customer.STATUS_REGISTERED);
        return custDao.listByExample(c, startPage, pageSize);
    }

    @Override
    public int getUnauthorizedCustomerCount() {
        return custDao.countCustomers(Customer.STATUS_REGISTERED);
    }

    /**
     * Get registered and authorized customers using optional paging.
     *
     * @param startPage Starting page number, first page is 0
     * @param pageSize Size of a single page, <=0 means no paging
     */
    @Override
    public List<Customer> getCustomers(int startPage, int pageSize) {
        Customer c = new Customer();
        c.setStatus(Customer.STATUS_ACTIVE);
        return custDao.listByExample(c, startPage, pageSize);
    }

    @Override
    public List<Customer> getAllCustomers(int startPage, int pageSize) {
        return custDao.listAll(startPage, pageSize);
    }
}

CustomerDao

public interface CustomerDao {

    Long addNew(Customer c);
    void delete(Customer c);
    void update(Customer c);
    Customer findById(Long id);
    Customer findByUserId(String uid);
    List<Customer> listAll();
    List<Customer> listAll(int startPage, int pageSize);
    List<Customer> listByExample(Customer c);
    List<Customer> listByExample(Customer c, int startPage, int pageSize);
    int countCustomers(int status);
}

HibernateCustomerDao

public class HibernateCustomerDao extends HibernateDaoSupport implements
        CustomerDao {

    @Override
    public Long addNew(Customer c) {
        return (Long) getHibernateTemplate().save(c);
    }

    @Override
    public void delete(Customer c) {
        getHibernateTemplate().delete(c);
    }

    @Override
    public void update(Customer c) {
        getHibernateTemplate().update(c);
    }

    @Override
    public Customer findById(Long id) {
        return (Customer) getHibernateTemplate().get(Customer.class, id);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Customer findByUserId(String uid) {
        List<Customer> clist =  getHibernateTemplate().find("from Customer c where c.userId = ?", uid);
        if (clist.isEmpty())
            return null;
        else
            return clist.get(0);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Customer> listAll() {
        return getHibernateTemplate().find("from Customer c");
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Customer> listAll(int startPage, int pageSize) {
        DetachedCriteria criteria = DetachedCriteria.forClass(Customer.class);
        criteria.addOrder(Order.asc("id"));
        return getHibernateTemplate().findByCriteria(criteria, startPage * pageSize, pageSize);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Customer> listByExample(Customer c) {
        return getHibernateTemplate().findByExample(c);
    }

    @SuppressWarnings("unchecked")
    @Override
    public List<Customer> listByExample(Customer c, int startPage, int pageSize) {
        return getHibernateTemplate().findByExample(c, startPage * pageSize, pageSize);
    }

    @Override
    public int countCustomers(int status) {
        DetachedCriteria criteria = DetachedCriteria.forClass(Customer.class)
            .add(Restrictions.eq("status", status))
            .setProjection(Projections.rowCount());
        return (Integer)getHibernateTemplate().findByCriteria(criteria).get(0);
    }
}

The Browsing JSP

The Model and Controller codes are done. Now you write the view – newcustlist.jsp. Create it by copying admin.jsp as usual. Following is the relevant additions you need to write:

            <div id="content">
                <div align="center">
                    <c:if test="${!pagedcust.navInfo.firstPage}">
                        <a href="newcustlist.htm?page=0">First</a>&nbsp;
                    </c:if>
                    <c:forEach var="i" items="${pagedcust.navInfo.indexList}">
                        <c:choose>
                        <c:when test="${i != pagedcust.navInfo.currentPage}">
                            <a href="newcustlist.htm?page=${i}">${i}</a>&nbsp;
                        </c:when>
                        <c:otherwise>
                            <b>${i}</b>&nbsp;
                        </c:otherwise>
                        </c:choose>
                    </c:forEach>
                    <c:if test="${!pagedcust.navInfo.lastPage}">
                        <a href="newcustlist.htm?page=${pagedcust.navInfo.pageCount – 1}">Last</a>
                    </c:if>
                </div>
                <form action="<%=request.getContextPath()%>/authorizecust.htm" method="post">
                <table>
                    <thead>
                    <tr>
                        <td>User ID</td>
                        <td>First Name</td>
                        <td>Last Name</td>
                        <td>Email</td>
                    </tr>
                    </thead>
                    <tbody>
                    <c:forEach var="cust" items="${pagedcust.customers}">
                    <tr>
                        <td>${cust.userId}</td>
                        <td>${cust.firstName}</td>
                        <td>${cust.lastName}</td>
                        <td>${cust.email}</td>
                        <td><input type="checkbox" value="${cust.id}" name="authorized"/></td>
                    </tr>
                    </c:forEach>
                    </tbody>
                </table>
                <input type="hidden" name="linkBackIndex" value="${pagedcust.navInfo.currentPage}"/>
                <input type="submit" value="Authorize"/>
                </form>
            </div>

  1. First, generate the navigation banner aligned in the center of the page. It shows optional First and Last links based on the current page. The center <c:forEach> loops through the index list taken from navInfo and generates the necessary page links excluding the current page.
  2. Declare a form which allows us to submit authorization approvals of selected customers. The code for authorization will be written later in another controller.
  3. Looping through the customer list is trivial. The important thing is generating a checkbox against each customer for selecting for approval. The name of the checkboxes must be same (authorize in the above code) but there values will be different – the internal ID of each customer. This ID is required for Hibernate.
  4. A hidden input field linkBackIndex is also declared which helps us to return to the current page after authorization is made. It will be clear in the authorization controller.

Processing Actual Authorization and Returning to the New Customer Browser

First, the code:

package org.himu.ibank.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.himu.ibank.domain.Customer;
import org.himu.ibank.service.CustomerRegistrationService;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;
import org.springframework.web.servlet.view.RedirectView;

public class AuthorizeCustController extends AbstractController {

    private CustomerRegistrationService custRegService;

    public void setCustRegService(CustomerRegistrationService custRegService) {
        this.custRegService = custRegService;
    }

    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request,
            HttpServletResponse response) throws Exception {
        String[] custids = request.getParameterValues("authorized");
        String linkBackIndex = request.getParameter("linkBackIndex");
        System.out.println("################### INSIDE AuthorizeCustController #############");
        for (String custid: custids) {
            Customer cust = custRegService.getCustomer(Long.parseLong(custid));
            custRegService.authorizeCustomer(cust);
            System.out.println("## Authorized: " + custid);
        }
        return new ModelAndView(new RedirectView(request.getContextPath() + "/newcustlist.htm?page=" + linkBackIndex));
    }
}

The logic is simple:

  1. Get the list of selected customer IDs using request.getParameterValues(“authorized”). It will contain only those IDs whose checkboxes are marked.
  2. Iterate through the list as ask our service class to make the authorizations.
  3. We use the linkBackIndex to redirect the flow to NewCustListController using its Spring MVC defined URL, i.e. /newcustlist.htm.

Notice how we are redirecting in the return statement. The RedirectView class helps us to go to back to the new customer browser and also pass the last page used as a query parameter. An alternative is to define the view in the bean configuration file using the pattern ‘redirect:<url>’, e.g. ‘redirect:/newcustlist.htm’ but we cannot pass the query parameter then.

Modified ibank-servlet.xml

Finally, our gluing file. I won’t be discussing it as there is nothing special.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean id="simpleUrlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <props>
                <prop key="/home.htm">homePageController</prop>
                <prop key="/adminhome.htm">adminHomePageController</prop>
                <prop key="/createcust.htm">newCustomerController</prop>
                <prop key="/newcustlist.htm">newCustomerListController</prop>
                <prop key="/authorizecust.htm">authorizeCustController</prop>
            </props>
        </property>
    </bean>
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
    <bean id="homePageController" class="org.himu.ibank.controller.HomePageController"/>
    <bean id="adminHomePageController" class="org.springframework.web.servlet.mvc.ParameterizableViewController">
        <property name="viewName" value="admin/admin"/>
    </bean>
    <bean id="newCustomerController" class="org.himu.ibank.controller.NewCustomerController">
        <property name="custRegService" ref="customerRegService"/>
        <property name="formView" value="admin/newcustomer"/>
        <property name="successView" value="admin/newcustomer-success"/>
        <property name="commandName" value="customer"/>
        <property name="commandClass" value="org.himu.ibank.domain.Customer"/>
    </bean>
    <bean id="newCustomerListController" class="org.himu.ibank.controller.NewCustListController">
        <property name="custRegService" ref="customerRegService"/>
    </bean>
    <bean id="authorizeCustController" class="org.himu.ibank.controller.AuthorizeCustController">
        <property name="custRegService" ref="customerRegService"/>
    </bean>
</beans>

Hope to hear from you.

Advertisements

Written by mhimu

November 26, 2009 at 3:31 pm

14 Responses

Subscribe to comments with RSS.

  1. […] Spring MVC Tutorial – Paging through Hibernate and Selection Handling – add table/list browsing support utilizing paging in Hibernate, add a page navigation bar, add checkbox handling against table rows. […]

  2. Thanks for the tutorial. You did great job.

    N C

    June 11, 2010 at 1:04 am

  3. Hi, how do I list out the paging to show just all the records and doesn’t need to care about the status??

    wdzul

    August 15, 2010 at 10:04 pm

    • Hello, what records are shown depends entirely on how you retrieve. I’ve used the getUnauthorizedCustomers method where the status is checked. If I want all customers (or some other criteria) then I’d use getAllCustomers (or write another specific method).

      mhimu

      August 17, 2010 at 10:44 am

  4. Thanks Realy helpfull article.

    Raj

    April 1, 2011 at 1:24 pm

  5. Thanks, a great help!

    james

    May 16, 2011 at 9:06 pm

  6. Great!!

    I do not see the reason to include in the session:

    in the first controller:
    request.getSession().setAttribute(“pagedcust”, pcustv);

    Do I miss something?

    Vassilis

    December 25, 2011 at 8:17 pm

  7. Himu, this looks like a great tutorial, but there is one problem: this and the ibank-v2.zip are missing the .htm files, which of course are key to one starting the navigation process. Can you re-post them somewhere? I know its “late” but this tutorial gets high ranking on Google, and apparently occasionally people are trying it..

    photodj

    January 12, 2012 at 12:47 pm

    • Hi!

      The .htm files are logical. The URLs are handled by the Spring dispatcher which routes the request to an appropriate controller. In other words, the URLs are simply aliases to internal logic. Hope that helps.

      mhimu

      January 12, 2012 at 1:09 pm

      • Thank you Himu. I started to realize that after reading the docs for org.springframework.web.servlet.DispatcherServlet.
        But on my Tomcat 6 instance, http://localhost/ibank/home.htm (and the other mappings in ibank-servlet.xml) yield 404s.. not sure why yet.

        photodj

        January 12, 2012 at 1:26 pm

  8. You’ll see the program asking for permission to continue, so press Y on your keyboard, power surge, leakage, or fire destroyed your entire company in a matter of a few short weeks. After a recent title update for online backup services Dragon’s Dogma:
    Dark Arisen is out now on the Xbox 360. Try to reproduce
    the process of recovering iTunes online backup services password
    recovery. The Memory Kick Si to store and backup all types of organizations and businesses are pushing their backups
    to the cloud are also very popular nowadays.
    If you are using.

  9. nice tutorial……

    AJ

    July 15, 2013 at 3:12 pm

  10. please give it in a clear way….
    It seems that u dont want to write it..
    Plz have some explanations, upload source code?….. etc
    THankssss

    D

    February 7, 2014 at 7:39 pm

  11. //The short code

    package com.business.paginator;

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

    public class PaginationDto
    {
    private int currentPage;
    private int pageSize;
    private int rowCount;

    public PaginationDto()
    {
    currentPage = 0;
    rowCount = 0;
    pageSize = 5;
    }

    public int getCurrentPage()
    {
    return currentPage;
    }

    public void setCurrentPage(int CurrentPage)
    {
    if(CurrentPage getPageCount())
    {
    this.currentPage = getPageCount() – 1;
    }
    else
    {
    this.currentPage = CurrentPage;
    }
    }

    public int getPageSize()
    {
    return pageSize;
    }

    public void setPageSize(int pageSize)
    {
    this.pageSize = pageSize;
    }

    public int getRowCount()
    {
    return rowCount;
    }

    public void setRowCount(int rowCount)
    {
    this.rowCount = rowCount;
    }

    public int getPageCount()
    {
    return (int)Math.ceil((double)rowCount / pageSize);
    }

    public int getPrevIndex()
    {
    int Previus = this.currentPage – 1;
    return Previus LastIndex ? LastIndex : Next;
    }

    public boolean isFirstPage()
    {
    return currentPage == 0;
    }

    public boolean isLastPage()
    {
    return currentPage == (getPageCount() – 1);
    }

    public int getStart()
    {
    return getCurrentPage() * getPageSize();
    }

    public List getIndexList()
    {
    List IntPageList = new ArrayList();
    for(int NroTot = 0; NroTot < getPageCount(); NroTot++)
    {
    IntPageList.add(NroTot);
    }
    return IntPageList;
    }
    }

    //Controller
    @RequestMapping(value = {"/index", "index/{Page}"}, method = RequestMethod.GET)
    public String IndexGET(ModelMap Model, Principal Principal, @PathVariable Optional Page)
    {
    Long Total = ClienteService.ClienteCount(true);
    int Pagina = Page.isPresent() ? Page.get() : 0;
    PaginationDto PaginationDTO = new PaginationDto();
    PaginationDTO.setRowCount(Total.intValue());
    PaginationDTO.setPageSize(4);
    PaginationDTO.setCurrentPage(Pagina);
    List ClienteList = ClienteService.GetListStartLimit(PaginationDTO.getStart(), PaginationDTO.getPageSize());

    Model.addAttribute(“ClienteList”, ClienteList);
    Model.addAttribute(“PaginationDTO”, PaginationDTO);
    return “cliente/index”;
    }

    //http://localhost:8080/cliente/index/2
    //http://localhost:8080/cliente/index/3
    //http://localhost:8080/cliente/index/4

    Carlos mollapaza

    July 3, 2016 at 1:24 am


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: