Plug-Ins & Java
by Michael Pilone

Listing One

package org.ddj.framework;

/* The plugin interface will be implemented by all plugins and
 * server as the common interface between a plugin and the application. */
public interface Plugin
{
  /** Start method is called by the application when the plugin is loaded. */
  public void start(PluginContext context);
  /** Stop method is called by the application when the plugin is unloaded.*/
  public void stop();
}

Listing Two

package org.ddj.plugin;
import org.ddj.app.SimpleContext;
import org.ddj.framework.*;
/* Simple plugin that will print to standard out when
 * started and stopped by the application. */
public class HelloWorldPlugin implements Plugin
{
    /** Called when application loads this plugin. Prints to stdout. */
    public void start(PluginContext context) {
        // At this point you can cast the context to something
        // that the application has defined and use data from the application. 
        SimpleContext simpleContext = (SimpleContext) context;
        System.out.println("Hello World: plugin started.");
        System.out.println("\tThe answer is " + simpleContext.getAnswer());
    }
    /** Called when application unloads this plugin. Prints to stdout. */
    public void stop() {
        System.out.println("Goodbye World: plugin stopped.");
    }
}

Listing Three

Manifest-Version: 1.0
Plugin-Class: org.ddj.plugin.HelloWorldPlugin

Listing Four

// List the directory using a filter to only accept the JAR files.
return dir.listFiles(new JarFilter());
/** File filter that accepts all files ending with .JAR. This filter
 * is case insensitive.
 */
private static class JarFilter implements FileFilter {
  /** The extension that this filter will search for. */
  private static final String JAR_EXTENSION = ".JAR";
  /** Accepts any file ending in .jar. The case of the filename is ignored. */
  public boolean accept(File f)
  {
    // Perform a case insensitive check.
    return f.toString().toUpperCase().endsWith(JAR_EXTENSION);
  }
}

Listing Five

public static Plugin createPlugin(String className, File f)
        throws RuntimeException {
  try
  {
    // Create a URL class loader for the given JAR
    URL[] urls = new URL[] { f.toURL() };
    URLClassLoader pluginClassLoader = new URLClassLoader(urls);
    // Ask the class loader to load the class
    Class pluginClass = pluginClassLoader.loadClass(className);
    // Once we have the class, we can do some checks on it to ensure
    // that it is a valid implementation of a plugin.
    int modifiers = pluginClass.getModifiers();
    if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) ||
       (!Plugin.class.isAssignableFrom(pluginClass))) {
      throw new RuntimeException("The plugin class is not compatible.");
    }
    // Now ask the class to create a new instance
    Object pluginInstance = pluginClass.newInstance();
    // Since the framework required the plugin to implement the plugin
    // interface, it can be safely cast
    Plugin plugin = (Plugin)pluginInstance;
    return plugin;
    }
  catch (MalformedURLException e)
  {
    throw new RuntimeException("Error in filename " + f.toString(), e);
  }
  catch (ClassNotFoundException e)
  {
    throw new RuntimeException("Class not found " + className, e);
  }
  catch (InstantiationException e)
  {
    throw new RuntimeException("Error instantiating " + className, e);
  }
  catch (IllegalAccessException e)
  {
    throw new RuntimeException("Illegal access to " + className, e);
  }
}
public static String extractPluginClassName(File f) throws IOException {    
  JarFile jarFile = new JarFile(f);     
  try 
  {
    // Extract the entire Manifest
    Manifest manifest = jarFile.getManifest();
    // Get the attributes from the Manifest
    Attributes attribs = manifest.getMainAttributes();
    // Get the class name
    return attribs.getValue(PLUGIN_CLASS_KEY);
  }
  finally
  {
    // Be sure that we always close the JAR file
    jarFile.close();
  }
}


Listing Six

package org.ddj.framework;

import java.io.*;
import java.lang.reflect.Modifier;
import java.net.*;
import java.util.*;

/** Provides the functionality for plugin management such as
 * discovery, initialization, and destruction. */
public class PluginService {
  /** The list of plugins this service is responsible for. */
  private List mPlugins;
  
  /** Constructs PluginService which will immediately discover and start all
   * plugins in the given directory.
   * @param workingDir the directory to search for plugins
   * @param context the context to give to all plugins */
  public PluginService(File workingDir, PluginContext context) {
     // Discover all of the jar files
     File[] jars = discoverPlugins(workingDir);
     // Instantiate each plugin
     mPlugins = new LinkedList();
     for (int i = 0; i < jars.length; i++) {
       try {
         File file = jars[i];
     String className = extractPluginClassName(file);
     Plugin plugin = createPlugin(className, file);
     // Start the plugin
     plugin.start(context);
     // Add it to the list for future reference
     mPlugins.add(plugin);
      }
      catch (Exception ex) {
        System.err.println("Error loading plugin: " + ex.getMessage());
    ex.printStackTrace();
      }
    }
  }
  /* Disposes of this service and unloads all active plugins. */
  public void dispose() {
    // Iterate the plugins and stop them
    for (Iterator iter = mPlugins.iterator(); iter.hasNext();) {
      Plugin plugin = (Plugin) iter.next();
      plugin.stop();
    }
    mPlugins.clear();
    mPlugins = null;
  }
  /** Discovers all JAR files in the given directory.
   * @param dir the directory to search
   * @return an array of all jar files in the given directory */
  public static File[] discoverPlugins(File dir) {
      // Refer to Listing 4 for implementation.
  }
  /** The key of the plugin class name defined in the manifest file. */
  private static final String PLUGIN_CLASS_KEY = "Plugin-Class";
  /** Instantiates the plugin with the given class name found in the
   * jar referenced by the given file.
   * @param className the name of the plugin class to instantiate
   * @param f the JAR file containing the given class
   * @return the plugin instance
   * @throws RuntimeException if there is an exception while trying
   * to instantiate the requested class */
  public static Plugin createPlugin(String className, File f)
    throws RuntimeException {
    // Refer to Listing 5 for implementation.   
  }
  /** Extracts the plugin class name from the manifest file of the
   * JAR file referenced by the given file.
   * @param f the file reference to a JAR
   * @return name of the plugin class as it is defined in the manifest file
   * @throws IOException if there is an error reading from the JAR file */
  public static String extractPluginClassName(File f)
    throws IOException {    
    // Refer to Listing 5 for implementation
  }
}
package org.ddj.app;
import java.io.File;
import org.ddj.framework.*;

/* Main class of the application. This application simply loads all of 
 * plugins found in a given directory and then immediately shuts them
 * down. It is not very exciting, but it gives a good example of how
 * simple a plugin loading application can be when using a good framework. */
public class Main {
  /** Constructs the main class. Upon construction all plugins will be
   * loaded, started, then stopped.
   * @param workingDir the directory containing all plugins */
  public Main(File workingDir) {
    // The context to share with the plugin. This is a very simple context
    // that does not contain much data. 
    PluginContext context = new SimpleContext(42);
    // Construct the plugin service, which will load the plugins.
    PluginService plugService = new PluginService(workingDir, context);
    // All of the plugins are now loaded and started. At this point
    // the application would do whatever it normally does.
    System.out.println("Application: now doing some processing.");
    // Assuming that the application is finished, we now shut everything down.
    plugService.dispose();
  }
   /** Main method of the application. The first argument must be
   * the path to the plugins.
   * @param args the command-line arguments */
  public static void main(String[] args) {
    // Check for the path.
    if (args.length != 1) {
      System.out.println("Usage: ");
      System.out.println("\torg.ddj.Main <plugin_directory>");
      System.exit(1);
    }
    // Start the application.
    new Main(new File(args[0]));
  }
}


5


