Sorting and paginating in the database with Richfaces

Richfaces is designed to easily add ajax capabilities to a Java Server Faces (JSF) application, without the developer having to write any javascript.  All Richfaces components are skinnable, and when a given skin is applied, all the components on a given website have the same look and feel.

Unfortunately, out of the box, Richfaces does not provide a way to sort and paginate results using the database–all results must be passed to Richfaces, which then sorts and paginates in memory.  This might prove to be showstopper if you have a large result set.

When a Richfaces component does not do what you need by default, you have several options.

  1. Use another component library to provide the missing component
  2. Use simple Richfaces components, such as Rich Panel, to provide a canvas that has the Richfaces look and feel, and then on that canvas, use regular (non-ajax) links.
  3. Extend the capability of one or more Richfaces components to provide the functionality you need.

The Seam example “Seamdiscs” takes the first approach.  It uses a Trinidad table component that natively supports paging and sorting via the database, and places this component in the middle of a site that uses Richfaces for the remainder of the components.  Of note, if you run the seamdiscs example, links at the bottom of the main page describe exactly what changes you need to make to your application to get both Trinidad and Richfaces working together smoothly.  Here is picture of what this approach looks like:

Richfaces and Trinidad components together

As you can see, the Trinidad table doesn’t quite match the look and feel of the rest of the site. Consequently, you may want to examine one of the other approaches. What I will explain here is the third approach.  The Richfaces documentation states that if you want to roll your own data model, to support such features as sorting and paginating in the database, or perhaps to pull from a non-standard data source, you should extend the org.ajax4jf.model.ExtendedDataModel.  Further, the documentation suggests inheriting in particular from the SerializableDataModel, which itself inherits from ExtendedDataModel.  The first time you create a concrete class that inherits from SerializableDataModel, you may be overwhelmed at the number of abstract methods that you have to implement.  But, don’t worry–those methods will all be explained here.

public Object getRowKey() and public void setRowKey(Object key)

These methods should just get and set an instance variable that is Serializable, and which you can use to look up an individual object that will populate a row in your table.  In my case, each row of my table will correspond to a Folder object, and I look up Folder objects in the database via a pk that is an Integer, so my instance variable is an Integer:

private Integer currentPk;
@Override
public Object getRowKey()
{
    return currentPk;
}

@Override
public void setRowKey(Object key)
{
    this.currentPk = (Integer) key;
}

The most important method is the walk method.  This method will be called by Richfaces, and will give you an opportunity to pull from the database just those records that will be shown to the user.  However, it is important to note that this method can potentially be called many times during one request, and we only want to make one call to the database.  So, we want to cache the results, but we also don’t want to cache them too aggressively, for if a person requests a different page of results, or changes the sort order, then we want to fetch new results.  So, two other methods that you must override, public void update() and public SerializableDataModel getSerializableModel(Range range) come into play as well.  Here is the code, discussion follows:


private boolean detached = false;
private List wrappedKeys = null;
private final Map wrappedData = new HashMap();
private String sortField = "lastname";
@In
private IFormsService serviceForms;

@Override
public void walk(FacesContext context, DataVisitor visitor, Range range, Object argument) throws IOException
{
    if (detached && getSortFieldObject() != null)
    {
        for (Integer key : wrappedKeys)
        {
            setRowKey(key);
            visitor.process(context, key, argument);
        }
    } else
    {
        int firstRow = ((SequenceRange) range).getFirstRow();
        int numberOfRows = ((SequenceRange) range).getRows();
        wrappedKeys = new ArrayList();
        for (FolderDTO folder : serviceForms.findFolders(firstRow, numberOfRows, sortField, descending)
        {
            wrappedKeys.add(folder.getPk());
            wrappedData.put(folder.getPk(), folder);
            visitor.process(context, folder.getPk(), argument);
        }
    }
}

@Override
public SerializableDataModel getSerializableModel(Range range)
{
    if (wrappedKeys != null)
    {
        detached = true;
        return this;
    }
    return null;
}


@Override
public void update()
{
    if (getSortFieldObject() != null)
    {
        String newSortField = getSortFieldObject().toString();
        if (newSortField.equals(sortField))
        {
            descending = !descending;
        }
        sortField = newSortField;
    }
    detached = false;
}

We have two checks to see if we should return cached data, or if we should fetch new data.  The first boolean, detached, starts as false.  It will be set to “true” when SerializableDataModel getSerializableModel(Range range) is called, and back to false when public void update() is called.  In this manner, the model will not be updated when the jsf component is rebuilt on postback, but rather when the new model is being built.  The other check, for getSortFieldObject, is in place for the following reason:  When it is not the table itself which has an action taken on it, but rather something outside the table (such as a DataScroller), the update method will not be called.  Since we will be passing our own parameter (the sort field) whenever a column header is clicked on, we know that getSortFieldObject() will be null if it is something outside the table that is clicked.  Here is the getSortFieldObject() method:


private Object getSortFieldObject()
{
    FacesContext context = FacesContext.getCurrentInstance();
    Object sortFieldObject =  context.getExternalContext().getRequestParameterMap().get("sortField");
    return sortFieldObject;
}

Continuing with methods we must override:  public int getRowCount():

private Integer rowCount;
@Override
public int getRowCount()
{
    if (rowCount == null)
    {
        rowCount = serviceForms.countFolders();
    }
    return rowCount;
}

Here, we are just returning the total number of folders.

The next method is public Object getRowData():

@Override
public Object getRowData()
{
    if (currentPk == null)
    {
        return null;
    } else
    {
        FolderDTO folder = wrappedData.get(currentPk);
        if (folder == null)
        {
            folder = serviceForms.getFolder(currentPk);
            wrappedData.put(currentPk, folder);
        }
        return folder;
    }
}

This method uses the currentPk, which is passed in via the setRowKey method, to look up an object.  Since the walk method will have been called first, all objects should be in wrappedKeys.  The lookup via the dao is primarily a failsafe.

public boolean isRowAvailable().

@Override
public boolean isRowAvailable()
{
    if (currentPk == null)
    {
        return false;
    }
    if (wrappedKeys.contains(currentPk))
    {
        return true;


    }
    if (wrappedData.entrySet().contains(currentPk))
    {
        return true;
    }
    {
    if (serviceForms.getFolder(currentPk) != null)
    {
        return true;
    }
    return false;
}

This method just does what its name suggests..it returns if a given pk (set via the setRowKey method) actually corresponds to an object.

public void setRowIndex(int rowIndex), public void setWrappedData(Object data), and public Object getWrappedData() are all unused, so you can simply throw an UnsupportedOperationException():

@Override
public int getRowIndex()
{
    throw new UnsupportedOperationException();
}

@Override
public Object getWrappedData()
{
    throw new UnsupportedOperationException();
}

@Override
public void setRowIndex(int rowIndex)
{
    throw new UnsupportedOperationException();
}

Now that you have implemented all the methods, there’s one more thing to note: this class should be either Session or Conversation scoped. Among other things, this ensures that the previous sort order will be preserved, so that we know if we should reverse it.

It’s time for the xhtml. We start with an ordinary Rich DataTable.  However, we add in an actionparam with each header.  This actionparam will be sent back to our table, and will let us know which header the person clicked on.  This information allows us to determine by what column we should sort the table.



    
        
            
                
            
        
        
    
    
        
            
                
            
        
        
        
    

To this, we add a datascroller, and we’re done:


Update: See the next post for instructions on abstracting the described class into a superclass and a subclass.