Статьи

Идиомы для платформы NetBeans: SafeChildFactory

Платформа NetBeansпредоставляет множество функций для упрощения создания настольного приложения, особенно для некоторых повседневных задач. Например, люди, работающие с Swing, часто сталкивались со сложностью управления древовидными структурами с помощью JTree, в то время как основная концепция относительно проста (шаблон Composite); как обычно, многопоточная модель Swing всегда ухудшает ситуацию. Напротив, платформа NetBeans предоставляет очень хорошую модель для составного шаблона (Node) и простой в использовании ChildFactory для заполнения своих дочерних элементов даже в асинхронном режиме: то есть операция может занять столько времени, сколько ей нужно и тем временем в графическом интерфейсе появляется симпатичный узел «Пожалуйста, подождите …». Предоставляются или упрощаются многие дополнительные функции, такие как контекстные действия, перетаскивание и т. Д.

Чтобы понять, насколько прост ChildFactory, рассмотрим следующий пример. У меня есть GeoCoder API, который представляет иерархическую перспективу географических данных, чья реализация шаблона Composite в основном следующая:

package it.tidalwave.geo.geocoding;

@ThreadSafe
public interface GeoCoderEntity
{
...

@Nonnull
public Finder<GeoCoderEntity> findChildren();
}

Все, что мне нужно для создания дерева узлов для платформы NetBeans, это:

package it.tidalwave.geo.geocoding.node.impl;

public final class GeoCoderEntityNode extends AbstractNode
{
private static class GeoCoderEntityChildFactory extends ChildFactory<GeoCoderEntity>
{
@Nonnull
private final GeoCoderEntity geoCoderEntity;

public GeoCoderEntityChildFactory (final @Nonnull GeoCoderEntity geoCoderEntity)
{
this.geoCoderEntity = geoCoderEntity;
}

@Override
protected boolean createKeys (final @Nonnull List<GeoCoderEntity> geoCoderEntities)
{
geoCoderEntities.addAll(geoCoderEntity.findChildren().results());
return true;
}

@Override @Nonnull
protected Node createNodeForKey (final @Nonnull GeoCoderEntity geoCoderEntity)
{
return new GeoCoderEntityNode(geoCoderEntity);
}
};

public GeoCoderEntityNode (final @Nonnull GeoCoderEntity geoCoderEntity)
{
this(geoCoderEntity, new GeoCoderEntityChildFactory(geoCoderEntity));
}

private GeoCoderEntityNode (final @Nonnull GeoCoderEntity geoCoderEntity,
final @Nonnull GeoCoderEntityChildFactory childFactory)
{
super(geoCoderEntity, Children.create(childFactory, true), createLookup(geoCoderEntity));
}

@Nonnull
private static Lookup createLookup (final @Nonnull GeoCoderEntity geoCoderEntity)
{
return new ProxyLookup(geoCoderEntity.getLookup(), Lookups.singleton(geoCoderEntity));
}
}

То есть мне просто нужен метод createKeys (), который с помощью GeoCoderEntity создает список дочерних элементов, и метод createNodeForKey (), который создает экземпляр Node для каждого экземпляра GeoCoderEntity. Это так же просто, как базовая концептуальная модель (реальный код в источниках немного сложнее, потому что также заботится о нескольких дополнительных функциях).

Хорошо? К сожалению нет, так как дьявол в деталях. Рассмотрим следующий пример, где я подключаюсь к удаленному веб-сервису для извлечения географических данных (извлечение данных, конечно, является инкрементным и ленивым, то есть при каждом расширении узла):

В последней части видео происходит то, что я отключил сеть для симуляции сбоя. Клиент веб-службы генерирует исключение, которое достигает и прерывает createKeys (), который, в свою очередь, завершает поток, который выполнял работу. Таким образом, узел «Пожалуйста, подождите …» остается навсегда.

Теперь у нас есть несколько проблем: «Пожалуйста, подождите …» должно исчезнуть, и, возможно, должно появиться уведомление об ошибке, но, прежде всего, пользователю не предлагается способ повторить попытку. Если бы проблема была просто временной проблемой в сети, данные могли бы все еще быть восстановлены, но на этом этапе — без какого-либо явного кода, предоставленного в приложении — будет невозможно просматривать внутри Испании, если пользователь не перезапустит приложение.

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

Мое решение состоит в том, чтобы заменить ChildFactory на SafeChildFactory из OpenBlueSky , чья соответствующая часть:

protected abstract boolean createKeysSafely (@Nonnull List<T> list)
throws Exception;

@Override
protected final boolean createKeys (final @Nonnull List<T> list)
{
try
{
return createKeysSafely(list);
}
catch (Exception e)
{
ChildFactoryExceptionHandler exceptionHandler = null;

try
{
exceptionHandler = findExceptionHandler(node);
}
catch (NotFoundException e2)
{
exceptionHandler = Locator.find(ChildFactoryExceptionHandler.class);
}

exceptionHandler.handleException(node, this, e);
return true;
}
}
public interface ChildFactoryExceptionHandler
{
public void handleException (@CheckForNull Node node,
@Nonnull ChildFactory childFactory,
@Nonnull Throwable throwable);
}

То есть клиентский код должен использовать createKeysSafely () вместо createKeys () — ожидается, что первый вызовет исключение, а универсальная реализация второго обработает исключение. Ниже я показываю единственные изменения, которые должны быть выполнены в исходном коде, который использовал ChildFactory (поиск комментариев ****):

package it.tidalwave.geo.geocoding.node.impl;

public final class GeoCoderEntityNode extends AbstractNode
{
// **** SafeChildFactory in place of ChildFactory
private static class GeoCoderEntityChildFactory extends SafeChildFactory<GeoCoderEntity>
{
@Nonnull
private final GeoCoderEntity geoCoderEntity;

public GeoCoderEntityChildFactory (final @Nonnull GeoCoderEntity geoCoderEntity)
{
this.geoCoderEntity = geoCoderEntity;
}

// **** createKeysSafely() in place of createKeys()
@Override
protected boolean createKeysSafely (final @Nonnull List<GeoCoderEntity> geoCoderEntities)
{
geoCoderEntities.addAll(geoCoderEntity.findChildren().results());
return true;
}

@Override @Nonnull
protected Node createNodeForKey (final @Nonnull GeoCoderEntity geoCoderEntity)
{
return new GeoCoderEntityNode(geoCoderEntity);
}
};

public GeoCoderEntityNode (final @Nonnull GeoCoderEntity geoCoderEntity)
{
this(geoCoderEntity, new GeoCoderEntityChildFactory(geoCoderEntity));
}

private GeoCoderEntityNode (final @Nonnull GeoCoderEntity geoCoderEntity,
final @Nonnull GeoCoderEntityChildFactory childFactory)
{
super(geoCoderEntity, Children.create(childFactory, true), createLookup(geoCoderEntity));
// **** added the line below
childFactory.setNode(this);
}

@Nonnull
private static Lookup createLookup (final @Nonnull GeoCoderEntity geoCoderEntity)
{
return new ProxyLookup(geoCoderEntity.getLookup(), Lookups.singleton(geoCoderEntity));
}
}

Более умная часть ChildFactoryExceptionHandler — это то, как он обрабатывает исключение, что, конечно, должно быть подключаемым поведением. В приведенном выше коде Locator.find () является просто ярлыком для Lookup.getDefault (). Lookup (), так что есть ChildFactoryExceptionHandler по умолчанию, который обрабатывает ошибку, и вы можете изменить ее с помощью обычного подхода META-INF / services. Я рассмотрю findExceptionHandler (node) позже — сначала я хотел бы проиллюстрировать два альтернативных способа обработки ошибки.

DefaultChildFactoryExceptionHandler просто отображает узел «Ошибка»:

RetringChildFactoryExceptionHandler вместо этого ждет пять секунд и затем повторяет попытку; после второй неудачной попытки он сдается. Затем узел ошибки предоставляет контекстное действие «Повторить», которое можно использовать, чтобы вручную принудительно повторить попытку. В последней части видео я снова подключился к сети, так что, по крайней мере, мы видим регионы Испании во всем их великолепии.

Это мощно, потому что это универсально — просто используйте SafeChildFactory вместо существующего ChildFactory и настройте его.

Но дьявол все еще рядом. Универсальная вещь должна быть реконфигурируемой — конечно, сообщения об ошибках, значки и т. Д. … используют пакет ресурсов, чтобы вы могли изменить их с помощью механизма брендинга по умолчанию, поддерживаемого платформой NetBeans. Но что, если вы хотите применить разные политики или свойства ошибок / повторов (сообщения, значки, количество макс. Повторов и т. Д.) В разных частях приложения? А возможно в разных поддеревьях одного и того же дерева ? Что, кстати, не так уж и безумно, если вспомнить более сложный пример, когда данные для некоторых поддеревьев могут быть получены из разных источников.

Теперь этот findExceptionHandler (узел) приходит на помощь.

Реализация этого метода:

@Nonnull
private ChildFactoryExceptionHandler findExceptionHandler (final @CheckForNull Node node)
throws NotFoundException
{
NotFoundException.throwWhenNull(node, "");
final ChildFactoryExceptionHandler exceptionHandler = node.getLookup().lookup(ChildFactoryExceptionHandler.class);

return (nodeExceptionHandler != null) ? nodeExceptionHandler : findExceptionHandler(node.getParentNode());
}

То есть, прежде чем прибегнуть к поиску по умолчанию, поиск ChildFactoryExceptionHandler ищется в поиске текущего узла и в конечном итоге в его иерархии. Таким образом, каждое поддерево может настраивать поведение, помещая различные обработчики в Lookup своего основного узла.

Возвратившись к мощной идиоме Lookup, мы можем использовать, например, Injectable Lookup Factory : она предоставляет средства для вставки содержимого поисков объектов подключаемым способом. Например, если вы посмотрите на модуль настройки forceTen (приложение, из которого были сделаны скринкасты), вы можете найти этот класс:

public class NodeExceptionHandlerProviderForGeoCoderEntity extends CapabilitiesProviderSupport<GeoCoderEntity>
{
private final RetryingChildFactoryExceptionHandler exceptionHandler = new RetryingChildFactoryExceptionHandler();

private final Collection<ChildFactoryExceptionHandler> nodeExceptionHandler =
Collections.<ChildFactoryExceptionHandler>singletonList(exceptionHandler);

public NodeExceptionHandlerProviderForGeoCoderEntity()
{
super(GeoCoderEntity.class);
exceptionHandler.setMaxRetryCount(2);
exceptionHandler.setRetryPeriod(5000);
}

@Override @Nonnull
public Collection<? extends Object> createCapabilities (final @Nonnull GeoCoderEntity geoCoderEntity)
{
final String geoCoderName = geoCoderEntity.getGeoCoder().getName();
return "GeoNames".equals(geoCoderName) ? nodeExceptionHandler : Collections.<Object>emptyList();
}
}

Хотя DefaultChildFactoryExceptionHandler сохраняется как решение проблемы по умолчанию, вышеприведенный класс внедряет RetringChildFactoryExceptionHandler только в экземпляры GeoCoderEntity, которые были созданы из определенного GeoCoder с именем «GeoNames». Поскольку хорошо написанный Узел включает в свой Lookup содержимое Lookup ссылающейся сущности, обработчик исключений находит свой путь вплоть до Nodes Lookup и, следовательно, SafeChildFactory.

Еще одна демонстрация того, насколько мощными являются основные идиомы платформы NetBeans!

Вы можете проверить код из OpenBlueSky v0.4.6 и forceTen v0.5.8:

hg clone https://kenai.com/hg/openbluesky~src
hg up -c 0.4.6

hg clone https://kenai.com/hg/forceten~src
hg up -c 0.5.8