About the Author
Julen ParraJulen Parra is a free-lance software consultant, obsessed with efficiency in software development. A lifetime programmer and analyst, has worked with companies in Germany and Spain, with the aim of improving their productivity. Since late 2002 he also collaborates in the QuantumDB project, an open source database plugin for the Eclipse platform. He's currently based in Madrid.

We encourage you to ask the author any questions or discuss the article here.

Spotlight Features

The Rich Engineering Heritage Behind Dependency Injection

Andrew McVeigh takes us on a tour of the rich heritage behind dependency injection, what it represents, and tells us why its here to stay.

Java, the OLPC, and community responsibility

The "One Laptop Per Child" project has a great device ready to ship, but there's no Java on there. Let's think about working together to put Java on OLPC!

Using Working sets and filters

Working sets are static filters for resources. You define a working set as a list of resources (files, for example) and then only those resources are shown. For example, if you want to work in project A, and see project B for reference, you don't want to have your Package Explorer cluttered with the rest of the alphabet. By default, Eclipse offers working sets for Resources, Breakpoints, Java Resources and Plug-ins. If you have your own plug-in, and some view in it can be cluttered with information, you may want to define your own working set type, and filter your view based on it. This article aims to describe how to add a new resource type to the working sets already defined in Eclipse. (Note: This article was developed and tested against Eclipse 3.2.)

The Problem

The QuantumDB plug-in is a database plug-in for Eclipse. Connections to databases are defined as bookmarks in the Bookmark View, which looks like:

Bookmark view in QuantumDB

With many bookmarks, the view can become cluttered, so some kind of filtering was needed. Each view can have its own filters, but may not easily be customisable by the user, so combining it with a working set allows the user to customise what they see. You can define several working sets (for example, “Production” and “Development”) and the working sets resources can overlap (for example, “Linux” and “Windows”). A filter can then be used to display only resources from specified working sets. This article explains how it was done.

Defining a new working set type

The resources in this Database Bookmarks view are BookmarkNode objects, a simple container adequate for use in Eclipse's content providers. They are uniquely identified by name, as no duplicates are allowed. We'll define a new working set for this kind of resources.

The org.eclipse.ui.workingsets extension point allows us to define a new working set for BookmarkNodes. To create this, open the project's plugin.xml file, and from the Extensions tab create a new extension of the org.eclipse.ui.workingsets extension point. (You can find a description of this extension point in the Eclipse help). You need to provide five pieces of information:

id:
A unique identifier for the extension; in this case, com.quantum.ui.BookmarkWorkingSet
name:
The display name in the Eclipse working sets view; in this case, Quantum Database Bookmarks
icon:
A graphic to be used in the views; in this case, reusing the bookmark view icon from before
pageClass:
Manages the user interface (the selection of the bookmarks that will belong to the working set)
updaterClass:
Updates the working sets we define

Eclipse can define new classes (implementing the correct interfaces) by clicking on the pageClass and updaterClass links. The newly generated code is placed in a new package com.quantum.ui.workingsets. The updater class is used to keep track of changes to the working sets, which we'll revisit towards the end of this article.

The page class (called BookmarkWorkingSetPage in this example) can be left with default fields, and having set the extension point up, we can run a runtime workbench to test the code. You should see that in the Working Sets view that a new working set type (Quantum Database Bookmarks) is listed in the available resources. Of course, it you press the Next button then nothing happens; that's because the newly-created BookmarkWorkingSetPage doesn't have a default constructor, and so can't be instantiated. We can add one as follows:

/**
 * Default constructor needed for instantiation by the framework
 */
public BookmarkWorkingSetPage() {
 super("com.quantum.ui.BookmarkWorkingSet", "Select the bookmarks you want",
  ImageStore.getImageDescriptor("bookmarks32.gif"));
}

To show something in the working set page, we need to implement the contents of the dialog. To do this, we implement the createControl() method to return a tree which shows all bookmarks with a checkbox next to them to allow us to select individual bookmarks in the list. The control will contain something like:

treeViewer = new CheckboxTreeViewer(composite,
 SWT.BORDER | SWT.H_SCROLL | SWT.V_SCROLL);
gd = new GridData(GridData.FILL_BOTH | GridData.GRAB_VERTICAL);
treeViewer.getControl().setLayoutData(gd);

treeViewer.setContentProvider(new BookmarkContentProvider());
treeViewer.setLabelProvider(new BookmarkLabelProvider());
treeViewer.setInput(BookmarkListNode.getInstance());

For more info on what each of these calls do, see this illustrative article. BookmarkContentProvider, BookmarkLabelProvider and BookmarkListNode are previously-defined classes that populate the Bookmark view and are included in the resources if you are interested.

Once the user has selected a set of the bookmarks, clicking Finish button instructs the code to calculate an IWorkingSet which represents the selection, using a a convenience method getCheckedList() that returns a list of the checked elements in the tree. We can store the working set in an instance variable and calculate it in the finish() method:

private IWorkingSet workingSet;
public IWorkingSet getSelection() {
 return workingSet;
}
public void finish() {
 String workingSetName = textName.getText();
 List elements = getCheckedList(treeViewer.getInput());
 IWorkingSetManager workingSetManager =
  PlatformUI.getWorkbench().getWorkingSetManager();
 workingSet = workingSetManager.createWorkingSet(workingSetName,
  (IAdaptable[])elements.toArray(new IAdaptable[elements.size()]));
}

The IWorkingSetManager will automatically take care of managing the working set, and the elements that we have selected. The dialog can also support editing existing working sets, but in order to achieve this the working set must be stored when the setSelection() method is invoked:

public void setSelection(IWorkingSet workingSet) {
 this.workingSet = workingSet;
}

Lastly, we can modify createControl() if we already have a working set, and initialize the checked state of the tree viewer with the already-selected bookmarks in the working set:

if (workingSet != null) {
 textName.setText(workingSet.getName());
}
setTreeChecks();

The setTreeChecks() convenience method sets the checkboxes for the elements in the tree viewer. We put it outside the if because we'll also check, for new working sets, the selected bookmarks in our bookmark view. The setTreeChecks() method is implemented as follows:

private void setTreeChecks() {
 List elementList = new ArrayList();
 Object[] elements;
 if (workingSet == null) {
  // Selected bookmarks in the bookmark view will be marked on init
  StructuredSelection selection = BookmarkView.getInstance().getSelection();
  for (Object element : selection.toArray()) {
   if (element instanceof BookmarkNode) {
    elementList.add((BookmarkNode) element);
   }
  }
  elements = elementList.toArray();
 } else {
  elements = workingSet.getElements();
 }
 // Mark the selected bookmarks in the tree
 treeViewer.setCheckedElements(elements);
}

Fortunately the TreeViewer class has the setCheckedElements() method that does most of the work. Tihs allows us to edit the working sets, and check that they keep their members; at least, until we exit Eclipse.

Persistency

Although we can edit working sets, the contents aren't persisted between restarts of the Eclipse workbench. The working set names are preserved, but if you edit them, they show no selected bookmarks. That's because we haven't persisted those selected bookmarks. The framework can persist (save) the working sets because it knows about working sets, but it has no idea what a bookmark is, and so no idea of how to persist it (or how to restore it later). We have to provide that information to the framework. We do that by implementing the IPersistableElement interface. The IPersistableElement has only two methods: saveState() defines what to write to the persistence file to persist our object; getFactoryId() returns a unique string that will allow the framework, when restoring the objects, to know which class is responsible of restoring that particular kind of objects. We'll use a unique string called "com.quantum.ui.bookmarkFactory", and for identification of the bookmark, the name is enough. We'll implement this interface in the TreeNode class, that is the parent of BookmarkNode.

public String getFactoryId() {
 return "com.quantum.ui.bookmarkFactory";
}
public void saveState(IMemento memento) {
 memento.putString("elementID", getName());
}

That has the effect of adding a line for each bookmark that we have selected in our working set, to an XML file where the working sets are persisted. (The file is called workingsets.xml which can be found in .metadata\.plugins\org.eclipse.ui.workbench if you're interested.) It looks like:

<?xml version="1.0" encoding="UTF-8"?>
<workingSetManager>
 <workingSet editPageId="com.quantum.ui.BookmarkWorkingSet"
  factoryID="org.eclipse.ui.internal.WorkingSetFactory" label="BookmarkWS" name="BookmarkWS">
  <item factoryID="com.quantum.ui.bookmarkFactory" elementID="AccessTest" />
 </workingSet>
 <workingSet editPageId="org.eclipse.jdt.ui.JavaWorkingSetPage"
  factoryID="org.eclipse.ui.internal.WorkingSetFactory" label="JavaWS" name="JavaWS">
  <item elementID="=Prueba/src" factoryID="org.eclipse.jdt.ui.PersistableJavaElementFactory"/>
 </workingSet>
 <workingSet aggregate="true" factoryID="org.eclipse.ui.internal.WorkingSetFactory"
  label="Window Working Set" name="Aggregate for window 1169719504828" />
</workingSetManager>

... or at least, that's how it should look like. The problem is that the line in italics isn't there. That is the line that should persist the one bookmark selected by our working set ("AccessTest"). Although the bookmark implements the IPersistableElement interface, the framework hasn't recognized the fact that the BookmarkNode class (via its parent TreeNode) implements IPersistableElement. Instead, an adapter has to be provided via the more generic IAdaptable interface. The IAdaptable interface is a very generic and rather ubiquitous (in Eclipse) interface that basically allows us not to define too many interfaces for our classes. As one can see, the more services that get implemented via interface compliance, like this working set persistence, the longer our list of interfaces will be. Long lists of interfaces are difficult to manage properly, and may lead to unintended conflicts. So, for many services, Eclipse does not ask if the class implements interface xxx, but rather if it implements IAdaptable, and then asks the IAdaptable interface if it can provide an object that implements the interface xxx. That allow us to define an external class to provide the interface, instead of piling them all up in ours. For a more detailled discussion on the IAdaptable interface, and of how you can define an external class to handle it, see this EclipseZone article.

In this case, TreeNode only implements three interfaces (one of them already being IAdaptable), so a fourth one is not too much. Then we simply instruct the IAdaptable interface part of TreeNode (by implementing getAdapter()), to return this when asked for an IPersistableElement:

public Object getAdapter(Class adapter) {
 if (adapter == IPersistableElement.class) {
  return this;
 }
 return null;
}

Now it will work, which we can verify by looking at the workingsets.xml file, but not because we notice any change in our application. The working sets are empty, because we still haven't told the framework how to restore them. The framework, when reading the XML file, will see the factoryID="com.quantum.ui.bookmarkFactory" and will search for a factory with that id. The factory is an Eclipse extension defined in org.eclipse.ui.elementFactories. This extension contains an id (that matches the factoryID string "com.quantum.ui.bookmarkFactory"), and a class name. The class must implement the IElementFactory interface, with an implementation of createElement() that returns an IAdaptable object:

public IAdaptable createElement(IMemento memento) {
 String identifier = memento.getString("elementID");
 if (identifier != null) {
  // If the bookmark node is not in the list, no object will be returned
  return BookmarkListNode.getInstance().find(identifier);
 }
 return null;
}

There is no provision in this code for creating bookmark nodes that don't exist in the BookmarkListNode, so you cannot restore a working set element if it has been deleted from the bookmarks list.

Filtering the view

Now to the most intricate part. We have to filter the bookmarks displayed to match only those in the selected working set(s). To do this, we need to know which working set, if any, is selected for our view. Usually views will implement a selection of working sets in the toolbar menu, but for this purpose it is enough to default always to the window working set. The Window Working Set is the one you select when you open the working sets dialog. To obtain it, we call:

IWorkingSet workingSet = getSite().getPage().getAggregateWorkingSet();

IWorkingSet is the interface for working sets. It gives the label, the name, the contents, etc. We are mostly interested in the contents; that is, the bookmarks that we want to be shown in the bookmark view. We get that with:

IAdaptable[] workingSetElements = workingSet.getElements();

This returns all the selected elements of all the selected working sets in the working set dialog. That includes our bookmark working sets, the resource working sets, the Java working sets, etc. That's because the working sets can be aggregate, in which case they are composed of other working sets. Fortunately, that's all transparent to us, as we just ask for the elements and get them, regardless of if our working set is a simple working set, or an aggregate. Of course we'll be only interested in the elements of type BookmarkNode.

Now that we have the elements, we can create a filter. In the BookmarkView, when creating the tree viewer, we add:

BookmarkWorkingSetFilter workingSetFilter = new BookmarkWorkingSetFilter();
 ...
treeViewer.setInput(input);
treeViewer.addFilter(workingSetFilter);

For some reason, the filter has to be set after the input, or the bookmark working sets won't be recreated correctly. The filter is a relatively simple class, that only has to know which elements (from working sets) of the BookmarkNode type are selected, and filter all those that aren't. The following is somewhat inefficient; each time that the filter is called, it checks all the working sets. That way, when a working set is changed (by selecting one new element, for example), you only need to refresh the tree to reflect all changes to the working sets. The filter method is:

public Object[] filter(Viewer viewer, Object parent, Object[] elements) {
 if (workingSet != null) {
  boolean isAnyBookmarkElement = false;
  // Check if there is at least an element of type BookmarkNode
  for(IAdaptable element : workingSet.getElements()) {
   if (element instanceof BookmarkNode || element.getAdapter( BookmarkNode.class ) != null) {
    isAnyBookmarkElement = true;
    break;
   }
  }
  // If no element of type BookmarkNode, we return all items
  if (!isAnyBookmarkElement) {
   return elements;
  }
 }
 return super.filter(viewer, parent, elements);
}

In the filter we store the window working set into the workingSet variable. Then, every time the filter is used, we get all the working set elements, and see if any one is a BookmarkNode. If any is, we filter based on these elements. If none is , we return all the received elements (the BookmarkNodes to be displayed). The super.filter() method will call our select() function if we have one defined, for each BookmarkNode to be displayed. So we filter like this:

public boolean select(Viewer viewer, Object parentElement, Object element) {
 BookmarkNode bookmarkNode = null;
 if (workingSet == null) {
  return true;
 }
 if (element instanceof BookmarkNode) {
  bookmarkNode = (BookmarkNode) element;
 } else if (element instanceof IAdaptable) {
  IAdaptable adaptable = (IAdaptable) element;
  bookmarkNode = (BookmarkNode) adaptable.getAdapter(IResource.class);
 }
 if (bookmarkNode != null) {
  return isInWorkingSet(bookmarkNode);
 }
 return true;
}

Where the isInWorkingSet() method will return true if the bookmarkNode is in the filter's working set. It's better to listen for changes to the working set, make a list of the elements when a change is processed, and filter based on that list; but this works for the purposes of this example.

Listening for Changes

Our implementation so far misses a couple of situations. The most important one is what happens when the user changes a working set, for example adding a BookmarkNode to the active working set. The user would expect (not altogether unreasonably) to see the added bookmark in the bookmark view. But the view is currently unaware of the changes in the working set. We have to listen to those changes; the object that will give us that service of telling us about changes is the working set manager, which implements the IWorkingSetManager interface:

IWorkingSetManager workingSetManager = QuantumPlugin.getDefault().getWorkbench().getWorkingSetManager();

This gives you access to query the existing working sets, but in this case we are just interested in changes so that we can re-filter the view:

workingSetManager.addPropertyChangeListener(propertyChangeListener);

You can get notifications of all kind of events related to working sets, such as if one has changed name, or content, or being deleted. You should add code for each situation, if you are selecting your working set at the view level. As we are using always the "window working set", we are just interested in the fact that something has changed that may affect us, and so we just need to refresh the viewer. As the viewer's filter reloads all working sets elements, no further processing is needed:

private IPropertyChangeListener propertyChangeListener = new IPropertyChangeListener() {
 public void propertyChange(PropertyChangeEvent event) {
  treeViewer.refresh();
 }
};

There is still one last scenario to consider. We have just processed the changes to our filters if the working set changes. What happens when we change something in the resource view, in our case the bookmark view? Suppose we change the name of one of the bookmarks. We should change it in every working set where it's selected. Fortunately Eclipse will take care of that (and also when you delete a resource). But if you add a resource, and you'd like to add it to the active working set, you need to use the updater class. (The updater class is the second class in the original working set extension point.)

The updater class must implement the IWorkingSetUpdater interface, so that it will be informed of changes to the working sets. It consists of four methods:

  • add(IWorkingSet)
  • remove(IWorkingSet)
  • contains(IWorkingSet)
  • dispose()

The working set manager calls the add() method on Eclipse startup with every working set of matching id that it has, allowing your updater class to build a list of the working sets that relate to it. Then, for each addition or deletion of a related working set, the working set manager will call add() or remove() as needed. The dispose() method will be called when the manager is disposed (usually at the closing of the Eclipse framework), to allow you to dispose allocated resources. You have also to ensure that the contains() method will return true if it receives a working set that your updater object contains, and false otherwise. The idea is to build a list of the working sets of your type (in our case BookmarkWorkingSet objects), to know what to update. When a change occurs, you can update the working set as follows:

workingSet.setElements((IAdaptable[]) elements.toArray(new IAdaptable[elements.size()]));

Where elements would be a list of the elements (BookmarkNodes) that will comprise that working set.

You can find the complete sources of the Quantum plug-in at http://quantum.sourceforge.net and working sets are implemented in the last release, 3.0.4.

Acknowledgements

I would like to thank Alex Blewitt for his invaluable input into this document. However, all opinions expressed are mine, as are all errors and omissions.

We encourage you to ask the author any questions or discuss the article here.