How Do NetBeans Extension Points Work?
Join the DZone community and get the full member experience.
Join For FreeOne of the main benefits of the NetBeans Platform is its module system. Regardless of which module system is best (my guess is there will soon be a version of NetBeans that can also run as OSGi bundles), it’s important to have a system that enables you to create a modular architecture for your application. A module system is an invitation to create a clean and maintainable architecture with defined dependencies and nice APIs with clearly defined and easy-to-use extension points. If you follow these principles, others can extend your application easily. Maybe the easiest way to provide extension points in NetBeans is via the layer.xml file (also known as the "layer file").
In a NetBeans module (also known as a "plugin"), the layer file is its central configuration file. NetBeans IDE uses the layer file to provide extension points for APIs. Objects can be created declaratively in the layer file and you can use the NetBeans Lookup API to listen for changes. You will use this approach whenever you create new Actions (which are invoked via menu items, toolbar buttons, and/or shortcut keys) or TopComponents (which provide "views" or "windows"), for example.
This quick tutorial shows how you can provide your own extension points via the layer file. The source code of the sample is found here in the NetBeans Plugin Portal.
Prerequisites
- NetBeans (I’m using 6.1, but this will also work with older/newer versions).
- Create a new module suite:
- Choose File > New Project (Ctrl-Shift-N). Under Categories, select NetBeans Modules. Under projects, select "NetBeans Platform Application" or ("Module Suite Project" in older versions) and click Next.
- In the Name and Location panel, type "layerextensionpoints" in Project Name. Change the Project Location to any directory on your computer, to store the application. Click Finish.
- Now create four modules inside the suite, called "extensionpointinterface", "messagereader", "messageprovider1" and "messageprovider2":
- Choose File > New Project (Ctrl-Shift-N) again. Under Categories, select NetBeans Modules. Under Projects, select Module and click Next.
- In the Name and Location panel, type the name in Project Name (i.e., one of the four names listed at the start of this step). The default in the wizard should be to create the module underneath the directory where you just created the suite, which is fine. Click Next.
- In the Basic Module Configuration panel, replace the Code Name Base with de.eppleton.<modulename>. Make sure to let the IDE create a layer file, by filling out the XML Layer field with de/eppleton/<modulename>/layer.xml. Click Finish.
Here is what you should now see:
Create a Service Provider Interface
We will use the module "extensionpointinterface" to define an interface that will be used by the two extension point providers as well as by the "messagereader" module, which uses the two extension points. Inside that module create interface "MessageProviderInterface" with the single method getMessage() that returns a String:
public interface MessageProviderInterface {
public String getMessage();
}
To make this part of the public API right click the project node, select API Versioning and select the check box of de.eppleton.extensionpointinterface. Now this package is accessible to other modules.
Create Service Provider Implementations
Now we need some modules that implement the MessageProviderInterface. We will use the modules "messageprovider1" and "messageprovider2" to do that. To implement the interface they both need a dependency on module "extensionpointinterface". For each of them do the following:
- Right click the project node, click Properties and, in the Project Properties dialog, select the "Libraries" category.
- Click "Add Dependency".
- Select "extensionpointinterface", and click "OK" twice.
Now that we have access to the interface in our two message providers, we can implement it. Again, for both modules, do the following:
- Select "New" > "Java Class".
- In the Wizard type "MessageProvider1" or "MessageProvider2" in the Class Name field, respectively, and select the main package in each case, for stroring the new class.
- Implement the interface. Each of them should provide a different String. In other words, we will provide "Hello " in MessageProvider1 and then we will provide "World!" in MessageProvider2:
import de.eppleton.extensionpointinterface.MessageProviderInterface;
public class MessageProvider1 implements MessageProviderInterface {
public String getMessage() {
return "Hello ";
}
}
import de.eppleton.extensionpointinterface.MessageProviderInterface;
public class MessageProvider2 implements MessageProviderInterface {
public String getMessage() {
return "World!";
}
}
}
In order to make our MessageProviders available as services, add these entries in the layer of the two modules. Between the the layer file's <filesystem> tags, add the following, i.e., in the first module add the first set of tags and add the second set of tags in the second module:
<folder name ="MessageProviders">
<file name="de-eppleton-messageprovider1-MessageProvider1.instance" />
</folder>
and
<folder name ="MessageProviders">
<file name="de-eppleton-messageprovider2-MessageProvider2.instance"/>
</folder>
This will create an instance of our MessageProviders using the standard constructor. The trick is that those instances will be accessible from outside the modules, via the NetBeans System FileSystem.
Now that you have created your services, the next step shows how you can access them, without even needing to set a module dependency for them.
Find and Use the Service Providers
We will use the module "messagereader" to display the messages provided by our two MessageProviders. To do so we will create a TopComponent, within the "messagereader" module:
- In the Project Properties dialog for "messagereader", set a dependency on "extensionpointinterface", exactly as shown earlier.
- Right-click on the "messagereader" project node and choose "New" > "Window Component". Choose "Output", which will determine where the new window will be displayed. Make it show on startup by ticking the checkbox. Click Next.
- Enter "MessageReader" for the class name prefix and click "Finish".
- The TopComponent class will open in Design View. Drop a JScrollPane from the palette in the window and make it fill the whole area. Add a JTextArea to it, making it fit the whole area too.
- In the source view (i.e., click "Source" at the top of the Design view) add this to the end of the constructor:
Lookup lkp = Lookups.forPath("MessageProviders");
for (MessageProviderInterface mi : lkp.lookupAll(MessageProviderInterface.class)) {
jTextArea1.append(mi.getMessage());
}
The code above will lookup the folder you created via the layer file (Lookups.forPath("MessageProviders")), search for classes implementing the interface (lookupAll(MessageProviderInterface.class)) and call the interface method on all instances.
Let’s try it out!
Run the Module Suite. You will see the window either displaying "Hello World!" or "World!Hello ". As we can see in this very simple example, the order in which the ServiceProviders are called can be important for the result. So in the next step I will show you a trick to guarantee the correct order.
Sort the Service Providers
The NetBeans Platform provides a way to sort layer entries. This mechanism is used, for example, to provide the order of actions in menus and toolbars. Since 6.0, this is done via the position attribute in the layer file. So this won’t work in older versions:
In the layer file of the two modules, insert the "position" attributes that you see below, i.e., the first set of tags below belongs to "messageprovider1", while the second belongs to "messageprovider2":
<folder name = "MessageProviders">
<file name="de-eppleton-messageprovider1-MessageProvider1.instance" >
<attr name="position" intvalue="30"/>
</file>
</folder>
and
<folder name = "MessageProviders">
<file name="de-eppleton-messageprovider2-MessageProvider2.instance" >
<attr name="position" intvalue="40"/>
</file>
</folder>
Now the messages will always be displayed in the correct order: "Hello world!". Simply swap these values to reverse the order of the messages. When you do something like this, make sure that you choose your values large enough to add services in between the initial ones, so that there’s room for your application to grow! Now try and see what happens when you set the same value for both attributes!
Summary
The intention of this article is to illustrate how simple it is to implement loose coupling in a NetBeans Platform application. Note that the module that uses the services (i.e., "messagereader") has no dependencies on the modules that provide the services ("messageprovider1" and "messageprovider2"). And not only does the NetBeans Platform provide a very simple mechanism to provide and lookup services, but it also provides an easy way to declaratively order them.
Appendix: Alternative Registration Mechanism, Using "META-INF/services"
In the previous sections, I showed how to register the Service Providers via the layer file. As stated correctly in the comment to this article, by Casper Bang, there's an alternative way to register the service providers. Using the META-INF/services folder is the standard approach that is supported by Java since version 1.3. This additional sections below show how it works.
Register the Services
There are only some small changes needed to change the registration mechanism:
- In module messageprovider1 create a folder via the context menu of "Source Packages" > "New" > "Other" > "Folder". Enter "META_INF/services" as Folder Name; the folder will show up like a normal package.
- Create a file via the context menu of this new package "META-INF.services" > "New" > "Other" > "Empty file", name the file "de.eppleton.extensionpointinterface.MessageProviderInterface".
- Edit the file and enter "de.eppleton.messageprovider1.MessageProvider1"
- Copy and paste the "META_INF" folder to the "Source packages" folder of module messageprovider2 and change the content of file "de.eppleton.extensionpointinterface.MessageProviderInterface" to "de.eppleton.messageprovider2.MessageProvider2"
There are different ways to lookup the services. You can either use the default lookup to do so, or you can use the ServiceLoader mechanism introduced in Java 1.6. Let's begin with the method using the default lookup.
Using Lookup to retrieve ServiceProviders
The global lookup will automatically provide these services for you, so only a little change is needed. In the module "messagereader", edit "MessagereaderTopComponent", and replace this line:
Lookup lkp = Lookups.forPath("MessageProviders");
with this line:
Lookup lkp = Lookup.getDefault();
Afterwards, you can build and run the application as before. You may notice that the order of the services has changed. As before you can fix this by adding an additional position attribute when you define the ServiceProvider. Edit both "de.eppleton.extensionpointinterface.MessageProviderInterface" files in modules messageprovider1 and messageprovider2. Add the lines "#position=10" and "#position=20" respectively. Run the application and play with the values to change the order as before.
Using ServiceLoader to Locate the ServiceProviders
If you're using JDK 1.6 or later, you can edit "MessagereaderTopComponent" and replace the code we've added before with the following:
ServiceLoader<MessageProviderInterface> serviceLoader = ServiceLoader.load(MessageProviderInterface.class);
for (MessageProviderInterface mi : serviceLoader) {
jTextArea1.append(mi.getMessage());
}
Note that the position attribute is now ignored, since it's not a part of the standard JDK but an extension added by the NetBeans Platform.
Opinions expressed by DZone contributors are their own.
Comments