Статьи

Шаблон публикации / подписки в поиске платформы NetBeans

Когда мы начали портирование нашего проекта jPlay , от платформы приложений Swing до платформы NetBeans (статья об этом здесь ), мы заменили наше обширное использование библиотеки Eventbus на просмотр платформы NetBeans . Цель состояла в том, чтобы реализовать шаблон публикации / подписки, который Eventbus очень успешно решил с помощью эквивалента NetBeans Platform Lookup.

Цели

Мы хотели добиться полной развязки компонентов Swing, вспомогательных классов, классов среднего уровня, веб-сервисов, всплывающих фреймов и тому подобного. Eventbus отличается простотой использования аннотаций для подписчиков, а также простыми и эффективными методами публикации. Ничего подобного, как и в платформе NetBeans, нет, но мы начали более внимательно рассматривать возможности Lookup.

Платформа NetBeans имеет ресурсы AbstractLookup и InstanceContent . Чтобы реализовать шаблон публикации / подписки, такой же простой и понятный, как и Eventbus, с точки зрения его использования, необходимо выполнить некоторую работу.

Шаблон публикации / подписки с помощью поиска на платформе NetBeans

Первая цель состояла в том, чтобы создать служебный класс, скрывающий весь код, связанный с Lookup, и предоставляющий открытые методы по шаблону публикации / подписки.

Глобальная идея состоит в том, чтобы установить тесную связь между классами простых данных и поиском платформы NetBeans, сохраняя при этом общедоступный API, независимый от всего остального:

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;
}
}
}

Ключевым моментом этой реализации является «шина», простая структура карты, имеющая классы данных в качестве ключей и AbstractLookup в качестве значений. Этот подход позволяет нам легко связать данные, которые мы хотим передать между классами, с объектами InstanceContent и Result платформы NetBeans.

Метод «setPublisher» гарантирует, что эта «шина» всегда автоматически обновляется всякий раз, когда мы делаем вызов либо «getPublisher», либо «getSubscriber».

Примеры использования

Предположим, у нас есть два класса в разных модулях, которые хранятся в открытых или закрытых пакетах (что не имеет значения), и мы хотим передать некоторые данные между ними. Их модули не имеют никакой зависимости между собой.

Сначала мы создаем простой класс данных в третьем модуле и устанавливаем два других модуля как зависимые от этого.

Итак, наш модуль C будет иметь класс, например так:


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;
}
}

 

Теперь мы создаем «PublisherClass» в модуле 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);
}
}

 

… и «SubscriberClass» в модуле 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
}
}
}
}
}

 

Вот и все!. Порядок создания экземпляров для обоих классов не имеет значения с точки зрения правильной настройки «шины».