Pagination

July 13, 2008

Yojava provides a range of classes and tags to help with pagination and sorting. They have the benefit of being MVC-friendly.

Installing

There are two jars: yojava-common.jar and yojava-common-web.jar (for tags). In future they will be combined into one. Third party dependencies are detailed in the javadoc.

Pagination

First of all, in the controller we might have something like

import org.yojava.common.search.*;
...
Paginator paginator = 
    new Paginator(maxResultsPerPage, currentPage);
List results = search.find(criteria,
    paginator.getIndexOfFirst(), maxResultsPerPage
paginator.setNumResults(search.getTotalNumResults());
/* Add paginator to the model, so it can be used by the view for rendering */

Tags are provided to simplify the view – illustrated below. A strength of the tags is that the results themselves can be displayed any way you want – it doesn’t have to be a table. (For those who need it, an additional tag is provided to automatically display table rows).

<!-- summary tag outputs something like: Showing results 20 to 30 out of 35 -->
<p><results:summary paginator="${paginator}" /></p>
<table class="results">
  <c:forEach items="${myResultsList}" var="item">
    <tr>table row goes here...</tr>
  </c:forEach>
<table>
<!-- paginator tag outputs links something like: Prev | 1 | 2 | 3 | Next -->
<p><results:paginator paginator="${paginator}" /></p>

Sorting

First, in the view, we add a tag to write clickable header (TH) cells.

<table>
  <results:easySortableHeader
      columnProps="uid,heading,modifiedDate"
      columnLabels="ID,Title,Date" />
  <c:forEach ... > etc.

Then, in the controller, we introduce the SearchParams class.

SearchParams searchParams = SearchParamsUtil.initSearchParams(
    request);
Paginator paginator = new Paginator(maxResultsPerPage,
    searchParams.getCurrentPage());
List results = search.find(criteria, paginator.getIndexOfFirst(),
    maxResultsPerPage, searchParams.getOrder());
paginator.setNumResults(search.getTotalNumResults());

Comparison with displaytag

Displaytag is useful for simple cases but can get messy when things get complicated. To customise links, for example, requires specifying a custom decorator class containing embedded HTML. It doesn’t provide any assistance for querying the relevant data in the first place.

Yojava doesn’t provide print-friendly or Excel functionality but these can be handled separately with SiteMesh and Spring’s AbstractExelView respectively. Additional support for Excel is planned. Print-friendly functionality isn’t specific to search results.

AJAX considerations

If you are considering AJAX to page/sort results, be aware that it doesn’t play well with SEO or tracking page/ad impressions. This is generally unimportant for admin applications.

Convenience

The previous examples have illustrated that yojava pagination isn’t limited to simple tables. When a simple table is what you want, you can do this

<results:easyCells object="${myList}"
    columnProps="uid,heading,modifiedDate" />

Customisation

Most of the pagination/sorting tags are implemented as tag files which means the source is in the jar – you can use it as a basis for you own scripts. For example, the results:summary tag looks something like this

Results ${paginator.indexOfFirst} to ${paginator.indexOfLast} of ${paginator.numResults}

Basic pagination links can be written like this

<c:forEach begin="1" varStatus="status"
    end="${(numPages < 10) ? numPages : 10}">
  <a href='<url:replaceParam name="page"
      newValue="${status.count}" />' >
    ${status.count}
  </a>
</c:forEach>

To style alternate rows

<tr class="${util:oddEven(status.count, 'odd', 'even')}">...</tr>

GET requests and SEO

It’s good practice to have search results that support GET requests. It enables linking to search results pages, bookmarking and the possibility of browser caching.

For SEO purposes, you may even want to include the query in the path instead of the querystring where possible. For example, “/paris+holidays/results.html” instead of “/results.html?query=paris+holidays”.

When designing for SEO be wary of unwittingly creating a spider trap.

Performance and scalability

There are several strategies for querying consecutive result pages. One approach is storing results in the session to minimise the number of queries to the database (at the cost of using more memory). Depending on your application, caching may also be an option.

Using the Paginator doesn’t restrict your options. It just help keeps the code lean and clean.

This post looks at how to make service and persistence layers re-usable by another application – even if the application extends your original domain model. Lets take a simple example…

public class Article {
  public String title;
}

public class TravelArticle extends Article {
  public String destination;
}

// We CAN'T do
// TravelArticle ta = articleService.findByTitle(myTitle);
public class ArticleService {
  public Article findByTitle(String title) {return null;}
}

Making it generic

We can make it generic at the method level like so…

public class ArticleService {
  public <T extends ArticleImpl> T findByTitle(String t) {
    return null;
  }
}

or at the class level

public class ArticleService<T extends ArticleImpl> {
  public T findByTitle(String title) {return null;}
}

Taking it further

If we need to instantiate domain classes from within ArticleService we need a bit extra - we can put it in a base class something like the one below. The domain class is infered from the type parameter but it can also be specified explicitly (in case the parameter type is an interface, for example).

public class BaseService<T> {
  private Class<T> domainClass;
   
  public Class<T> getDomainClass() {
    if (domainClass == null) {
      domainClass = (Class<T> ) ((ParameterizedType) getClass()
          .getGenericSuperclass()).getActualTypeArguments()[0];
    }
    return domainClass;
  }

  public T newDomainInstance() {
    try {
      return (T) getDomainClass().newInstance();
    } catch (InstantiationException e) {
      throw new RuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  public void setDomainClass(Class<?> domainClass) {
    this.domainClass = (Class<T> ) domainClass;
  }
}

// Generified
public class ArticleService<T extends ArticleImpl>
    extends BaseService<T> {
  public T findByTitle(String title) {
    return newDomainInstance();
  }
}

Of course, if you have logic specific to the TravelArticle subclass then you have to subclass ArticleService.