First let us consider requirements. At the highest level, it is important to design an API that strips out all of the blocking and tackling; the last thing one needs is an API that encourages developers to cut-and-paste. This, then, is the primary requirement: the extension points must be stripped of all excess. Every keystroke of new code should be necessary.
The API must also support sorting a list of beans by any field that a developer wishes to expose — without requiring endless hours of comparator maintenance. The API should allow filtering the list in a straightforward manner, and it must support scrolling through pages of very long lists. One final requirement is that we should support inline expansion of a record in a list to show a more detailed view.
Note that all of the code is available. (Click the Download link at the top of the page.) The archive includes an example application that can be used for more detailed exploration. Have a look at the screen shots as well; this will put the discussion in some context.
At the top of the API, ListController extends Spring's MultiActionController and provides the operations listed below. The controller delegates all list-specific details, and maintains a small amount of information in the user's session; these DisplayPreferences amount to metadata about the current state of the view. We will examine all of this in more detail shortly.
/**
* Return to the list using any existing DisplayPreferences that
* may exist in the session. See also reset().
*/
public ModelAndView homeView(HttpServletRequest request, HttpServletResponse response)
throws CreateModelException {
// ...
}
/**
* Remove all existing preferences and display the first page of the list.
*/
public ModelAndView reset(HttpServletRequest request, HttpServletResponse response)
throws CreateModelException {
// ...
}
/**
* Remove existing preferences, create a new filter instance, bind with the
* request, and finally filter the list for display.
*/
public ModelAndView filter(HttpServletRequest request, HttpServletResponse response)
throws CreateModelException {
// ...
}
/**
* Get the request parameter indicating which comparator to use, update the
* display preferences, and finally sort the list.
*/
public ModelAndView sort(HttpServletRequest request, HttpServletResponse response)
throws CreateModelException {
// ...
}
/**
* Advance the list to the next page.
*/
public ModelAndView next(HttpServletRequest request, HttpServletResponse response)
throws CreateModelException {
// ...
}
/**
* Rewind the list to the previous page.
*/
public ModelAndView previous(HttpServletRequest request, HttpServletResponse response)
throws CreateModelException {
// ...
}
/**
* Attempt to select a record from a filtered list. If the record can be found,
* attach data for a more detailed view. If the record cannot be found (if the
* session has expired, for example, and the display preferences no longer exist)
* then reset the page.
*/
public ModelAndView selectRecord(HttpServletRequest request, HttpServletResponse response)
throws CreateModelException {
// ...
}
public interface ListProvider<T>
{
/** Get raw data from an application-specific source. */
public List<T> getUnfilteredList(HttpServletRequest request)
throws CreateModelException;
/**
* Create specific filter for lists of type T.
*/
public ListFilter<T> createFilter();
/**
* Create display preferences for the specific view. This includes
* some view-specific information — such as a collection of
* comparators for instances of type T — and some
* basic infrastructure for paging through a list.
*/
public DisplayPreferences<T> createDisplayPreferences();
// other methods ...
}
For the purposes of this discussion, I will focus on the three methods listed above. The others are mostly self-explanatory and can be investigated in the API documentation and the example application.
First, the provider must produce the raw list of beans by implementing getUnfilteredList(). How this is done is entirely up to the developer. The data may be static, in which case this method could extract the list from an in-memory cache. For more volatile data one may need to go to the database each time the data are requested. And there are myriad intermediate cases between these extremes.
Second, the provider must create an appropriate ListFilter through the createFilter() method. In order to fulfill this requirement, developers will return an instance of some object that implements the ListFilter interface, listed below. In practice this is often best done be extending the bean represented in the list, adding fields as necessary, and finally implementing the single method specified in the interface. Imagine a page that displays several columns of data — each representing a bean field — with text boxes atop each column. (Have a look at the screen shots, too.) A user might type in some sort of matching criteria for one or more fields and submit the filter request. The controller then uses an instance of the ListFilter created by the provider to bind with the request; it will then iterate through the list of beans removing those that do not match the filter criteria.
Now take a look at some code. The ListFilter interface:
public interface ListFilter<T>
{
/**
* Filter the specified object according to specific rules. A return
* value of true indicates that the record should be
* filtered out of the list.
*/
public boolean filter(T t);
}
The implementation of the filter method within the controller (and we'll look at DisplayPreferences next):
public ModelAndView filter(HttpServletRequest request, HttpServletResponse response)
throws CreateModelException {
WebUtils.setSessionAttribute(request, displayPreferencesName(), null);
ListFilter<T> filter = getListProvider().createFilter();
bindValues(request, filter);
DisplayPreferences<T> dp = getDisplayPreferences(request);
dp.setListFilter(filter);
dp.resetFrame();
return new ModelAndView(getListProvider().getViewName(), createModel(request));
}
public class CountryFilter extends Country implements ListFilter<Country>
{
public boolean filter(Country bean) {
if (bean == null) return false;
boolean match = StringUtils.match(bean.getName(), this.getName());
return !match;
}
}
// from CountryProvider...
public DisplayPreferences<Country> createDisplayPreferences() {
DisplayPreferences<Country> dp = new DisplayPreferences<Country>(Country.class);
dp.setComparator(LIFE_EXP);
return dp;
}
// from PlayerProvider...
public DisplayPreferences<Player> createDisplayPreferences() {
ComparatorMap<Player> cMap = new ComparatorMap<Player>(Player.class);
DisplayPreferences<Player> dp = new DisplayPreferences<Player>(cMap);
List<Comparator<Player>> comparators = new ArrayList<Comparator<Player>>();
comparators.add(cMap.getComparator(PLAYER_NAME));
comparators.add(cMap.getComparator(TEAM_NAME));
dp.setComparatorList(comparators);
return dp;
}
The API includes a utility class used to generate comparators. This works well in practice, and saves a great deal of time and energy. Purists will recoil from the need to supress a compiler warning in one of the methods. It bothers me, too. However, there is nothing nefarious about the code; the suppression stems from Java's implementation of generics. We also provide an invertible comparator, making it easy to invert the natural ordering of a field. And lastly, the API includes a ListFrame — a class that keeps track of which portion of the list is currently being viewed.
Our hope is that other developers will find the API useful. The example application shows just how little code is necessary to present new lists of data. Try it and please let us know what you think.