Platonos PluginEngine Quickstart Guide

The Platonos PluginEngine is a small, fast, and powerful library for building pluggable applications in Java. This quickstart guide will jump right into it to get you going as quickly as possible. The PluginEngine has extensive yet concise javadocs that should be referred to along the way.

Contents
Hello World
Plugin Archives
Project Setup
Logging
Plugin Dependencies
Plugin Resolution
Plugin Start
Plugin Stop
Extension Points and Extensions
Optional Dependencies
Extension XML
Versioning

Hello World

Plugins add functionality by providing resources such as Java classes, XML documents, or images. While Plugins can be completely created, configured, and added to the PluginEngine programmatically, generally they are defined in a plugin.xml file. Here is an example:

plugin.xml (Hello World Plugin)
<plugin start="true">
   <uid>hello.world</uid>
   <name>Hello World</name>
   <lifecycleclass>example.helloworld.HelloWorldLifecycle</lifecycleclass>
</plugin>

This creates a Plugin which will be started as soon as possible by the PluginEngine. It has the unique ID "hello.world" and the name "Hello World". Finally, the class that manages the Plugin's lifecycle is "example.helloworld.HelloWorldLifecycle". A lifecycle class must extend PluginLifecycle. Here is the class used in this example:

example.helloworld.HelloWorldLifecycle
public class HelloWorldLifecycle extends PluginLifecycle {
   public void start () {
      log("Hello world!");
   }
   public void log (String message) {
      System.out.println(message);
   }
}

This lifecycle class has overidden the PluginLifecycle#start method, which is invoked when the Plugin is started by the PluginEngine.

Plugin Archives

Plugins are packaged very similiar to JAR files. The difference is that the root of the Plugin archive must contain the plugin.xml file. Also, any JAR files at the root of Plugin archive will be added to the Plugin's classpath and any native libraries at the root of the Plugin archive will be found if the Plugin's code invokes System.loadLibrary(String).

Coming back to the HelloWorld example, the Plugin would be packaged like this:

HelloWorld.jar
/plugin.xml
/example/helloworld/HelloWorldLifecycle.class

Project Setup

First, take a look at this simple "launcher" class that loads all Plugins in a specified directory:

Launcher.java
public class Launcher {
   static public void main (String[] args) {
      PluginEngine pluginEngine = new PluginEngine("app name");
      pluginEngine.loadPlugins("plugins_bin");
      pluginEngine.start();
   }
}

This loads all Plugins found in the "plugins_bin" directory into the PluginEngine. Before taking a look at exactly what happens when a Plugin is loaded, its important to understand how to set up your development environment.

Both a Plugin archive or an exploded directory structure (a directory containing an unzipped Plugin archive) may be loaded into the PluginEngine using either PluginEngine#loadPlugin or PluginEngine#loadPlugins. During development it is useful to take advantage of loading Plugins as exploded directory structures. This avoids having to zip the Plugins before each run of your application. The directory structure for the projects in this example would look similiar to this:

Example project structure
/src/example/Launcher.java
/lib/pluginengine.jar
/bin/example/Launcher.class
/plugins/helloworld/src/plugin.xml
/plugins/helloworld/src/example/helloworld/HelloWorldLifecycle.java
/plugins_bin/helloworld/plugin.xml
/plugins_bin/helloworld/example/helloworld/HelloWorldLifecycle.class

This format stores Plugin projects within the "/plugins/" directory and each Plugin project outputs to its own directory within "/plugins_bin/". Since the Launcher class will load all Plugins within "/plugins_bin/", this allows new Plugin projects to be added without modifying the Launcher class.

The secret to setting up Eclipse using this format can be seen here:

  1. On the project properties dialog, click "Browse..." to select a "Default output folder".
  2. On the folder selection dialog, you must click the root project node, then click the "Create New Folder" button.
  3. On the new folder dialog, click Advanced, check the box, and enter the path to the project's bin directory under "plugins_bin". The directory under "plugins_bin" must have been created before this step.
Logging

The logging for the PluginEngine can be integrated into an application's logging system using the ILogger interface. The default logging will use System.out and System.err. Here the Launcher class has been modified to use the java.util.logging.Logger:

Launcher.java
public class Launcher {
   static public void main (String[] args) {
      final Logger logger = Logger.getLogger("app name");
      ILogger engineLogger = new ILogger() {
         public void log (LoggerLevel level, String message, Throwable thr) {
            if (level == LoggerLevel.SEVERE) {
               logger.log(Level.SEVERE, message, thr);
            } else if (level == LoggerLevel.WARNING) {
               logger.log(Level.WARNING, message, thr);
            } else if (level == LoggerLevel.INFO) {
               logger.log(Level.INFO, message); // Ignore throwable.
            } else if (level == LoggerLevel.FINE) {
               logger.log(Level.FINE, message); // Ignore throwable.
            }
         }
      };
      PluginEngine pluginEngine = new PluginEngine("app name", engineLogger);
      pluginEngine.loadPlugins("plugins_bin");
      pluginEngine.start();
   }
}

Plugin Dependencies

Each Plugin uses its own class loader. This means a Plugin's classes cannot use classes from another Plugin. To use classes from another Plugin, a Dependency on that other Plugin must be defined. Here is a Plugin that only provides a class:

plugin.xml (Util Plugin)
<plugin>
   <uid>util</uid>
   <name>Util</name>
</plugin>

example.util.SystemOutLogger
public class SystemOutLogger {
   static public void message (String message) {
      System.out.println(message);
   }
}

Here the Hello World Plugin has been modified to use the SystemOutLogger class from the Util Plugin by adding a required Dependency on the Util Plugin:

plugin.xml (Hello World Plugin)
<plugin start="true">
   <uid>hello.world</uid>
   <name>Hello World</name>
   <lifecycleclass>example.helloworld.HelloWorldLifecycle</lifecycleclass>
   <dependencies>
      <dependency uid="util" />
   </dependencies>
</plugin>

example.helloworld.HelloWorldLifecycle
public class HelloWorldLifecycle extends PluginLifecycle {
   public void start () {
      log("Hello world!");
   }
   public void log (String message) {
      SystemOutLogger.message("Hello world!");
   }
}

A Plugin first looks in its own class loader, which will find classes in the Plugin archive and any embedded libraries in the root of the Plugin archive. If a class is not found there, it then looks in each Plugin that it has a Dependency on. Finally, it looks in the class loader that loaded the PluginEngine class. This lookup mechanism ensures that a Plugin can be distributed with its own versions of classes and libraries without conflicting with any classes or libraries provided by other Plugins or by the application.

Plugin Resolution

When Plugins are loaded into the PluginEngine, only their plugin.xml is parsed. None of their classes are instantiated. The PluginEngine automatically marks Plugins as "resolved" or "unresolved" (see Plugin#isResolved). All Plugins are unresolved until the PluginEngine starts. From that point on, Plugins who have all their required Dependencies satisfied are considered resolved. Unresolved Plugins cannot be seen by other Plugins.

Plugin Start

Only Plugins that have been resolved can be started. A Plugin that specifies start="true" within the "plugin" element in its plugin.xml will be started immediately after it has resolved (see Plugin#setStartWhenResolved). Otherwise it will be started when its Plugin#start method is invoked or the first time one of its classes are requested. This means that Plugins are started only when needed and as late as possible.

When a Plugin is started, its PluginLifecycle class is instantiated and the PluginLifecycle#initialize methid is invoked. If the Plugin was started by another Plugin requesting one of its classes, the class request will block until the initialize method returns. During the initialize method, the Plugin can access other Plugins but other Plugins can only access the Plugin in the same thread that invoked the initialize method. This means the initialize method can be used by the Plugin to do any setup that must happen before other classes are able to access the Plugin.

The PluginLifecycle#start method is invoked by a different thread as soon as possible after the initialize method returns. It is possible that other Plugins can make use of the Plugin after the initialize method returns but before the PluginLifecycle#start() method is invoked. This is why it is vital for only the initialize method to be used for setup that must happen before other Plugins can access the Plugin. The PluginLifecycle#start() method should only be used for the Plugin to begin its work.

Plugin Stop

When a resolved Plugin becomes unresolved, its PluginLifecycle#stop method is invoked. Within this method the Plugin must clean up all of its resources and references to other Plugins. A stopped Plugin can be started again in the usual way: class access or the Plugin#start method.

Extension Points and Extensions

If a resolved Plugin becomes unresolved, all Plugins that have a required Dependency on that Plugin also become unresolved. This is a significant drawback to using required Dependencies to communicate between Plugins. Coming back to the example from earlier, the Hello World Plugin will not be able to function unless the Util Plugin is loaded. Plugins that can be added and removed without a required Dependency are much more useful. To achieve this, Plugins can provide ExtensionPoints that other Plugins can attach to. ExtensionPoints can be provided in the plugin.xml:

plugin.xml (Hello World Plugin)
<plugin start="true">
   <uid>hello.world</uid>
   <name>Hello World</name>
   <lifecycleclass>example.helloworld.HelloWorldLifecycle</lifecycleclass>
   <extensionpoints>
      <extensionpoint name="loggers" interface="example.helloworld.GenericLogger" />
   </extensionpoints>
</plugin>

The Hello World Plugin now provides a single ExtensionPoint named "loggers" for other Plugins to attach to. Using the "interface" attribute, an ExtensionPoint can optionally provide a class that other Plugins must provide an implementation of:

example.helloworld.GenericLogger
public interface GenericLogger {
   public String message (String message);
}

Plugins attach to ExtensionPoints using Extensions. Here the Util Plugin has been modified to attach an Extension to the Hello World Plugin:

plugin.xml (Util Plugin)
<plugin>
   <uid>util</uid>
   <name>Util</name>
   <dependencies>
      <dependency uid="hello.world" />
   </dependencies>
   <extensions>
      <extension uid="hello.world" name="loggers" class="example.util.extensions.SystemOutLogger" />
   </extensions>
</plugin>

Because the Util Plugin now has a Dependency on the Hello World Plugin, it can implement the "example.helloworld.GenericLogger" interface that it must provide for the "loggers" ExtensionPoint:

example.util.extensions.SystemOutLogger
public class SystemOutLogger implements example.helloworld.GenericLogger {
   public String message (String message) {
      System.out.println(message);
   }
}

Here the Hello World Plugin has been modified to use the Extensions attached to its "loggers" ExtensionPoint:

example.helloworld.HelloWorldLifecycle
public class HelloWorldLifecycle extends PluginLifecycle {
   public void start () {
      log("Hello world!");
   }
   public void log (String message) {
      List extensions = getExtensionPoint("loggers").getExtensions();
      for (Iterator iter = extensions.iterator(); iter.hasNext(); ) {
         Extension extension = (Extension)iter.next();
         ILogger logger = (ILogger)extension.getExtensionInstance();
         logger.message(message);
      }
   }
}

Now the Util Plugin can be added and removed and the Hello World Plugin will remain functional.

When a Plugin is started it often makes use of the Extensions that are attached to it. After that point, if a new Plugin is loaded that attaches another Extension, that should be handled by overiding the PluginLifecycle#extensionResolved method. Here is a simple example of a Plugin that uses an ExtensionPoint to allow other Plugins to contribute menu items to a menu:

example.uiplugin.UIPluginLifecycle
public class UIPluginLifecycle extends PluginLifecycle {
   public void start () {
      List extensions = getExtensionPoint("menuitems").getExtensions();
      for (Iterator iter = extensions.iterator(); iter.hasNext(); ) {
         Extension extension = (Extension)iter.next();
         addMenuItem(extension);
      }
   }
   public void extensionResolved (ExtensionPoint extensionPoint, Extension extension) {
      if (extensionPoint.getName().equals("menuitems")) {
         addMenuItem(extension);
      }
   }
   private void addMenuItem (Extension extension) {
      // Add the menu item to the UI.
   }
   public void extensionUnresolved (ExtensionPoint extensionPoint, Extension extension) {
      if (extensionPoint.getName().equals("menuitems")) {
         removeMenuItem(extension);
      }
   }
   private void removeMenuItem (Extension extension) {
      // Remove the menu item to the UI.
   }
}

The PluginEngine could be used in an application that only loads Plugins before the PluginEngine is started. In this scenario, the extensionResolved and extensionUnresolved methods can be ignored because no Extensions will ever be attached after the Plugin has been started.

Optional Dependencies

Here the Util Plugin has been modified to attach Extensions to two different Plugins:

plugin.xml (Util Plugin)
<plugin>
   <uid>util</uid>
   <name>Util</name>
   <dependencies>
      <dependency uid="hello.world" />
      <dependency uid="another.plugin" />
   </dependencies>
   <extensions>
      <extension uid="hello.world" name="loggers" class="example.util.extensions.SystemOutLogger" />
      <extension uid="another.plugin" name="actions" class="example.util.extensions.SomeAction" />
   </extensions>
</plugin>

Because of the required Dependencies, both Plugins that the Extensions attach to must be loaded and resolved before the Util Plugin can be resolved. This is not desired since the Util Plugin is perfectly functional even when only one of its Extensions are attached. Adding the "optional" attribute to the Dependency produces the desired behavior:

plugin.xml (Util Plugin)
<plugin>
   <uid>util</uid>
   <name>Util</name>
   <dependencies>
      <dependency uid="hello.world" optional="true" />
      <dependency uid="another.plugin" optional="true" />
   </dependencies>
   <extensions>
      <extension uid="hello.world" name="loggers" class="example.util.extensions.SystemOutLogger" />
      <extension uid="another.plugin" name="actions" class="example.util.extensions.SomeAction" />
   </extensions>
</plugin>

An optional Dependency will allow the Plugin to resolve even if the Dependency is not satisfied. Now the Util Plugin will resolve even if neither of the Plugins it attaches Extensions to are resolved. There is a shortcut to achieve the same behavior:

plugin.xml (Util Plugin)
<plugin>
   <uid>util</uid>
   <name>Util</name>
   <extensions>
      <extension uid="hello.world" name="loggers" class="example.util.extensions.SystemOutLogger" />
      <extension uid="another.plugin" name="actions" class="example.util.extensions.SomeAction" />
   </extensions>
</plugin>

This works identically to defining optional Dependencies on "hello.world" and "another.plugin". If an Extension is attached to a Plugin and no Dependency is explicitly defined for that Plugin, then an optional Dependency on that Plugin is used.

Extension XML

The "extension" element in the plugin.xml file can contain any extra attributes or XML for use by the ExtensionPoint to which they attach:

plugin.xml (Util Plugin)
<plugin>
   <uid>util</uid>
   <name>Util</name>
   <extensions>
      <extension uid="hello.world" name="loggers" class="example.util.extensions.SystemOutLogger" logLevel="3" >
         <extraNode1 someAttribute="true">
            </extraNode2>
         </extraNode1>
      </extension>
   </extensions>
</plugin>

The ExtensionPoint can make use of the Extensions' XML through the Extension#getExtensionXmlNode method:

example.helloworld.HelloWorldLifecycle
public class HelloWorldLifecycle extends PluginLifecycle {
   public void start () {
      log("Hello world!");
   }
   public void log (String message) {
      List extensions = getExtensionPoint("loggers").getExtensions();
      for (Iterator iter = extensions.iterator(); iter.hasNext(); ) {
         Extension extension = (Extension)iter.next();
         String extraAttribute = extension.getExtensionXmlNode().getAttribute("extraAttribute");
         if ("3".equals(extraAttribute)) {
            ILogger logger = (ILogger)extension.getExtensionInstance();
            logger.message(message);
         }
      }
   }
}

An ExtensionPoint can specify an XML Schema that will be used to validate the Extension's XML:

plugin.xml (Hello World Plugin)
<plugin start="true">
   <uid>hello.world</uid>
   <name>Hello World</name>
   <lifecycleclass>example.helloworld.HelloWorldLifecycle</lifecycleclass>
   <extensionpoints>
      <extensionpoint name="loggers" interface="example.helloworld.GenericLogger" schema="logger.xsd" />
   </extensionpoints>
</plugin>

The schema file path is relative to the root of the Plugin archive. If an Extension's XML does not match the schema, the Extension will not attach to the ExtensionPoint.

Validation is not supported for Java 1.4 unless the JAXP 1.2+ JARs are on the classpath.

Versioning

Plugins can specify a version in their plugin.xml file. A PluginVersion is specified in the format "release.update.patch build". "release", "update", and "patch" are integers. "build" can be any String. "update" and "patch" are optional. Here the Hello World Plugin has a version specified:

plugin.xml (Hello World Plugin)
<plugin start="true">
   <uid>hello.world</uid>
   <name>Hello World</name>
   <version>1.4.10 beta 3</version>
   <lifecycleclass>example.helloworld.HelloWorldLifecycle</lifecycleclass>
   <extensionpoints>
      <extensionpoint name="loggers" interface="example.helloworld.GenericLogger" schema="logger.xsd" />
   </extensionpoints>
</plugin>

Dependencies can specify a version or range of versions required for the Plugin to satisfy the Dependency. Here the Util Plugin has been modified to only resolve to the Hello World Plugin with the version "1.4":

plugin.xml (Util Plugin)
<plugin>
   <uid>util</uid>
   <name>Util</name>
   <dependencies>
      <dependency uid="hello.world" version="1.4" optional="true" />
   </dependencies>
   <extensions>
      <extension uid="hello.world" name="loggers" class="example.util.extensions.SystemOutLogger" />
   </extensions>
</plugin>

If the "update" or "patch" versions are omitted from the "dependency" element they will be ignored when determining if the Plugin satisfies the Dependency.

Versions can also be specifed with a range:

plugin.xml (Util Plugin)
<plugin>
   <uid>util</uid>
   <name>Util</name>
   <dependencies>
      <dependency uid="hello.world" minversion="1" maxversion="1.6.2" optional="true" />
   </dependencies>
   <extensions>
      <extension uid="hello.world" name="loggers" class="example.util.extensions.SystemOutLogger" />
   </extensions>
</plugin>

Either of the "minversion" or "maxversion" attributes can be omitted to have no minimum or no maximum to the range.