Copyright © 2003-2020
Jesse Wilson - Holger Brands
Welcome to the Glazed Lists tutorial.
This guide shows you step by step how to create a simple application for browsing issues of a bug database. It’ll show you the basics of sorting, filtering and transforming lists of data with the help of Glazed Lists.
Let’s get started!
1. Hello World
You’re going to create a simple app for browsing an Issuezilla bug database.
The annoying work like loading the issues data into Java has been taken care of by the included issuezilla.jar
file.
It’ll parse an XML file (or stream!) into simple Issue objects.
If you’d prefer, substitute Issuezilla with another data source.
Regardless of what the data looks like, we’re going to sort, filter and transform it using Glazed Lists.
First off, you’ll write "Hello World" with Glazed Lists by displaying issues data within a JList
.
1.1. EventList, like ArrayList
The EventList
interface extends the familiar java.util.List
interface.
This means it has the same methods found in ArrayList like add(), set() and remove().
But there are some extra features in EventList:
-
Event listeners: An EventList fires events when it’s modified to keep your GUI models (Swing, SWT, JavaFX) up-to-date. The
EventList
interface contains methods to add and remove event listeners. -
Concurrency:
EventList
has locks so you can share it between threads. You can worry about this later on.
In this tutorial we’ll conventrate on building a Swing-based application.
1.2. JList, JComboBox and JTable: Components with models
The Swing UI toolkit uses the Model-View-Controller pattern throughout. This means you get to separate code for the data from code for the display.
DefaultEventListModel
is Glazed Lists' implementation of ListModel
, which provides the data for a JList
.
The DefaultEventListModel gets all of its data from an EventList, which you supply as a constructor argument.
As you add and/or remove elements to/from your EventList, the DefaultEventListModel updates automatically, and in turn your JList
updates automatically!
Similarly, DefaultEventTableModel
will update your JTable
and DefaultEventComboBoxModel
takes care of JComboBox
.
The following diagram gives on overview of the available Swing models.
The class GlazedListsSwing
provides convenient factory methods for creating these models.

1.3. A simple issue browser
Now it’s time to write some code.
You’ll create a BasicEventList
and populate it with issues.
Next, create a DefaultEventListModel
and a corresponding JList
.
Finally you can place it all in a JFrame
and show that on screen.
package com.publicobject.glazedlists.tutorial.chapter1;
import static ca.odell.glazedlists.swing.GlazedListsSwing.eventListModelWithThreadProxyList;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.swing.DefaultEventListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import ca.odell.issuezilla.Issue;
import ca.odell.issuezilla.IssuezillaXMLParser;
/**
* An IssueBrowser is a program for finding and viewing issues.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssuesBrowser1 {
/** event list that hosts the issues */
private EventList<Issue> issuesEventList = new BasicEventList<>(); (1)
/**
* Create an IssueBrowser for the specified issues.
*/
public IssuesBrowser1(Collection<Issue> issues) {
issuesEventList.addAll(issues); (2)
}
/**
* Display a frame for browsing issues.
*/
public void display() {
JPanel panel = new JPanel(new GridBagLayout());
DefaultEventListModel<Issue> listModel = eventListModelWithThreadProxyList(issuesEventList);(3)
JList<Issue> issuesJList = new JList<>(listModel); (4)
JScrollPane issuesListScrollPane = new JScrollPane(issuesJList);
panel.add(issuesListScrollPane, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
// create a frame with that panel
JFrame frame = new JFrame("Issues");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(540, 380);
frame.getContentPane().add(panel);
frame.setVisible(true);
}
/**
* Launch the IssuesBrowser from the commandline.
*/
public static void main(String[] args) {
// load some issues
final Collection<Issue> issues;
IssuezillaXMLParser parser = new IssuezillaXMLParser();
try (InputStream inputStream = IssuesBrowser1.class.getResourceAsStream("/issues.xml")) {
issues = parser.loadIssues(inputStream, null);
} catch (IOException e) {
e.printStackTrace();
return;
}
// Schedule a job for the event-dispatching thread:
// creating and showing this application's GUI.
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// create the browser
IssuesBrowser1 browser = new IssuesBrowser1(issues);
browser.display();
}
});
}
}
1 | use a BasicEventList as source for issues |
2 | loaded issues are added to the BasicEventList |
3 | create the DefaultEventListModel with GlazedListsSwing -factory method |
4 | construct JList with the new ListModel |
Glazed Lists provides different variants of factory methods for creating the Swing model classes. The examples in this tutorial use those factories which internally wrap the source list with a SwingThreadProxyList and use that one as source list for the models. Those factory methods have WithThreadProxyList in their name. This way, list events are delivered on the Swing event dispatch thread. The Concurrency chapter provides some more information. |
1.4. So What?

So far you haven’t seen the real benefits of Glazed Lists. But filtering and sorting are now easy to add. You can now swap the JList for a JTable without touching your data layer. Without Glazed Lists, such a change would have you throw out your ListModel code and implement TableModel instead.
2. Sorting, Tables & Sorting Tables
Now that you’ve got "Hello World" out of the way, it’s time to see Glazed Lists shine.
You’ll upgrade the JList
to a JTable
and let your users sort by clicking on the column headers.
2.1. SortedList, a list transformation
SortedList
is a decorator that shows a source EventList
in sorted order.
Every TransformedList
including SortedList
listens for change events from a source EventList
.
When that source is changed, the TransformedList
changes itself in response.
By layering TransformedLists like SortedList
and FilterList
you can create flexible and powerful programs with ease.
2.2. Comparators, Comparable and SortedList
To sort in Java, you must compare elements that are Comparable
or create an external Comparator
.
By creating a Comparator or implementing Comparable, we gain full control of the sort order of our elements.
For the Issue
class, you can sort using the priority
property:
package com.publicobject.glazedlists.tutorial.chapter2;
import java.util.Comparator;
import ca.odell.issuezilla.Issue;
/**
* Compare issues by priority.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssueComparator implements Comparator<Issue> {
@Override
public int compare(Issue issueA, Issue issueB) {
// rating is between 1 and 5, lower is more important
int issueAValue = issueA.getPriority().getValue();
int issueBValue = issueB.getPriority().getValue();
return issueAValue - issueBValue;
}
}
With Java 8+ you can also use helper functions of
This works, because |
Now that you can compare elements, create a SortedList
using the issues EventList
and the IssueComparator
.
The SortedList
will provide a sorted view of the issues list.
It keeps the issues sorted dynamically as the source EventList changes.
SortedList<Issue> sortedIssues = new SortedList<>(issuesEventList, new IssueComparator());
2.3. Using TableFormat to specify columns
Although the DefaultEventTableModel
takes care of the table’s rows, you must specify columns.
This includes how many columns, their names, and how to get the column value from an Issue
object.
To specify columns, implement the TableFormat
interface. The IssueTableFormat
shows how to do this for Issue
elements:
package com.publicobject.glazedlists.tutorial.chapter2;
import ca.odell.glazedlists.gui.TableFormat;
import ca.odell.issuezilla.Issue;
/**
* Display issues in a tabular form.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssueTableFormat implements TableFormat<Issue> {
@Override
public int getColumnCount() {
return 6;
}
@Override
public String getColumnName(int column) {
switch (column) {
case 0:
return "ID";
case 1:
return "Type";
case 2:
return "Priority";
case 3:
return "State";
case 4:
return "Result";
case 5:
return "Summary";
}
throw new IllegalStateException("Unexpected column: " + column);
}
@Override
public Object getColumnValue(Issue issue, int column) {
switch (column) {
case 0:
return issue.getId();
case 1:
return issue.getIssueType();
case 2:
return issue.getPriority();
case 3:
return issue.getStatus();
case 4:
return issue.getResolution();
case 5:
return issue.getShortDescription();
}
throw new IllegalStateException("Unexpected column: " + column);
}
}
There are a few mixin interfaces that allow you to do more with your table:
WritableTableFormat allows you to make your JTable editable.
AdvancedTableFormat allows you to specify the class and a Comparator for each column, for use with specialized renderers and TableComparatorChooser .
|
2.4. The DefaultEventTableModel and TableComparatorChooser
With your columns prepared, replace the JList
with a JTable
.
This means exchanging the DefaultEventListModel
with a DefaultEventTableModel
, which requires your IssueTableFormat
for its constructor.
The SortedList
is the data source for the DefaultEventTableModel
.
Although it’s initially sorted by priority, your users will want to reorder the table by clicking on the column headers.
For example, clicking on the "Type" header shall sort the issues by type.
For this, Glazed Lists provides TableComparatorChooser
, which adds sorting to a JTable
using your SortedList
.
/**
* Display a frame for browsing issues.
*/
public void display() {
SortedList<Issue> sortedIssues = new SortedList<>(issuesEventList, new IssueComparator());(1)
// create a panel with a table
JPanel panel = new JPanel(new GridBagLayout());
AdvancedTableModel<Issue> tableModel = eventTableModelWithThreadProxyList(
sortedIssues, new IssueTableFormat()); (2)
JTable issuesJTable = new JTable(tableModel); (3)
TableComparatorChooser.install(issuesJTable, sortedIssues, TableComparatorChooser.MULTIPLE_COLUMN_MOUSE); (4)
JScrollPane issuesTableScrollPane = new JScrollPane(issuesJTable);
panel.add(issuesTableScrollPane, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
// create a frame with that panel
JFrame frame = new JFrame("Issues");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(540, 380);
frame.getContentPane().add(panel);
frame.setVisible(true);
}
1 | decorate the source list with a SortedList providing the issue Comparator as the sort criteria |
2 | create the TableModel with a GlazedListsSwing -factory method |
3 | create a JTable with the new table model |
4 | install a TableComparatorChooser with multi-column sorting |
While the eventTableModel factory method creates an instance of DefaultEventTableModel , its return type is the interface AdvancedTableModel .
It’s an extension of the standard TableModel and is implemented by DefaultEventTableModel .
|
TableComparatorChooser supports both single column sorting (simpler) and multiple column sorting (more powerful).
This is configured by the third argument in the constructor.
|
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 tableSorter.getComparatorsForColumn(column).clear() .
|
2.5. So What?

Now you have built an issue table, that is sortable by clicking on the desired column headers. Next, we’ll add filtering!
3. Text Filtering
With all issues on screen it’s already time to remove some of them!
Your users can filter the table simply by entering words into a JTextField
, just like in Apple iTunes.
Text filtering is a fast and easy way to find a needle in a haystack!
3.1. TextFilterator
You need to tell Glazed Lists which Strings to filter against for each element in your EventList
.
Implement the TextFilterator
interface by adding all the relevant Strings from an Issue
to the List provided.
package com.publicobject.glazedlists.tutorial.chapter3;
import java.util.List;
import ca.odell.glazedlists.TextFilterator;
import ca.odell.issuezilla.Issue;
/**
* Get the Strings to filter against for a given Issue.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssueTextFilterator implements TextFilterator<Issue> {
public void getFilterStrings(List<String> baseList, Issue issue) {
baseList.add(issue.getComponent());
baseList.add(issue.getIssueType());
baseList.add(issue.getOperatingSystem());
baseList.add(issue.getResolution());
baseList.add(issue.getShortDescription());
baseList.add(issue.getStatus());
baseList.add(issue.getSubcomponent());
baseList.add(issue.getVersion());
}
}
The getFilterStrings() method is awkward because the List of Strings is a parameter rather than the return type.
This approach allows Glazed Lists to skip creating an ArrayList each time the method is invoked.
We’re generally averse to this kind of micro-optimization.
In this case this performance improvement is worthwhile because the method is used heavily while filtering.
|
3.2. FilterList, Matcher, and MatcherEditor
To do filtering you’ll need:
-
A
FilterList
, aTransformedList
that filters elements from a sourceEventList
. As the source changes,FilterList
observes the change and updates itself automatically. -
An implementation of the
Matcher
interface, which instructsFilterList
whether to include or exclude a given element from the source EventList.
This is all you’ll need to do static filtering - the filtering criteria doesn’t ever change.
When you need to do dynamic filtering you’ll need a MatcherEditor
.
This interface allows you to fire events each time the filtering criteria changes.
The FilterList
responds to that change and notifies its listeners in turn.
The main difference between Matchers and MatcherEditors is that Matchers should be immutable whereas MatcherEditors can be dynamic. The motivation for the distinction lies in thread safety. If Matchers were mutable, filtering threads and Matcher editing threads could interfere with one another. |
3.3. Adding the FilterList and a TextComponentMatcherEditor
The FilterList
works with any Matcher
or MatcherEditor
.
In this case, we’ll use a TextComponentMatcherEditor
.
It accepts any JTextComponent
for editing the filter text - in most cases you’ll use a JTextField
.
Creating the FilterList
and getting your DefaultEventTableModel
to use it takes only a few lines of new code:
/**
* Display a frame for browsing issues.
*/
public void display() {
SortedList<Issue> sortedIssues = new SortedList<>(issuesEventList, new IssueComparator());
JTextField filterEdit = new JTextField(10);
IssueTextFilterator filterator = new IssueTextFilterator(); (1)
MatcherEditor<Issue> matcherEditor = new TextComponentMatcherEditor<>(filterEdit, filterator); (2)
FilterList<Issue> textFilteredIssues = new FilterList<>(sortedIssues, matcherEditor); (3)
// create a panel with a table
JPanel panel = new JPanel(new GridBagLayout());
AdvancedTableModel<Issue> tableModel = eventTableModelWithThreadProxyList(
textFilteredIssues, new IssueTableFormat()); (4)
JTable issuesJTable = new JTable(tableModel);
TableComparatorChooser.install(issuesJTable, sortedIssues, TableComparatorChooser.MULTIPLE_COLUMN_MOUSE); (5)
JScrollPane issuesTableScrollPane = new JScrollPane(issuesJTable);
panel.add(new JLabel("Filter: "), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(filterEdit, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(issuesTableScrollPane, new GridBagConstraints(0, 1, 2, 1, 1.0, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
// create a frame with that panel
JFrame frame = new JFrame("Issues");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(540, 380);
frame.getContentPane().add(panel);
frame.setVisible(true);
}
1 | create the TextFilterator for the issues |
2 | provide it and the text field to the MatcherEditor |
3 | stack the FilterList on top of the SortedList providing the filter criteria |
4 | use the filterable (and sortable) list as source for the table model |
5 | install the TableComparatorChooser with a reference to the SortedList |
3.4. So What?

You’ve added filtering that’s independent of sorting, display and changes in your data.
4. TransformedList and UniqueList
In part four of this tutorial, you will derive a list of users from your issues EventList
. Once you have a list of users, you can use that to filter the main issues list.
4.1. TransformedList and ListEventAssembler
ListEvents are sophisticated objects, providing fine-grained details on each insert, update and delete to your EventList
.
To simplify the process, ListEventAssembler
manages ListEvents and ListEventListeners for you:
-
It provides natural methods like
addInsert(index)
,addUpdate(index)
andaddDelete(index)
which map toList.add()
,set()
andremove()
. -
To group multiple changes into a single
ListEvent
, there’sbeginEvent()
andcommitEvent()
. -
For your convenience, you can fire an event identical to that which was received using
forwardEvent()
.
4.2. TransformedList and ListEvents
Each of the issues contains a 'reported by' user.
With the appropriate transformation, you can create an EventList
of users from that EventList
of issues.
As issues list is changed, the users list changes automatically.
If your first issue’s user is "jessewilson", then the first element in the derived users list will be "jessewilson".
There will be a simple one-to-one relationship between the issues list and the users list.
For this kind of arbitrary list transformation, extend the abstract TransformedList
.
By overriding the get()
method to return an issue’s user rather than the issue itself, you make the issues list look like a users list!
We’re required to write some boilerplate code to complete our users list transformation:
-
Second, when the source
EventList
changes, we forward an equivalent event to our listeners as well. This is taken care of by callingupdates.forwardEvent()
within thelistChanged()
method.
package com.publicobject.glazedlists.tutorial.chapter4;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.issuezilla.Issue;
/**
* An IssuesToUserList is a list of users that is obtained by getting the users from an issues list.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssueToUserList extends TransformedList<Issue, String> { (1)
/**
* Construct an IssuesToUserList from an EventList that contains only Issue objects.
*/
public IssueToUserList(EventList<Issue> source) {
super(source);
source.addListEventListener(this); (2)
}
/**
* Gets the user at the specified index.
*/
public String get(int index) { (3)
Issue issue = source.get(index);
return issue.getReporter();
}
/**
* When the source issues list changes, propogate the exact same changes for the users list.
*/
public void listChanged(ListEvent<Issue> listChanges) {
updates.forwardEvent(listChanges); (4)
}
/** {@inheritDoc} */
protected boolean isWritable() { (5)
return false;
}
}
1 | extend TransformedList to implement a custom list transformation |
2 | observe the source issues list by registering an event listener |
3 | apply the transformation from issue to user |
4 | when the source EventList changes, we forward an equivalent event to our listeners as well |
5 | this TransformedList is not modifiable |
4.3. Eliminating duplicates with UniqueList
Although the issues list contains over 100 issues, there’s only a few unique users.
Our users list has many duplicates - one for each occurrence of a user in the issues list.
Duplicate removal is solved quickly and easily by UniqueList
.
// derive the users list from the issues list
EventList<String> usersNonUnique = new IssueToUserList(issuesEventList);
UniqueList<String> usersEventList = new UniqueList<>(usersNonUnique);
Finally, you can display the users list in a JList
.
In the next chapter, we’ll use that JList
in a filter for our issues list.
/**
* Display a frame for browsing issues.
*/
public void display() {
SortedList<Issue> sortedIssues = new SortedList<>(issuesEventList, new IssueComparator());
JTextField filterEdit = new JTextField(10);
IssueTextFilterator filterator = new IssueTextFilterator();
MatcherEditor<Issue> textMatcherEditor = new TextComponentMatcherEditor<>(filterEdit, filterator);
FilterList<Issue> textFilteredIssues = new FilterList<>(sortedIssues, textMatcherEditor);
// derive the users list from the issues list
EventList<String> usersNonUnique = new IssueToUserList(issuesEventList);
UniqueList<String> usersEventList = new UniqueList<>(usersNonUnique);
// create the issues table
AdvancedTableModel<Issue> tableModel = eventTableModelWithThreadProxyList(
textFilteredIssues, new IssueTableFormat());
JTable issuesJTable = new JTable(tableModel);
TableComparatorChooser.install(issuesJTable, sortedIssues, TableComparatorChooser.MULTIPLE_COLUMN_MOUSE);
JScrollPane issuesTableScrollPane = new JScrollPane(issuesJTable);
// create the users list
DefaultEventListModel<String> usersListModel = eventListModelWithThreadProxyList(usersEventList);
JList<String> usersJList = new JList<>(usersListModel);
JScrollPane usersListScrollPane = new JScrollPane(usersJList);
// create the panel
JPanel panel = new JPanel(new GridBagLayout());
panel.add(new JLabel("Filter: "), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(filterEdit, new GridBagConstraints(0, 1, 1, 1, 0.15, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(new JLabel("Reported By: "), new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(usersListScrollPane, new GridBagConstraints(0, 3, 1, 1, 0.15, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(issuesTableScrollPane, new GridBagConstraints(1, 0, 1, 4, 0.85, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
// create a frame with that panel
JFrame frame = new JFrame("Issues");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(540, 380);
frame.getContentPane().add(panel);
frame.setVisible(true);
}
4.4. So What?

5. DefaultEventSelectionModel and Custom Filter Lists
Now that you’ve got the JList
displaying issue users, it’s just a few steps to make that filter the main issues table.
5.1. DefaultEventSelectionModel
Along with ListModel
and TableModel
, Glazed Lists provides a ListSelectionModel
that eliminates all of the index manipulation related to selection handling.
DefaultEventSelectionModel
brings you three advantages over Swing’s standard DefaultListSelectionModel
:
-
It publishes an
EventList
containing a live view of the current selection. Access the selected elements like they’re in anArrayList
. -
It fixes a problem with the standard ListSelectionModel’s
MULTIPLE_INTERVAL_SELECTION
mode. In that mode, rows inserted within the selected range become selected. This is quite annoying when removing a filter because restored elements become selected. The fix is in DefaultEventSelectionModel’s default selection mode,MULTIPLE_INTERVAL_SELECTION_DEFENSIVE
. In this mode, rows must be explicitly selected by your user. -
It provides another improvement in the user experience related to table sorting. When row selections exist and a table is resorted,
DefaultListSelectionModel
responds by clearing the selections, which is an undesirable reaction. The reason is that insufficient information exists in aTableModelEvent
to do anything more intelligent. But that limitation does not exist withDefaultEventSelectionModel
because it receives a fine-grainedListEvent
detailing the reordering. Consequently,DefaultEventSelectionModel
is able to preserve row selections after sorts.
You’ll enjoy accessing selection from an EventList
.
For example, you can use the familiar methods List.isEmpty()
and List.contains()
in new ways:
if (usersSelectedList.isEmpty()) return true;
...
String user = issue.getReporter();
return usersSelectedList.contains(user);
5.2. Custom filtering using Matchers
Just as you’ve seen TextComponentMatcherEditor
filter issues with a JTextField
, you can create a custom MatcherEditor
to filter with the users JList
.
The first step is to create a simple Matcher
for static filtering.
Then we’ll create MatcherEditor
to implement dynamic filtering using our static Matcher
.
Implementing the Matcher
will require you to write a single method, matches()
to test whether a given element should be filtered out.
You’ll need to create a Matcher
that accepts issues for a list of users
It’s unfortunate that Glazed Lists' Matcher uses the same class name as java.util.regex.Matcher .
If you find yourself implementing a Glazed Lists Matcher that requires regular expressions, you’ll need to fully qualify classnames throughout your code, and we apologize.
We considered 'Predicate' for the interface name but decided it was too presumptuous.
Naming is very important to us at Glazed Lists!
|
package com.publicobject.glazedlists.tutorial.chapter5;
import ca.odell.glazedlists.matchers.Matcher;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import ca.odell.issuezilla.Issue;
/**
* This {@link Matcher} only matches users in a predefined set.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssuesForUsersMatcher implements Matcher<Issue> {
/** the users to match */
private Set<String> users = new HashSet<String>();
/**
* Create a new {@link IssuesForUsersMatcher} that matches only {@link Issue}s that have one or
* more user in the specified list.
*/
public IssuesForUsersMatcher(Collection<String> users) {
// defensive copy all the users
this.users.addAll(users);
}
/**
* Test whether to include or not include the specified issue based on whether or not their user
* is selected.
*/
@Override
public boolean matches(Issue issue) {
if (issue == null)
return false;
if (users.isEmpty())
return true;
String user = issue.getReporter();
return users.contains(user);
}
}
With this IssuesForUsersMatcher
in place, create an EventList
that contains the issues that match only the specified users:
List<String> users = Arrays.asList("jessewilson", "kevinmaltby", "tmao");
Matcher<Issue> usersMatcher = new IssuesForUsersMatcher(users);
EventList<Issue> issues = ...
FilterList<Issue> issuesForUsers = new FilterList<>(issues, usersMatcher);
To avoid concurrency problems, make your Matchers immutable.
This enables your matches() method to be used from multiple threads without synchronization.
|
5.3. Dynamic filtering using MatcherEditors
Static filtering with just Matchers means that the filtering logic is fixed.
We need the filtering logic to change as the selection in the users list changes.
For this, there’s MatcherEditor
.
It provides the mechanics for FilterLists to observe changes to the filtering logic.
In your MatcherEditor
implementation, you change the filtering logic by creating a new Matcher that implements the new logic.
Then fire an event to all listening MatcherEditorListeners.
You can implement this quickly by extending our AbstractMatcherEditor
.
To implement the users filter, create an IssuesForUsersMatcher
each time the selection changes.
Then notify all your MatcherEditor’s listeners using the method fireChanged()
inherited from AbstractMatcherEditor
.
package com.publicobject.glazedlists.tutorial.chapter5;
import static ca.odell.glazedlists.swing.GlazedListsSwing.eventListModelWithThreadProxyList;
import static ca.odell.glazedlists.swing.GlazedListsSwing.eventSelectionModelWithThreadProxyList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.UniqueList;
import ca.odell.glazedlists.matchers.AbstractMatcherEditor;
import ca.odell.glazedlists.matchers.Matcher;
import ca.odell.glazedlists.matchers.MatcherEditor;
import ca.odell.glazedlists.swing.AdvancedListSelectionModel;
import ca.odell.glazedlists.swing.DefaultEventListModel;
import javax.swing.JList;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import ca.odell.issuezilla.Issue;
/**
* This {@link MatcherEditor} matches issues if their user is selected.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class UsersSelect extends AbstractMatcherEditor<Issue> implements ListSelectionListener { (1)
/** a list of users */
private EventList<String> usersEventList;
private EventList<String> usersSelectedList;
/** a widget for selecting users */
private JList<String> usersJList;
/**
* Create a {@link IssuesForUsersMatcherEditor} that matches users from the specified
* {@link EventList} of {@link Issue}s.
*/
public UsersSelect(EventList<Issue> source) {
// derive the users list from the issues list
EventList<String> usersNonUnique = new IssueToUserList(source);
usersEventList = new UniqueList<>(usersNonUnique);
// create a JList that contains users
DefaultEventListModel<String> usersListModel = eventListModelWithThreadProxyList(usersEventList);
usersJList = new JList<>(usersListModel);
// create an EventList containing the JList's selection
AdvancedListSelectionModel<String> userSelectionModel = eventSelectionModelWithThreadProxyList(usersEventList);
usersJList.setSelectionModel(userSelectionModel);
usersSelectedList = userSelectionModel.getSelected();
// handle changes to the list's selection
usersJList.addListSelectionListener(this); (2)
}
/**
* Get the widget for selecting users.
*/
public JList<String> getJList() {
return usersJList;
}
/**
* When the JList selection changes, create a new Matcher and fire an event.
*/
@Override
public void valueChanged(ListSelectionEvent e) {
Matcher<Issue> newMatcher = new IssuesForUsersMatcher(usersSelectedList); (3)
fireChanged(newMatcher); (4)
}
}
1 | exend AbstractMatcherEditor to simplify implementation |
2 | observe changes to the selection in the user list |
3 | on selection change create new IssuesForUsersMatcher with the currently selected user names |
4 | fire an change event to all listening MatcherEditorListeners, e.g. the FilterList |
Configure the new MatcherEditor
to be used by your FilterList
:
EventList<Issue> issues = ...
UsersSelect usersSelect = new UsersSelect(issues);
FilterList<Issue> userFilteredIssues = new FilterList<>(issues, usersSelect);
While the eventSelectionModel factory method creates an instance of DefaultEventSelectionModel , its return type is the interface AdvancedListSelectionModel .
It’s an extension of the standard ListSelectionModel and is implemented by DefaultEventSelectionModel .
|
5.4. So What?

You’ve exploited advanced Glazed Lists functionality to build a user filter.
First with static filtering using a Matcher
, then dynamic filtering by creating instances of that Matcher
from a MatcherEditor
.
6. Concurrency
Concurrency support is built right into the central interface of Glazed Lists, EventList
.
This may seem like mixing unrelated concerns, but the advantages are worth it:
-
Populate your user interface components from a background thread without having to use
SwingUtilities.invokeLater()
-
You can rely on explicit locking policies - you have to worry that you’re calling synchronize on the wrong object!
-
Glazed Lists provides means to queue updates to the Swing event dispatch thread when the source of the change is a different thread.
-
Filtering and sorting can be performed in the background
If your EventLists are used by only one thread, you don’t need to worry about locking. |
6.1. Read/Write Locks
Every EventList
has a method getReadWriteLock()
that should be used for threadsafe access.
Read/Write locks are designed to allow access by multiple readers or a single writer.
The locks in Glazed Lists are reentrant which allows you to lock multiple times before you unlock.
To read from an EventList
that is shared with another thread:
EventList myList = ...
myList.getReadWriteLock().readLock().lock();
try {
// perform read operations like myList.size() and myList.get()
} finally {
myList.getReadWriteLock().readLock().unlock();
}
To write to a shared EventList
:
EventList myList = ...
myList.getReadWriteLock().writeLock().lock();
try {
// perform write operations like myList.set() and myList.clear()
} finally {
myList.getReadWriteLock().writeLock().unlock();
}
6.2. GlazedLists.threadSafeList
Glazed Lists provides a thread safe EventList
that you can use without calling lock()
and unlock()
for each access.
Wrap your EventList
using the factory method GlazedLists.threadSafeList()
.
Unfortunately, this method has its drawbacks:
-
Performing a
lock()
andunlock()
for every method call can hurt performance. -
Your
EventList
may change between adjacent calls to the thread safe decorator.
6.3. The Swing Event Dispatch Thread
Swing requires that all user interface access be performed by the event dispatch thread. You won’t have to worry when you’re using Glazed Lists, however.
By using the appropriate factory methods as shown in this tutorial, the model adapter classes automatically use a special EventList that copies your list changes to the Swing event dispatch thread.
If you need, you can create instances of this proxy list yourself by using the factory method GlazedListsSwing.swingThreadProxyList(EventList)
.
Here are the two possibilities side by side:
// option 1: call the appropriate factory method to let GLazed Lists create the proxy list
EventList<Issue> sourceIssues = ...
AnvancedTableModel<Issue> issuesTableModel =
GlazedListsSwing.eventTableModelWithThreadProxyList(sourceIssues, new IssueTableFormat());
// option 2: create the proxy list yorself
EventList<Issue> sourceIssues = ...
EventList<Issue> threadProxyList = GlazedListsSwing.swingThreadProxyList(sourceIssues);
AnvancedTableModel<Issue> issuesTableModel =
GlazedListsSwing.eventTableModel(threadProxyList, new IssueTableFormat());
While option 1 is more convenient, option 2 is more flexible.
For example, you could use your own implementation of a ThreadProxyList
for special needs.
When your code accesses DefaultEventTableModel
and other Swing classes it must do so only from the Swing event dispatch thread.
For this you can use the SwingUtilities.invokeLater()
method.
6.4. Multithreading our IssuesBrowser
Adding background loading support to our IssuesBrowser
isn’t too tough.
Create a Thread
that loads the issues XML from a file or web service and populates the issues EventList
with the result.
The provided issue XML parser provides a callback issueLoaded()
which allows you to show each issue as it arrives.
package com.publicobject.glazedlists.tutorial.chapter6;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import java.io.IOException;
import java.io.InputStream;
import ca.odell.issuezilla.Issue;
import ca.odell.issuezilla.IssuezillaXMLParser;
import ca.odell.issuezilla.IssuezillaXMLParserHandler;
/**
* Loads issues on a background thread.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssuesLoader implements Runnable, IssuezillaXMLParserHandler {
/** the issues list */
private EventList<Issue> issues = new BasicEventList<>();
/**
* Get the list that issues are being loaded into.
*/
public EventList<Issue> getIssues() {
return issues;
}
/**
* Load the issues.
*/
public void load() {
// start a background thread
Thread backgroundThread = new Thread(this); (1)
backgroundThread.setName("Issues from resource");
backgroundThread.setDaemon(true);
backgroundThread.start();
}
/**
* When run, this fetches the issues from the issues URL and refreshes the issues list.
*/
@Override
public void run() { (2)
// load some issues
IssuezillaXMLParser parser = new IssuezillaXMLParser();
try (InputStream inputStream = IssuesLoader.class.getResourceAsStream("/issues.xml")) {
parser.loadIssues(inputStream, this);
} catch (IOException e) {
e.printStackTrace();
return;
}
}
/**
* Handles a loaded issue.
*/
@Override
public void issueLoaded(Issue issue) { (3)
issues.getReadWriteLock().writeLock().lock();
try {
issues.add(issue); (4)
} finally {
issues.getReadWriteLock().writeLock().unlock();
}
}
}
1 | when the laoding is triggered a dedicated Thread is started |
2 | the background thread will execute the run() -method which delegates to the IssuezillaXMLParser for issue loading |
3 | the parser invokes the issueLoaded -callback method for each loaded issue |
4 | the issue is added to the issues EventList while holding a write lock |
You’ll also need to make IssuesBrowser
threadsafe:
package com.publicobject.glazedlists.tutorial.chapter6;
import static ca.odell.glazedlists.swing.GlazedListsSwing.eventTableModelWithThreadProxyList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.matchers.MatcherEditor;
import ca.odell.glazedlists.swing.AdvancedTableModel;
import ca.odell.glazedlists.swing.TableComparatorChooser;
import ca.odell.glazedlists.swing.TextComponentMatcherEditor;
import com.publicobject.glazedlists.tutorial.chapter2.IssueTableFormat;
import com.publicobject.glazedlists.tutorial.chapter7.IssueTextFilterator;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import ca.odell.issuezilla.Issue;
/**
* An IssueBrowser is a program for finding and viewing issues.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssuesBrowser6 {
/** reads issues from a stream and populates the issues event list */
private IssuesLoader issueLoader = new IssuesLoader();
/** event list that hosts the issues */
private EventList<Issue> issuesEventList = issueLoader.getIssues();
/**
* Create an IssueBrowser for the specified issues.
*/
public IssuesBrowser6() {
issueLoader.load();
}
/**
* Display a frame for browsing issues. This should only be run on the Swing event dispatch
* thread.
*/
public void display() {
issuesEventList.getReadWriteLock().readLock().lock(); (1)
try {
// create the transformed models
SortedList<Issue> sortedIssues = new SortedList<>(issuesEventList, new IssueComparator());
UsersSelect usersSelect = new UsersSelect(sortedIssues);
FilterList<Issue> userFilteredIssues = new FilterList<>(sortedIssues, usersSelect);
JTextField filterEdit = new JTextField(10);
IssueTextFilterator filterator = new IssueTextFilterator();
MatcherEditor<Issue> textMatcherEditor = new TextComponentMatcherEditor<>(filterEdit, filterator);
FilterList<Issue> textFilteredIssues = new FilterList<>(userFilteredIssues, textMatcherEditor);
// create the issues table
AdvancedTableModel<Issue> tableModel = eventTableModelWithThreadProxyList(
textFilteredIssues, new IssueTableFormat());
JTable issuesJTable = new JTable(tableModel);
TableComparatorChooser.install(issuesJTable, sortedIssues, TableComparatorChooser.MULTIPLE_COLUMN_MOUSE);
JScrollPane issuesTableScrollPane = new JScrollPane(issuesJTable);
// create the users list
JScrollPane usersListScrollPane = new JScrollPane(usersSelect.getJList());
// create the panel
JPanel panel = new JPanel();
panel.setLayout(new GridBagLayout());
panel.add(new JLabel("Filter: "), new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(filterEdit, new GridBagConstraints(0, 1, 1, 1, 0.15, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(new JLabel("Reported By: "), new GridBagConstraints(0, 2, 1, 1, 0.0, 0.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(usersListScrollPane, new GridBagConstraints(0, 3, 1, 1, 0.15, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
panel.add(issuesTableScrollPane, new GridBagConstraints(1, 0, 1, 4, 0.85, 1.0,
GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(5, 5, 5, 5), 0, 0));
// create a frame with that panel
JFrame frame = new JFrame("Issues");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setSize(540, 380);
frame.getContentPane().add(panel);
frame.setVisible(true);
} finally {
issuesEventList.getReadWriteLock().readLock().unlock();
}
}
/**
* Launch the IssuesBrowser from the commandline.
*/
public static void main(String[] args) {
// create the browser and start loading issues
final IssuesBrowser6 browser = new IssuesBrowser6();
// Schedule a job for the event-dispatching thread:
// creating and showing this application's GUI.
SwingUtilities.invokeLater(new Runnable() { (2)
@Override
public void run() {
browser.display();
}
});
}
}
1 | The constructors for SortedList and our FilterLists require that we have acquired the source EventList’s read lock |
2 | the Swing components should be constructed on the event dispatch thread |
6.5. So What?

You’ve exploited concurrency to simultaneously load and display data.
7. Filter with ThresholdList
The issues are assigned one of five priorities: P1 through P5. You can use a JSlider
to filter the EventList
using ThresholdList
.
7.1. ThresholdList
ThresholdList
requires you to provide an integer for each element in your EventList.
Then, it filters all elements whose integers fall outside the provided range.
The endpoints of the range can be controlled with a JSlider
, JSpinner
or even a JComboBox
.
To provide the mapping from your list elements to integers, you must implement the simple ThresholdList.Evaluator
interface.
7.2. Implementing ThresholdList.Evaluator
To get an integer from an Issue, extract the priority.
The issue priorities are P1 (most important) to P5 (least important), but this is the opposite of what works best for JSlider . It uses low values on the left and high values on the right, but we want P1 to be furthest to the right. Therefore we flip the priority value by subtracting it from six.
|
package com.publicobject.glazedlists.tutorial.chapter7;
import ca.odell.glazedlists.ThresholdList;
import ca.odell.issuezilla.Issue;
/**
* Evaluates an issue by returning its threshold value.
*
* @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
*/
public class IssuePriorityThresholdEvaluator implements ThresholdList.Evaluator<Issue> {
public int evaluate(Issue issue) {
// rating is between 1 and 5, lower is more important
int issueRating = issue.getPriority().getValue();
// flip: now rating is between 1 and 5, higher is more important
int inverseRating = 6 - issueRating;
return inverseRating;
}
}
Create a ThresholdList
with the new IssuePriorityThresholdEvaluator
, and embed it in the pipeline of list transformations:
/**
* Display a frame for browsing issues. This should only be run on the Swing
* event dispatch thread.
*/
public void display() {
...
UsersSelect usersSelect = new UsersSelect(issuesEventList);
FilterList<Issue> userFilteredIssues = new FilterList<>(issuesEventList, usersSelect);
IssueTextFilterator filterator = new IssueTextFilterator();
MatcherEditor<Issue> matcherEditor = new TextComponentMatcherEditor<>(filterEdit, filterator);
FilterList<Issue> textFilteredIssues = new FilterList<>(userFilteredIssues, matcherEditor);
IssuePriorityThresholdEvaluator evaluator = new IssuePriorityThresholdEvaluator();
priorityFilteredIssues = new ThresholdList<>(textFilteredIssues, evaluator);
SortedList<Issue> sortedIssues = new SortedList<>(priorityFilteredIssues, new IssueComparator());
...
}
A side effect of ThresholdList is that it sorts your elements by their integer evaluation.
This makes ThresholdList particularly performant when adjusting the range values, but it may override your preferred ordering.
You can overcome this issue by applying the SortedList transformation after the ThresholdList transformation.
|
7.3. A BoundedRangeModel
You can create a model for your JSlider
to adjust either the upper or lower bound of your ThresholdList
.
Two factory methods are provided by the GlazedListsSwing
factory class:
-
GlazedListsSwing.lowerRangeModel()
adjusts the lower bound of yourThresholdList
. -
GlazedListsSwing.upperRangeModel()
adjusts the upper bound of yourThresholdList
.
The Issue Browser priority range is between 1 and 5. The slider can adjust the minimum priority displayed in the table. This is the ThresholdList’s lower bound.
// create the threshold slider
// range model handles locking itself
BoundedRangeModel priorityFilterRangeModel = lowerRangeModel(priorityFilteredIssues);
priorityFilterRangeModel.setRangeProperties(1, 0, 1, 5, false);
JSlider priorityFilterSlider = new JSlider(priorityFilterRangeModel);
Hashtable<Integer, JLabel> priorityFilterSliderLabels = new Hashtable<>();
priorityFilterSliderLabels.put(new Integer(1), new JLabel("Low"));
priorityFilterSliderLabels.put(new Integer(5), new JLabel("High"));
priorityFilterSlider.setLabelTable(priorityFilterSliderLabels);
priorityFilterSlider.setMajorTickSpacing(1);
priorityFilterSlider.setSnapToTicks(true);
priorityFilterSlider.setPaintLabels(true);
priorityFilterSlider.setPaintTicks(true);
7.4. Other Models
In addition to the JSlider
, the ThresholdList
can be paired with a JComboBox
, JTextField
and JSpinner
.
7.5. So What?

You’ve made it possible to filter the table simply by dragging a slider.
8. Conclusion
In this tutorial you saw how to incrementally build a Swing-based application to present issue data, that can be dynamically sorted and filtered by different criteria.
By applying the list transformations and Swing model adapters provided by Glazed Lists, we were able to build a responsive UI with managable effort.
Similar tasks can be accomplished for JavaFX- or SWT-based applications as well.
We hope you enjoyed this tutorial and got an impression what Glazed Lists can do for you.
Stay tuned for more exiting stuff!