Plugin Ready Java Application

Jean Lazarou
June 10, 2007

Preface

Everything started with the need of adding plugin support to some Java/SWT application.

Plugin support is far from being easy to implement, unless you use a plugin framework to help. We first looked at the Eclipse Equinox plugin framework (implementing the OSGi framework specification). Later on, I found another project named Java Plugin Framework (JPF).

The standard way of using JPF is: start the plugin framework, by launching the main method of the framework. The framework loads the plugins and searches for the application plugin. Using the application plugin, it launches your application. Equinox implements the same scheme.

We didn't want to convert the original application to a plugin. We wanted to keep on starting the application in a more standard way. The application would, at some point, start the plugin framework and load the plugins. Doing so with JPF appeared to be quite easy.

This article presents a simple application and how to add plugin support, in two phases: the first is very simple, the second is more elegant.

The application, named Bookmark, displays a tree view with bookmarks, on the left-side, and a browser view on the right-side (see picture). The application loads plugins that add new sections with bookmarks to the default bookmarks. The goal here is not to build a useful application, but to show how to add plugin support. We won't even perform any validation or merge sections.

The application uses the Eclipse/SWT for the GUI because integrating a Web browser is really easy.

The application without plugin support

Let's first create the main class, named Main. Add the main method that creates an instance of the Main class and calls its start method.

    public static void main(String[] args) {
        new Main().start();
    }

The start method creates first the shell (the main window in SWT), using the createShell method. The whole code is presented later.

Then, it adds the default bookmarks to the tree view (addDefaultBookmarks), sets the listeners to respond to user's clicks (setListeners), opens the shell (shell.open()) and runs the event loop (the event loop is explicit in SWT).

    private void start() {
       
        createShell();
        
        addDefaultBookmarks();
        
        setListeners();
       
        shell.open();
       
        Display display = shell.getDisplay();
       
        while (!shell.isDisposed()) {
           
            if (!display.readAndDispatch())
                display.sleep();
           
        }
       
        display.dispose();
       
    }

In SWT the tree nodes are TreeItem objects. A TreeItem object has a text and can have any object as data. We are going to set the target URL as data (see next snippet of code showing how we set the URL for the Google entry).

    TreeItem item = new TreeItem(root, SWT.NULL);
    item.setText("Google");
    item.setData("http://www.google.com");

The TreeItem constructor requires the parent tree or another TreeItem, here the root object is the Bookmarks node (a TreeItem object).

Click here to show the whole code.

Adding Plugin Support

The bookmark application could benefit of plugins to extend the bookmarks presented to the user. Sure, the real purpose is adding plugin support here.

We are going to be very basic and simple to keep this presentation easier. Let's add an interface, named BookmarkSection, to get additional bookmarks from implementing classes.

public interface BookmarkSection {
    
	String category();

	int count();
	String getName(int index);
	String getUrl(int index);

}

The idea is that any plugin found in the plugins directory should implement the above interface. The application can retrieve a section name and add every entry, below the given section (the picture, shown previously, has two such sections named Java and Ruby).

The start method now loads the plugins and then use them to add new (dynamic) bookmarks. Hereafter is the new version of the method, new code is highlighted.

    private void start() {

        loadPlugins();
       
        createShell();
        
        addDefaultBookmarks();
        addDynamicBookmarks();
        
        setListeners();
       
        shell.open();
       
        Display display = shell.getDisplay();
       
        while (!shell.isDisposed()) {
           
            if (!display.readAndDispatch())
                display.sleep();
           
        }
       
        display.dispose();
       
    }

Now we need to implement the new methods.

Loading the plugins

Let's look at the implementation of the loadPlugins method.

First, we must create a plugin manager object.

    pluginManager = ObjectFactory.newInstance().createManager();

Next, get all the plugins found in the plugins directory (any file name ending with .zip extension, for instance).

    File pluginsDir = new File("plugins");
        
    File[] plugins = pluginsDir.listFiles(new FilenameFilter() {

        public boolean accept(File dir, String name) {
            return name.toLowerCase().endsWith(".zip");
        }
            
    });

Last step is the actual loading, create an array of PluginLocation objects for every plugin file. Loading the plugins is easy, just call the plugin manager's publishPlugins method. The exception management policy here is very basic, stop the application if we cannot load the plugins.

    try {
            
        PluginLocation[] locations = new PluginLocation[plugins.length];
            
        for (int i = 0; i < plugins.length; i++) {
            locations[i] = StandardPluginLocation.create(plugins[i]);
        }

        pluginManager.publishPlugins(locations);
            
    } catch (Exception e) {
        throw new RuntimeException(e);
    }

Using the plugins

The addDynamicBookmarks method uses the plugins to extend the bookmark list.

We must start iterating over all the plugins that were loaded, we use the plugin manager's registry to get an iterator of the plugin descriptors.

    Iterator it = pluginManager.getRegistry().getPluginDescriptors().iterator();

We get an instance of every plugin with the help of the PluginManager, using its plugin ID provided by the PluginDescriptor object. As we expect any plugin to implement our BookmarkSection interface, we just cast the plugin instance.

    PluginDescriptor p = (PluginDescriptor) it.next();
	
    BookmarkSection section = (BookmarkSection) pluginManager.getPlugin(p.getId());

The plugin manager uses plugin specific class loaders so that the plugin bundle is used in isolation.

Finally, we use the BookmarkSection objects to retrieve additional bookmarks.

    TreeItem entry = new TreeItem(root, SWT.NULL);
    entry.setText(section.category());

    for (int i = 0; i < section.count(); i++) {
			
        TreeItem item = new TreeItem(entry, SWT.NULL);
			
        item.setText(section.getName(i));
        item.setData(section.getUrl(i));
			
    }

Click here to show the whole code with the first version of plugin support.

Creating a plugin

Let's write a plugin to add some URLs, that creates a Ruby section. We must create a class that implements the BookmarkSection. We name the class RubyURLs.

package org.alef1.bookmark;

public class RubyURLs implements BookmarkSection {

    public String category() {
        return "Ruby";
    }

    public int count() {
        return names.length;
    }

    public String getName(int index) {
        return names[index];
    }

    public String getUrl(int index) {
        return urls[index];
    }

    final static String[] names = {
        "Ruby Home", 
        "Try Ruby!", 
        "Rake",
        "Rails",
    };
    
    final static String[] urls = {
        "http://www.ruby-lang.org/", 
        "http://tryruby.hobix.com/", 
        "http://rubyforge.org/projects/rake/",
        "http://www.rubyonrails.org/",
    };

}

But as we want it to be a plugin, we need to extend the org.java.plugin.Plugin class and implement the doStart and doStop methods. The implementations are empty as we don't need to do anything. See following code for the new version of the RubyURLs class.

public class RubyURLs extends Plugin implements BookmarkSection {

    public String category() {
        return "Ruby";
    }

    public int count() {
        return names.length;
    }

    public String getName(int index) {
        return names[index];
    }

    public String getUrl(int index) {
        return urls[index];
    }

    final static String[] names = {
        "Ruby Home", 
        "Try Ruby!", 
        "Rake",
        "Rails",
    };
    
    final static String[] urls = {
        "http://www.ruby-lang.org/", 
        "http://tryruby.hobix.com/", 
        "http://rubyforge.org/projects/rake/",
        "http://www.rubyonrails.org/",
    };

    protected void doStart() throws Exception {
    }

    protected void doStop() throws Exception {
    }
    
}

To bundle the plugin, we must create a ZIP file. It must contain a plugin manifest file.

<?xml version="1.0" ?>
<!DOCTYPE plugin PUBLIC "-//JPF//Java Plug-in Manifest 1.0" "http://jpf.sourceforge.net/plugin_1_0.dtd">
<plugin id="org.alef1.bookmarks.ruby" version="1.0.0"
    class="org.alef1.bookmarks.RubyURLs">
    <runtime>
        <library id="ruby" path="/" type="code"/>
    </runtime>
</plugin>

The plugin manifest contains the plugin id, org.alef1.bookmarks.ruby, the plugin version and the plugin class, org.alef1.bookmarks.RubyURLs. We also declare the plugin code as starting at the zip root.

The plugin zip file, we name org.alef1.bookmarks.ruby-1.0.0.zip, contains two files: /org/alef1/bookmarks/RubyURLs.class and /plugin.xml.

Once the zip file is ready, copy it in the plugins directory of the application.

Before copying the plugin to the plugins directory, the application shows:

After copying the org.alef1.bookmarks. ruby-1.0.0.zip plugin the application show a new entry:

Improve plugin support

If an application supports different kind of plugins (like extending application menu, adding new import/export formats, adding more GUI views), a better approach is to use the extension points mecanism provided by the Java Plugin Framework.

With the extension point mechanism a plugin can declare one or more extension points, other plugins then can extend the point declared. In our case, we would declare (publish) a Section extenstion point and our previous plugin is going to create an Section extension.

A Core Plugin

We are first going to create a plugin that declares the extension point. We name this plugin org.alef1.bookmarks.core. Here is the plugin manifest part that declares the extension point.

<extension-point id="Section">
    <parameter-def id="class"/>
    <parameter-def id="name"/>
</extension-point>

The extension point id is Secion and requires two parameters: the implementing class and a name, for a given extension. A plugin that wants to extend the Secion extension point must provide a class and a name.

The core plugin class is an empty plugin.

public class PluginCore extends Plugin {

    protected void doStart() throws Exception {
    }

    protected void doStop() throws Exception {
    }

}

We can create a zip file to bundle the core plugin.

Click here to show the whole manifest file.

Using the plugin (revisited)

Loading the plugin does not change, the same loadPlugins method applies. The difference arises in the way we use the plugin, when adding the dynamic bookmarks.

In the addDynamicBookmarks, we start retrieving the Secion extension point declared in the core plugin.

    PluginDescriptor core = pluginManager.getRegistry().getPluginDescriptor("org.alef1.bookmarks.core");
            
    ExtensionPoint point = pluginManager.getRegistry().getExtensionPoint(core.getId(), "Section");

Then, we iterate over all extensions to get their plugin desciptors.

     for (Iterator it = point.getConnectedExtensions().iterator(); it.hasNext();) {
                
        Extension ext = (Extension) it.next();
                
        PluginDescriptor descr = ext.getDeclaringPluginDescriptor();
				
        pluginManager.activatePlugin(descr.getId());

For each plugin, that extends the Section point, we retrieve the class parameter to get the implementing class.

    ClassLoader classLoader = pluginManager.getPluginClassLoader(descr);
    Class pluginCls = classLoader.loadClass(ext.getParameter("class").valueAsString());

The remaining code is unchanged, from previous implementation, we create an instance and cast it to our BookmarkSection interface. Using the BookmarkSection we add new bookmarks.

Look at the complete method or see the whole code.

The plugin again

Our plugin only needs to declare that it extends the Section point, our previous class is declared as the one that extends the Section point.

<extension plugin-id="org.alef1.bookmarks.core" point-id="Section" id="rubySection">
    <parameter id="class" value="org.alef1.bookmarks.RubyURLs"/>
    <parameter id="name" value="Ruby"/>
</extension>

But we also need to declare that the plugin requires the core plugin.

<requires>
    <import plugin-id="org.alef1.bookmarks.core"/>
</requires>

Check the whole manifest here.

Our previous RubyURLs class does not need to extend the framework's Plugin class anymore.

package org.alef1.bookmarks;

import org.alef1.bookmark.BookmarkSection;

public class RubyURLs implements BookmarkSection {

    public String category() {
        return "Ruby";
    }

    public int count() {
        return names.length;
    }

    public String getName(int index) {
        return names[index];
    }

    public String getUrl(int index) {
        return urls[index];
    }

    final static String[] names = {
        "Ruby Home", 
        "Try Ruby!", 
        "Rake",
        "Rails",
    };
    
    final static String[] urls = {
        "http://www.ruby-lang.org/", 
        "http://tryruby.hobix.com/", 
        "http://rubyforge.org/projects/rake/",
        "http://www.rubyonrails.org/",
    };

}

Download

You can download the first version here and the second version here.

Links

The Java Plug-in Framework (JPF) Project

OSGi Alliance - The Dynamic Module System for Java

Eclipse Equinox plugin framework.

Colored syntax was generated with jEdit