NetBeans Platform Idioms: Pluggable TopComponent (Part 4)
Join the DZone community and get the full member experience.
Join For FreeLet's go on with the analysis of the Pluggable TopComponent idiom. Today we're going to look at the main class, EnhancedTopComponent, that acts as the connection point between the idiom participants and the NetBeans Platform runtime, as well as a few more reusable roles.
This is the fourth part of this series. I'll make many references to portions of the API described in the previous parts, that you should read before going on: 1, 2, 3. I'm also happy to say that the related code has been moved to the project at Kenai, and most of it is ready for inclusion in form of Maven artifacts.
The core of EnhancedTopComponent is very simple: each instance contains an instance of RoleSet (that has been described in previous articles) and that gets populated from the XML FileSystem (layer.xml) - for instance with a XML fragment such as:
<filesystem> <folder name="Roles"> <folder name="it.tidalwave.geo.explorer.GeoExplorerPresentation"> <file name="DataLoader.instance"> <attr name="instanceClass" stringvalue="it.tidalwave.geo.explorer.impl.role.DefaultGeoCoderDataLoader"/> </file> <file name="SelectionStrategy.instance"> <attr name="instanceClass" stringvalue="it.tidalwave.geo.explorer.impl.role.AutoSelectionStrategy"/> </file> ... <file name="NodeView.instance"> <attr name="instanceCreate" methodvalue="it.tidalwave.netbeans.role.util.BeanFactory.createInstance"/> <attr name="class" stringvalue="it.tidalwave.netbeans.explorer.view.EnhancedBeanTreeView"/> <attr name="rootVisible" boolvalue="false"/> <attr name="dropTarget" boolvalue="false"/> <attr name="dragSource" boolvalue="false"/> </file> </folder> </folder> ... <filesystem>
It's an excerpt of the real configuration of the forceTen project, and as you can see the roles can be instantiated in multiple ways (in the most sophisticated case some property values are initialized thanks to the BeanFactory utility that has been described in the previous post). I'm using a naming convention where the roles are instantiated from the path "/Roles/<topComponentId>".
package it.tidalwave.netbeans.windows; public abstract class EnhancedTopComponent extends TopComponent { @Nonnull private final Lookup lookup; private final RoleSet roleSet = new RoleSet(); @CheckForNull private final String id; public EnhancedTopComponent() { this(null); } public EnhancedTopComponent (@CheckForNull final String id) { this.id = id; lookup = new ProxyLookup(roleSet.getLookup(), Lookups.fixed(this), // needed by roles super.getLookup()); setLayout(new BorderLayout()); } public void addRole (@Nonnull final Object role) { roleSet.addRole(role); } public void removerole (@Nonnull final Object role) { roleSet.removeRole(role); } private void installRoles() { final String path = "/Roles/" + ((id != null) ? id : preferredID()); logger.finer(">>>> looking up default roles from path %s...", path); roleSet.setInjectedLookup(getLookup()); // a subclass might have enhanced it roleSet.setStaticRoles(Lookups.forPath(path).lookupAll(Object.class)); roleSet.initialize(); final GUIBuilder guiBuilder = roleSet.getLookup().lookup(GUIBuilder.class); if (guiBuilder != null) { add(guiBuilder.createGUI(), BorderLayout.CENTER); } } }
The initialization of roles is performed by installRoles(). An interesting point is that it manages in a special way a role, named GUIBuilder:
public interface GUIBuilder { @Nonnull public JComponent createGUI(); }
It's a simple factory to which the creation of the Swing user interface is delegated. In this way, any Swing responsibility is moved away from EnhancedTopComponent, that becomes a mere controller. This also means that any specific TopComponent in a desktop application can just inherit from EnhancedTopComponent and specify the UI in a separate class; it also means that an existing UI can be replaced by an alternate one, by just overriding the configuration of a GUIFactory.
As I wrote in the preamble of this article, EnhancedTopComponents acts as the bridge between the Platform runtime and the roles. On this purpose, all the life-cycle methods have been implemented in the following way:
interface RoleRunner { public void run (final @Nonnull TopComponentRole role); } @Override protected void componentActivated() { super.componentActivated(); runRoles(new RoleRunner() { @Override public void run (final @Nonnull TopComponentRole role) { role.notifyActivated(); } }); } private void runRoles (final @Nonnull RoleRunner roleRunner) { if (!rolesInstalled) { installRoles(); RoleInjector.injectLookup(this, getLookup()); PostConstructorCaller.callPostConstructors(this); rolesInstalled = true; } assert rolesInstalled : "roles not initialized"; for (final TopComponentRole role : getLookup().lookupAll(TopComponentRole.class)) { roleRunner.run(role); } }
Each method makes sure that the roles have been loaded and initialized, and then calls a method with the same name on all the registered roles that implement TopComponentRole:
public interface TopComponentRole { public void notifyActivated(); public void notifyDeactivated(); public void notifyOpened(); public void notifyClosed(); public void notifyShowing(); public void notifyHidden(); }
In this way, multiple behaviours can be plugged from different modules and take part in the life cycle of the TopComponent. A companion abstract class, TopComponentRoleSupport, implements all the methods with empty body, so one can subclass it and only implement the ones he needs.
A typical operation that must be bound to the TopComponent life-cycle is loading the data that are going to be rendered. On this purpose, we can define an interface and a few classes:
public interface DataLoader { public void loadData(); } public abstract class TopComponentDataLoaderStrategy extends TopComponentRoleSupport implements DataLoader { @Inject private DataLoader dataLoader; @Override public final void loadData() { dataLoader.loadData(); } } @NotThreadSafe public final class EagerDataLoaderStrategy extends TopComponentDataLoaderStrategy { private boolean initialized; @PostConstruct private void initialize() { if (!initialized) { loadData(); initialized = true; } } } @NotThreadSafe public final class LazyDataLoaderStrategy extends TopComponentDataLoaderStrategy { private boolean initialized; @Override public void notifyShowing() { if (!initialized) { loadData(); initialized = true; } } }
The concrete code for loading the data must be provided in an implementation of DataLoader; then, either EagerDataLoaderStrategy or LazyDataLoaderStrategy must be configured in layer.xml. The following layer.xml fragment is taken again from the forceTen project:
<filesystem> <folder name="Roles"> <folder name="it.tidalwave.geo.explorer.GeoExplorerPresentation"> <file name="DataLoader.instance"> <attr name="instanceClass" stringvalue="it.tidalwave.geo.explorer.impl.role.DefaultGeoCoderDataLoader"/> </file> <file name="DataLoaderStrategy.instance"> <attr name="instanceClass" stringvalue="it.tidalwave.netbeans.windows.role.LazyDataLoaderStrategy"/> </file> </folder> </folder> ... <filesystem>
Splitting the application behaviour in these fine grained roles makes it possible to achieve a great flexibility. For instance, a customization module can change the policy from lazy to eager and a different data provider than the default one can be specified.
Opinions expressed by DZone contributors are their own.
Comments