Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Idioms for the NetBeans Platform: SafeChildFactory

DZone's Guide to

Idioms for the NetBeans Platform: SafeChildFactory

· Java Zone
Free Resource

Just released, a free O’Reilly book on Reactive Microsystems: The Evolution of Microservices at Scale. Brought to you in partnership with Lightbend.

The NetBeans Platform provides a lot of features for simplifying the creation of a desktop application, especially for some mundane tasks. For instance, people working with Swing have often experienced the complexity of managing tree-like structures with JTree, while the underlying concept is relatively easy (the Composite pattern); as usual, the threading model of Swing always makes things worse. On the contrary, the NetBeans Platform provides a very good model for the Composite pattern (the Node) and an easy-to-use ChildFactory for populating its children, even in asynchronous mode: that is, the operation can take as long as it needs, and in the meantime a nice "Please wait..." node appears on the GUI. Many further features are provided or made easier, such as contextual actions, drag and drop, and so on.

To understand how simple the ChildFactory is, consider the following example. I have a GeoCoder API which represents a hierarchical perspective of geographic data, whose implementation of the Composite pattern is basically the following:

package it.tidalwave.geo.geocoding;

@ThreadSafe
public interface GeoCoderEntity
{
...

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

All I need to create a tree of Nodes for the NetBeans Platform is:

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

That is I just need a createKeys() method that given a GeoCoderEntity creates a list of children and a createNodeForKey() method that creates a Node instance for every GeoCoderEntity instance. It's as easy as the underlying conceptual model (the actual code in the sources is a bit more complex because also takes care of a few more features).

All right? Unfortunately not, as the devil is in details. Consider the following example, where I'm connecting to a remote web service for retrieving the geographic data (data retrieval of course is incremental and lazy, that is at each node expansion):

What happens in the last part of the video is that I've disconnected the network to simulate a failure. The web service client throws an exception that reaches and breaks createKeys(), which in turn terminates the thread that was doing the job. Thus, the "Please wait..." node stays forever.

Now, we have multiple problems here: "Please wait..." must disappear and possibly an error notification must be rendered, but above all the user is not offered a way to retry. If the problem was just a transitory network problem, the data could be still retrieved, but at this point - without any explicit code provided in the application - it will be impossible to browse inside Spain unless the user restarts the application.

Rendering an error and offering a chance to retry are such general operations that it makes sense to provide a generic infrastructure for solving the problem once and for all.

My solution is to replace ChildFactory with SafeChildFactory from OpenBlueSky, whose relevant part is:

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

That is, client code should use createKeysSafely() in place of createKeys() - the former is expected to throw an exception, and the universal implementation of the latter handles the exception. I show below the only changes that must be performed to the original code that used ChildFactory (search for the **** comments):

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

The smarter part of ChildFactoryExceptionHandler is how it handles the exception, that of course must be a pluggable behavior. In the code above, Locator.find() is just a shortcut for Lookup.getDefault().lookup(), so there's a default ChildFactoryExceptionHandler that handles the error and you can change it with the usual approach of META-INF/services. I'll discuss findExceptionHandler(node) later - I'd first like to illustrate two alternate ways to handle the error.

DefaultChildFactoryExceptionHandler just renders an "Error" node:

RetryingChildFactoryExceptionHandler instead waits for five seconds and then retries; after the second failed attempt it gives up. Then, the error node provides a contextual action "Retry" that you can use to manually force the thing to retry again. In the last part of the video I reconnected the network, so at least we see the Spain regions in all of their splendour.

This is powerful because it's universal - just use a SafeChildFactory in place of the existing ChildFactory and configure the thing.

But the devil is still around. A universal thing must be reconfigurable - of course the error messages, the icons etc... use a resource bundle, so you can change them by means of the default branding mechanism supported by the NetBeans Platform. But what if you want to apply different error / retry policies or properties (messages, icons, number of max. retries, etc...) in different parts of the application? And possibly in different subtrees of the same tree? Which by the way is not so crazy, if you think of a more complex example where the data for some subtrees could be retrieved by different sources.

Now that findExceptionHandler(node) comes to the rescue.

The implementation of that method is:

@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());
}

That is, before resorting to the default Lookup, the ChildFactoryExceptionHandler is searched for in the Lookup of the current Node and eventually up in its hierarchy. Thus every subtree can customize the behavior by putting different handlers in the Lookup of its main Node.

Having fallen back to the powerful Lookup idiom, we can use for instance the Injectable Lookup Factory: it provides the means for injecting the contents of objects' Lookups in a pluggable way. For instance, if you look at the customization module of forceTen (the application from where the screencasts were taken) you can find this class:

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

While the DefaultChildFactoryExceptionHandler is kept as the default solution to the problem, the above class injects the RetryingChildFactoryExceptionHandler only in the GeoCoderEntity instances which have been created from a specific GeoCoder named "GeoNames". Since a well written Node includes in its Lookup the contents of the Lookup of the referring entity, the exception handler finds its way up to the Nodes Lookup and thus the SafeChildFactory.

Another demonstration of how powerful are the basic idioms of the NetBeans Platform!

You can check out the code from OpenBlueSky v0.4.6 and 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

 

 

Strategies and techniques for building scalable and resilient microservices to refactor a monolithic application step-by-step, a free O'Reilly book. Brought to you in partnership with Lightbend.

Topics:

Opinions expressed by DZone contributors are their own.

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

{{ parent.tldr }}

{{ parent.urlSource.name }}