Overview

Javadoc

The primary class involved is ServiceType, and the collection of them is the ServiceType.Registry.

The standard subclasses are CompilerType, DebuggerType, and Executor.

For lookup, this centers around Lookup and helper implementations in org.openide.util.lookup, as well as interactions with the DataSystems API in FolderLookup.

Contents

Services API

What are Services?

Since there can be quite a number of modules installed in the IDE at once, many of which are frequently dedicated to providing a way of handling certain types of files - e.g. deploying JARs to certain types of servers, compiling classes with special preprocessors, etc. - it is desirable that there be a consistent way of configuring these offerings from different modules, and permitting the user to select which ones should actually be used for each file. To this end, the concept of services is introduced to provide for consistent configuration and management of a few very common types of objects that modules can install and users can configure.

Presently there are several varieties of services in the IDE - compiler types, debugger types, executors, script types, and perhaps search types. (It is possible for a module to add more to this list.) Each variety serves quite a different function in the IDE - the only reason they are grouped together as services is that they share characteristics of needing to be installed, configured, stored, and associated with files. Within each variety, there can be any number of service types (Java classes implementing the abstract class for one of these varieties), each of which is installed with a manifest tag. Finally, each service type has at least one default instance but may have additional instances as configured by the user (or, occasionally, also installed by the module).

The user interface to services is fairly neutral. First of all, the implementation is responsible for providing a way of configuring all registered services somehow. This is currently done by providing nodes under Project Settings (because the list of configured services is part of a project) which use node substructure to display the service varieties, service types, and instances. All of this display is performed according to JavaBeans introspection, which means that implementors of all levels have control over the appearance of these nodes and their behavior towards the user - and this also keeps the API rather thin.

Service instances may be configured by the user as Beans and these changes are saved with the project. Then, services can be associated with files that they should be applied to; the standard interface to this uses API support classes which display the current service selection for each variety on the Property Sheet, for every file that can use services.

Newer idioms in the APIs emphasize lookup and instances rather than just service types. As of NetBeans 3.2, there is a full system for registering arbitrary objects into the system (normally using XML layers) and independently finding all registered objects - this is known as lookup, and the registered objects are normally represented as files called instances. In NetBeans 3.3 there is additionally generic API support for declaring templates for services that can be used to form new instances based on some model, and a property editor providing the generic UI for selecting, modifying, and creating instances of some constrained type.

Creating a Service Type

Creating a service type means creating a new class which subclasses the correct abstract superclass according to the service variety.

Creating a Subclass

Currently, there are several varieties of service type you can create:
  1. Executor is used for objects which can run data files in some defined way, such as internal or external or applet execution. Details on subclassing can be found in the Execution API.
  2. CompilerType is used for objects which know how to create the correct compilers to add to a compilation job, e.g. internal Javac compilation, external process compilation, or RMI stub compilation. Details are in the Compiler API.
  3. DebuggerType is used for objects which know how to invoke the IDE debugger correctly according to the type of file, e.g. default debugging, JPDA debugging, or applet debugging. Details are in the Debugger API.
  4. ScriptType permits scripting languages to be plugged in.
  5. IndentEngine can control the automatic formatting of text files.
Each variety has at least one abstract method specific to that variety that determines the basic behavior of the service type. The remainder of the Services API, and of this document, describes generic aspects of service types.

Bean Configuration

Every instance of a service type must be configurable as a JavaBean. This means that it should have a list of exposed properties according to the JavaBeans specification. Writing Bean properties for service types is not difficult; you will need to have private nontransient serializable fields containing the current configuration, and a list of getter and setter methods to access this configuration. Property change support is built into ServiceType, and may be used from subclasses via ServiceType.firePropertyChange(...). For example:
private String someOption;
public String getSomeOption () {
  return someOption;
}
public synchronized void setSomeOption (String nue) {
  if (! isValid (nue)) throw new IllegalArgumentException ("Invalid: " + nue);
  String old = someOption;
  someOption = nue;
  firePropertyChange ("someOption", old, nue);
}

Display Characteristics and Bean Info

All service types should have matching bean info classes to present them to the user. The common customizations made in such bean infos are:
  1. Declaring all Bean properties provided by the service type, giving display names and short descriptions, and possibly marking them as expert.
  2. Providing a BeanDescriptor with a display name and possibly a short description for the service type. This descriptor will be used for both the category node (i.e. display of the class itself, rather than any particular instance) as well as providing information for the instance nodes.

    Providing the display name in the descriptor is especially useful as it will also serve as the default for the display name of each instance, though this may be overridden.

  3. Providing an icon for the service type. Again, this will be used on the category node, as well as instance nodes.
  4. Important: using inherited bean infos. This is necessary because all service types should at least include the display name property defined in ServiceType, and generally also need to include some properties from a superclass. For example:
    public BeanInfo[] getAdditionalBeanInfo () {
      try {
        return new BeanInfo[] { Introspector.getBeanInfo (DirectSuperclass.class) };
      } catch (IntrospectionException ie) {
        ie.printStackTrace ();
        return null;
      }
    }
    
Service types may define additional standard bean behaviors, such as declared event sets (though only property change support is used by the IDE), or implementing BeanContext (in which case the displayed node can contain children according to the context).

One method which can be overridden in service types is ServiceType.displayName(), which provides a default display name for the service type instance. Generally you do not need to do anything beyond providing a suitable display name in this method; the standard bean info for ServiceType exposes the display name (property name) as a writable property, modifiable either through the Property Sheet or by directly renaming the representation node; and when new instances are created under Project Settings, they will automatically be given unique names if needed. Note that just specifying a display name for the bean descriptor suffices, if you do not need there to be any difference.

You may override ServiceType.getHelpCtx() to provide a context-help association for your service type.

Persistence Considerations

Service types must be serializable - this capability is routinely used when storing the list of service type instances in a project according to the registry, or when simply copying existing instances or creating them from prototype.

The association of service types with files is not done using serialization of the instance itself, however. Rather, ServiceType.Handle is used to actually store the reference. This handle currently specifies both the class name of the service type, as well as the display name of the service instance. When the handle is resolved to a real instance, the lookup is done first by display name, and if that fails, by retrieving the default instance of the indicated class in the registry. The handle accomplishes two things: first, it saves space in extended file attributes by not storing the complete instance configuration (which could be somewhat large); second, it permits users to associate service instances to files by name, so that the instance can be customized in the registry and the customizations will then apply to all files referring to it.

Like all JavaBeans, service types should have a public default constructor to enable them to be instantiated afresh.

Installation

Installation of service types into the IDE is trivial - you need only add a section to your module's manifest specifying the service type class according to the Modules API, and a default instance of the service type will be created and made available to the user. However it is much preferable to register them via lookup.

Normally the manifest section specifies the service type class, and the default constructor is called to make the default instance. However, you may instead specify a serialized instance in the manifest section, according to the JavaBeans specification, and this preconfigured instance will be installed instead (possibly adding multiple predefined entries to a category node). This option is especially appropriate for those who only need a single Java class to represent different configurations used by their module, but wish to install some convenient preconfigured variations.

Using Existing Service Types

Occasionally it may be useful to use existing service types in an unusual way. For example, you may define a service type which is actually configured with a bean property whose value is a service variety, for purposes of delegation.

Finding Services

Services may be found using the registry (available via TopManager.getServices()), by using the calls ServiceType.Registry.find(String) (to find an instance by display name), or ServiceType.Registry.find(Class) (to find the default instance of the specified class). Available services can also be listed using ServiceType.Registry.services(); or more commonly, restricted to some superclass (typically a variety) using ServiceType.Registry.services(Class).

You may also create handles using new ServiceType.Handle(ServiceType), which creates a small, serializable handle storing the display name and class name, and resolve these handles later (if that is still possible) using ServiceType.Handle.getServiceType(). Whenever service types are to be stored, it is desirable to use a handle instead of the actual instance.

Varieties of service generally have their own convenience accessors, such as Executor.executors() or Executor.find(String), which just delegate to the more general methods in the service registry.

Finally, the IDE implementation includes standard property editors for ServiceType as well as some standard varieties, which displays a dialog listing all registered services of that variety, permitting the user to select one, as well as configure the details of the service instance with a property sheet. Although these property editors are a convenient way to select service instances, remember that the declared property type they use is ServiceType or a subclass, while typically code wishing to store service references will wish to retain only the handle.

Using Service Supports

Most data objects which wish to have one or more varieties of service associated with them persistently will use standard service supports which are cookie supports available in the APIs. The two existing supports are ExecSupport (for both Executor and DebuggerType associations) and CompilerSupport or one of its inner classes (for CompilerType associations). These supports implement the appropriate cookie by delegating the requested operation to the associated service instance, which is stored persistently in the extended attributes of the data object's primary file, and which may be accessed via e.g. ExecSupport.getExecutor(MultiDataObject.Entry) and ExecSupport.setExecutor(...).

Note that data object templates often come with appropriate service instances preconfigured on them, which will be automatically applied to objects created from these templates. When no specific association has been made for a particular file (either because the template did not specify one, or because the file in question was created outside the IDE and "discovered" by it), the support will use a default instance as specified by e.g. ExecSupport.defaultExecutor(); the default implementation will look for the default instance of the default service type for the appropriate variety, but often data objects (or whatever code is creating a support) will use a customized subclass of the support that overrides this method. Thus, a typical chain of defaults for a compiler type setting could look like this:

  1. The IDE default is (e.g.) a plain ExternalCompilerType.
  2. The Java sources data object creates a CompilerSupport specifying as the default internal Javac (rather, the default instance in the registry of this class).
  3. A Java source file template from another module overrides this choice with a compiler type from that module.
  4. The user, after having created a file from such a template, switches the compiler to external Javac.
Data object implementations may also wish to provide e.g. a system option (according to the Options API) for the "default default", i.e. the user would be able to configure the default instance class (say, external Javac) in case there were a large number of foreign (imported) files which the user wished to uniformly use this compiler for.

Service supports will also typically provide methods, e.g. ExecSupport.addProperties(Sheet.Set), to add one or more properties to a property set permitting the user to configure the desired service instance on the represented file. Data objects generally use these methods to build their nodes' property sheets.

Creating a New Variety of Service Type

It is possible for specialized purposes to create a new variety of service, i.e. a direct subclass of ServiceType that will have concrete service type subclasses. Nothing special is required to install such a new variety - simply begin installing specific service types into the IDE via module manifest as usual, and appropriate variety and category nodes will automatically appear under Project Settings. Of course, most of the work is up to you:
  1. Defining the proper abstract methods, or other behavior hooks, in the variety's superclass - i.e. providing an API. This API can be present in a module, and other modules may implement it, if desired.
  2. Writing supports in the style of ExecSupport to associate the services to files.
  3. Ensuring that data objects which can take advantage of the variety are aware of it and use the correct support.
  4. Use a property editor for your superclass. The best way to do this is to have properties be declared to be of type ServiceType, then set the custom attribute (on the Node.Property or PropertyDescriptor) superClass to the java.lang.Class of your superclass. See the Explorer API for more details on this.

Lookup

While service types are a useful abstraction for things like executors and compiler types, they are not flexible enough to handle all of the IDE's needs for registering and finding services. For one thing, the IDE is moving toward installation of services via XML layer rather than manifest, and service types are normally defined via manifest. Layer-based installation is more flexible in many ways. Also, many things need to be registered that practically cannot be subclasses of ServiceType, and there is no particular reason to make this restriction; similarly, ServiceType registration is oriented towards registering service classes, whereas in many cases it is necessary to use one implementing class with many instances. Finally, a more generic registration system was desired in order to simplify the APIs and make it easier to implement some APIs independently of the others.

These considerations gave rise to the lookup system first introduced in NetBeans 3.2 and expanded upon for NetBeans 3.3. The center of this API from the client perspective is very simple - you can look up a class, and get an instance of that class (or a collection of them). The service provider side of it is more complex but useful lookup implementations are already provided in the IDE core; for common cases you can register an object into lookup just by adding one simple file to an XML layer.

This section of the Services API will first discuss what instances are and how to create them from files, as this is the core concept for service providers. It will discuss how you can manually retrieve sets of instances as a client, which is not used very frequently in new code but helps to understand what lookup is doing behind the scenes. Then lookup itself is discussed, and how the standard instance lookup works. Lookup templates, which separate the provision of instances from the provision of categories, will be explained. Finally, a note is made of the compatibility system which ensures that older manifest-installed services (such as service types) are available via lookup transparently.

Working with Instances

Central to the management of services and many other aspects of the IDE's configuration is the notion of instances. An instance is just any Java object, generally of a particular type appropriate to its use, which is provided from some object (generally, a data object) using InstanceCookie. As an example, menu items may be added by inserting data objects that provide this cookie into the proper folder, and having the instance be a system action (or other things). Or an XML DTD may be registered by placing an object with an instance of org.xml.sax.EntityResolver in the proper folder.

Where do these instances come from? Technically, it is up to you to decide how to provide InstanceCookie; you could if really necessary create your own data loader that provides it according to some unusual scheme, and add files recognized by that loader to the right folder. Practically, the APIs provide implementations of this cookie sufficient for normal purposes.

The most common way to provide an instance is using InstanceDataObject. This is a type of data object whose sole purpose is to provide the instance cookie. It typically does so based on a class name you supply. There are several styles of instance file; all are empty (i.e. the file contents are of zero length, and just the file name and attributes matter). There are then other ways of providing instances which rely on the file contents. Here are the methods of providing instances defined in the APIs:

Default instance - InstanceDataObject

If there is a file with the extension *.instance, then its name (minus extension) will be converted to a class name by replacing dashes with dots; and a fresh instance of that class will be created and used as the instance. For example, com-mycom-mymodule-MyAction.instance produces an instance of the class com.mycom.mymodule.MyAction.

Since reflection is used to create the new instance, just as in the realm of JavaBeans, the class must be loadable from your module or otherwise from within the IDE (technically, via TopManager.currentClassLoader); public; and have a public no-argument constructor. This form is often used for singleton classes such as system actions: there is no need to specify any parameters to the constructor, any instance will suffice.

Named default instance - InstanceDataObject

This is similar to the default instance above, but square brackets are used within the file name to produce a different DataObject name. For example, C-F6[com-mycom-mymodule-MyAction].instance will still produce a default instance of com.mycom.mymodule.MyAction, but DataObject.getName() will produce C-F6. This form is most frequently used to specify keyboard shortcuts, for which the data object name gives the key binding; or when multiple default instances of the same class are desired in a single folder. However, it is now deprecated in favor of other forms which are more general; additionally, square brackets in file names, if ever copied to disk from a layer, can cause problems under the OpenVMS operating system, so they should be avoided.

Default instance with separate class - InstanceDataObject

Rather than shoving both the object name and the class name into the file name, you can name the file more normally and specify the class with a file attribute. Then the file name is used directly for DataObject.name, and the class name is specified as a string-valued attribute on the instance named instanceClass. For example, the shortcut example above could be written as follows in an XML layer:

<file name="C-F6.instance">
    <attr name="instanceClass" stringvalue="com.mycom.mymodule.MyAction"/>
</file>

In addition to instanceClass you may specify an additional attribute instanceOf giving the name of a superclass (or implemented interface) of the instance class. In fact it may be a comma-separated list of superclasses and interfaces. While its purpose is explained more fully below, essentially it lets you give the system a hint as to what this instance is for before your instance class is even loaded into the VM. For example:

<file name="com-me-some-service.instance">
    <attr name="instanceClass" stringvalue="com.me.FactoryForEverything"/>
    <attr name="instanceOf"
     stringvalue="org.xml.sax.EntityResolver,org.openide.cookies.ExecCookie"/>
</file>
Non-default instance - InstanceDataObject

A powerful way of providing instances is to use the expressiveness of the XML layer syntax to handle the instance creation. In this case the file attribute instanceCreate can be defined and the attribute value becomes the instance. Typically the attribute value would be specified using the methodvalue syntax of layers. For example:

<file name="com-me-some-service.instance">
    <attr name="instanceClass" stringvalue="com.me.FactoryForEverything"/>
    <attr name="instanceCreate" methodvalue="com.me.FactoryForEverything.configure"/>
    <attr name="myParam" urlvalue="nbres:/com/me/config-1.properties"/>
    <attr name="instanceOf"
     stringvalue="org.xml.sax.EntityResolver,org.openide.cookies.ExecCookie"/>
</file>

According to the general system for XMLFileSystem, you now need a method configure in FactoryForEverything which must be static; the method need not be public (if you do not want other Java code to see it). It may take a file object as argument if you wish - this will be the instance file; typically you use this to pass extra configuration from the layer, useful if you want to create multiple instances with the same creation method. So for example you might implement such a method like this:

public class FactoryForEverything extends SomeBaseFactory
        implements EntityResolver, ExecCookie {
    public FactoryForEverything(Map props) {
        // ...
    }
    // ...
    // Called directly from XML layer. Pass URL to
    // properties file from attr 'myParam'.
    private static Object configure(FileObject inst) throws IOException {
        URL u = (URL)inst.getAttribute("myParam");
        Properties p = new Properties();
        p.load(u.openStream());
        return new FactoryForEverything(p);
    }
}
Serialized beans

A simple way to provide an instance is to serialize it as a JavaBean, into a file with the extension *.ser. This is not very useful from a layer, because you should avoid putting binary data into a layer, but may be useful in some circumstances.

XML-based instances

This is probably the most powerful option. You can define an XML DTD which provides a rich configuration structure for your instances.

First, you should implement Environment.Provider which will be used to serve cookies and perhaps other things from the XML file. It will be passed a data object; you can cast this to XMLDataObject and use getDocument() to retrieve the DOM tree, typically. It should create a lookup (such as AbstractLookup with InstanceContent); you might want to service requests for InstanceCookie, ExecCookie, or whatever you want the XML file to support. You can change cookies on the fly by firing changes in the lookup result (done automatically if you use the proper lookup implementation).

To register the lookup provider, you should create an instance of it (in your layer, typically) beneath xml/lookups/, named according to the public ID of the DTD you wish to recognize. EntityCatalog describes the general public ID escaping algorithm, but to that filename should be appended either .instance or .xml. For example, a handler for the public ID -//NetBeans//My Format 1.0//EN could be registered as follows:

<filesystem>
    <folder name="xml">
        <folder name="lookups">
            <folder name="NetBeans">
                <file name="My_Format_1_0.instance">
                    <attr name="instanceClass" stringvalue="my.module.MyFormatHandler"/>
                </file>
            </folder>
        </folder>
    </folder>
</filesystem>

Here my.module.MyFormatHandler implements Environment.Provider.

You may also in this way register mappings from public IDs to instances of either XMLDataObject.Processor or XMLDataObject.Info, but these are both deprecated in favor of the more powerful Environment.Provider.

Again, modules may also have additional ways of providing instances from files. For example, currently the utilities module enables any URL file (*.url) to be used directly in a menu, as the URL file provides an instance of Presenter.Menu.

As an interactive demonstration of these things, first go into Filesystem Settings and make the system file system (first in the list) visible; then explore its contents in Filesystems, going into some subdirectory of Menu. Note the various actions and menu separators; these are all by default instance data objects. You may find some of these on disk in your installation directory under system/ if you have customized them, but by default they live in memory only. You may copy-and-paste these instances from one place to another; create new ones on disk and watch them be recognized and inserted into the menus after a few seconds; and you may also choose Customize Bean (which really customizes the provided instance) on (say) a toolbar separator (under Toolbars) to change the separator size, and serialize the result to a new *.ser file which should then create a toolbar separator.

Folders of Instances

In many cases instances by themselves are not useful, as the whole purpose of using them is to make it possible for modules to register new instances and have some infrastructure find them; if you need to also register the positions of every instance (file name) explicitly, then this defeats the purpose. So it is common to assemble a number of instances together into some compound object (instance) based on the contents of folders.

For example, if one instance file represents a menu item (or a system action which can be represented by one), then a folder full of them represents a menu. A folder full of menu folders represents a menu bar. A subfolder underneath a menu folder with some instances in it, represents a submenu. Specifically, the menu bar for the IDE's Main Window is represented by the folder Menu/ in the system file system, so you can add a menu item very easily from a layer:

<filesystem>
    <folder name="Menu">
        <folder name="Tools">
            <file name="com-me-MyAction.instance"/>
        </folder>
    </folder>
</filesystem>
In a more general way, the lookup system (more on this later) looks for services of all sorts in the folder Services/ in the system file system; they may be concealed from the UI by placing them in a subfolder Hidden/. So a typical way to add a new service is as follows:
<filesystem>
    <folder name="Services">
        <folder name="Hidden">
            <file name="com-me-SomeService.instance"/>
        </folder>
    </folder>
</filesystem>

FolderInstance is typically used to supply instances from entire folders, by recursively scanning the contents and collecting them together in a meaningful fashion. (You do not need to know this to install instances into existing systems, but this class will be useful if you wish to create your own instance-based configuration system.) FolderInstance is fairly flexible and allows you to control the superclass (or interface) of instances that you wish to pay attention to; accept certain instances but not others; transform instances; control how to assemble them into compound instances; and so on.

There are a few things you should know about dealing with folders containing instances that are used by the system:

InstanceCookie.Of and lazy class loading

Now it is time to mention the purpose of InstanceCookie.Of. Suppose that there are two generic interfaces under consideration: e.g. javax.swing.Action and org.xml.sax.EntityResolver. For each interface, there is code to find registered instances, all under the Services/ folder. Furthermore, using either of these interfaces is relatively rare, and might not happen at all during an IDE session; and the implementations of the instances are complicated and involve a lot of code, so it is undesirable (for performance reasons) to load these implementations unless and until they are really needed. If you write the layers simply like this:
<filesystem>
    <folder name="Services">
        <folder name="Hidden">
            <file name="com-me-MyAction.instance"/>
            <file name="com-me-MyResolver.instance"/>
        </folder>
    </folder>
</filesystem>
everything will work, but this is inefficient. Consider some piece of code asking for all actions. The search through the services folder for actions would ask each of these files if it provides an instance cookie assignable to javax.swing.Action. For com-me-MyAction.instance, this will load the class com.me.MyAction, determine that it implements Action, and thus create a MyAction instance and return it; so far so good. But when com-me-MyResolver.instance is encountered, it will again load com.me.MyResolver, only to find that this does not implement Action and skip the instance. The behavior is correct, but now the MyResolver class has been loaded into the VM even though no one will ever use it (unless a resolver search is made). This will degrade startup time and memory usage (and thus performance).

So the better solution is to mark each file in advance, saying what interfaces it is intended to provide in its instance:

<filesystem>
    <folder name="Services">
        <folder name="Hidden">
            <file name="com-me-MyAction.instance">
                <attr name="instanceOf" stringvalue="javax.swing.Action"/>
            </file>
            <file name="com-me-MyResolver.instance">
                <attr name="instanceOf" stringvalue="org.xml.sax.EntityResolver"/>
            </file>
        </folder>
    </folder>
</filesystem>
Now the folder instance processor for Action will pass over com-me-MyResolver.instance without needing to load com.me.MyResolver, since it sees that its interfaces are declared, and Action is not among them. Of course, the interface classes - Action and EntityResolver - need to be loaded right away, but they were probably already loaded anyway, so this is acceptable.

Lookup and Service Installation

The client side of the lookup system centers around one class, Lookup. In the simplest usage, all that is needed is to get some single instance of a given class (or subclass). For example, if some kind of service has been defined as an interface or abstract class, and you wish to find the implementation of it (which you expect to have been installed by some module, typically via layer as described above), you may simply use:
MyService impl = (MyService)Lookup.getDefault().lookup(MyService.class);
if (impl == null) /* nothing registered */ ...
impl.useIt();
If more than one implementation has been registered, the "first" will be returned. For example, if the implementations were present in the Services folder as *.instance files, then folder order would control this.

As mentioned above, the IDE's default lookup searches in the Services folder and its subfolders for instances whose class matches the requested class. Technically, it looks for data objects with InstanceCookie.Of claiming to match the requested superclass, or plain InstanceCookie whose instanceClass is assignable to it.

Note that you may use this method to find singleton instances of subclasses of SharedClassObject that have been registered in lookup (as is normally the case for system options). However for this purpose it is simpler to use the static finder method SharedClassObject.findObject(Class, true) which is guaranteed to find the singleton whether it was registered in lookup or not (if necessary it will first initialize the object according to saved state).

In many situations it is normal for there to be more than one registered implementation of a service. In such a case you use a more general method:

Lookup.Template templ = new Lookup.Template(MyService.class);
final Lookup.Result result = Lookup.getDefault().lookup(templ);
Collection impls = result.allInstances(); // Collection<MyService>
// use Java Collections API to get iterator, ...
// Pay attention to subsequent changes in the result.
result.addLookupListener(new LookupListener() {
    public void resultChanged(LookupEvent ev) {
        // Now it is different.
        Collection impls2 = result.allInstances();
        // use the new list of instances...
    }
});
Here you receive a collection of all instances matching your query, again in the order found if this matters. You can also listen to changes in the list (additions, deletions, and reorderings). It is fine to keep a Lookup.Result for a long period of time as it may implement its own caching scheme and only really compute the instances when allInstances is called; in fact it may be more efficient to keep a result, and listen for changes in it, than to repeatedly call lookup and create fresh result objects.

When a lookup query is finished - for example when Lookup.Result.allInstances() returns some Collection of instances - it is guaranteed that all objects registered in the same thread prior to the call to lookup() or prior to some change notification on the Lookup.Result, will be returned. Specifically, lookup instances registered via module layer will be available by the time ModuleInstall.restored() (or .installed()) is called. There are two situations in which lookup results may be incomplete: when you are currently inside the dynamic scope of some method providing a lookup instance itself; and when you are dynamically inside a DataLoader method involved in recognizing data objects.

Persisting Lookup Information

In some circumstances it is necessary to not only find registered objects, but to select some of them and make this selection persistent. For example, some setting may have as its value a choice among available services matching some interface; the value needs to be persisted, but it is the identity of the choice, rather than the full state of the instance itself, which must be stored.

In such cases it is possible to use code like this:

Lookup.Template templ = new Lookup.Template(MyService.class);
Lookup.Result result = Lookup.getDefault().lookup(templ);
Iterator it = result.allItems().iterator();
while (it.hasNext()) {
    Lookup.Item item = (Lookup.Item)it.next();
    String displayName = item.getDisplayName();
    if (/* user accepts displayName as the right one */) {
        MyService instance = (MyService)item.getInstance();
        // use instance for now, and ...
        String id = item.getId();
        someSettings.setChosenService(id);
        break;
    }
}
// later...
String storedID = someSettings.getChosenService();
Lookup.Template templ = new Lookup.Template(MyService.class, storedID, null);
Iterator it = Lookup.getDefault().lookup(templ).allInstances().iterator();
if (! it.hasNext()) /* failed to find it... */
MyService instance = (MyService)it.next();
// use instance again
The ID permits you to track which instance from all those available in the lookup result was last selected by the user, and find the "same" instance later, perhaps after a restart of the IDE. The exact form of the ID is the private knowledge of the implementor of the lookup, but typically if the instance has been provided via layer the ID will mention the name of the file from which it was derived.

Creating Lookups

There are a number of reasons to create your own lookup implementation. For one thing, the lookup system which scans the Services/ folder will recognize instances of subclasses of Lookup specially by proxying to them. This can be very powerful because you may register just one layer file which points to your custom lookup, which in turn may provide an unlimited number of actual instances/items for queries (and compute them in a manner other than registration by files). Another reason is to associate a context with a data object using Environment.Provider; for example the data object might be an XML file and this provider might be registered with the file by means of the public ID of the doctype (informally, the DTD). Here the provider has an associated lookup which can serve requests for cookies and such things. See more information about associating lookups with XML files.

The simplest way to create a fresh lookup is to base it on other existing ones. ProxyLookup accepts a list of other lookup implementations (in the constructor and also changeable later). The results it provides are constructed by merging together the results of the delegate lookups.

If you want to use the common mechanism of finding instances in folders (or subfolders) and serving these as the results, FolderLookup makes this possible: you need only provide a folder to look in, and use getLookup to retrieve a lookup implementation which will scan this folder and its subfolders for data objects with InstanceCookie matching the lookup template. Furthermore, any instance cookies whose instance class is assignable to Lookup will be treated specially: they will be proxied to, so these sub-lookups may provide additional instances if they match the lookup template.

The most powerful way to provide a lookup is to directly define what instances and items it should provide, by subclassing. For this, AbstractLookup is recommended as it is easiest to use.

The simplest way to use AbstractLookup is to use its public constructor (in which case you need not subclass it). Here you provide an AbstractLookup.Content object which you have created and hold on to privately, and which keeps track of instances and permits them to be registered and deregistered. Often InstanceContent is used as the content implementation. To add something to the lookup, simply use add(Object) (and remove(Object) for the reverse). These may be called at any time and will update the set of registered instances (firing result changes as needed).

In case it is expensive to actually compute the object in the lookup, but there is some cheap "key" which can easily generate it, you may instead register the key by passing in an InstanceContent.Convertor. This convertor translates the key to the real instance that the lookup client sees, if and when needed. For example, if you have a long list of class names and wish to register default instances of each class, you might actually register the class name as the key, and supply a convertor which really loads the class and instantiates it. This makes it easy to set up the lookup, but nothing is really loaded until someone asks for it.

Settings

Settings require special support in the lookup system: these are objects (perhaps singletons but not necessarily) which should be made available to lookup, yet whose content can be changed and stored to disk (typically as a result of user interaction with the GUI). *.instance files and similar constructions are fine for registering fixed objects from layer - "fixed" in the sense that while the user might copy, delete, move, or reorder them, the actual object they provide is statically determined and does not generally have a means of being modified. In contrast, settings have nontrivial content. A typical setting is a system option, simply a singleton bean with a set of properties and a structured GUI presentation driven by BeanInfo.

In order to save such settings, an XML file is normally used, and the APIs provide a convenient DTD for settings which can be represented as a single bean. In typical usage, the module layer declares an initial settings file which instructs lookup to create a default instance of the settings class. This might look like the following:

<?xml version="1.0"?>
<!DOCTYPE settings PUBLIC "-//NetBeans//DTD Session settings 1.0//EN" "http://www.netbeans.org/dtds/sessionsettings-1_0.dtd">
<settings version="1.0">
    <module name="com.foo/1" spec="1.0"/>
    <instanceof class="org.openide.options.SystemOption"/>
    <instanceof class="com.foo.MyOption"/>
    <instance class="com.foo.MyOption"/>
</settings>
Such a file might be placed in Services/com-foo-my-settings.xml (the exact name inside the Services folder is not important but ought to be globally unique to avoid conflicts with other modules). It provides an InstanceCookie with the settings object as instance.

The interesting parts of this file are:

There are actually three ways that the instance may be declared:

  1. Using <instance/> as above to generate a default instance. This is most common.

  2. You may pass an additional attribute method indicating a static method to call in the supplied class to produce the instance, rather than using a default constructor. The method may optionally take a FileObject argument which will be the settings file itself. This is analogous to the mechanism used for creating complex *.instance files. For example:

    <file name="some-difficult-to-make-instance.settings">
        <![CDATA[<?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE settings PUBLIC
              "-//NetBeans//DTD Session settings 1.0//EN"
              "http://www.netbeans.org/dtds/sessionsettings-1_0.dtd">
    <settings version="1.0">
        <module name="my.module/1" spec="1.0"/>
        <instanceof class="javax.swing.Action"/>
        <instanceof class="my.module.MyAction"/>
        <instance class="my.module.MyAction" method="createAction"/>
    </settings>
    ]]>
        <attr name="param" stringvalue="someval"/>
    </file>
    
    package my.module;
    public class MyAction extends javax.swing.AbstractAction {
        public MyAction(String name) {/* ... */}
        // ...
        public static MyAction createAction(FileObject fo) {
            return new MyAction((String)fo.getAttribute("param"));
        }
    }
    

    This will result in an instance of MyAction being created with name someval.

  3. You may use the <serialdata> element. Its textual contents are a hexadecimal dump (whitespace ignored) of the raw serialized bytes of an object to serve as the instance. Naturally this is the least preferred mechanism as it is not human-readable.

In the future it is planned for a fourth mechanism to be supported, taking advantage of the JDK 1.4 XML persistence ("archiver") system.

A client can find the current setting in a couple of ways:

How does the customization of setting instances work? After finding a setting instance via this DTD, the system will automatically look for a beans-style event set of type PropertyChangeListener, and add its own listener. If the bean changes state (either programmatically or as a result of user manipulation), the property change will cause the new state to be written out to the original XML file, keeping the same name. (Normally this would mean the XML would be written to disk in the user directory.)

(Currently the state is simply serialized into the XML file using <serialdata>, meaning the class must be Serializable, but in the future if an alternate persistence form is available, such as the JDK 1.4 archiver, this will be used instead.)

Conversely, changes to the setting file on disk should trigger a reload of the state and modification of the in-memory bean (or creation of a new instance cookie with a new bean).

In NetBeans 3.3 there is an additional file attribute that is recognized by the system when set on settings files: SystemFileSystem.layer, which if set to the string value project will indicate that changes to the setting should be written to the project rather than global settings, and if set to session will request storage with the session, if this is in fact implemented. However there is no guarantee that in the future this attribute will continue to be recognized, or that these values function in the same way, so consider it a hint only.

UI for Services

There are several things you can do to not only have services and lookup function programmatically but also look good and behave nicely within the user's view of configuration options and settings.

Service Templates

For many kinds of services, especially ServiceTypes but also others, it is necessary to permit the user to create new instances of the service. Generally two criteria should be met for such services:

  1. The service is not a singleton, so it is meaningful to have more than one instance.
  2. The service has some user-configurable properties, so it is useful to have more than one instance.

Creation of new service instances may be supported simply by producing a template residing beneath Templates/Services/. If the service normally resides in a subfolder of services, for example Services/Executor/, then the template should correspondingly be placed in Templates/Services/Executor/.

The template should work like regular source code templates do: a file giving the initial structure of the service (typically a *.settings file), with the file attribute template set to true, and optionally a templateWizardDescription and templateWizardURL. For example:

<folder name="Templates">
    <folder name="Services">
        <file name="my-module-executor.settings" url="executor.settings">
            <attr name="template" boolvalue="true"/>
            <!-- SystemFileSystem.localizedName and SystemFileSystem.icon as usual -->
        </file>
    </folder>
</folder>

If the user selects New From Template on the corresponding options folder, the template will be available.

Services display area and mirroring

In addition to providing services, it is desirable to display them to the user as well. This is done, as is customary in other aspects of the IDE's configuration, by displaying customized variants of the data nodes coming from the system file system. The root folder for displaying options is called UI/Services/. Its subfolders govern the display of the options available in the system.

As a rule, it is undesirable to place any actual settings in this folder (nor would they be recognized by the default lookup anyway). That is because the organization of this folder is driven by UI needs, without regards to API maintenance or compatibility of persisted user settings. So this folder solely mirrors configuration available elsewhere. You may freely reorganize the mirror according to current UI needs: existing modules plugging into services areas will continue to work unmodified, and existing user customizations stored to disk will continue to apply, since both of these act on the original files (which should not be moved frivolously).

While technically you could place anything you wish in the UI folder, in practice a few types of things are used:

No particular substructure of UI/Services/ is defined by the APIs. For optimal UI integration you may wish to examine the categories used by other modules and try to reuse an existing category appropriate to your needs.

In some cases it is necessary to hide a service file, or a whole folder of services. While you can place files into Services/ and simply not make any corresponding mirror in UI/Services/, you may wish to create services or subfolders inside existing displayable folders (for purposes of lookup, generally) yet not have them be visible in the UI. In this case you should mark the file or subfolder with the file attribute hidden (set to boolean true).

Property editor for services

If you wish to permit the user to select a service as part of the Property Sheet (from a node property, or as a PropertyPanel, providing general GUI embeddability), this is supported. You should use the property editor assigned to java.lang.Object (not your desired service interface). Various hints defined in the Explorer API permit you to control the result.

As an example, here is a node property permitting you to ask the user to select a value of type my.module.Thing, being some interface or abstract superclass, where some instances are registered to lookup, conventionally in the Services/Things/ folder which the module has provided:

public abstract class ThingProperty extends PropertySupport.ReadWrite {
    protected ThingProperty(String name, String displayName, String shortDescription) throws IOException {
        super(name, Object.class, displayName, shortDescription);
        setValue("superClass", Thing.class); // NOI18N
        setValue("nullValue", NbBundle.getMessage(ThingProperty.class, "LBL_no_thing")); // NOI18N
        DataFolder thingsFolder = DataFolder.create(
            DataFolder.findFolder(Repository.getDefault().getDefaultFileSystem().getRoot()),
            "Services/Things" // NOI18N
        );
        setValue("node", thingsFolder.getNodeDelegate()); // NOI18N
    }
    public final Object getValue() {
        return getThing();
    }
    public final void setValue(Object o) {
        if (o != null) {
            Lookup.Template templ = new Lookup.Template(Thing.class, o, null);
            Iterator it = Lookup.getDefault().lookup(templ).allItems().iterator();
            if (it.hasNext()) {
                setThingID(((Lookup.Item)it.next()).getId());
            } else {
                // Thing was registered but is not persistable.
                setThingID(null);
            }
        } else {
            setThingID(null);
        }
    }
    public final boolean supportsDefaultValue() {
        return true;
    }
    public final void restoreDefaultValue() {
        setValue(null);
    }
    // May be used by code wishing to get the actual Thing (or null):
    public final Thing getThing() {
        String id = getThingID();
        if (id != null) {
            Lookup.Template templ = new Lookup.Template(Thing.class, null, id);
            Iterator it = Lookup.getDefault().lookup(templ).allInstances().iterator();
            if (it.hasNext()) {
                return (Thing)it.next();
            } else {
                // Invalid ID.
                return null;
            }
        } else {
            return null;
        }
    }
    // Subclasses implement to actually read/write Thing persistent IDs (or null):
    protected abstract String getThingID();
    protected abstract void setThingID(String id);
}

A property extending this class would in the current UI display a pull-down list of all Thing implementations available in lookup; the custom property editor dialog would display the Things folder with anything contained inside it for the user to select from, provided it in fact had an instance assignable to Thing. The special null value is explicitly permitted here and would be displayed with the label given in the bundle.

Context help for services

You may also specify a context help ID for services or settings you are installing (typically through a layer). Although ServiceTypes or SystemOptions themselves can return help IDs (see ServiceType.getHelpCtx() and SystemOption.getHelpCtx()), folders which represent their categories cannot use this mechanism. You may declare the desired context ID (e.g. in the layer) by adding the file attribute helpID for the folder's primary file, with a value of type String:

<folder name="Services">
    <folder name="SomeCategory">
        <attr name="helpID" stringvalue="the.help.ID"/>
    </folder>
</folder>

You may also attach the help ID to individual services or options in the same way, by adding the helpID attribute to their primary files, most useful for service objects which do not inherit from either ServiceType or SystemOption.

Compatibility with Manifest Installation

For compatibility with older modules which registered objects via module manifest, there is a bridge which makes many such objects available via lookup. The Modules API lists possible manifest section classes and notes where they may (and should) be replaced by direct lookup registration.

A guide is available to help you move old manifest-based settings to the lookup system.


Built on December 12 2001.  |  Portions Copyright 1997-2001 Sun Microsystems, Inc. All rights reserved.