Abstract

ZoneTrak was designed to be easily expandable.  In result, one of its main features is the ability of loading plugins during startup. The following article explains how this feature is implemented, how it works and what problems had to be solved. This article is intended for people who want to take a look at internal activities of ZoneTrak and people who want to learn how applications can be made plugin-capable. This article will contain Java source code. Hence, previous knowledge of Java is assumed.

Some theory before

Before we start, we should spend some time to clarify what we consider to be a plugin. As in general known, plugins extend an applications functionality. They are often implemented with scripting languages parsed by the main application or using compiled libraries with a specified entry point called by the application. We will focus on the second one below.  ZoneTrak uses regular jar containers and Java as the plugin language.

One of the most difficult tasks is to determine the entry point, e.g. the part of the plugin which initializes the plugin to operate and communicate with the application. There are a lot of possibilities to achieve this. One approach could be using a method called main, but this could lead to confusion because Java reserves this method as an application entry point. Another approach  can be realized by using interfaces, garuanteeing compatibility with the application. We decided to use this approach which causes another problem: Where should the interface be? This could be done by adding the main application’s to the plugin’s build path, but this seems to be an unclean solution. We decided to put used interfaces in an external jar which is used by ZoneTrak itself and the plugins. To sum up, following figure visualizes the relations between plugins, interfaces and the main application.

ZoneTrak Plugin Class Dependencies

ZoneTrak Plugin Class Dependencies

Plugin Management Implementation

In the previous section we described some basics which have to be clear before we could start. We introduced the interface IPlugin and insinuated the method initPlugin() which is used as entry point. Java itself uses the main() method which has an array of strings as parameters containing command-line arguments passed to the application. Because of our main application that initializes and starts the plugin, we don’t have command-line arguments. But there are some variables we might want to use in our plugin, e.g. an object which is able to inject commands into the main application. These objects seem to be more complex and different. It might also be possible that we will need more variables passed to the plugin. Changing the entry point’s specification could lead to incompatibility problems. The best way to circumvent this is by introducing supplementary methods setting the needed variables.

The next step is to analyze what a plugin needs to know and needs to be able to. And this question is already the answer: It needs to know information and it must be able to communicate with the main application. Due to reliability reasons we decided to use the publish/subscribe communication paradigm1. This forces us to introduce an event manager accepting subscriptions and calling subscribers. A subscriber class needs to be informed when an event occured, so it needs a method that can be called in case the class subscribed to this event. To disolve confusion, we demonstrate how publish/subscribe works:

Simplified Publish/Subscribe Communication Paradigm

Simplified Publish/Subscribe Communication Paradigm

We usually don’t want a plugin to know much about the main application’s implementation, so we will use interfaces:

package zonetrak.ifaces;
 
import zonetrak.ifaces.ISubscriber;
 
public interface IEventManager
{
	/**
	 * Subscribes a subscriber to a specific event.
	 *
	 * @param type Identifies the event.
	 * @param subscriber The subscriber which will be called on any occurance of an event.
	 */
	public abstract void subscribe(String type, ISubscriber subscriber);
 
	/**
	 * Unsubscribes an subscriber from a specific event.
	 *
	 * @param type Identifies the event.
	 * @param subscriber The subscriber which will be called on any occurance of an event.
	 */
	public abstract void unsubscribe(String type, ISubscriber subscriber);
 
	/**
	 * Called whenever an event occurs.
	 *
	 * @param type Identifies the event.
	 * @param args Event data of any kind.
	 */
	public abstract void fireEvent(String type, Object... args);
}

The javadoc comments should be self-explanatory. Noticeable is the string type, which is used as an unique identifier for events, and of course the variable number of arguments which can be passed through fireEvent. The IEventManager interface uses another interface well-known since the introduction of the publish/subscribe communication paradigm. The interface ISubscriber is declared as follows:

package zonetrak.ifaces;
 
public interface ISubscriber
{
	void eventFired(String type, Object... args);
}

The source documentation has been left out because it is already explained in the IEventManager interface.

In an application’s life there is more than just events, even with the event-driven programming paradigm. The plugin needs to know information, e.g. in ZoneTrak’s case there are zones and entities the plugin might want to know about. Therefore, we introduced another manager explicitly for data exchange which is defined in the following way:

package zonetrak.ifaces;
 
public interface IDataManager
{
	/**
	 * Gets data set by setData.
	 *
	 * @see zonetrak.ifaces.IDataManager.setData()
	 * @param name - Object identifier
	 * @return Either an object identified by name or (if not available) null.
	 */
	public abstract Object getData(String name);
 
	/**
	 * Sets data.
	 *
	 * @param name - Object identifier
	 * @param data - An object identified by name.
	 */
	public abstract void setData(String name, Object data);
 
	/**
	 * Lists names for data which has already been set.
	 *
	 * @return A String array containing names.
	 */
	public abstract String[] getAvailableData();
}

The implementation of the interface works as simple as it’s interface is defined: Any kind of data can be assigned to the data manager as long as it is identified by an unique name. The plugin can browse through set data using the getAvailableData() method or simply access data by name.

Last but not least, we need a possibility to set the data manager, the event manager and initialize the plugin. This is guaranteed by following interface which should be self-explanatory, too. The URLClassLoader is used to access resources within the plugin’s archive:

package zonetrak.ifaces;
 
import java.net.URLClassLoader;
 
public interface IPlugin
{
	/**
	 * Sets the event manager which allows to (un-)subscribe
	 * to events raised by the main application.
	 *
	 * @param eventManager
	 */
	public void setEventManager(IEventManager eventManager);
 
	/**
	 * Sets the data manager which allows to access
	 * data managed by the main application.
	 *
	 * @param dataManager
	 */
	public void setDataManager(IDataManager dataManager);
 
	/**
	 * Sets the URL class loader which can be used
	 * to load plugin resources.
	 *
	 * @param urlClassLoader
	 */
	public void setURLClassLoader(URLClassLoader urlClassLoader);
 
	/**
	 * Is called after the event and data managers were
	 * set. The plugin logic should start here.
	 */
	public void initPlugin();
}

Again, to decrease complexity, we sum all interfaces up into a small class diagram:

Plugin Architecture

Plugin Architecture

Last but not least, there is the PluginLoader class which initializes the plugin import process:

package zonetrak.plugins;
 
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
 
import zonetrak.common.AbstractPlugin;
import zonetrak.events.EventManager;
import zonetrak.ifaces.IPlugin;
 
public class PluginLoader
{
	public PluginLoader()
	{
	}
 
	public void execute()
	{
		final File pluginDir = new File("plugins/");
 
		final File[] pluginJars = pluginDir.listFiles(new PluginFileFilter());
 
		for (final File pluginJar : pluginJars)
		{
			this.loadPlugin(pluginJar);
		}
	}
 
	public void loadPlugin(File jarFile)
	{
		try
		{
			final JarFile jar = new JarFile(jarFile);
			final URLClassLoader loader = URLClassLoader.newInstance(new URL[] { jarFile.toURI().toURL() });
 
			Class<?> startClass = null;
 
			for (Enumeration<JarEntry> entries = jar.entries(); entries.hasMoreElements();)
			{
				JarEntry entry = entries.nextElement();
				String name = entry.getName();
 
				if (!name.endsWith(".class"))
				{
					continue;
				}
 
				String className = name.replaceAll("/", ".");
 
				className = className.substring(0, className.length() - ".class".length());
 
				Class<?> possibleStartClass = loader.loadClass(className);
				Class<?>[] interfaces = possibleStartClass.getInterfaces();
 
				if (possibleStartClass.getSuperclass().equals(AbstractPlugin.class))
				{
					startClass = possibleStartClass;
				}
				else
				{
					for (Class<?> iface : interfaces)
					{
						if (iface.equals(IPlugin.class))
						{
							startClass = possibleStartClass;
							break;
						}
					}
				}
			}
 
			if (startClass == null)
			{
				System.err.println("Could not find plugin start class for " + jarFile.getAbsolutePath() + ".");
			}
			else
			{
				final IPlugin plugin = (IPlugin) startClass.newInstance();
				plugin.setEventManager(EventManager.getInstance());
				plugin.setDataManager(DataManager.getInstance());
				plugin.setURLClassLoader(loader);
				plugin.initPlugin();
			}
		}
		catch (final Exception e)
		{
			e.printStackTrace();
		}
	}
}

This class is also using an AbstractPlugin class which is an abstract class implementing the interface IPlugin with abstract methods. AbstractPlugin has been introduced for developer’s convenience.

References

  1. P.T. Eugster, P. Felber, R. Guerraoui, and A.-M. Kermarrec, “The many faces of publish/subscribe,” Tech. Rep. DSC ID:2000104, EPFL, January
    2001.
    []

Comments are closed.