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.

image00

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.

IssueBrowser
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?

image01

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:

IssueComparator that compares by priority
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 Comparator to define comparators:

Comparator<Issue> issueComparator = Comparator.comparing(Issue::getPriority);

This works, because Priority implements Comparable as needed.

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.

Create SortedList with comparator
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:

IssueTableFormat
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 sortable table
/**
 * 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?

image02

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.

IssueTextFilterator
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:

  1. A FilterList, a TransformedList that filters elements from a source EventList. As the source changes, FilterList observes the change and updates itself automatically.

  2. An implementation of the Matcher interface, which instructs FilterList 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 sortable and filterable table
/**
 * 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?

image03

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) and addDelete(index) which map to List.add(), set() and remove().

  • To group multiple changes into a single ListEvent, there’s beginEvent() and commitEvent().

  • 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 calling updates.forwardEvent() within the listChanged() method.

IssueToUserList
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.

Eliminate duplicate users
// 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 list with unique users
/**
 * 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?

image04

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:

  1. It publishes an EventList containing a live view of the current selection. Access the selected elements like they’re in an ArrayList.

  2. 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.

  3. 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 a TableModelEvent to do anything more intelligent. But that limitation does not exist with DefaultEventSelectionModel because it receives a fine-grained ListEvent 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!
IssuesForUsersMatcher
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.

UsersSelect
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?

image05

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() and unlock() 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.

IssuesLoader
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:

IssuesBrowser
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?

image06

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.
IssuePriorityThresholdEvaluator
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:

IssuesBrowser setup with priority filter
/**
 * 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 your ThresholdList.

  • GlazedListsSwing.upperRangeModel() adjusts the upper bound of your ThresholdList.

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.

IssuesBrowser with priority filter
// 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?

image07

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!