Programming Thoughts
J2EE - Pagination
Full List

Displaying search results in pages

Most applications are data driven, users search large tables, and expect search results split into multiple pages. This series of articles describes a simplified version of a Template helper class, named list cache, that was developed over the years according to user requirements. This articles starts with a basic design for displaying search results in pages.

Raison d'ĂȘtre

Consider typical application pages, such as below, where a user searches for records of interest, scrolls through them, selects some to view in greater detail, and scrolls in the detail page as well.

Figure 1: Example search results page

Figure 2: Example search results detail page

As scrolling to a different page or record should not re-query the database, search results should obviously be kept in session. As other search pages should have similar pagination, this is best done with a Template helper class with clients supplying configuration and code particular to their use case.

Basic design

The functionality list cache must provide are the following.

  • Store and retrieve search results.
  • Invalidate the search result and re-execute the current query if needed.
  • Split the search results into pages of a specified size so the current page can be retrieved.
  • Retain and change the current page no.
  • Retain and change the current record no. The current page always contains the current record.
  • Be serializable as clients keep instances in session.

The client's settings are the following.

Setting Scope Notes
Page size Per app page
Query Run-time Whenever client creates a query and always displays the results
Search results and query Run-time Whenever client executes a query and only uses a display page if there are results.

Query Strategies

Executing a client supplied query is an example of the Strategy design pattern. It's based on the following interface, where T is the class describing records, typically the class of a Data Transfer Object. As instances will be retained in sessions, it must be serializable.

/** * Strategy for finding a sorted list. Items are usually Data Transfer Objects describing a record. */ @FunctionalInterface public interface ListFinder<T extends Serializable> extends Serializable { /** * Returns list, sorted if needed, of Data Transfer Objects. Null may be returned if nothing found. */ public List<T> getList() throws Exception; }

An example of a list finder is shown below.

public static class GameListFinderByFamily implements ListFinder<GameDTO> { private String family; public GameListFinderByFamily(String family) { = family; } @Override public List<GameDTO> getList() throws Exception { GameTable gameTable; gameTable = GameTable.getInstance(); return gameTable.findByFamily(family, GameOption.DESIGNERS); } public String getFamily() { return family; } }

Member fields

The class must know the current search result, current query used to retrieve, page size, current record index, and invalidation state. The core member variables are shown below. Use of a custom class, ItemData, seems redundant but is necessary for features described in later articles.

public class ListCache<T extends Serializable> implements Serializable { private static class ItemData<T> implements Serializable { private T item; // Record, which is null if not yet loaded public ItemData() { item = null; } public T getItem() { return item; } public void setItem(T value) { item = value; } } private List<ItemData<T>> list; // List of records or placeholders private ListFinder<T> listFinder; // Strategy to load entire list in full list pagination mode private int pageSize; private boolean reload; // Whether list must be reloaded private int selectedIndex; // Index of currently selected record, 0 based, or -1 for no selected record

Core functions

Retrieving the full list or the current page must check if it needs to be loaded first. The core display functions are below.

/** * Loads list if not yet loaded or reload requested. */ private void checkReloadState() throws Exception { if (reload) { loadList(); } } /** * Ensures selected item index is within list boundary or -1 for empty list. */ private void checkSelectedIndex() { int size; size = list.size(); if (selectedIndex >= size) { selectedIndex = size - 1; } else if (selectedIndex < 0 && size > 0) { selectedIndex = 0; } } /** * Returns cached, record list from session, if it is present. */ private List<T> getListUnchecked() { List<T> result; result = new ArrayList<T>(list.size()); for (ItemData<T> itemData: list) { result.add(itemData.getItem()); } return result; } /** * Loads records or placeholders, according to pagination mode. */ private void loadList() throws Exception { List<T> itemList; itemList = listFinder.getList(); if (itemList == null) { itemList = new ArrayList<T>(); } setList(itemList); } /** * Caches new, record list. */ private synchronized void setList(List<T> value) { ItemData<T> itemData; reload = false; list = new ArrayList<ItemData<T>>(value.size()); for (T item: value) { itemData = new ItemData<T>(); itemData.setItem(item); list.add(itemData); } checkSelectedIndex(); } /** * Returns entire record list, loading it if needed. */ public synchronized List<T> getList() throws Exception { checkReloadState(); return getListUnchecked(); } /** * Returns current page containing selected record, loading records if needed. */ public synchronized List<T> getPage() throws Exception { int pageNo, startIndex, endIndex; checkReloadState(); if (list.size() > 0) { pageNo = getPageNo(); pageNo = (pageNo < 1)?1:pageNo; startIndex = (pageNo - 1) * pageSize; endIndex = startIndex + pageSize - 1; endIndex = (endIndex >= list.size())?list.size() - 1:endIndex; return getListUnchecked().subList(startIndex, endIndex + 1); } else { return new ArrayList<T>(); } } /** * Returns number of current page, starting at 1, containing selected record. */ public synchronized int getPageNo() throws Exception { if (selectedIndex > -1) { return (selectedIndex / pageSize) + 1; } else { return 0; } } /** * Returns index of currently selected item, starting at 0, in list. */ public synchronized int getSelectedIndex() throws Exception { checkReloadState(); return selectedIndex; } /** * Marks record list as requiring a reload. */ public synchronized void markReload() { list.clear(); reload = true; } /** * Sets index of currently selected item to be first item of page where pageNo starts at one. */ public void setPageNo(int pageNo) { selectedIndex = (pageNo - 1) * pageSize; checkSelectedIndex(); } /** * Sets currently selected item in list by index. Sets to last item if index exceeds last item. Sets no currently * selected item if index is -1. */ public synchronized void setSelectedIndex(int index) { int size; checkReloadAccess(); size = list.size(); if (index >= size) { selectedIndex = size - 1; } else if (index < 0) { selectedIndex = -1; } else { selectedIndex = index; } }

Setting full list

The search results cannot be set on its own as the query strategy used to retrieve it is also needed. Some workflow only displays the search results page if the query finds any results, so both query and results are set together, but it's more common to set the query and let the display page display what it wants.

/** * Caches list already loaded, sets full list Strategy used to load it, and sets full list pagination mode. */ public synchronized void setListAndFinder(List<T> list, ListFinder<T> listFinder) { setList(list); this.listFinder = listFinder; setSelectedIndex(0); } /** * Sets full list Strategy to retrieve list when needed and sets full list pagination mode. */ public synchronized void setListFinder(ListFinder<T> listFinder) { setList(new ArrayList<>()); this.listFinder = listFinder; markReload(); }

Notes and Limitations

Not shown is a getListFinder function, which can be used to display search criteria. As shown in the list finder example, they can have public, read-only properties. If a display page recognises the list finder class, it can read and display the search criteria to the user.

Next part

Continued in Base Record List.