Programming Thoughts
J2EE - Pagination
Results Sorting
Displaying search results in pages
The previous set of articles described a Template class, list cache, that aids pagination of search results. This article adds another user requested feature, search result sorting.
Raison d'ĂȘtre
Once they've made a search, some users like to click on a table column header and sort results by the column's values like a spreadsheet. This often looks like below.

Figure 1: Sort table by name
Sorting by a column's values requires pre-loading all such values beforehand, which already happens in full list pagination mode and probably for base record pagination mode. For the other modes, no columns are pre-loaded, so the entire list must be eagerly loaded, negating the point of pagination. Dealing with contradictory user requirements is the fine art of managing user expectations and not discussed here. What is discussed is the changes to list cache needed, even if they can be inefficient.
Post query Strategy
Sorting a list requires a Comparator
instance and required column values already loaded. All
columns are already loaded in full list pagination mode and none in page by ids and page by index modes. Base
record mode is less obvious, so the comparator must state if base record assembly is required.
/** * Comparator used by {@link ListCache}. */ @FunctionalInterface public interface ListCacheRecordComparator<K extends Serializable> extends Comparator<T>,Serializable { /** * Returns whether this comparator uses data that must be loaded by a page extension assembler. This is only used * in base record pagination mode. */ default public boolean usesPageExtensionData() { return true; } }
An example of a list finder is shown below.
public static class GameItemFinder implements SingleItemFinder
Updating core member fields
If an item sorter is set, it's used for post query sorting, which can also be requested.
public class ListCache<K extends Serializable,T extends Serializable> implements Serializable { private ListFinder<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<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 ListCacheRecordComparator<T> itemSorter; // Re-sorts entire list private KeyExtractor<K,T> keyExtractor; private List<ItemData<K,T>> list; // List of records or placeholders private ListFinder<T> listFinder; // Strategy to load entire list in full list pagination mode private ListSizeFinder listSizeFinder; // Strategy to get list size for page by index pagination mode 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 boolean reSort; // Whether list must be re-sorted. private int selectedIndex; // Index of currently selected record, 0 based, or -1 for no selected record
Updating core functions
If an item sorter is set, the loadList
function must sort with it.
private static class ItemDataComparator<K,T> implements Comparator<ItemData<K,T>> { private Comparator<T> itemComparator; public ItemDataComparator(Comparator<T> itemComparator) { super(); this.itemComparator = itemComparator; } @Override public int compare(ItemData<K,T> o1, ItemData<K,T> o2) { return itemComparator.compare(o1.getItem(), o2.getItem()); } } private void loadList() throws Exception { List<K> idList; List<T> itemList; M selectedMaster; int listSize; boolean pageExtensionsLoaded; if (masterList == null) { selectedMaster = null; } else { selectedMaster = masterList.getSelected(); } switch (paginationMode) { case BASE_RECORD_LIST: itemList = baseRecordListFinder.getList(selectedMaster); pageExtensionsLoaded = baseRecordListFinder.getLoadsDetails(); if (itemList == null) { itemList = new ArrayList<T>(); } else if (itemSorter != null) { if (!pageExtensionsLoaded && itemSorter.usesPageExtensionData()) { pageExtensionAssembler.assemblePageExtensions(itemList); } Collections.sort(itemList, itemSorter); } setList(itemList, pageExtensionsLoaded, baseRecordListFinder.getLoadsDetails()); break; case FULL_LIST: itemList = listFinder.getList(selectedMaster); if (itemList == null) { itemList = new ArrayList<T>(); } else if (itemSorter != null) { Collections.sort(itemList, itemSorter); } setList(itemList, true, listFinder.getLoadsDetails()); break; case PAGE_BY_IDS: idList = idListFinder.getIds(selectedMaster); if (idList == null) { idList = new ArrayList<K>(); } setIdList(idList); if (itemSorter != null) { checkEntireListDataByIds(); Collections.sort(list, new ItemDataComparator<K,T>(itemSorter)); } break; case PAGE_BY_INDEX_RANGE: listSize = listSizeFinder.getSize(selectedMaster); setListSize(listSize); if (itemSorter != null) { checkEntireListDataByIds(); Collections.sort(list, new ItemDataComparator<K,T>(itemSorter)); } break; } }
Checking if a list needs to be lazy loaded should also check if a list needs to be sorted.
/** * Sorts entire list if requested and an item sorter is set. */ private void checkSort() throws Exception { if (reSort && itemSorter != null) { switch (paginationMode) { case BASE_RECORD_LIST: if (itemSorter.usesPageExtensionData()) { checkEntireListDataByBaseRecords(); } break; case FULL_LIST: // Full list already loaded break; case PAGE_BY_IDS: checkEntireListDataByIds(); break; case PAGE_BY_INDEX_RANGE: checkEntireListDataByIndexRange(); break; } Collections.sort(list, new ItemDataComparator<K,T>(itemSorter)); } reSort = false; } private void checkReloadState() throws Exception { if (reload) { loadList(); } else { checkSort(); } }
The public functions that set the list, id list or list size sort them if required.
/**
* Marks record list as requiring a re-sort.
*/
public synchronized void markReSort() {
reSort = true;
notifyChanged();
}
public synchronized void setBaseRecordListAndFinder(List<T> list, ListFinder<T> baseRecordListFinder) {
setList(list, baseRecordListFinder.getLoadsDetails(), baseRecordListFinder.getLoadsDetails());
this.baseRecordListFinder = baseRecordListFinder;
this.idListFinder = null;
this.listFinder = null;
this.listSizeFinder = null;
this.pageByIndexRangeFinder = null;
this.paginationMode = PaginationMode.BASE_RECORD_LIST;
markReSort();
setSelectedIndex(0);
notifyChanged();
}
public synchronized void setListAndFinder(List<T> list, ListFinder<T> listFinder) {
setList(list, true, listFinder.getLoadsDetails());
this.baseRecordListFinder = null;
this.idListFinder = null;
this.listFinder = listFinder;
this.listSizeFinder = null;
this.pageByIndexRangeFinder = null;
this.paginationMode = PaginationMode.FULL_LIST;
markReSort();
setSelectedIndex(0);
notifyChanged();
}
public synchronized void setIdListAndFinder(List<K> idList, IdListFinder
Next part
Continued in Results Sorting Example.