Platinum Partner
netbeans,netbeans platform

EventBus: How to Publish & Subscribe on the NetBeans Platform

So you are using the NetBeans Platform and you know that it allows you to create a really modular design, in which components can be plugged in and out at your wish. But now, which pattern can you use to reduce the coupling between components that must talk together? Consider for instance the problem I face with blueMarine: some components ('explorers') allow you to query the internal database of pictures with different criteria (e.g. browsing the filesystem, by metadata, by tag...), while other components ('views') show the result in different ways (e.g. thumbnails, or property panels, or full rendering). Which is the better approach: to have explorers depending on viewers, so they can directly call a method on them? Or the opposite?

The best solution is to have neither of the two depending on each other. You can achieve that with my favorite decoupling pattern, "publish and subscribe": define a set of messages ("topics"), define portions of code that register interest in those topics and receive an asynchronous notification ("subscribers") , and define portions of code that publish and unpublish those topics ("publishers"). Then you're done. All the components can now communicate without knowing about each other, while only depending on the messaging infrastructure and on the topics.

Indeed, this is one of the first parts that I developed in blueMarine when I ported it to the NetBeans Platform more than two years ago, and it was a pretty good improvement from the previous design. The implementation was focused on the NetBeans Platform's Lookup class, one of the most powerful in the NetBeans Platform. While it is mostly used for looking up services registered in the application, it is also used by the NetBeans Platform runtime to store relevant objects (such as selected items in a list) and to notify listeners (such context sensitive actions). So I basically used a global Lookup provided by the NetBeans Platform, "Utilities.actionsGlobalContext()", to listen for selections that were made in TopComponents. It seemed a pretty logical way to work, since TopComponents usually contain an ExplorerManager that automatically receives notification of selected Nodes in trees or lists that are placed inside the TopComponent. So, the flow of events is:

select a node in a tree -> notification published to ExplorerManager -> notification published to the actionsGlobalContext() -> notification received by my listener.

With this approach, you can use the class information as the 'topic': when you select a Node, everything that is inside its private Lookup (for instance, the DataObject) gets published. DataObject is obviously one of the most used topics by blueMarine (representing photos) and delivers information about selecting a single object: to notify that a set of DataObjects has been prepared, I defined a specific class named DataObjectSelection; to notify that there is a selection task running in the background, another specific object (DataObjectSelectionInProgress) is used, so viewers can display a "Please wait" message.

The idea was neat, the implementation code a bit cumbersome and I experienced some annoying bugs. The major problem came from relying on actionGlobalContext() and depending on the TopComponent's behavior. One of the things that sometimes happened was this sequence: you get the file explorer and recursively select a directory; at this point a background scanning task starts and lasts some time; in the meantime, you activate another TopComponent (for instance the thumbnail viewer where you expect to see the results in a short time); at this point, the scanner completes and selects the new Nodes in the first TopComponent, which unfortunately is no longer the active TopComponent. So, the results don't appear. To see them, you needed to activate again the first TopComponent. Too cumbersome!

A few months ago, I learnt from Wade Chandler, another member of the NetBeans Dream Team, that things could be done in a better way. He published a very compact and neat class named CentralLookup: it is a singleton that contains a new Lookup that you can use as you want, as the NetBeans Platform is not aware of it. In this way, you are free to decide your own publishing policy without interfering in the way TopComponents work. Sometimes problems have neat and simple solutions.

Wade published his code in the incubator of PlatformX, and today I've used it as a base for a new facility in blueMarine, named EventBus. The core functions are provided by Wade's CentralLookup, while EventBus wraps it with a more focused interface. Basically, my explorers now use this code for publishing:

DataObject myDataObject = ...
EventBus.getDefault().publish(myDataObject):

Viewers use this code for receiving notifications:

    private final EventBusListener<DataObject> listener = new EventBusListener<DataObject>()
{
@Override
public void notify (final DataObject dataObject)
{
if (dataObject != null)
{
// do something
}
}
};

where the listener is registered like this:

EventBus.getDefault().subscribe(DataObject.class, listener);

The most up-to-date version is published in OpenBlueSky, you can check it out with Subversion from https://openbluesky.dev.java.net/svn/openbluesky/trunk/src/OpenBlueSky/EventBus), I'm also posting the code below:

import it.tidalwave.openide.eventbus.impl.ListenerAdapter;
import java.util.HashMap;
import java.util.Map;
import org.netbeans.platformx.centrallookup.api.CentralLookup;
import org.openide.util.Lookup.Result;

public class EventBus
{
private static final EventBus instance = new EventBus();

private final CentralLookup centralLookup = CentralLookup.getDefault();

private final Map<Class<?>, Result<?>> resultMapByClass = new HashMap<Class<?>, Result<?>>();

private final Map<EventBusListener<?>, ListenerAdapter<?>> adapterMapByListener = new HashMap<EventBusListener<?>, ListenerAdapter<?>>();

private EventBus()
{
}

public static EventBus getDefault()
{
return instance;
}

public void publish (final Object object)
{
if (object == null)
{
throw new IllegalArgumentException("object is mandatory");
}

for (final Object old : centralLookup.lookupAll(object.getClass()))
{
centralLookup.remove(old);
}

if (object != null)
{
centralLookup.add(object);
}
}

public void unpublish (final Class<?> topic)
{
for (final Object old : centralLookup.lookupAll(topic))
{
centralLookup.remove(old);
}
}

public synchronized <T> void subscribe (final Class<T> topic, final EventBusListener<T> listener)
{
Result<?> result = resultMapByClass.get(topic);

if (result == null)
{
result = centralLookup.lookupResult(topic);
resultMapByClass.put(topic, result);
result.allInstances();
}

final ListenerAdapter<T> adapter = new ListenerAdapter<T>(listener);
adapterMapByListener.put(listener, adapter);
result.addLookupListener(adapter);
}

public synchronized <T> void unsubscribe (final Class<T> topic, final EventBusListener<T> listener)
{
final Result<?> result = resultMapByClass.get(topic);

if (result == null)
{
throw new IllegalArgumentException(String.format("Never subscribed to %s", topic));
}

final ListenerAdapter<T> adapter = (ListenerAdapter<T>)adapterMapByListener.remove(listener);
result.removeLookupListener(adapter);
}
}

This is the EventBusListener:

public interface EventBusListener<T>
{
public void notify (T object);
}

and, finally, this is a utility implementation class:

import it.tidalwave.openide.eventbus.EventBusListener;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;

public class ListenerAdapter<T> implements LookupListener
{
private final EventBusListener eventBusListener;

public ListenerAdapter (final EventBusListener eventBusListener)
{
this.eventBusListener = eventBusListener;
}

public void resultChanged (final LookupEvent event)
{
final Lookup.Result result = (Lookup.Result)event.getSource();

if (!result.allInstances().isEmpty())
{
eventBusListener.notify((T)result.allInstances().iterator().next());
}
else
{
eventBusListener.notify(null);
}
}
}

 

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}