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
:
- Event listeners:
EventList
fires events when its modified to keep your Swing models up-to-date.
- Concurrency:
EventList
has locks so you can share it between threads. You can worry about this later on.
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 TransformedList
s 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 String
s 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 String
s 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:
- A
FilterList
, a TransformedList
that filters elements from a source EventList
. As the source changes, FilterList
observes the change and updates itself automatically.
- 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.
- Note
- The main difference between
Matcher
s and MatcherEditor
s is that Matcher
s should be immutable whereas MatcherEditor
s can be dynamic. :The motivation for the distinction lies in thread safety. If Matcher
s 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
:
- It publishes an
EventList
containing a live view of the current selection. Access the selected elements like they're in an ArrayList
.
- 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 EventSelectionModel
'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 a TableModelEvent to do anything more intelligent. But that
limitation does not exist with EventSelectionModel because it receives
a fine-grained ListEvent detailing the reordering. Consequently,
EventSelectionModel 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.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
Matcher
s immutable. This
enables your matches()
method to be used from
multiple threads without synchronization.
Dynamic filtering using MatcherEditors
Static filtering with just Matcher
s 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 MatcherEditorListener
s.
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:
- 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!
- Models like EventTableModel can 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
- Note
- If your
EventList
s 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:
- Performing a
lock()
and unlock()
for every method call can hurt performance.
- Your
EventList
may change between adjacent calls to the thread safe decorator.
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:
- The Swing components should be constructed on the event dispatch thread. Using
SwingUtilities.invokeLater()
can accomplish this.
- The constructors for
SortedList
and our FilterList
s require that we have acquired the source EventList
's write lock.
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:
-
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.
// 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.