Glazed Lists Tutorial 1.0.0

Jesse Wilson

Copyright © 2003-2006 Jesse Wilson

Download the tutorial source code: glazedlists-1.0.0-tutorial.zip

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.

EventList, like ArrayList or Vector

The EventList interface extends the familiar java.util.List interface. This means it has the same add(), set() and remove() methods found in ArrayList and Vector.

But there are some extra features in EventList:

JList, JComboBox and JTable: Components with models

Swing uses Model-View-Controller throughout. This means you get to separate code for the data from code for the display.

EventListModel is Glazed Lists' implementation of ListModel, which provides the data for a JList. The EventListModel gets all of its data from an EventList, which you supply when the EventListModel is constructed.

As you call add() and remove() on your EventList, the EventListModel updates automatically, and in turn your JList updates automatically! Similarly, EventTableModel will update your for a JTable and EventComboBoxModel takes care of JComboBox.

A simple issue browser

Now it's time to write some code. You'll create a BasicEventList and populate it with issues. Next, create an EventListModel and a corresponding JList. Finally you can place it all in a JFrame and show that on screen.

import java.util.*;

import java.io.*;

import javax.swing.*;

import java.awt.GridBagLayout;

import java.awt.GridBagConstraints;

import java.awt.Insets;

// a simple issues library

import ca.odell.issuezilla.*;

// glazed lists

import ca.odell.glazedlists.*;

import ca.odell.glazedlists.swing.*;

/**

 * An IssueBrowser is a program for finding and viewing issues.

 * 

 * @author <href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssuesBrowser {

 

 /** event list that hosts the issues */

 private EventList issuesEventList = new BasicEventList();

 

 /**

  * Create an IssueBrowser for the specified issues.

  */

 public IssuesBrowser(Collection issues) {

   issuesEventList.addAll(issues);

 }

 

 /**

  * Display a frame for browsing issues.

  */

 public void display() {

   // create a panel with a table

   JPanel panel = new JPanel();

   panel.setLayout(new GridBagLayout());

   EventListModel issuesListModel = new EventListModel(issuesEventList);

   JList issuesJList = new JList(issuesListModel);

   JScrollPane issuesListScrollPane = new JScrollPane(issuesJList);

   panel.add(issuesListScrollPane, new GridBagConstraints(...));

   

   // 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.show();

 }

 

 /**

  * Launch the IssuesBrowser from the commandline.

  */

 public static void main(String[] args) {

   if(args.length != 1) {

     System.out.println("Usage: IssuesBrowser <file>");

     return;

   }

   

   // load some issues

   Collection issues = null;

   try {

     IssuezillaXMLParser parser = new IssuezillaXMLParser();

     InputStream issuesInStream = new FileInputStream(args[0]);

     issues = parser.loadIssues(issuesInStream, null);

     issuesInStream.close();

   } catch(IOException e) {

     e.printStackTrace();

     return;

   }

   // create the browser

   IssuesBrowser browser = new IssuesBrowser(issues);

   browser.display();

 }

}

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.

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.

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.

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

import java.util.Comparator;

// a simple issues library

import ca.odell.issuezilla.*;

/**

 * Compare issues by priority.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssueComparator implements Comparator {

 public int compare(Object a, Object b) {

   Issue issueA = (Issue)a;

   Issue issueB = (Issue)b;

   

   // rating is between 1 and 5, lower is more important

   int issueAValue = issueA.getPriority().getValue();

   int issueBValue = issueB.getPriority().getValue();

   

   return issueAValue - issueBValue;

 }

}

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 sortedIssues = new SortedList(issuesEventList, new IssueComparator());

Using TableFormat to specify columns

Although the EventTableModel 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.

import java.util.Comparator;

// a simple issues library

import ca.odell.issuezilla.*;

// glazed lists

import ca.odell.glazedlists.gui.TableFormat;

/**

 * Display issues in a tabular form.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssueTableFormat implements TableFormat {

   

   public int getColumnCount() {

       return 6;

   }

   

   public String getColumnName(int column) {

       if(column == 0)      return "ID";

       else if(column == 1) return "Type";

       else if(column == 2) return "Priority";

       else if(column == 3) return "State";

       else if(column == 4) return "Result";

       else if(column == 5) return "Summary";

       throw new IllegalStateException();

   }

   

   public Object getColumnValue(Object baseObject, int column) {

       Issue issue = (Issue)baseObject;

       if(column == 0)      return issue.getId();

       else if(column == 1) return issue.getIssueType();

       else if(column == 2) return issue.getPriority();

       else if(column == 3) return issue.getStatus();

       else if(column == 4) return issue.getResolution();

       else if(column == 5) return issue.getShortDescription();

       throw new IllegalStateException();

   }

}

Note

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.

The EventTableModel and TableComparatorChooser

With your columns prepared, replace the JList with a JTable. This means exchanging the EventListModel with an EventTableModel, which requires your IssueTableFormat for its constructor. The SortedList is the data source for the EventTableModel.

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 sortedIssues = new SortedList(issuesEventList, new IssueComparator());

   

   // create a panel with a table

   JPanel panel = new JPanel();

   panel.setLayout(new GridBagLayout());

   EventTableModel issuesTableModel = new EventTableModel(sortedIssues, new IssueTableFormat());

   JTable issuesJTable = new JTable(issuesTableModel);

   TableComparatorChooser tableSorter = new TableComparatorChooser(issuesJTable, sortedIssues, true);

   JScrollPane issuesTableScrollPane = new JScrollPane(issuesJTable);

   panel.add(issuesTableScrollPane, new GridBagConstraints(...));

   

   // 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.show();

}

Note

TableComparatorChooser supports both single column sorting (simpler) and multiple column sorting (more powerful). This is configured by the third argument in the constructor.

Warning

By default, TableComparatorChooser sorts by casting column values to Comparable. If your column's values are not Comparable, you'll have to manually remove the default Comparator using TableComparatorChooser.getComparatorsForColumn(column).clear().

So What?

We have constructed a sortable JTable with a simple and elegant API.

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!

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 Issue to the List provided.

import java.util.List;

// glazed lists

import ca.odell.glazedlists.TextFilterator;

// a simple issues library

import ca.odell.issuezilla.*;

/**

 * Get the Strings to filter against for a given Issue.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssueTextFilterator implements TextFilterator {

 public void getFilterStrings(List baseList, Object element) {

   Issue issue = (Issue)element;

   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());

 }

}

Note

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.

FilterList, Matcher, and MatcherEditor

To do filtering you'll need:

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.

Note

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.

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 EventTableModel to use it takes only a few lines of new code.

 /**

  * Display a frame for browsing issues.

  */

 public void display() {

   SortedList sortedIssues = new SortedList(issuesEventList, new IssueComparator());

   JTextField filterEdit = new JTextField(10);

   FilterList textFilteredIssues = new FilterList(sortedIssues, new TextComponentMatcherEditor(filterEdit, new IssueTextFilterator()));

   

   // create a panel with a table

   JPanel panel = new JPanel();

   panel.setLayout(new GridBagLayout());

   EventTableModel issuesTableModel = new EventTableModel(textFilteredIssues, new IssueTableFormat());

   JTable issuesJTable = new JTable(issuesTableModel);

   TableComparatorChooser tableSorter = new TableComparatorChooser(issuesJTable, sortedIssues, true);

   JScrollPane issuesTableScrollPane = new JScrollPane(issuesJTable);

   panel.add(new JLabel("Filter: "), new GridBagConstraints(...));

   panel.add(filterEdit,             new GridBagConstraints(...));

   panel.add(issuesTableScrollPane,  new GridBagConstraints(...));

       

   // 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.show();

 }

So What?

You've added filtering that's independent of sorting, display and changes in your data.

TransformedList and UniqueList

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 "jesse wilson". 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. First, we must observe the source issues list by registering a listener: source.addListEventListener(this). 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.

// glazed lists

import ca.odell.glazedlists.*;

import ca.odell.glazedlists.event.*;

// a simple issues library

import ca.odell.issuezilla.*;

/**

 * An IssuesToUserList is a list of users that is obtained by getting

 * the reporters from the issues list.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssueToUserList extends TransformedList {

 

  /**

   * Constructan IssueToUserList from an EventList that contains only 

   * Issue objects.

   */

  public IssuesToUserList(EventList source) {

    super(source);

    source.addListEventListener(this);

  }

 

  /**

   * Gets the user at the specified index.

   */

  public Object get(int index) {

    Issue issue = (Issue)source.get(index);

    return issue.getReport

  }

 

  /**

   * When the source issues list changes, propogate the exact same changes

   * for the users list.

   */

  public void listChanged(ListEvent listChanges) {

    updates.forwardEvent(listChanges);

  }

}

So What?

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

EventSelectionModel

Along with ListModel, and TableModel, Glazed Lists provides a ListSelectionModel that eliminates all of the index manipulation related to selection handling. EventSelectionModel brings you two advantages over Swing's standard DefaultListSelectionModel:

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.getAssignedTo();

return usersSelectedList.contains(user);

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.

Warning

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!

import java.util.*;

// glazed lists

import ca.odell.glazedlists.*;

import ca.odell.glazedlists.matchers.*;

// a simple issues library

import ca.odell.issuezilla.*;

/**

 * This {@link Matcher} only matches users in a predefined set.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssuesForUsersMatcher implements Matcher {

   

   /** the users to match */

   private Set users = new HashSet();

   

   /**

    * Create a new {@link IssuesForUsersMatcher} that matches only 

    * {@link Issue}s that have one or more user in the specified list.

    */

   public IssuesForUsersMatcher(Collection users) {

       // make a defensive copy of 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.

    */

   public boolean matches(Object o) {

       if(o == null) return false;

       if(users.isEmpty()) return true;

       

       Issue issue = (Issue)o;

       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 users = new ArrayList();

users.add("jessewilson");

users.add("kevinmaltby");

users.add("tmao");

Matcher usersMatcher = new IssuesForUsersMatcher(users);

        

EventList issues = ...

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

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.

import javax.swing.event.*;

import javax.swing.*;

// glazed lists

import ca.odell.glazedlists.*;

import ca.odell.glazedlists.matchers.*;

import ca.odell.glazedlists.swing.*;

// a simple issues library

import ca.odell.issuezilla.*;

/**

 * This {@link MatcherEditor} matches issues if their user is selected.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class UsersSelect extends AbstractMatcherEditor implements ListSelectionListener {

   

   /** a list of users */

   EventList usersEventList;

   EventList usersSelectedList;

   /** a widget for selecting users */

   JList usersJList;

   

   /**

    * Create a {@link IssuesForUsersMatcherEditor} that matches users from the

    * specified {@link EventList} of {@link Issue}s.

    */

   public UsersSelect(EventList source) {

       // derive the users list from the issues list

       EventList usersNonUnique = new IssueToUserList(source);

       usersEventList = new UniqueList(usersNonUnique);

       // create a JList that contains users

       EventListModel usersListModel = new EventListModel(usersEventList);

       usersJList = new JList(usersListModel);

       // create an EventList containing the JList's selection

       EventSelectionModel userSelectionModel = new EventSelectionModel(usersEventList);

       usersJList.setSelectionModel(userSelectionModel);

       usersSelectedList = userSelectionModel.getSelected();

       

       // handle changes to the list's selection

       usersJList.addListSelectionListener(this);

   }

   

   /**

    * Get the widget for selecting users.

    */

   public JList getJList() {

       return usersJList;

   }

   /**

    * When the JList selection changes, create a new Matcher and fire 

    * an event.

    */

   public void valueChanged(ListSelectionEvent e) {

       Matcher newMatcher = new IssuesForUsersMatcher(usersSelectedList);

       fireChanged(newMatcher);

   }

}

Configure the new MatcherEditor to be used by your FilterList:

EventList issues = ...

UsersSelect usersSelect = new UsersSelect(issues);

FilterList userFilteredIssues = new FilterList(issues, usersSelect);

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.

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:

Note

If your EventLists are used by only one thread, you don't need to worry about locking.

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();

 }

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:

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. The model adapter classes automatically use a special EventList that copies your list changes to the Swing event dispatch thread. You can create instances of this proxy using the factory method GlazedListsSwing.swingThreadProxyList(EventList).

When your code accesses EventTableModel and other Swing classes it must do so only from the Swing event dispatch thread. For this you can use the SwingUtilities.invokeLater() method.

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.

import java.io.*;

// a simple issues library

import ca.odell.issuezilla.*;

// glazed lists

import ca.odell.glazedlists.*;

/**

 * Loads issues on a background thread.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssuesLoader implements Runnable, IssuezillaXMLParserHandler {

 /** the issues list */

 private EventList issues = new BasicEventList();

 

 /** where issues shall be found */

 private String file;

 

 /**

  * Get the list that issues are being loaded into.

  */

 public EventList getIssues() {

   return issues;

 }

 

 /**

  * Load the issues from the specified file.

  */

 public void load(String file) {

   this.file = file;

   

   // start a background thread

   Thread backgroundThread = new Thread(this);

   backgroundThread.setName("Issues from " + file);

   backgroundThread.setDaemon(true);

   backgroundThread.start();

 }

 

 /**

  * When run, this fetches the issues from the issues URL and refreshes

  * the issues list.

  */

 public void run() {

   // load some issues

   try {

     IssuezillaXMLParser parser = new IssuezillaXMLParser();

     InputStream issuesInStream = new FileInputStream(file);

     parser.loadIssues(issuesInStream, this);

     issuesInStream.close();

   } catch(IOException e) {

     e.printStackTrace();

     return;

   }

 }

 

 /**

  * Handles a loaded issue.

  */

 public void issueLoaded(Issue issue) {

   issues.getReadWriteLock().writeLock().lock();

   try {

     issues.add(issue);

   } finally {

     issues.getReadWriteLock().writeLock().unlock();

   }

 }

}

You'll also need to make IssuesBrowser threadsafe:

import java.util.*;

import javax.swing.*;

import java.awt.GridBagLayout;

import java.awt.GridBagConstraints;

import java.awt.Insets;

// a simple issues library

import ca.odell.issuezilla.*;

// glazed lists

import ca.odell.glazedlists.*;

import ca.odell.glazedlists.swing.*;

/**

 * An IssueBrowser is a program for finding and viewing issues.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssuesBrowser implements Runnable {

 

 /** reads issues from a stream and populates the issues event list */

 private IssuesLoader issueLoader = new IssuesLoader();

 

 /** event list that hosts the issues */

 private EventList issuesEventList = issueLoader.getIssues();

 /**

  * Create an IssueBrowser for the specified issues.

  */

 public IssuesBrowser(String file) {

   SwingUtilities.invokeLater(this);

   issueLoader.load(file);

 }

 

 /**

  * Display a frame for browsing issues. This should only be run on the Swing

  * event dispatch thread.

  */

 public void run() {

   issuesEventList.getReadWriteLock().readLock().lock();

   try {

     // create the transformed models

     SortedList sortedIssues = new SortedList(issuesEventList, new IssueComparator());

     UsersSelect usersSelect = new UsersSelect(sortedIssues);

     FilterList userFilteredIssues = new FilterList(sortedIssues, usersSelect);

     JTextField filterEdit = new JTextField(10);

     FilterList textFilteredIssues = new FilterList(userFilteredIssues, new TextComponentMatcherEditor(...));

       

     // create the issues table

     EventTableModel issuesTableModel = new EventTableModel(textFilteredIssues, new IssueTableFormat());

     JTable issuesJTable = new JTable(issuesTableModel);

     TableComparatorChooser tableSorter = new TableComparatorChooser(issuesJTable, sortedIssues, true);

     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(...));

     panel.add(filterEdit,                  new GridBagConstraints(...));

     panel.add(new JLabel("Reported By: "), new GridBagConstraints(...));

     panel.add(usersListScrollPane,         new GridBagConstraints(...));

     panel.add(issuesTableScrollPane,       new GridBagConstraints(...));

           

     // 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.show();

   } finally {

     issuesEventList.getReadWriteLock().readLock().unlock();

   }

 }

 

 /**

  * Launch the IssuesBrowser from the commandline.

  */

 public static void main(String[] args) {

   if(args.length != 1) {

     System.out.println("Usage: IssuesBrowser <file>");

     return;

   }

   

   // create the browser

   IssuesBrowser browser = new IssuesBrowser(args[0]);

 }

}

So What?

You've exploited concurrency to simultaneously load and display data.

ThresholdList

The issues are assigned one of five priorities: P1 through P5. You can use a JSlider to filter the EventList using ThresholdList.

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.

Implementing ThresholdList.Evaluator

To get an integer from an Issue, extract the priority.

Note

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.

import ca.odell.glazedlists.*;

// a simple issues library

import ca.odell.issuezilla.*;

/**

 * Evaluates an issue by returning its threshold value.

 * 

 * @author <a href="mailto:jesse@odel.on.ca">Jesse Wilson</a>

 */

public class IssuePriorityThresholdEvaluator implements ThresholdList.Evaluator {

 public int evaluate(Object a) {

   Issue issue = (Issue)a;

   

   // 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 run() {

  ...

  UsersSelect usersSelect = new UsersSelect(issuesEventList);

  FilterList userFilteredIssues = new FilterList(issuesEventList, usersSelect);

  TextFilterList textFilteredIssues = new TextFilterList(userFilteredIssues, new IssueTextFilterator());

  ThresholdList priorityFilteredIssues = new ThresholdList(textFilteredIssues, new IssuePriorityThresholdEvaluator());

  SortedList sortedIssues = new SortedList(priorityFilteredIssues, new IssueComparator());

  ...

}

Warning

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.

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:

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

BoundedRangeModel priorityFilterRangeModel = GlazedListsSwing.lowerRangeModel(priorityFilteredIssues);

priorityFilterRangeModel.setRangeProperties(1, 0, 1, 5, false);

JSlider priorityFilterSlider = new JSlider(priorityFilterRangeModel);

Hashtable 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);

Other Models

In addition to the JSlider, the ThresholdList can be paired with a JComboBox, JTextField and JSpinner.

So What?

You've made it possible to filter the table simply by dragging a slider.