You are here: Downloads / Fourty Pound Sparrow / Extending the FPS
Thursday, February 23, 2017

Introducing Extension points and Plugins

written by Wolfgang Lohmann (Meister of Evermore).

Here, I will make a short tutorial, how you can create very simple plugins for the FourtyPoundSparrow (FPS for short).

Then, I show, how the the necessary Extension point (EP) has been created in case you have access to the sources of FourtyPoundSparrow. Later, when we have some Tool EPs, I will put it into another Plugin-test plugin.

In some areas, it is common to provide a vanilla-version of the program to see the most simple representation of the problem. I will use a similar metaphor:

  • We have some plain Ice in the FPS, which can be extended by certain Flavours.
  • The place, where the Flavour can be added, is the (ice) extension point.

A Vanilla plugin

The creation using Eclipse can be found at the end of the text, including the build.xml.

The plugin will extend the Ice by some Flavour at the IceExtensionPoint.
Each plugin providing a new flavour has to implement the Flavour interface:

  • (note: could have been any other suitable class of the FPS, decided by EP designer)
  • It is defined in package net.evermore.mud.fourtypoundsparrow.ice.
  • The interface is necessary for code at the Flavour extension point.
  • It provides a method createTaste() that will be called at some point from the code to use the flavour extension

Our example is a very simple (thats why Vainilla-) flavour extension. Its only task is to respond to loading with a message.

package net.evermore.mud.fourtypoundsparrow.plugin.vanilla; import javax.swing.JOptionPane; // for a simple dialog // the extension point will use functionality as defined here, // i.e. createTaste() import net.evermore.mud.fourtypoundsparrow.ice.Flavour; public class Vanilla implements Flavour { @Override public void createTaste() { // Show a simple dialog so we see the plugin is active JOptionPane.showMessageDialog(null, "Vanilla flavour is added to the ice."); } }

Each Plugin comes with a configuration file plugin.xml that tells the FourtyPoundSparrow and its extension points:

  • the id of the plugin (Vanilla in our example)
  • the id of the extension point addressed (IceExtensionPoint for us)
  • a list of parameters as required by the extension point, in our case:
    • name: containing a name for the plugin data base in FPS, for example
    • class: the fully qualified name of the class of the plugin to be loaded (Note, your plugin could consist of many classes. Note also, other extension points can designed to need more and other kind of parameters.)
  • in our example, the plugin.xml looks as follows

<?xml version="1.0" ?> <!DOCTYPE plugin PUBLIC "-//JPF//Java Plug-in Manifest 0.4" "http://jpf.sourceforge.net/plugin_0_4.dtd"> <plugin id="Vanilla" version="0.0.1" vendor="Meister" > <requires> <import plugin-id="net.evermore.mud.fourtypoundsparrow"/> </requires> <runtime> <library id="core" path="lib/Vanilla.jar" type="code"/> </runtime> <extension plugin-id = "net.evermore.mud.fourtypoundsparrow" point-id = "IceExtensionPoint" id = "Vanilla"> <parameter id = "class" value = "net.evermore.mud.fourtypoundsparrow.plugin.vanilla.Vanilla"/> <parameter id = "name" value = "Vanilla Flavour Plugin"/> </extension> </plugin>

If everything is ok, you should find FourtyPoundSparrow/package/plugins/Vanilla/lib/Vanilla.jar.
Pay attention that every appearance of the names and paths are written correctly. Everything is case sensitive. 
The plugin id does not need to be identical to the name of the plugin class.

Now, you can add other flavours, e.g. Caramel, to get a feeling for a plugin. You can simply copy the build.xml/ plugin.xml, adapt them and implement the <Pluginname>.java similar to the Vanilla.java. Use the createTaste() to implement your plugin behaviour, which will be executed right at the start of the FPS.

The Flavour interface

The extension point is simply a special point in a program, where we instantiate other subclasses we do not know yet. Therefore, we instantiate it from a class of our choice and call adhering methods. A plugin that is willing to contribute at this point has to subclass that class, or, as in our case, implement at least the necessary interface Flavour which looks as follows

package net.evermore.mud.fourtypoundsparrow.ice; // new flavour have to implement it or the Ice EP won't instantiate it // after successful load, the method 'createTaste' is called public interface Flavour void createTaste(); }

You can implement the createTaste() method in your plugin to fill it with any action you want to be performed by the EP. The more parameters the interface or the class to be extend provides, the more you can do in your plugin.

The Ice Extension Point interface

Here, interface is used as the open information provided by the plugin user to the plugin writer. To connect our Vanilla plugin with the IceExtensionPoint we have to know its point-id and the parameters we have to provide in our Vanilla/plugin.xml. This information is contained in FPSCore/plugin.xml, see the following extract:

<?xml version="1.0" ?> <!DOCTYPE plugin PUBLIC "-//JPF//Java Plug-in Manifest 0.4" "http://jpf.sourceforge.net/plugin_0_4.dtd"> <plugin id="net.evermore.mud.fourtypoundsparrow" version="1.3" class="net.evermore.mud.fourtypoundsparrow.FourtyPoundSparrow"> <runtime> <library id="core" path="classes/" type="code"> <!-- trigger action types --> <export prefix="net.evermore.mud.fourtypoundsparrow.controller.trigger.action.simple.SimpleAction"/> ... <!-- all ice components --> <export prefix="net.evermore.mud.fourtypoundsparrow.ice.Flavour"/> </library> <library id="commons-net" path="lib/commons-net-2.0.0-SNAPSHOT.jar" type="code"/> <library id="tar" path="lib/tar.jar" type="code"/> <library id="core-resources" path="resources/" type="resources"/> </runtime> <extension-point id="Simple-Action"> ... </extension-point> <extension-point id = "IceExtensionPoint"> <parameter-def id = "name"> </parameter-def> <parameter-def id = "class"> </parameter-def> </extension-point> </plugin>

  • It is part of the plugin net.evermore.mud.fourtypoundsparrow
  • For the runtime, a list of libraries is used and exported, with identifier, a path and type.
  • Part of the export is the interface net.evermore.mud.fourtypoundsparrow.ice.Flavour, which is the interface our plugins have to implement (could be a real class too, as the SimpleAction some lines earlier)
  • Finally, a list of extension-points are published, together with their id and the requred parameter definitions. For our Vanill plugin, we are interested in the IceExtensionPoint that reqires the parameters name and class. What these parameters are for we know from the description of the EP in the program, in the documentation or we have to guess it...
  • The extension-point id has to match with the point-id used in our Vanilla/plugin.xml.

An Ice Extension Point for Flavours for the FourtyPoundSparrow

For the purpose of this example, we provide a method to be placed into FourtyPoundSparrow.java, say readFlavourPlugins, and call it from within startUpClient so our test plugins are called before the main client. Inside, we provide a simple extension point, our IceExtensionPoint, which gets extended by the Flavour plugins, if present. The nearly most simple (hence Vanilla-) plugin from above is one of them.

The FlavourExtensionPoint requires the name of the plugin and the name of the class to be loaded. If loading is successful, we only call createTaste() in the plugin. This method exists, as we demand every plugin to implement the interface Flavour.

public void readFlavourPlugins() { PluginManager manager = getManager(); // (1) ExtensionPoint iceExtensionPoint = // (2) manager.getRegistry().getExtensionPoint( getDescriptor().getId(), "IceExtensionPoint"); for ( Iterator it = iceExtensionPoint.getConnectedExtensions().iterator(); it.hasNext(); // emtpy Operation -- // ) { Extension flavourExtension = (Extension) it.next(); // (4) // (5) Parameter classParameter = flavourExtension.getParameter("class"); Parameter nameParameter = flavourExtension.getParameter("name"); try { manager.activatePlugin( // (6) flavourExtension.getDeclaringPluginDescriptor().getId()); ClassLoader classLoader = // (7) manager.getPluginClassLoader( flavourExtension.getDeclaringPluginDescriptor() ); Class flavourPluginClass = // (8) classLoader.loadClass(classParameter.valueAsString()); // (9) Flavour flavour = (Flavour) flavourPluginClass.newInstance(); flavour.createTaste(); } catch(Throwable t) { showAnIceErrorMessage(t); // presentation later on } } }

  1. Get the manager (provided by the JPF framework).
  2. Get an EP iceExtensionPoint, ask the manager for all information he has found about plugins which refer to the EP with the id IceExtensionPoint. This id is also exported by our applications plugin.xml, see later.
    Iterate through all found plugins, which have IceExtensionPoint as their desired extension point-id.
  3. The extension contains the description of the plugin as provided in the plugins plugin.html, which in our case are simply its parameters like the name and class definition location. Depending on how complicated the EP is designed, it could contain much more information and address many more classes.
  4. Read out what is assigned to parameter class in the plugin.xml. Here, we expect the location of the class definition of the plugin we have to load. classParameter.valueAsString should return something like net.evermore.mud.fourtypoundsparrow.plugin.vanilla.Vanilla. Note: Case sensitive as given in the plugin implementation and in plugin.xml. Other parameters are read out similarly, e.g., name which in our case should contain Vanilla.
  5. Tell manager to activate the plugin that defines the extension.
  6. Create a class loader.
  7. Create a class using the class loader by loading the class code of the plugin. Use the information from the class- parameter from the plugin file as a string (points to the class-file with the code of the starting class.
  8. Instantiate the plugin class, here the Vanilla class, which extended the Flavour interface. Now, a class of the plugin is running in our programm.
  9. Call the intialisation or execution method of the plugin. That could be a factory to construct further classes or any other magic stuff. Here, we simply call the createTaste() method. Our Vanilla plugin will react with a simple message.

A method to show an error

The description of the Ice EP we used a method showAnIceErrorMessage in the catch block. A possible implementation is given here (copied from SendCommand EP, A.Arnold).

private void showAnIceErrorMessage(Throwable t) {
   JFrame f = new JFrame();
   f.setSize(200,200);
   f.setVisible(true);
   f.setLayout(new BorderLayout());
   f.add(new JLabel(t.toString()), BorderLayout.NORTH);
   JScrollPane scrollPane = new JScrollPane();
   f.add(scrollPane, BorderLayout.CENTER);
   StringBuffer sb = new StringBuffer();
   String nl = System.getProperty("line.separator");
   Throwable err = t;

   while (err != null) {
       if (err != t) {
           sb.append(nl).append("Caused by " + err).append(nl).append(nl);
      }
      StackTraceElement[] stackTrace = err.getStackTrace();
      for (int i = 0; i < stackTrace.length; i++) {
          sb.append(stackTrace[i].toString()).append(nl);
      }
      err = err.getCause();
   }

   JTextArea textArea = new JTextArea(sb.toString());
   textArea.setBackground(java.awt.SystemColor.control);
   textArea.setEditable(false);

   scrollPane.setViewportView(textArea);
   textArea.setCaretPosition(0);
}