Apache NetBeans
Apache NetBeans
Latest release

Apache NetBeans 23

Download

I want to allow other modules to inject objects into my Node’s Lookup or elsewhere (Actions, Properties…​)

There is a simple general method for allowing you to define your own registry of objects as a folder in the System Filesystem, and look them up on demand.

class BaseNode extends AbstractNode {
  //...
  private static final String PATH_IN_SFS = "path/to/some/folder/actions";
  public Action[] getActions(boolean ignored) {
    return Lookups.forPath(PATH_IN_SFS).lookupAll(Action.class).toArray(new Action[0]);
  }
  //...
}

You can use this pattern for properties, or whatever. If you want your Node to respond to new modules being loaded on the fly, you may want to get a Lookup.Result and listen for changes on it (not necessary in the example above, but necessary for things like Lookup contents or Properties, which are cached).

Injecting Lookup Contents

First, be sure this is something you really need. Typically, you expose some model object from your Node, and write Actions that are sensitive to it.

However, if you want to use built-in actions (such as OpenAction) over your custom Nodes, and the module which created the Node does not provide the Openable or OpenCookie object which, for example, OpenAction needs, then you do need some way for other modules to inject contents into your lookup. If you are both injecting an object into the lookup, and writing an action against that object in the same module (and not expecting other modules to also add actions sensitive to your Converter’s type), you can probably skip the injecting of lookup contents, and just go straight to the Node’s model object.

Lookup contents should not be added programmatically - that would mean every module that cares about a Node type would have to be called to add contents (which may never be used) to it - meaning a performance penalty. Also this breaks things like FilterNode, which cannot transparently proxy methods that exist on the Node it is acting as a clone of.

It is simple to create a declarative registry for lookup contents. It makes use of the fact that the contents of an AbstractLookup are provided by a mutable InstanceContent object, and that a factory class can be added to an InstanceContent, InstanceContent.Converter. So you can create a folder where other modules will register instances of InstanceContent.Converter for Nodes which hold an object of your type. When you create the Node’s lookup, you can collect all such Converters, and add them to your Lookup’s contents. Unless the lookup is queried for the type one Converter creates, it will never be called.

Here is an example base Node class that will do this:

public class BaseNode<T> extends AbstractNode {
  final InstanceContent content;
  final Class<T> type;
  public BaseNode(Class<T> type, T modelObject) {
    this(type, modelObject, new InstanceContent());
  }
  public BaseNode(Class<T> type, T modelObject, InstanceContent content){
    super(Children.LEAF, new ProxyLookup(Lookups.fixed(modelObject), new AbstractLookup(content)));
    this.content = content;
    this.type = type;
    //Populate lookup based on declaratively registered factories
    String pathInSystemFS = getRegistrationPath("lookupContents");
    Collection<? extends InstanceContent.Convertor> all =
       Lookups.forPath(pathInSystemFS).
       lookupAll(InstanceContent.Convertor.class);
    for (InstanceContent.Convertor<T, ?> factory : all) {
      content.add(modelObject, factory);
    }
    //if you want to handle modules being loaded/unloaded in a running app,
    //use lookupResult() instead of lookupAll(), retain a reference to the
    //Lookup.Result, listen on it for changes, and remove all acquired
    //InstanceContent objects if it changes, then rerun the above code
  }
  @Override
  public Action[] getActions(boolean context) {
    return Lookups.forPath(getRegistrationPath("actions")).
         lookupAll(Action.class).toArray(new Action[0]);
  }
  String getRegistrationPath(String subfolder) {
    //e.g. pass "lookupContents" and get
    //MyModule/com/foo/mymodule/MyType/lookupContents
    return "MyModule/" + type.getName().replace('.', '/') + "/" + subfolder;
  }
}

Suppose that we have some BaseNodes whose model objects are instances of Strings. We want to add a Foo object to their Lookups, and register an action which operates against Foo objects. So, we have an InstanceContent.Converter implementation:

public class FooFactory implements InstanceContent.Convertor<String, Foo> {
  @Override
  public Foo convert(String string) {
    return new Foo(string);
  }
  @Override
  public Class<? extends Foo> type(String obj) {
    return Foo.class;
  }
  @Override
  public String id(String obj) {
    return getClass().getName() + obj;
  }
  @Override
  public String displayName(String obj) {
    return obj;
  }
}

The action implementation can be any Action subclass, so we can omit the code for that - but its classname for this example will be org.netbeans.demo.elookup.FooAction.

All we need to do now is register both of these objects in the System Filesystem and we will have working code.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE filesystem PUBLIC "-//NetBeans//DTD Filesystem 1.1//EN"
"http://www.netbeans.org/dtds/filesystem-1_1.dtd">
<filesystem>
    <folder name="MyModule">
        <folder name="java">
            <folder name="lang">
                <folder name="String">
                    <folder name="lookupContents">
                        <file name="org-netbeans-demo-elookup-FooFactory.instance"/>
                    </folder>
                    <folder name="actions">
                        <file name="org-netbeans-demo-elookup-FooAction.instance"/>
                    </folder>
                </folder>
            </folder>
        </folder>
    </folder>
</filesystem>

Note that objects created by such factories will be weakly cached by the lookup - if no object is holding a reference to the object, it can be garbage collected. If such objects are expensive to create, or if you expect callers to attach listeners to the factory-created objects, you may want to cache them in your implementation of InstanceContent.Converter.