Most of the component toolkits have build in support for server-side paging this days but in rest of the cases you need to customize a little the data model or data provider or component itself to have data paging. The reason why I write this post is because when I first saw richfaces everythink was perfect except the server-side paging. They have paging but it was client side based on JavaScript witch just doesn't work in many cases and I loose a lot of time to understand all models, which model I must extend and how to do it to create a data model with server side paging. At the current version of RichFaces on the demo page information about how to do server-side paging at least exist http://livedemo.exadel.com/richfaces-demo/richfaces/dataTable.jsf?tab=dataModel&cid=3608154 but like always the JBoss/RedHat/Exadel doesn’t provide us a fast full easy working example and we must loose a lot of time to search for classes in the demo.
The reason why I write this post is to give you simple application that uses server side paging and a little “directions” what you need to customize to have server side paging in all cases. If you have read my previous post about hibernate + spring + jsf + richfaces you will have very easy way to extend this simple demo and to make all Richfaces tables to have server-side paging instead of client-side javascript paging.
So lets start.
First what we want to create ? We want to create a serveri-side paging that looks like this :
When you click on the pager at the bottom it will make ajax call to the server and will fetch the the results for the next page. As you can read in the richFaces demo page you must make custom(extended) data model extending org.ajax4jsf.model.ExtendedDataModel and org.ajax4jsf.model.SerializableDataModel. These two classes work together to provide functions that missing in the standard DataModel.
The most important additional functions are:
- access for rows by primary keys instead of index position
- implementation of "visitor" pattern over the "range" of rows to support "table scroller" or "paginator" functions
- ability to serialize table data, so it can be used on post-back processing without additional database query
In most cases you don’t care about most of this. In our Simple Server-Side richfaces application we will have a simple entity called User. It can be Hibernate/JPA entity or whatever you want for now it will be just a POJO with getters and setters.
package org.joke.demo.extendeddatamodel;
public class User {
private Integer pk;
private String username;
private String password;
private String fullName;
public Integer getPk() {
return pk;
}
public void setPk(Integer pk) {
this.pk = pk;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getFullName() {
return fullName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
private String email;
public User(Integer pk) {
this.pk = pk;
}
}
next we will use a simple RandomDataHelper class that will give us random strings and data. This class is downloaded from exadel simple(test) sources.
package org.joke.demo.utils;
public class RandomDataHelper {
public static int random(int min, int max) {
assert(min<=max);
return min+(int)Math.round(Math.random()*(double)(max-min));
}
public static Object random(Object values[]) {
assert(values!=null);
return values[random(0,values.length-1)];
}
private static char randomChar() {
if (Math.random()>0.5) {
return (char)((int)'0'+random(0,9));
} else {
return (char)((int)'A'+random(0,25));
}
}
public static String randomString(int length) {
StringBuffer buf = new StringBuffer();
for (int counter=0;counter<length;counter++) {
buf.append(randomChar());
}
return buf.toString();
}
}
When we have this we are ready to create our model. The model is copy paste from the exadel model shown in the demo page
package org.joke.demo.extendeddatamodel;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.faces.context.FacesContext;
import org.ajax4jsf.model.DataVisitor;
import org.ajax4jsf.model.Range;
import org.ajax4jsf.model.SequenceRange;
import org.ajax4jsf.model.SerializableDataModel;
/**
*
* @author ias
* This is example class that intended to demonstrate use of ExtendedDataModel and SerializableDataModel.
* This implementation intended to be used as a request scope bean. However, it actually provides serialized
* state, so on a post-back we do not load data from the data provider. Instead we use data that was used
* during rendering.
* This data model must be used together with Data Provider, which is responsible for actual data load
* from the database using specific filtering and sorting. Normally Data Provider must be in either session, or conversation
* scope.
*/
public class AuctionDataModel extends SerializableDataModel {
private AuctionDataProvider dataProvider;
private Integer currentPk;
private Map<Integer,User> wrappedData = new HashMap<Integer,User>();
private List<Integer> wrappedKeys = null;
/**
*
*/
private static final long serialVersionUID = -1956179896877538628L;
/**
* This method never called from framework.
* (non-Javadoc)
* @see org.ajax4jsf.model.ExtendedDataModel#getRowKey()
*/
@Override
public Object getRowKey() {
return currentPk;
}
/**
* This method normally called by Visitor before request Data Row.
*/
@Override
public void setRowKey(Object key) {
this.currentPk = (Integer) key;
}
/**
* This is main part of Visitor pattern. Method called by framework many times during request processing.
*/
@Override
public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) throws IOException {
int firstRow = ((SequenceRange)range).getFirstRow();
int numberOfRows = ((SequenceRange)range).getRows();
wrappedKeys = new ArrayList<Integer>();
for (User item:dataProvider.getItemsByrange(new Integer(firstRow), numberOfRows, null, true)) {
wrappedKeys.add(item.getPk());
wrappedData.put(item.getPk(), item);
visitor.process(context, item.getPk(), argument);
}
}
/**
* This method must return actual data rows count from the Data Provider. It is used by pagination control
* to determine total number of data items.
*/
private Integer rowCount; // better to buffer row count locally
@Override
public int getRowCount() {
if (rowCount==null) {
rowCount = new Integer(getDataProvider().getRowCount());
return rowCount.intValue();
} else {
return rowCount.intValue();
}
}
/**
* This is main way to obtain data row. It is intensively used by framework.
* We strongly recommend use of local cache in that method.
*/
@Override
public Object getRowData() {
if (currentPk==null) {
return null;
} else {
User ret = wrappedData.get(currentPk);
if (ret==null) {
ret = getDataProvider().getAuctionItemByPk(currentPk);
wrappedData.put(currentPk, ret);
return ret;
} else {
return ret;
}
}
}
/**
* Unused rudiment from old JSF staff.
*/
@Override
public int getRowIndex() {
return 0;
}
/**
* Unused rudiment from old JSF staff.
*/
@Override
public Object getWrappedData() {
throw new UnsupportedOperationException();
}
/**
* Never called by framework.
*/
@Override
public boolean isRowAvailable() {
if (currentPk==null) {
return false;
} else {
return getDataProvider().hasAuctionItemByPk(currentPk);
}
}
/**
* Unused rudiment from old JSF staff.
*/
@Override
public void setRowIndex(int rowIndex) {
//ignore
}
/**
* Unused rudiment from old JSF staff.
*/
@Override
public void setWrappedData(Object data) {
throw new UnsupportedOperationException();
}
/**
* This method suppose to produce SerializableDataModel that will be serialized into View State and used on a post-back.
* In current implementation we just mark current model as serialized. In more complicated cases we may need to
* transform data to actually serialized form.
*/
public SerializableDataModel getSerializableModel(Range range) {
if (wrappedKeys!=null) {
return this;
} else {
return null;
}
}
/**
* This is helper method that is called by framework after model update. In must delegate actual database update to
* Data Provider.
*/
@Override
public void update() {
getDataProvider().update();
}
public AuctionDataProvider getDataProvider() {
return dataProvider;
}
public void setDataProvider(AuctionDataProvider dataProvider) {
this.dataProvider = dataProvider;
}
}
Just some notes about it. The important parts for you are two : method getRowIndex() must not be used in JSF 1.2 but MyFaces implementation invokes it some times so in model provided by exadel the method throws exception here we make it to return 0; just because we don't want to have exception with myfaces implementation.
The method public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) is the miracle this is the method invoked every time when you click the pager. The parameter Range contains the firstRow and countRows that you want to show in the dataTable so you just need to get them. When you have them you need to call your class that provides data. This can be a Service or some kind of data provider. In this simple case we have :
for (User item:dataProvider.getItemsByrange(new Integer(firstRow), numberOfRows, null, true))
so we have class dataProvider and method getItemsByRange. This dataProvider is a field in our dataModel but you can pass it or inject it from spring or from jsf faces-config.
The auction data provider class looks like this :
package org.joke.demo.extendeddatamodel;
import java.util.ArrayList;
import java.util.List;
import org.joke.demo.utils.RandomDataHelper;
public class AuctionDataProvider {
private String allNames[] = {
"Naiden Gochev",
"Toshko Poshkov",
"Chocho Monchov",
"Trallaa Trart",
"Peter Taharov",
"Djanko bakov",
"Hektor Mektor",
"Tartal tartalov",
"Djadja badja",
"Aza Daraz",
"Arazd azasd ",
"Max Payne",
"Max Damage",
"Nqkoi Drug",
"Treti peti",
"Shesti Sedmi",
"Talafal Kalafalov",
"Iumgur mungurov",
"Hrisi hrisi",
"Tartalan tartalanov",
"Az Taz",
"Maz Praz",
"Liz languare",
"Axa Ratra",
"Daraz rasas",
"acb asd",
"ascv asdv",
"ara araaaa",
"mon4o gon4o",
"petq petrova" };
private List<User> allItems = null;
private static final int VOLUME = 200;
private synchronized void initData() {
List<User> data = new ArrayList<User>();
for (int counter = 0; counter < VOLUME; counter++) {
User item = new User(new Integer(counter));
item.setFullName((String) RandomDataHelper
.random(allNames));
item.setUsername(RandomDataHelper.randomString(8));
item.setPassword(RandomDataHelper.randomString(8));
data.add(item);
}
allItems = data;
}
public List<User> getAllItems() {
if (allItems != null && allItems.size() > 0) {
return allItems;
} else {
initData();
return allItems;
}
}
public User getAuctionItemByPk(Integer pk) {
for (User item : getAllItems()) {
if (item.getPk().equals(pk)) {
return item;
}
}
throw new RuntimeException("Auction Item pk=" + pk.toString()
+ " not found");
}
public boolean hasAuctionItemByPk(Integer pk) {
for (User item : getAllItems()) {
if (item.getPk().equals(pk)) {
return true;
}
}
return false;
}
public List<User> getItemsByrange(Integer startPk,
int numberOfRows, String sortField, boolean ascending) {
System.out.println("load items from "+startPk + " and count of rows " + numberOfRows);
List<User> ret = new ArrayList<User>();
for (int counter = 0; counter < numberOfRows; counter++) {
ret.add(getAllItems().get(startPk.intValue() + counter));
}
return ret;
}
public void update() {
// nothing need to do
}
public int getRowCount() {
return getAllItems().size();
}
}
Thats all this provider contains all the operations that are needed from the model to work. Note the names of the provider and model AuctionDataProvider and AuctionDataModel this was the names in the exadel demo/test application which is not included in exadels demo site. You can find them easy in google, the difference there is that they use AuctionItem not User and the Exadel demo is not so simple and short.
All we need is the view and the faces-config.
The view looks like this :
<?xml version="1.0" encoding="UTF-8" ?>
<jsp:root xmlns:jsp="http://java.sun.com/JSP/Page"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:a4j="http://richfaces.org/a4j"
xmlns:rich="http://richfaces.org/rich"
xmlns:h="http://java.sun.com/jsf/html" version="2.1">
<jsp:directive.page language="java"
contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" />
<jsp:text>
<![CDATA[ <?xml version="1.0" encoding="UTF-8" ?> ]]>
</jsp:text>
<jsp:text>
<![CDATA[ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> ]]>
</jsp:text>
<f:view>
<h:form>
<rich:dataTable id="auction" value="#{auctionDataModel}"
var="item" rows="10">
<rich:column>
<f:facet name="header">
<h:outputText value="full name" />
</f:facet>
<h:outputText id="fullName" value="#{item.fullName}" />
</rich:column>
<rich:column>
<f:facet name="header">
<h:outputText value="username" />
</f:facet>
<h:outputText id="username" value="#{item.username}">
</h:outputText>
</rich:column>
<rich:column>
<f:facet name="header">
<h:outputText value="password" />
</f:facet>
<h:inputText id="password" value="#{item.password}" label="password">
</h:inputText>
</rich:column>
<f:facet name="footer">
<rich:datascroller for="auction" maxPages="5" />
</f:facet>
</rich:dataTable>
</h:form>
</f:view>
</jsp:root>
And the faces-config.xml looks like this :
<?xml version="1.0" encoding="UTF-8"?>
<faces-config xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-facesconfig_1_2.xsd"
version="1.2">
<managed-bean>
<managed-bean-name>auctionDataModel</managed-bean-name>
<managed-bean-class>org.joke.demo.extendeddatamodel.AuctionDataModel</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
<managed-property>
<property-name>dataProvider</property-name>
<value>#{auctionDataProvider}</value>
</managed-property>
</managed-bean>
<managed-bean>
<managed-bean-name>auctionDataProvider</managed-bean-name>
<managed-bean-class>org.joke.demo.extendeddatamodel.AuctionDataProvider</managed-bean-class>
<managed-bean-scope>session</managed-bean-scope>
</managed-bean>
</faces-config>
You can notice that I inject the auctionDataProvider in the auctionDataModel here. If you use spring and Spring EL Resolver you can use spring bean container as well.
The full source of this example can be found here: http://dl.getdropbox.com/u/887821/JsfHelloWorld.zip
Comments
Its a nice post.I have implemented this in a with a few additional features (hybrid pagination/row selection etc).
However I have one issue, I am not sure this is a inherent behavior of the rich data model.
The walk method being executed for every event being fired by the page , where I have the data table.
Is there a work around to prevent this?
Shaiju
this might help.... http://mustansaranwar.blogspot.com/2010/01/richfaces-server-side-paging.html
When you say "server side" paging, you mean, "Web Server Side".
For example, in my case, I'm looking for "Database Server Side" where the middleware java bean only requests a block of data that is appropriate for the bean to handle (as in, I would not want to grab a million records into a List on a web server...or would I?).
Just asking whether you have any insight into my problem of large data sets and scrolling...
Thank You! Please don't stop publishing ;)
Please don't stop publishing helpful stuff like this.
Thank You very much.
hope that helps...