In working with the Netbeans Rich Client Platform in building Blue, I have created a number of plugin types for the program. Following a standard Netbeans convention, I have implemented plugin registration by having plugins register themselves with the System Filesystem. This is done either through XML (layer.xml) or through annotations I have created for each plugin type.
This works very well, but over time, I found two things I wanted to address:
- I was using a lot of boilerplate code for loading plugins and wanted to encapsulate that into an API to simplify things.
- I needed a way to lazily load the plugins, such that each are only instantiated only when they are requested for use.
To those ends, I wanted to share a couple of classes I have recently worked on: LazyPlugin and LazyPluginFactory. The code for these two are as follows:
LazyPlugin:
package blue.ui.nbutilities.lazyplugin; import java.util.HashMap; import java.util.Map; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; /** * Lazy Plugin class that wraps a NB FileObject. * * @author stevenyi */ public class LazyPlugin<T extends Object> { private final String displayName; private final String path; private final Class clazz; private final Map<String, Object> metaData; public LazyPlugin(FileObject fObj, Class c) { this.displayName = (String) fObj.getAttribute("displayName"); this.path = fObj.getPath(); this.clazz = c; metaData = new HashMap<>(); } public String getDisplayName() { return displayName; } public T getInstance() { return (T) FileUtil.getConfigObject(path, clazz); } public Object getMetaData(String key) { return metaData.get(key); } public void setMetaData(String key, Object value) { metaData.put(key, value); } }
LazyPluginFactory:
package blue.ui.nbutilities.lazyplugin; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileUtil; /** * * @author stevenyi */ public class LazyPluginFactory { private LazyPluginFactory() { } public static <T> List<LazyPlugin<T>> loadPlugins(String folder, Class z) { return loadPlugins(folder, z, null, null); } public static <T> List<LazyPlugin<T>> loadPlugins(String folder, Class z, MetaDataProcessor processor) { return loadPlugins(folder, z, processor, null); } public static <T> List<LazyPlugin<T>> loadPlugins(String folder, Class z, Filter filter) { return loadPlugins(folder, z, null, filter); } public static <T> List<LazyPlugin<T>> loadPlugins(String folder, Class pluginClass, MetaDataProcessor processor, Filter f) { List<LazyPlugin<T>> plugins = new ArrayList<>(); FileObject files[] = FileUtil.getConfigFile( folder).getChildren(); List<FileObject> orderedFiles = FileUtil.getOrder( Arrays.asList(files), true); for (FileObject fObj : orderedFiles) { if(fObj.isFolder()) { continue; } if(f != null && !f.accept(fObj)) { continue; } LazyPlugin<T> plugin = new LazyPlugin<>(fObj, pluginClass); if (processor != null) { processor.process(fObj, plugin); } plugins.add(plugin); } return plugins; } public static interface Filter { public boolean accept(FileObject fObj); } public static interface MetaDataProcessor { public void process(FileObject fObj, LazyPlugin plugin); } }
LazyPlugin encapsulates the plugin and defers loading until the plugin user calls getInstance(). This allows the plugin’s displayName to be used for displaying to the end-user (i.e. “Add GenericScore” for a GenericScore plugin). There is also a metadata map that is useful for storing additional properties. To note, the class does not itself hold on to any references of the FileObject.
The LazyPluginFactory is used by giving a folder name to load plugins from, as well as the type of plugin to load. This then returns a List<LazyPlugin<T>> of the plugins found. The loadPlugins() method does allow taking in a Filter as well as MetaDataProcessor. The Filter class can be used to inspect a FileObject and skip over it or not. (I have a pre-made AttributeFilter class that checks if a given attribute name is set to true. I also have a pre-made ClassAssociationProcessor that reads an attribute string assumed to be a fully qualified class name, loads the class, and adds to the LazyPlugin’s metadata as “association”.)
Having LazyPlugin as a class has–in my opinion–made my code a little clearer. It’s easy for me to see the code and say “Oh, I’m dealing with plugins here, and I’m lazily loading them.” The classes are also generic enough that I can handle two scenarios that appear in my program:
- I need to lazily load plugins that are model classes.
- I need to lazily load plugins at are view/controller classes and need to have an association with the model class. (i.e. this is an editor for class X)
Using generated XML, the following are two fragments, one for a model class, the other for a view/controller class:
Model:
<file name="blue-orchestra-BlueSynthBuilder.instance"> <!--blue.orchestra.BlueSynthBuilder--> <attr intvalue="50" name="position"/> <attr name="displayName" stringvalue="BlueSynthBuilder"/> </file>
View/Controller:
<file name="blue-ui-core-orchestra-editor-BlueSynthBuilderEditor.instance"> <!--blue.ui.core.orchestra.editor.BlueSynthBuilderEditor--> <attr name="instrumentType" stringvalue="blue.orchestra.BlueSynthBuilder"/> </file>
When loaded within the program, I would get a LazyPlugin<Instrument> and a LazyPlugin<InstrumentEditor>. The former would get used when adding an instance of an Instrument to the project’s main model class. The latter is used when the Instrument is selected. Note the metadata for the LazyPlugin has an association class loaded via a ClassAssociationProcessor that reads in classes using “instrumentType”. It has enough information then to setup the application’s cache of editors for instruments without having to load the actual editor until requested.
So far I have been happy with this design and it has served all of the use cases I currently have. I imagine these classes may get further refined in the future as other uses cases come up. I hope others may find these classes useful in the Netbeans RCP programs.