October 2, 2007

User Friendly Paging with Seam

Paging, also known as pagination, has many different implementations on the PHP universe. Developers just pick the one they like. However, there are not that many on the Java playground. You might have heard about the Value List Handler design pattern or used the Data Scroller component from the MyFaces project. Developers might find these approaches to be a bit complex or restrictive. Personally, I have read about them but never got the chance to use them.

With Hibernate and the Java Persistence API, the game has changed and Paging is much simpler now. Back in 2004, Gavin King blogged about a pattern to do Pagination in Hibernate and EJB3. The pattern is so simple and I really like it. The only thing that is remaining now is to integrate this pattern with Seam ;)

Update: Gavin King has suggest that Seam page parameter be used rather than @RequestParameter. Take a look here for using Seam page parameters with custom validators and converters.

Enhancing the pattern

The pattern Gavin introduced allows you to display "next" and "previous" buttons to the user and only retrieve the needed query results. This is nice and handy, however, users are impatient and get frustrated from repeatedly hitting the "next" and "previous" buttons to browse through a collection of items. I used to have a "browse" page with only these two buttons and one day I received an email from a user requesting that I offer pagination similar to the one seen on phpBB powered forums.




So, I have decided to do something similar but a bit different.

The Addons


Similar to phpBB pagination, the pattern discussed next has "first", "previous", "next", "last" and a list of links that the user can directly navigate two. As a developer you can specify the number of items to display per listing.



Notice that the total number of pages is displayed and users get to know which page they are currently browsing. In case there are not that many records in the database and there are only few pages to browse, page numbers are displayed rather than the verbose words.



The Components

To achieve this you two a component. One to validate data passed in the URL and to define the database queries. The second is responsible for identifying the format of the paging and the links to show.

The first component is a stateless session bean called MemberListingService that has a single method called setMemberList:


@Local
public interface MemberListing {
void setMemberList();
}

This method perform validation on the three parameters passed in the URL and set using the @RequestParameter annotation. If required, default values will be set for parameters that are missing or have invalid values.

@Stateless
@Name("memberListingService")
public class MemberListingService implements MemberListing {

// By default page number/index is NOT zero based. The first value is ONE.
private static int FIRST_PAGE_INDEX = 1;
private static String DEFAULT_STATUS = MemberStatus.ACTIVE.getStatus();

@RequestParameter("st")
@Out(scope = EVENT, value = "st", required = false)
private String status;

@RequestParameter("pn")
@Out(scope = EVENT, value = "pn", required = false)
private String pn = null;

@RequestParameter("ps")
@Out(scope = EVENT, value = "ps", required = false)
private String ps = null;

public void setMemberList() {
Integer pageIndex, pageSize = -1;

if (status == null || status.trim().equals("")) {
FacesMessages.instance().add("No status was specified.");
status = DEFAULT_STATUS;
}

if (pn == null || pn.trim().equals("")) {
FacesMessages.instance().add("No page number was specified.");
pageIndex = FIRST_PAGE_INDEX;
} else {
try {
pageIndex = Integer.parseInt(pn);
} catch (NumberFormatException e) {
FacesMessages.instance().add("invalid page number specified.");
pageIndex = FIRST_PAGE_INDEX;
}
}

if (ps == null || ps.trim().equals("")) {
FacesMessages.instance().add("No page size was specified.");
} else {
try {
pageSize = Integer.parseInt(ps);
} catch (NumberFormatException e) {
FacesMessages.instance().add("invalid page size specified.");
}
}
}
}

The next step is to create two Query objects. The first one is for the columns you want to show in you listing and the second is for getting a count of the records.

@Stateless
@Name("memberListingService")
public class MemberListingService implements MemberListing {

@In(create = true, value = "entityManager")
private EntityManager em;

@Out(scope = EVENT, value = "page")
private Page page;

public void setMemberList() {

String selectStatement = "from gs_member m " + "WHERE m.status = :status "
+ "order by m.joinDate";
Query selectQuery = em.createQuery(selectStatement).setParameter("status",
status);

String countStatement = "SELECT COUNT(m) " + "FROM gs_member m "
+ "WHERE m.status = :status ";

Query countQuery = em.createQuery(countStatement).setParameter("status",
status);

this.page = new Page(selectQuery, countQuery, pageIndex, pageSize);
}
}

These two Query objects are then passed to a new instance of the Page class. If you execute the select query as is, you will retrieve all the records from the database. In the constructor of the Page class, further operations are applied to limit the list of records you get. The count query on the other hand will be executed as is without any further modifications.

public class Page {

private static int DEFAULT_PAGE_SIZE = 8;
private static int DEFAULT_STEP_SIZE = 2;
private static int MIN_PAGE_SIZE = 2;
private static int MAX_PAGE_SIZE = 16;

private List results;
private int pageIndex;
private int pageSize;
private int stepSize;
private int lastPageIndex;
private int leftSteps;
private int rightSteps;
private boolean leftDots;
private boolean rightDots;
private boolean simplePaging;

@In(create = true, value = "entityManager")
private EntityManager em;

public Page(Query selectQuery, Query countQuery, int index, int size) {
if (index <= 0)
index = 1;

// If page index is not zero based
pageIndex = index;

// Make sure that the page size is within its limits
if (size < MIN_PAGE_SIZE || size > MAX_PAGE_SIZE)
pageSize = DEFAULT_PAGE_SIZE;
else
pageSize = size;

// You might consider having a min and max as boundaries
stepSize = DEFAULT_STEP_SIZE;

// Get the total number of items in the database.
// If count was of type Long you would lose the precision when
// calling Math.ceil() --> count is declared as double
double count = (Long) countQuery.getSingleResult();
if (count <= 0) {
// You need to handle this situation. One option is to throw an
// exception or display an error message using FacesMessages.
return;
}

// calculate the number of pages and set last page index
lastPageIndex = (int) Math.ceil(count / pageSize);

// make sure that the page index in not out of scope
if (pageIndex > lastPageIndex)
pageIndex = 1;

// calculate the minimum number of paging items to show
int minPagination = 3 + 2 * stepSize;
if (lastPageIndex <= minPagination) {
leftDots = rightDots = false;
simplePaging = true;
} else {
simplePaging = false;
setLeftDots();
setRightDots();
setLeftSteps();
setRightSteps();
}

results = selectQuery.setMaxResults(pageSize).setFirstResult(
pageSize * (pageIndex - 1)).getResultList();
}

private void setLeftDots() {
// current page - (step size + 1) > 1
// current page - step size > 1 + 1
// ex, current page = 5 and step size = 2
// output 1 ... 3 4 5
// 5 - 2 > 2 --> 3 > 2 --> true
leftDots = pageIndex > (stepSize + 2);
}

private void setRightDots() {
// current page + (step size + 1) < last page
// current page + step size < last page - 1
// ex, current page = 25, step size = 2 and last page = 29
// output 25 26 27 ... 29
// 25 + 2 < 29 - 1 --> 27 < 28 --> true
rightDots = (pageIndex + stepSize) < (lastPageIndex - 1);
}

private void setLeftSteps() {
if (leftDots)
leftSteps = stepSize;
else {
// count = current page - (first + 1)
// ex, first = 1 and page = 4
// count = 4 - (1 + 1) = 2
// output 1 2 3 4
leftSteps = pageIndex - 2;
}
}

private void setRightSteps() {
if (rightDots)
rightSteps = stepSize;
else {
// count = last page - ( current page + 1)
// ex, last = 29 and current = 26
// count = 29 - 27 = 2
// output 26 27 28 29
rightSteps = lastPageIndex - pageIndex - 1;
}
}

public boolean isLeftDots() {
return leftDots;
}

public boolean isRightDots() {
return rightDots;
}

public int getLeftSteps() {
return leftSteps;
}

public int getRightSteps() {
return rightSteps;
}

public List getResults() {
return results;
}

public int getPageIndex() {
return pageIndex;
}

public int getLastPageIndex() {
return lastPageIndex;
}

public boolean showPageIndex() {
return pageIndex != 1 && pageIndex != lastPageIndex;
}

public boolean isSimplePaging() {
return simplePaging;
}
}

I have added comments to the code since it's simpler to explain it that way, otherwise, I would end up with a long paragraph. Feel free to ask a question or a comment if you need further explanation. Note that certain links are displayed based on the current page






The members.xhtml page makes use of the "page" component outjected at the call to setMemberList is done. There is not much to explain here also so you just go through it and ask any question if something is not clear.

<?xml version="1.0" encoding="UTF-8" ?>
<!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"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:c="http://java.sun.com/jstl/core">

<ui:composition template="layout/main_template.xhtml">
<ui:define name="title">True Runes Member Listing</ui:define>
<ui:define name="content">
<!-- cellspacing is used to remove the default padding between cells -->
<h:dataTable cellspacing="0" id="memberListTable" value="#{page.results}"
var="mem" rowClasses="odd, even">
<caption>Members List</caption>
<h:column col="memberGroup">
<f:facet name="header">Member Name</f:facet>
<h:outputText value="#{mem.memberName}"/>
</h:column>
<h:column>
<f:facet name="header">Joined on</f:facet>
<h:outputText value="#{mem.joinDate}">
<f:convertDateTime type="date"/>
</h:outputText>
</h:column>
<h:column styleClass="last">
<f:facet name="header">Status</f:facet>
<h:outputText value="#{mem.status}"/>
</h:column>
</h:dataTable>

<c:choose>
<c:when test="#{page.simplePaging}">
<div id="paging">
<h:outputText value="Page #{page.pageIndex} of #{page.lastPageIndex} pages "
rendered="#{page.lastPageIndex > 1}"/>
<c:forEach var="i" begin="1" end="#{page.lastPageIndex}">
<h:outputLink value="members.seam">
<h:outputText value="#{i}"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{i}" />
</h:outputLink>
</c:forEach>
</div>
</c:when>

<c:otherwise>
<div id="paging">
<h:outputText value="Page #{page.pageIndex} of #{page.lastPageIndex} pages "
rendered="#{page.lastPageIndex > 1}"/>

<h:outputLink value="members.seam" rendered="#{page.pageIndex != 1}">
<h:outputText value="First"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="1" />
</h:outputLink>

<h:outputText value=" 1 " rendered="#{page.pageIndex == 1}"/>

<h:outputLink value="members.seam" rendered="#{page.pageIndex != 1}">
<h:outputText value="Previous"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{page.pageIndex - 1}" />
</h:outputLink>

<h:outputText value=" ... " rendered="#{page.leftDots}"/>

<c:forEach var="i" begin="#{page.pageIndex - page.leftSteps}"
end="#{page.pageIndex - 1}">
<h:outputLink value="members.seam">
<h:outputText value="#{i}"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{i}" />
</h:outputLink>
</c:forEach>

<h:outputText value=" #{page.pageIndex} "
rendered="#{page.showPageIndex()}"/>

<c:forEach var="i" begin="#{page.pageIndex + 1}"
end="#{page.pageIndex + page.rightSteps}">
<h:outputLink value="members.seam">
<h:outputText value="#{i}"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{i}" />
</h:outputLink>
</c:forEach>

<h:outputText value=" ... " rendered="#{page.rightDots}"/>

<h:outputLink value="members.seam"
rendered="#{page.pageIndex != page.lastPageIndex}">
<h:outputText value="Next"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{page.pageIndex + 1}" />
</h:outputLink>

<h:outputLink value="members.seam"
rendered="#{page.pageIndex != page.lastPageIndex}">
<h:outputText value="Last"/>
<f:param name="st" value='#{st != null ? st : ""}'/>
<f:param name="ps" value='#{ps != null ? ps : 0}'/>
<f:param name="pn" value="#{page.lastPageIndex}" />
</h:outputLink>

<h:outputText value=" #{page.lastPageIndex}"
rendered="#{page.pageIndex == page.lastPageIndex}"/>
</div>
</c:otherwise>
</c:choose>
</ui:define>
</ui:composition>
</html>

The last part of the buzzle is in pages.xml, you need to define setMemberList as a page action for the members.xhtml page.

<page view-id="/members.xhtml" action="#{memberListingService.setMemberList}"/>

9 comments:

Shyam Prasad said...

Hi,

Thanks for your article.

Whether the above scenario is good for handling large recordset from database ? DO you have the Full code ? Can you pls email me to "khtshyam@gmail.com". It will be of great help.

We want to do Paging support for JSF frontend for large records ? Pls kindly tell me seam is efficent for large records for paging mechnaism ?

Thanks

Shyam

EnRokuta said...

Hi Shyam,

You would not know till you try it yourself. I have not had any efficiency issues with Seam in the past so it should be fine. The code in this blog entry has everything you need.

matrix said...

would you plz send me the src code ? thanks

matrix said...

my email address is usaykili@gmail.com

Sheng said...

Hi there,
Thank you for the nice sharing. It seems it is very good pagination pattern (stateless scope + event scope) for a large data and large scale server site. Is it correct? Another question is, if you click back from browser, the previous viewed page can be displayed? Thank you.

pitangso said...

Hi,
Thanks for your explanation.
It really help me.
Can you plz send me the code?
My Email Address is pitangso@gmail.com

Anonymous said...

Hi dude,

can u please send me the code for pagination. It will be great help to me.

Id: shirin@promactinfo.co.in

Thanks
Shirin Joshi

stijn said...

I'm trying to make a facelets tag of your paging code:
check :
http://seamframework.org/Community/PagingTagWithFacelets

Arron said...

Nice post. However, I"m confused. You have two classes called MemberListingService. Is this correct? Or is the idea that you're simply updating the MemberListingService class and the second code snippet follows the first code snippet of the MemberListingService's setMemberList method? That is, the error checking for the parameters is performed first, and then create the two query objects. Is that correct?