Java generic paged lazy List with JSF/JPA example implementation

Here’s a list implementation I came up with to enable true paged data fetching completely transparent to any user. It works independent of persistence layers, such as JPA implementations etc.

Lazy loading with the PagedList

Originally I made the PagedList because I needed paged data fetching using JMS queues for a JSF Rich datatable and datascroller; a common problem in JSF but less commonly solved, even less documented and less still solved in an elegant way. Most people are able to solve this problem by using an OpenSessionInView antipattern (keeping Hibernate-oid session open until the view requests the lazy loaded data) but since I’m using JMS queue, this was not an option. I decided to circumvent the entire JSF to JMS queue integration by providing my data in a paging lazy list. It works beautifully.

public class PagedList<Dto, QueryParameters> extends AbstractList<Dto> {

    private final QueryParameters queryParameters;

    private final PagedDataProvider<Dto, QueryParameters> pagedDataProvider;

    private final Map<Integer, List<Dto>> fetchedPages;

    private final int dataSize;

    private final int pageSize;

    public PagedList(PagedDataProvider<Dto, QueryParameters> pagedDataProvider, QueryParameters queryParameters) {
        this.pagedDataProvider = pagedDataProvider;
        this.queryParameters = queryParameters;
        fetchedPages = new HashMap<Integer, List<Dto>>();
        dataSize = pagedDataProvider.getDataSize();
        pageSize = pagedDataProvider.getPageSize();
    }

    public Dto get(int index) {
        int pageNr = (int) Math.floor(index / pageSize);
        if (fetchedPages.get(pageNr) == null) {
            fetchedPages.put(pageNr, pagedDataProvider.provide(pageNr, queryParameters));
        }
        return fetchedPages.get(pageNr).get(index % pageSize);
    }

    public int size() {
        return dataSize;
    }
}

It is different from Apache’s LazyList (and many other implementations) in that it relies on an external dataprovider to provide the dataset’s total size, pagesize and data provision itself. For example, using my lazy list version the size() method actually returns the total size of data potentially available, not just physically available in the list at the present. This way the list works completely transparent to its users. Also, there is no simple factory being passed in that creates dummy objects or something: actual remote data is being fecthed in pages by a data provider. Pages being fetched are independent of each other; when requesting page 5, page 1 through 4 are not need or fetched.

The PagedList works in combo with a PagedDataProvider interface that external dataproviders (some dao for example) could implement. So it has no dependencies whatsoever on some specific implementation. The paged data provider provides the total size of the dataset, the page size and pages of the dataset itself when needed.

Example implementation for JSF and JPA dao

Here’s an example implementation which ties a JSF Richfaces datatable to a JPA dao using a PagedList. I cut out the service layer, view handlers/controllers and whatnot for sake of simplicity.

<rich:dataTable id="searchResults" value="#{appleService.apples}" var="apple">
...
</rich:dataTable>
/**
 * JPA dao which acts as paged data provider for the paged list.
 *
 * Relies on two JPA named queries specified on the Entity being fetched.
 *
 * @author Benny Bottema
 * @see PagedDataProvider
 */
public class AppleJPADao implements AppleDao, PagedDataProvider<AppleDto, AppleQueryParameters> {

    // entitymanager stuff omitted for example (real world: injected by Spring)

    private static final int PAGESIZE = 50;

    /**
     * @see AppleDao#getAllApples(String, String)
     */
    public List<AppleDto> getAllApples(String type, String quality) {
        // optional query parameters: can be null
        AppleQueryParameters params = new AppleQueryParameters();
        params.setAppleType(type);
        params.setAppleQuality(quality);
        return new PagedList<AppleDto, AppleQueryParameters>(this, params);
    }

    /**
     * @see PagedDataProvider#provide(int, Object)
     */
    public List<AppleDto> provide(int page, AppleQueryParameters queryCriteria) {
        Query query = entityManager.createNamedQuery("Apple.findAll");
        query.setParameter("appleType", queryCriteria.getAppleType());
        query.setParameter("appleQuality", queryCriteria.getAppleQuality());
        // JPA's paging mechanism
        query.setFirstResult(PAGESIZE * page);
        query.setMaxResults(PAGESIZE);
        return (List<AppleDto>) query.getResultList();
    }

    /**
     * @see PagedDataProvider#getDataSize()
     */
    public int getDataSize() {
        Query countQuery = entityManager.createNamedQuery("Apple.count");
        return ((Long) countQuery.getSingleResult()).intValue();
    }

    /**
     * @see PagedDataProvider#provide()
     */
    public int getPageSize() {
        return PAGESIZE;
    }
}

Known issues

So far there is only one known issue, which is more of a problem with lazy loading in general: synchronizing the lazy loading list with the remote dataset to avoid becoming stale when changes happen to the dataset. Example: when the dataset changes, the size of the list is not being updated and the get() method may try to fetch wrong or even unavailable data throwing an exception. Since I’m using the paged list in a limited way, I’ve not yet encountered this problem.

One way would be to solve this problem for the Rich datable and datascroller specifically is to have a a4j poll component that listens for dataset changes on the server and refreshes the entire datatable as necessary. Just thinking out loud here…

trackback

Tags: , , , , , ,

Leave a Reply