Programming Thoughts
J2EE - Pagination
Composite Cache

Displaying search results in pages

The previous set of articles described a Template class, list cache, that aids pagination of search results. This article expands it for related list pages.

Raison d'ĂȘtre

Records in key tables often have related records in other tables in one to many relationships, such as customers and orders. Like record details, such records are linked to the main record and lazy loaded for separate pages but there can be multiple lists, each with their own page and even change according to user's search criteria. If the user selects a different record in the main list, all other lists are invalid and they, if loaded, must re-read in reference to the new, main record. There is a master list cache and zero to many slave list caches.

Existing query Strategies

That slaves lists are read in reference to the selected record of the master list means list finders must know the master record. If the list is the master list, the master record is null. The updated Strategies are shown below.

@FunctionalInterface public interface ListFinder<M extends Serializable,T extends Serializable> extends Serializable { /** * Returns list, sorted if needed, of Value Objects. For slave lists, these are related to the currently selected * record of the master list. Null may be returned if nothing found. * * @param selectedMaster Selected record in master list or null for loading master list. */ public List<T> getList(M selectedMaster) throws Exception; /** * Returns true if finder eager loads any optional details of each item. This is usually false as loading * details results in poor performance or there are no optional details. */ default public boolean getLoadsDetails() { return false; } } @FunctionalInterface public interface IdListFinder<M extends Serializable,K extends Serializable> extends Serializable { /** * Returns list of ids of record according to implementing search criteria. Null may be returned if nothing found. * * @param selectedMaster Selected record in master list or null for loading master list. */ public List<K> getIds(M selectedMaster) throws Exception; } @FunctionalInterface public interface ListSizeFinder<M extends Serializable> extends Serializable { /** * Returns size of list records according to implementing search criteria. * * @param selectedMaster Selected record in master list or null for loading master list. */ public int getSize(M selectedMaster) throws Exception; }

Change notification

Generic frameworks cannot know what assemblage of master and slave lists clients require, so slave lists must be assembled as listeners for changes to the master list. Listeners implement the following interface. The type parameters of ListCache are explained in the next section.

public interface ListCacheListener<T extends Serializable> extends Serializable { /** * Notifies that list, selected item, or selection index changed or marked for reload. */ public void notifyChanged(ListCache<?,?,T> listCache); }

Updating core member fields

Reference to master records and use of notifications requires changes to list cache's member fields. The notifying field prevents the danger of client code triggering further notifications in an endless loop.

public class ListCache<M extends Serializable,K extends Serializable,T extends Serializable> implements Serializable { private ListFinder<M,T> baseRecordListFinder; // Strategy to load base record list in base record list pagination mode private boolean hasDetails; // Whether records have additional data for a detail page. private IdListFinder<M,K> idListFinder; // Strategy to load ids of list for page by ids pagination mode private SingleItemFinder<K,T> itemFinder; // Reloads single item with details private KeyExtractor<K,T> keyExtractor; private List<ItemData<K,T>> list; // List of records or placeholders private Collection<ListCacheListener<T>> listeners; private ListFinder<M,T> listFinder; // Strategy to load entire list in full list pagination mode private ListSizeFinder<M> listSizeFinder; // Strategy to get list size for page by index pagination mode private ListCache<?,?,M> masterList; // Master list if this cache is a slave list. private String name; // Name of list used in logging or errors private boolean notifying; private PageByIdsFinder<K,T> pageByIdsFinder; // Strategy to retrieve page in page by ids pagination mode private PageByIndexRangeFinder<T> pageByIndexRangeFinder; // Strategy to retrieve page by indices private PageExtensionAssembler<T> pageExtensionAssembler; // Adds additional data for current page for base record list pagination mode private int pageSize; private PaginationMode paginationMode; 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

The functions changed by notifications and master record are too numerous to list. Nonetheless, a few are shown to illustrate the changes.

protected void notifyChanged(boolean listChanged, boolean selectionIndexChanged, boolean otherChanged) { if (notifying) return; try { notifying = true; for (ListCacheListener<T> listener: listeners) { listener.notifyChanged(this); } } finally { notifying = false; } } private void loadList() throws Exception { List<K> idList; List<T> itemList; M selectedMaster; int listSize; if (masterList == null) { selectedMaster = null; } else { selectedMaster = masterList.getSelected(); } switch (paginationMode) { case BASE_RECORD_LIST: itemList = baseRecordListFinder.getList(selectedMaster); if (itemList == null) { itemList = new ArrayList<T>(); } setList(itemList, baseRecordListFinder.getLoadsDetails(), baseRecordListFinder.getLoadsDetails()); break; case FULL_LIST: itemList = listFinder.getList(selectedMaster); if (itemList == null) { itemList = new ArrayList<T>(); } setList(itemList, true, listFinder.getLoadsDetails()); break; case PAGE_BY_IDS: idList = idListFinder.getIds(selectedMaster); if (idList == null) { idList = new ArrayList<K>(); } setIdList(idList); break; case PAGE_BY_INDEX_RANGE: listSize = listSizeFinder.getSize(selectedMaster); setListSize(listSize); break; } } private synchronized void setList(List<T> value, boolean pageExtensionsLoaded, boolean detailLoaded) { ItemData<K,T> itemData; reload = false; list = new ArrayList<ItemData<K,T>>(value.size()); for (T item: value) { itemData = new ItemData<K,T>(); itemData.setDetailsReload(!detailLoaded); itemData.setId(keyExtractor.getKey(item)); itemData.setItem(item); itemData.setPageExtensionReload(!pageExtensionsLoaded); itemData.setReload(false); list.add(itemData); } checkSelectedIndex(); notifyChanged(); }

Composite cache

Each use case has its own, unique assemblage of list caches and query strategies but there is still scope for abstract Template classes aiding with some of the assembly. Mostly, requiring concrete classes to supply configuration for each list cache, creating each, and setting the master list cache for each slave list cache. As Java cannot describe a generic class with an arbitrary number of type parameters, there must be a specific Template class for a master list cache and no slaves, a class for master with one slave, a class for master with two slaves etc. Seven slave lists is the highest seen in practice.

A few are shown below. They refer to ListCacheConfig class not described here but they are just POJOs describing list cache member field values that are set by the concrete class. The list cache constructor copies initialises from the ListCacheConfig instance and sets itself as a listener to the master list cache if it's set.

public abstract class AbstractCompositeCache<KM extends Serializable,TM extends Serializable> implements Serializable { private ListCache<NA,KM,TM> masterList; /** * Creates session-based, composite cache with a master list and no slave lists. */ protected AbstractCompositeCache() { super(); ListCacheConfig<NA,KM,TM> masterListCacheConfig = getMasterListCacheConfig(); masterList = new ListCache<NA,KM,TM>(masterListCacheConfig, null); masterList.addListCacheListener(listCache -> {notifyMasterListCacheChanged(listCache);}); } /** * Overridden by concrete implementations to configure master list cache. */ protected abstract ListCacheConfig<NA,KM,TM> getMasterListCacheConfig(); /** * Notification that the master list cache has changed. */ protected void notifyMasterListCacheChanged(ListCache<?,?,TM> listCache) { // Nothing but may be overridden. } /** * Returns cache of master list. */ public ListCache<NA,KM,TM> getMasterListCache() { return masterList; } } public abstract class AbstractCompositeCache_1S<KM extends Serializable,TM extends Serializable, K1 extends Serializable,T1 extends Serializable> extends AbstractCompositeCache<KM,TM> { private ListCache<TM,K1,T1> slaveList1; /** * Creates session-based, composite cache with a master list and 1 slave list. */ protected AbstractCompositeCache_1S() { super(); ListCacheConfig<TM,K1,T1> slaveListCacheConfig1 = getSlaveListCache1Config(); slaveList1 = new ListCache<TM,K1,T1>(slaveListCacheConfig1, getMasterListCache()); slaveList1.addListCacheListener(listCache -> {notifySlaveListCache1Changed(listCache);}); } /** * Overridden by concrete implementations to configure slave list cache 1. */ protected abstract ListCacheConfig<TM,K1,T1> getSlaveListCache1Config(); /** * Notification that the slave list cache 1 has changed. */ protected void notifySlaveListCache1Changed(ListCache<?,?,T1> listCache) { // Nothing but may be overridden. } /** * Returns cache of slave list 1. */ public ListCache<TM,K1,T1> getSlaveListCache1() { return slaveList1; } }

An example is shown below.

Next part

Continued in Composite Cache Example.