Archive
Glazed Lists – an essential Java library for lists and tables
Swing is the built in toolkit for creating user interfaces for Java programs. While these types of standalone desktop applications are becoming less prevalent, perhaps due to increasing functionality of webapps, there are still some industries which are highly reliant on them. If you find yourself creating a Java desktop application, you will probably have to learn Swing, and you will also probably have to learn to display information to the user in list or table form. In standard Java Swing applications, it is difficult, or at least annoying, to do the following three tasks:
- Displaying domain specific models
- Filtering
- Sorting
Glazed Lists is an open source project that makes all three of these tasks trivial. Its primary author, Jesse Wilson, is a current Google employee. Let’s examine each of these aspects in turn.
Provides a simplified API for representing objects within a JTable
Swing uses the Model View Controller paradigm throughout. Thus the table or list merely presents a view for an underlying model data structure. Part of your job in displaying data in a Swing table is to define the TableModel
implementation which provides the data for the JTable
to display.
Swing provides an AbstractTableModel that does most of the work for you, requiring you only to implement the following methods:
public int getRowCount(); public int getColumnCount(); public Object getValueAt(int row, int column);
Here’s a simple domain model object we might want to visualize in a table:
public class Person { int age; String name; public Person(String name, int age) { this.name = name; this.age = age; } public int getAge() { return age; } public String getName() { return name; } }
The logical way of doing that would be two have two columns, one for the age, one for the name. Let’s make a table model for this case:
public class PersonTableModel extends AbstractTableModel { private static final String[] columns = {"Name", "Age"}; private final List people; public PersonTableModel(List people) { // Make a defensive copy this.people = new ArrayList(people); } public int getRowCount() { return people.size(); } public int getColumnCount() { return columns.length; } public Object getValueAt(int row, int column) { Person p = people.get(row); if (column == 0) { return p.getName(); } else { return p.getAge(); } } }
This certainly works, but it requires a fair bit of boilerplate. Furthermore, the code above does not provide any way of modifying the list of people after it is copied by the TableModel.
Glazed Lists simplifies your life by treating the table not as an arbitrary two dimensional grid, but instead as a collection of rows, where the rows are kept in sync with changes to the domain models that they represent. All you have to do is define how a row is laid out, and Glazed Lists takes care of the rest.
The interface you need to use in order to define how the table looks and which aspects of your model objects are exposed is called [TableFormat][].
The interface is as follows:
int getColumnCount()
– The number of columns to display.String getColumnName(int column)
– Gets the title of the specified column.Object getColumnValue(E baseObject, int column)
– Gets the value of the specified field for the specified object.
This should remind you of the TableModel interface presented previously, but note how the getColumnValue
method is different – rather than getting a row and column, and forcing you to look up the object corresponding to that row, you are provided the object directly.
Here is a TableFormat
which allows Person
objects to be easily visible in a JTable:
public class PersonTableFormat implements TableFormat { String[] columnNames = {"Name", "Age"}; private static final int NAME_INDEX = 0; private static final int AGE_INDEX = 1; public int getColumnCount() { return columnNames.length; } public String getColumnName(int column) { return columnNames[i]; } public Object getColumnValue(Person baseObject, int column) { switch (column) { case NAME_INDEX: return baseObject.getName(); case AGE_INDEX: return baseObject.getAge(); default: throw new IllegalArgumentException("Expected column 0 or 1, got " + column); } } }
While this isn’t too hard to write, it’s still a lot of boilerplate (and not significantly different from the previous example). Glazed Lists makes it even easier than this. The entire class definition above can be replaced with three lines:
TableFormat personTableFormat = GlazedLists.tableFormat(Person.class, // Names of the properties to fetch new String[] {"name","age"}, // Names for the columns new String[] {"Name", "Age"});
What’s this doing? And how can it do all that I had previously in one line of code? Well, it requires and takes advantage of JavaBeans naming convention. The static function uses reflection to find the methods mapping to properties named “name” and “age”. In this case, it looks for two methods, getName()
and getAge()
, both of which it finds. (If I didn’t name my methods appropriately, I would get a runtime exception). The second array defines the strings that should be used to identify the corresponding entry in the properties array. In other words, element 0 in the names column is used to identify the property name at index 0.
This TableFormat class alone is insufficient to display data in a table. To do that, you need a class which fulfills the TableModel interface I described previously. Fortunately, Glazed Lists makes this easy.
The fundamental building block of Glazed Lists is the EventList class. It is similar to the ArrayList
class in Java, except that it has support for observers. If you’re not familiar with the Observer/Observable design pattern, it allows objects (observers) to register themselves and receive notifications whenever a different object (the observable) is changed. For instance, when a new item is added to the EventList
, the UI element representing it on screen automatically refreshes itself.
The EventTableModel class fulfills the TableModel
interface, making use of the EventList
and TableFormat
we described earlier. The EventList is the data provider, and the TableFormat determines how to extract the data from the EventList and display it in the table.
EventList people = new BasicEventList(); // Add all the elements for (Person p : getPeople()) { personList.add(p); } TableFormat personTableFormat = GlazedLists.tableFormat(Person.class, // Names of the properties to fetch new String[] {"name","age"}, // Names for the columns new String[] {"Name", "Age"}); EventTableModel tableModel = new EventTableModel(people, personTableFormat); JTable table = new JTable(tableModel); // Any modifications to the ‘people’ list is automatically reflected in the table
Provides a simplified means of filtering a table or list
Perhaps one of the most important features of any interactive table is the ability to filter out extraneous information. Glazed Lists makes this possible by chaining together EventList transformations; these transformations provide a different view of the underlying data. When the original model is modified, the filtered views automatically pick up the changes and update accordingly.
Say we want to provide the ability to filter the list based on people’s names. We will add a listener to a text field which listens for changes (new letters typed or deleted), and filters the list in real time. Once we have an EventList
of some sort, it is easy to create a new “view” of that same list, filtering out entries you don’t want to see. You do this by wrapping the list in a FilterList
, and then assigning some sort of filter criterion. Let’s start simple with a filtered list which only shows those users whose names start with the letter ‘A’.
EventList personList = new BasicEventList(); personList.add(new Person("Anthony Hopkins", 74)); personList.add(new Person("Barack Obama", 50)); personList.add(new Person("American McGee", 39)); Matcher personFilter = new Matcher() { public boolean matches(Person p) { return p.getName().startsWith("A"); } }; // Create a filtered list FilterList filteredList = new FilterList(personList, personFilter); // Displaying the people in a list as opposed to a table; could also create EventTableModel // as in the last example. EventListModel filteredListModel = new EventListModel(personList) JList list = new JList(filteredListModel); // At this point, shows Anthony Hopkins and American McGee
The filter I’ve defined above is static – once it’s instantiated, its filter condition never changes. Glazed Lists supports dynamic filters as well, through the MatcherEditor interface. We will see how to use a MatcherEditor
instance for a text field, but first we need to tell Glazed Lists which strings to use when filtering for a given object. We do this with the TextFilterator interface.
public class PersonTextFilterator imlements TextFilterator { // Slightly strange interface, but done for efficiency reasons public getFilterStrings(List baseList, Person element) { baseList.add(element.getName()); // Allow users to filter by age as well baseList.add(String.valueOf(element.getAge())); } }
The MatcherEditor class to use in our case is TextComponentMatcherEditor. We provide it with the text field that it will use as the filter source, as well as an instance of the PersonTextFilterator
class we just defined.
EventList personList = new BasicEventList(); personList.add(new Person("Anthony Hopkins", 74)); personList.add(new Person("Barack Obama", 50)); personList.add(new Person("American McGee", 39)); JTextField filterTextField = new JTextField(); // Add the text field to the UI - add to a JPanel // Hook the text field up to a filter list MatcherEditor filter = new TextComponentMatcherEditor(filterTextField, new PersonTextFilterator()); // Create a filtered list FilterList filteredList = new FilterList(personList, filter); EventListModel filteredListModel = new EventListModel(filteredList) JList list = new JList(filteredListModel); // List automatically updates in response to typing in the text field
Each transformed EventList
is itself an EventList
, meaning it can also be used as the basis of an EventListModel
or EventTableModel
. This chaining capability is extremely powerful.
Provides sorting capabilities
Finally, Glazed Lists makes it extremely easy to implement rich sorting capabilities in your tables or lists.
As we saw in the last example, it is possible to wrap a given EventList to provide a different view. In this case, we will have a sorted view of the data, which automatically updates whenever the underlying data changes.
To create the SortedList
, you need to make your domain object implement Comparable, or create a Comparator. For instance,
public class PersonNameComparator implements Comparator { public int compare(Person p1, Person p2) { return p1.getName().compareTo(p2.getName()); } }
EventList personList = new BasicEventList(); personList.add(new Person("Anthony Hopkins", 74)); personList.add(new Person("Barack Obama", 50)); personList.add(new Person("American McGee", 39)); Comparator nameComparator = new PersonNameComparator(); // Create a sorted list decorator SortedList sortedList = new SortedList(personList, nameComparator); EventListModel sortedListModel = new EventListModel(sortedList) JList list = new JList(filteredListModel);
While the above example works for JLists
, it’s nice to be able to sort a JTable
as well. This is not too hard, either, as long as you have set up a TableFormat instance as described in the first section of this post. In essence, the TableFormat
defines the type of each column, which is then used to sort the table whenever the corresponding column header is clicked. This behavior is defined in the TableComparatorChooser class, which exposes a static method to perform the installation on the target JTable. Here’s an example:
Comparator nameComparator = new PersonNameComparator(); // Create a sorted list decorator SortedList sortedList = new SortedList(personList, nameComparator); EventTableModel peopleTableModel = new EventTableModel(sortedList, new PersonTableFormat()); JTable peopleTable = new JTable(peopleTableModel); // Use MULTIPLE_COLUMN_MOUSE to allow sorting by multiple columns, or SINGLE_COLUMN // to sort by just a single column TableComparatorChooser tableSorter = TableComparatorChooser.install( peopleTable, sortedList, TableComparatorChooser.MULTIPLE_COLUMN_MOUSE); // At this point, clicking on the table headers will sort by this column
As the more detailed Glazed Lists tutorial warns,
By default, TableComparatorChooser sorts by casting column values to Comparable. If your column’s values are not Comparable, you’ll have to manually remove the default Comparator using
TableComparatorChooser.getComparatorsForColumn(column).clear()
.
As long as your columns are represented by Comparable classes such as Number or String, you shouldn’t have to worry about this caveat.
Conclusion
Glazed Lists is one of the best Java Swing libraries I’ve ever used. It simplifies life for the programmer as well as the end user of the software project, since tables that allow sorting and filtering are far more useful than those which do not. If you do any sort of Swing programming, you owe it to yourself to try this library out. You can find much more information, including the aforementioned tutorial, on the Glazed List website.