Over a million developers have joined DZone.

Publish/Subscribe Pattern in NetBeans Platform Lookup

· Java Zone

Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code! Brought to you in partnership with ZeroTurnaround.

When we started the porting of our jPlay project, from the Swing Application Framework to the NetBeans Platform (article about that here), we replaced our extensive usage of the Eventbus library with the NetBeans Platform's Lookup. The goal was to implement the publish/subscribe pattern Eventbus has very successfully solved, by means of a NetBeans Platform Lookup equivalent.

Objectives

We wanted to achieve a complete decoupling of Swing components, helper classes, mid-tier classes, web services, pop-up frames, and the like. Eventbus shines with its easy-to-use annotations for subscribers, as well as its simple and effective publishing methods. There is nothing similar, as is, in the NetBeans Platform, but we began to take a close look at Lookup's possibilities.

The NetBeans Platform has the AbstractLookup and the InstanceContent resources. In order to implement a publish/subscribe pattern, as easy and straightforward as Eventbus in terms of its usage, some work needed doing.

Publish/Subscribe Pattern via NetBeans Platform Lookup

The first goal was to create an utility class, keeping hidden all Lookup related code, while exposing public methods following the publish/subscribe pattern.

The global idea is to make a close relationship between plain data classes and the NetBeans Platform Lookup, while maintaining the public API agnostic to anything else:

package org.jptools.tools.lookup;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import org.openide.util.Lookup;
import org.openide.util.LookupListener;
import org.openide.util.lookup.AbstractLookup;
import org.openide.util.lookup.InstanceContent;
import org.openide.util.lookup.InstanceContent.Convertor;

/**
*
* @author Carlos Hoces
*/
public final class LookupUtils {

private static LookupUtils instance;
private static final Map<Class<?>, JPAbstractLookup> LOOKUP_PS = new HashMap<Class<?>, JPAbstractLookup> ();

private LookupUtils() {
}

/**
* It will return this singleton instance
* @return
*/
public synchronized static LookupUtils getInstance() {
if (instance == null) {
instance = new LookupUtils();
}
return instance;
}

/**
* Returns the InstanceContent defined for param clazz.
* Classes may use this method to gain access to the InstanceContent associated to clazz.
* @param <T>
* @param clazz
* @return
*/
public synchronized static <T> InstanceContent getPublisher(Class<T> clazz) {
if (clazz == null) {
throw new IllegalArgumentException("clazz cannot be null");
}
setPublisher(clazz);
return LOOKUP_PS.get(clazz).getContent();
}

/**
* It will return a Lookup.Result for data stored in param clazz, and use the defined lookupListener to retrieve it.
* It will throw an IllegalArgumentException in case either param is null.
* @param <T>
* @param clazz
* @param lookupListener
* @return
*/
public synchronized static <T> Lookup.Result<T> getSubscriber(Class<T> clazz, LookupListener lookupListener) {
if (lookupListener == null || clazz == null) {
throw new IllegalArgumentException("params cannot be null");
}
setPublisher(clazz);
final Lookup.Result<T> result = LOOKUP_PS.get(clazz).lookupResult(clazz);
result.addLookupListener(lookupListener);
return result;
}

/**
* Allows to publish data to an InstanceContent.
* It will throw an IllegalArgumentException in case either param is null, or no InstanceContent found
* @param <T>
* @param content
* @param dataInstance
*/
public synchronized static <T> void publish(InstanceContent content, T dataInstance) {
publish(content, dataInstance, null);
}

/**
* Allows to publish data to an InstanceContent, using a Convertor
* It will throw an IllegalArgumentException in case either param is null, or no InstanceContent found
* @param <T>
* @param <R>
* @param content
* @param dataInstance
* @param convertor
*/
public synchronized static <T, R> void publish(InstanceContent content, T dataInstance, Convertor<T, R> convertor) {
checkContent(content, dataInstance);
content.set(Collections.singleton(dataInstance), convertor);
}

/**
* It will create a lookup and an InstanceContent associated to the param clazz.
* Classes may get either the Lookup or the InstanceContent by using getClassLookup and getPublisher methods.
* @param <T>
* @param clazz
*/
public synchronized static <T> void setPublisher(Class<T> clazz) {
if (!LOOKUP_PS.containsKey(clazz)) {
LOOKUP_PS.put(clazz, new JPAbstractLookup(new InstanceContent()));
}
}

/**
* Returns the Lookup associated to param clazz.
* If no lookup was defined for that class, it will throw an IllegalArgumentException.
* @param <T>
* @param clazz
* @return
*/
public synchronized static <T> Lookup getClassLookup(Class<T> clazz) {
if (!LOOKUP_PS.containsKey(clazz)) {
throw new IllegalArgumentException("no Lookup defined for param clazz");
}
return LOOKUP_PS.get(clazz);
}

/**
* This method returns a Lookup.Result for class type T, defined via param resultClass.
* A lookup must be supplied at param lookup. If lookup or resultClass params are set to null, it will throw an IllegalArgumentException.
* Param resultClass is the Lookup.Result class that will hold data.
* Param lookupListener is a listener defined to collect Lookup.Result data
*
* @param <T>
* @param lookup
* @param resultClass
* @param lookupListener
* @return
* @return: a Lookup.Result for class T
*
*/
public synchronized static <T> Lookup.Result<T> getLookupResult(Lookup lookup, Class<T> resultClass, LookupListener lookupListener) {
if (lookup == null || resultClass == null) {
throw new IllegalArgumentException("lookup or resultClass params must not be null");
}
final Lookup.Result<T> result = lookup.lookupResult(resultClass);
result.addLookupListener(lookupListener);
return result;
}

/**
* This will return either the first object found which implements the class passed as a parameter, or null if not anyone found.
* @param <T>
* @param clazz
* @return
*/
public static <T> T getInstance(Class<T> clazz) {
return Lookup.getDefault().lookup(clazz);
}

/**
* It will return a collection of all instances of clazz
* @param <T>
* @param clazz
* @return
*/
public static <T> Collection<? extends T> getAlIinstances(Class<T> clazz) {
return Lookup.getDefault().lookupAll(clazz);
}

private static <T> void checkContent(InstanceContent content, T dataInstance) {
if (content == null) {
throw new IllegalArgumentException("InstanceContent null");
}
if (dataInstance == null) {
throw new IllegalArgumentException("dataInstance cannot be null");
}
boolean found = false;
final String className = dataInstance.getClass().getName();
final Iterator<Entry<Class<?>, JPAbstractLookup>> checkLookupMap = LOOKUP_PS.entrySet().iterator();
checkLoop:
while (checkLookupMap.hasNext()) {
final Entry<Class<?>, JPAbstractLookup> entry = checkLookupMap.next();
if (entry.getValue().getContent().equals(content)) {
if (!entry.getKey().getName().equals(className)) {
throw new IllegalStateException("dataInstance and data class do not match");
}
found = true;
break checkLoop;
}
}
if (!found) {
throw new IllegalArgumentException("InstanceContent not found");
}
}

private static class JPAbstractLookup extends AbstractLookup {

private static final long serialVersionUID = 5429372940135351125L;
private transient final InstanceContent content;

public JPAbstractLookup(InstanceContent content) {
super(content);
this.content = content;
}

/**
* @return the content
*/
public InstanceContent getContent() {
return content;
}
}
}

The key point of this implementation is the “bus”, a simple map structure having data classes as keys, and AbstractLookup as values. This approach allows us to easily associate the data we want to transfer among classes, with NetBeans Platform InstanceContent and Result objects.

The "setPublisher" method ensures that this “bus” is always automatically updated, whenever we do a call to either "getPublisher" or "getSubscriber".

Usage examples

Let's assume we have two classes in different modules, kept in public or private packages (which makes no difference), and we want to transfer some data between them. Their modules do not have any dependency between themselves.

We first create a plain data class in a third module and set the other two modules as dependent from this one.

So, our module C will have a class, like this:


package org.jptools.tools.lookup;

public class DataClass {

private String name;
private Integer number;
private Boolean state;

/**
* @return the name
*/
public String getName() {
return name;
}

/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}

/**
* @return the number
*/
public Integer getNumber() {
return number;
}

/**
* @param number the number to set
*/
public void setNumber(Integer number) {
this.number = number;
}

/**
* @return the state
*/
public Boolean getState() {
return state;
}

/**
* @param state the state to set
*/
public void setState(Boolean state) {
this.state = state;
}
}

 

Now we create a "PublisherClass" in Module A:


package org.jptools.tools.lookup;

import org.openide.util.lookup.InstanceContent;

public class PublisherClass {

private transient final InstanceContent dataClassContent = LookupUtils.getPublisher(DataClass.class);

public void publishing() {

final DataClass dataClass = new DataClass();

dataClass.setName("publishing test");
dataClass.setNumber(Integer.SIZE);
dataClass.setState(Boolean.TRUE);

LookupUtils.publish(dataClassContent, dataClass);
}
}

 

...and a "SubscriberClass" in Module B:


package org.jptools.tools.lookup;

import java.util.Collection;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;

public class SubscriberClass {

private transient final Lookup.Result<DataClass> dataClassResults = LookupUtils.getSubscriber(DataClass.class, new SubscriberListener());

private class SubscriberListener implements LookupListener {

@Override
public void resultChanged(LookupEvent ev) {
final Collection<? extends DataClass> items = dataClassResults.allInstances();
if (!items.isEmpty()) {
final DataClass data = items.iterator().next();
final String name = data.getName();
if (name != null) {
// name holds "publishing test"
// process name
}
final Integer number = data.getNumber();
if (number != null) {
// number holds Integer.SIZE
// process number
}
final Boolean state = data.getState();
if (state != null) {
// state holds Boolean.TRUE
// process state
}
}
}
}
}

 

That's all!. The order of instantiation for both classes doesn't matter, in terms of setting the "bus" properly.

The Java Zone is brought to you in partnership with ZeroTurnaround. Check out this 8-step guide to see how you can increase your productivity by skipping slow application redeploys and by implementing application profiling, as you code!

Topics:

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

{{ parent.tldr }}

{{ parent.urlSource.name }}