Java tutorial
/** * $RCSfile$ * $Revision: 3001 $ * $Date: 2005-10-31 05:39:25 -0300 (Mon, 31 Oct 2005) $ * * Copyright (C) 2004-2008 B5Chat Community. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.b5chat.crossfire.core.plugin; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArraySet; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; import java.util.jar.Pack200; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.b5chat.crossfire.core.server.Version; import org.b5chat.crossfire.core.util.LocaleUtils; import org.b5chat.crossfire.database.DbConnectionManager; import org.b5chat.crossfire.plugin.admin.AdminConsole; import org.b5chat.crossfire.xmpp.server.XmppServer; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Loads and manages plugins. The <tt>plugins</tt> directory is monitored for any * new plugins, and they are dynamically loaded.<p/> * * An instance of this class can be obtained using:<p/> * * <tt>XmppServer.getInstance().getPluginManager()</tt> * * @author Matt Tucker * @see IPlugin * @see org.b5chat.crossfire.xmpp.server.XmppServer#getPluginManager() */ public class PluginManager { private static final Logger Log = LoggerFactory.getLogger(PluginManager.class); private File pluginDirectory; private Map<String, IPlugin> plugins; private Map<IPlugin, PluginClassLoader> classloaders; private Map<IPlugin, File> pluginDirs; /** * Keep track of plugin names and their unzipped files. This list is updated when plugin * is exploded and not when is loaded. */ private Map<String, File> pluginFiles; private ScheduledExecutorService executor = null; private Map<IPlugin, PluginDevEnvironment> pluginDevelopment; private Map<IPlugin, List<String>> parentPluginMap; private Map<IPlugin, String> childPluginMap; private Set<String> devPlugins; private PluginMonitor pluginMonitor; private Set<IPluginListener> pluginListeners = new CopyOnWriteArraySet<IPluginListener>(); private Set<IPluginManagerListener> pluginManagerListeners = new CopyOnWriteArraySet<IPluginManagerListener>(); /** * Constructs a new plugin manager. * * @param pluginDir the plugin directory. */ public PluginManager(File pluginDir) { this.pluginDirectory = pluginDir; plugins = new ConcurrentHashMap<String, IPlugin>(); pluginDirs = new HashMap<IPlugin, File>(); pluginFiles = new HashMap<String, File>(); classloaders = new HashMap<IPlugin, PluginClassLoader>(); pluginDevelopment = new HashMap<IPlugin, PluginDevEnvironment>(); parentPluginMap = new HashMap<IPlugin, List<String>>(); childPluginMap = new HashMap<IPlugin, String>(); devPlugins = new HashSet<String>(); pluginMonitor = new PluginMonitor(); } /** * Starts plugins and the plugin monitoring service. */ public void start() { executor = new ScheduledThreadPoolExecutor(1); // See if we're in development mode. If so, check for new plugins once every 5 seconds. // Otherwise, default to every 20 seconds. if (Boolean.getBoolean("developmentMode")) { executor.scheduleWithFixedDelay(pluginMonitor, 0, 5, TimeUnit.SECONDS); } else { executor.scheduleWithFixedDelay(pluginMonitor, 0, 20, TimeUnit.SECONDS); } } /** * Shuts down all running plugins. */ public void shutdown() { // Stop the plugin monitoring service. if (executor != null) { executor.shutdown(); } // Shutdown all installed plugins. for (IPlugin plugin : plugins.values()) { try { plugin.destroyPlugin(); } catch (Exception e) { Log.error(e.getMessage(), e); } } plugins.clear(); pluginDirs.clear(); pluginFiles.clear(); classloaders.clear(); pluginDevelopment.clear(); childPluginMap.clear(); pluginMonitor = null; } /** * Installs or updates an existing plugin. * * @param in the input stream that contains the new plugin definition. * @param pluginFilename the filename of the plugin to create or update. * @return true if the plugin was successfully installed or updated. */ public boolean installPlugin(InputStream in, String pluginFilename) { if (in == null || pluginFilename == null || pluginFilename.length() < 1) { Log.error( "Error installing plugin: Input stream was null or pluginFilename was null or had no length."); return false; } try { byte[] b = new byte[1024]; int len; // If pluginFilename is a path instead of a simple file name, we only want the file name int index = pluginFilename.lastIndexOf(File.separator); if (index != -1) { pluginFilename = pluginFilename.substring(index + 1); } // Absolute path to the plugin file String absolutePath = pluginDirectory + File.separator + pluginFilename; // Save input stream contents to a temp file OutputStream out = new FileOutputStream(absolutePath + ".part"); while ((len = in.read(b)) != -1) { //write byte to file out.write(b, 0, len); } out.close(); // Delete old .jar (if it exists) new File(absolutePath).delete(); // Rename temp file to .jar new File(absolutePath + ".part").renameTo(new File(absolutePath)); // Ask the plugin monitor to update the plugin immediately. pluginMonitor.run(); } catch (IOException e) { Log.error("Error installing new version of plugin: " + pluginFilename, e); return false; } return true; } /** * Returns true if the specified filename, that belongs to a plugin, exists. * * @param pluginFilename the filename of the plugin to create or update. * @return true if the specified filename, that belongs to a plugin, exists. */ public boolean isPluginDownloaded(String pluginFilename) { return new File(pluginDirectory + File.separator + pluginFilename).exists(); } /** * Returns a Collection of all installed plugins. * * @return a Collection of all installed plugins. */ public Collection<IPlugin> getPlugins() { return Collections.unmodifiableCollection(plugins.values()); } /** * Returns a plugin by name or <tt>null</tt> if a plugin with that name does not * exist. The name is the name of the directory that the plugin is in such as * "broadcast". * * @param name the name of the plugin. * @return the plugin. */ public IPlugin getPlugin(String name) { return plugins.get(name); } /** * Returns the plugin's directory. * * @param plugin the plugin. * @return the plugin's directory. */ public File getPluginDirectory(IPlugin plugin) { return pluginDirs.get(plugin); } /** * Returns the JAR or WAR file that created the plugin. * * @param name the name of the plugin. * @return the plugin JAR or WAR file. */ public File getPluginFile(String name) { return pluginFiles.get(name); } /** * Returns true if at least one attempt to load plugins has been done. A true value does not mean * that available plugins have been loaded nor that plugins to be added in the future are already * loaded. :)<p> * * TODO Current version does not consider child plugins that may be loaded in a second attempt. It either * TODO consider plugins that were found but failed to be loaded due to some error. * * @return true if at least one attempt to load plugins has been done. */ public boolean isExecuted() { return pluginMonitor.executed; } /** * Loads a plug-in module into the container. Loading consists of the * following steps:<ul> * <p/> * <li>Add all jars in the <tt>lib</tt> dir (if it exists) to the class loader</li> * <li>Add all files in <tt>classes</tt> dir (if it exists) to the class loader</li> * <li>Locate and load <tt>module.xml</tt> into the context</li> * <li>For each b5chat.module entry, load the given class as a module and start it</li> * <p/> * </ul> * * @param pluginDir the plugin directory. */ private void loadPlugin(File pluginDir) { // Only load the admin plugin during setup mode. if (XmppServer.getInstance().isSetupMode() && !(pluginDir.getName().equals("admin"))) { return; } Log.debug("PluginManager: Loading plugin " + pluginDir.getName()); IPlugin plugin; try { File pluginConfig = new File(pluginDir, "plugin.xml"); if (pluginConfig.exists()) { SAXReader saxReader = new SAXReader(); saxReader.setEncoding("UTF-8"); Document pluginXML = saxReader.read(pluginConfig); // See if the plugin specifies a version of crossfire // required to run. Element minServerVersion = (Element) pluginXML.selectSingleNode("/plugin/minServerVersion"); if (minServerVersion != null) { String requiredVersion = minServerVersion.getTextTrim(); Version version = XmppServer.getInstance().getServerInfo().getVersion(); String hasVersion = version.getMajor() + "." + version.getMinor() + "." + version.getMicro(); if (hasVersion.compareTo(requiredVersion) < 0) { String msg = "Ignoring plugin " + pluginDir.getName() + ": requires " + "server version " + requiredVersion; Log.warn(msg); System.out.println(msg); return; } } PluginClassLoader pluginLoader; // Check to see if this is a child plugin of another plugin. If it is, we // re-use the parent plugin's class loader so that the plugins can interact. Element parentPluginNode = (Element) pluginXML.selectSingleNode("/plugin/parentPlugin"); String pluginName = pluginDir.getName(); String webRootKey = pluginName + ".webRoot"; String classesDirKey = pluginName + ".classes"; String webRoot = System.getProperty(webRootKey); String classesDir = System.getProperty(classesDirKey); if (webRoot != null) { final File compilationClassesDir = new File(pluginDir, "classes"); if (!compilationClassesDir.exists()) { compilationClassesDir.mkdir(); } compilationClassesDir.deleteOnExit(); } if (parentPluginNode != null) { String parentPlugin = parentPluginNode.getTextTrim(); // See if the parent is already loaded. if (plugins.containsKey(parentPlugin)) { pluginLoader = classloaders.get(getPlugin(parentPlugin)); pluginLoader.addDirectory(pluginDir, classesDir != null); } else { // See if the parent plugin exists but just hasn't been loaded yet. // This can only be the case if this plugin name is alphabetically before // the parent. if (pluginName.compareTo(parentPlugin) < 0) { // See if the parent exists. File file = new File(pluginDir.getParentFile(), parentPlugin + ".jar"); if (file.exists()) { // Silently return. The child plugin will get loaded up on the next // plugin load run after the parent. return; } else { file = new File(pluginDir.getParentFile(), parentPlugin + ".war"); if (file.exists()) { // Silently return. The child plugin will get loaded up on the next // plugin load run after the parent. return; } else { String msg = "Ignoring plugin " + pluginName + ": parent plugin " + parentPlugin + " not present."; Log.warn(msg); System.out.println(msg); return; } } } else { String msg = "Ignoring plugin " + pluginName + ": parent plugin " + parentPlugin + " not present."; Log.warn(msg); System.out.println(msg); return; } } } // This is not a child plugin, so create a new class loader. else { pluginLoader = new PluginClassLoader(); pluginLoader.addDirectory(pluginDir, classesDir != null); } // Check to see if development mode is turned on for the plugin. If it is, // configure dev mode. PluginDevEnvironment dev = null; if (webRoot != null || classesDir != null) { dev = new PluginDevEnvironment(); System.out.println("IPlugin " + pluginName + " is running in development mode."); Log.info("IPlugin " + pluginName + " is running in development mode."); if (webRoot != null) { File webRootDir = new File(webRoot); if (!webRootDir.exists()) { // Ok, let's try it relative from this plugin dir? webRootDir = new File(pluginDir, webRoot); } if (webRootDir.exists()) { dev.setWebRoot(webRootDir); } } if (classesDir != null) { File classes = new File(classesDir); if (!classes.exists()) { // ok, let's try it relative from this plugin dir? classes = new File(pluginDir, classesDir); } if (classes.exists()) { dev.setClassesDir(classes); pluginLoader.addURLFile(classes.getAbsoluteFile().toURI().toURL()); } } } String className = pluginXML.selectSingleNode("/plugin/class").getText().trim(); plugin = (IPlugin) pluginLoader.loadClass(className).newInstance(); if (parentPluginNode != null) { String parentPlugin = parentPluginNode.getTextTrim(); // See if the parent is already loaded. if (plugins.containsKey(parentPlugin)) { pluginLoader = classloaders.get(getPlugin(parentPlugin)); classloaders.put(plugin, pluginLoader); } } plugins.put(pluginName, plugin); pluginDirs.put(plugin, pluginDir); // If this is a child plugin, register it as such. if (parentPluginNode != null) { String parentPlugin = parentPluginNode.getTextTrim(); List<String> childrenPlugins = parentPluginMap.get(plugins.get(parentPlugin)); if (childrenPlugins == null) { childrenPlugins = new ArrayList<String>(); parentPluginMap.put(plugins.get(parentPlugin), childrenPlugins); } childrenPlugins.add(pluginName); // Also register child to parent relationship. childPluginMap.put(plugin, parentPlugin); } else { // Only register the class loader in the case of this not being // a child plugin. classloaders.put(plugin, pluginLoader); } // Check the plugin's database schema (if it requires one). if (!DbConnectionManager.getSchemaManager().checkPluginSchema(plugin)) { // The schema was not there and auto-upgrade failed. Log.error(pluginName + " - " + LocaleUtils.getLocalizedString("upgrade.database.failure")); System.out.println( pluginName + " - " + LocaleUtils.getLocalizedString("upgrade.database.failure")); } // Load any JSP's defined by the plugin. File webXML = new File(pluginDir, "web" + File.separator + "WEB-INF" + File.separator + "web.xml"); if (webXML.exists()) { PluginServlet.registerServlets(this, plugin, webXML); } // Load any custom-defined servlets. File customWebXML = new File(pluginDir, "web" + File.separator + "WEB-INF" + File.separator + "web-custom.xml"); if (customWebXML.exists()) { PluginServlet.registerServlets(this, plugin, customWebXML); } if (dev != null) { pluginDevelopment.put(plugin, dev); } // Configure caches of the plugin configureCaches(pluginDir, pluginName); // Init the plugin. ClassLoader oldLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(pluginLoader); plugin.initializePlugin(this, pluginDir); Thread.currentThread().setContextClassLoader(oldLoader); // If there a <adminconsole> section defined, register it. Element adminElement = (Element) pluginXML.selectSingleNode("/plugin/adminconsole"); if (adminElement != null) { Element appName = (Element) adminElement .selectSingleNode("/plugin/adminconsole/global/appname"); if (appName != null) { // Set the plugin name so that the proper i18n String can be loaded. appName.addAttribute("plugin", pluginName); } // If global images are specified, override their URL. Element imageEl = (Element) adminElement .selectSingleNode("/plugin/adminconsole/global/logo-image"); if (imageEl != null) { imageEl.setText("plugins/" + pluginName + "/" + imageEl.getText()); // Set the plugin name so that the proper i18n String can be loaded. imageEl.addAttribute("plugin", pluginName); } imageEl = (Element) adminElement.selectSingleNode("/plugin/adminconsole/global/login-image"); if (imageEl != null) { imageEl.setText("plugins/" + pluginName + "/" + imageEl.getText()); // Set the plugin name so that the proper i18n String can be loaded. imageEl.addAttribute("plugin", pluginName); } // Modify all the URL's in the XML so that they are passed through // the plugin servlet correctly. @SuppressWarnings("unchecked") List<Object> urls = adminElement.selectNodes("//@url"); for (Object url : urls) { Attribute attr = (Attribute) url; attr.setValue("plugins/" + pluginName + "/" + attr.getValue()); } // In order to internationalize the names and descriptions in the model, // we add a "plugin" attribute to each tab, sidebar, and item so that // the the renderer knows where to load the i18n Strings from. String[] elementNames = new String[] { "tab", "sidebar", "item" }; for (String elementName : elementNames) { @SuppressWarnings("unchecked") List<Object> values = adminElement.selectNodes("//" + elementName); for (Object value : values) { Element element = (Element) value; // Make sure there's a name or description. Otherwise, no need to // override i18n settings. if (element.attribute("name") != null || element.attribute("value") != null) { element.addAttribute("plugin", pluginName); } } } AdminConsole.addModel(pluginName, adminElement); } firePluginCreatedEvent(pluginName, plugin); } else { Log.warn("IPlugin " + pluginDir + " could not be loaded: no plugin.xml file found"); } } catch (Throwable e) { Log.error("Error loading plugin: " + pluginDir, e); } } private void configureCaches(File pluginDir, String pluginName) { File cacheConfig = new File(pluginDir, "cache-config.xml"); if (cacheConfig.exists()) { PluginCacheConfigurator configurator = new PluginCacheConfigurator(); try { configurator.setInputStream(new BufferedInputStream(new FileInputStream(cacheConfig))); configurator.configure(pluginName); } catch (Exception e) { Log.error(e.getMessage(), e); } } } private void firePluginCreatedEvent(String name, IPlugin plugin) { for (IPluginListener listener : pluginListeners) { listener.pluginCreated(name, plugin); } } private void firePluginsMonitored() { for (IPluginManagerListener listener : pluginManagerListeners) { listener.pluginsMonitored(); } } /** * Unloads a plugin. The {@link IPlugin#destroyPlugin()} method will be called and then * any resources will be released. The name should be the name of the plugin directory * and not the name as given by the plugin meta-data. This method only removes * the plugin but does not delete the plugin JAR file. Therefore, if the plugin JAR * still exists after this method is called, the plugin will be started again the next * time the plugin monitor process runs. This is useful for "restarting" plugins.<p> * <p/> * This method is called automatically when a plugin's JAR file is deleted. * * @param pluginName the name of the plugin to unload. */ public void unloadPlugin(String pluginName) { Log.debug("PluginManager: Unloading plugin " + pluginName); IPlugin plugin = plugins.get(pluginName); if (plugin != null) { // Remove from dev mode if it exists. pluginDevelopment.remove(plugin); // See if any child plugins are defined. if (parentPluginMap.containsKey(plugin)) { String[] childPlugins = parentPluginMap.get(plugin) .toArray(new String[parentPluginMap.get(plugin).size()]); parentPluginMap.remove(plugin); for (String childPlugin : childPlugins) { Log.debug("Unloading child plugin: " + childPlugin); childPluginMap.remove(plugins.get(childPlugin)); unloadPlugin(childPlugin); } } File webXML = new File(pluginDirectory, pluginName + File.separator + "web" + File.separator + "WEB-INF" + File.separator + "web.xml"); if (webXML.exists()) { AdminConsole.removeModel(pluginName); PluginServlet.unregisterServlets(webXML); } File customWebXML = new File(pluginDirectory, pluginName + File.separator + "web" + File.separator + "WEB-INF" + File.separator + "web-custom.xml"); if (customWebXML.exists()) { PluginServlet.unregisterServlets(customWebXML); } // Wrap destroying the plugin in a try/catch block. Otherwise, an exception raised // in the destroy plugin process will disrupt the whole unloading process. It's still // possible that classloader destruction won't work in the case that destroying the plugin // fails. In that case, crossfire may need to be restarted to fully cleanup the plugin // resources. try { plugin.destroyPlugin(); } catch (Exception e) { Log.error(e.getMessage(), e); } } // Remove references to the plugin so it can be unloaded from memory // If plugin still fails to be removed then we will add references back // Anyway, for a few seconds admins may not see the plugin in the admin console // and in a subsequent refresh it will appear if failed to be removed plugins.remove(pluginName); File pluginFile = pluginDirs.remove(plugin); PluginClassLoader pluginLoader = classloaders.remove(plugin); // Try to remove the folder where the plugin was exploded. If this works then // the plugin was successfully removed. Otherwise, some objects created by the // plugin are still in memory. File dir = new File(pluginDirectory, pluginName); // Give the plugin 2 seconds to unload. try { Thread.sleep(2000); // Ask the system to clean up references. System.gc(); int count = 0; while (!deleteDir(dir) && count < 5) { Log.warn("Error unloading plugin " + pluginName + ". " + "Will attempt again momentarily."); Thread.sleep(8000); count++; // Ask the system to clean up references. System.gc(); } } catch (InterruptedException e) { Log.error(e.getMessage(), e); } if (plugin != null && !dir.exists()) { // Unregister plugin caches PluginCacheRegistry.getInstance().unregisterCaches(pluginName); // See if this is a child plugin. If it is, we should unload // the parent plugin as well. if (childPluginMap.containsKey(plugin)) { String parentPluginName = childPluginMap.get(plugin); IPlugin parentPlugin = plugins.get(parentPluginName); List<String> childrenPlugins = parentPluginMap.get(parentPlugin); childrenPlugins.remove(pluginName); childPluginMap.remove(plugin); // When the parent plugin implements IPluginListener, its pluginDestroyed() method // isn't called if it dies first before its child. Athough the parent will die anyway, // it's proper if the parent "gets informed first" about the dying child when the // child is the one being killed first. if (parentPlugin instanceof IPluginListener) { IPluginListener listener; listener = (IPluginListener) parentPlugin; listener.pluginDestroyed(pluginName, plugin); } unloadPlugin(parentPluginName); } firePluginDestroyedEvent(pluginName, plugin); } else if (plugin != null) { // Restore references since we failed to remove the plugin plugins.put(pluginName, plugin); pluginDirs.put(plugin, pluginFile); classloaders.put(plugin, pluginLoader); } } private void firePluginDestroyedEvent(String name, IPlugin plugin) { for (IPluginListener listener : pluginListeners) { listener.pluginDestroyed(name, plugin); } } /** * Loads a class from the classloader of a plugin. * * @param plugin the plugin. * @param className the name of the class to load. * @return the class. * @throws ClassNotFoundException if the class was not found. * @throws IllegalAccessException if not allowed to access the class. * @throws InstantiationException if the class could not be created. */ public Class<?> loadClass(IPlugin plugin, String className) throws ClassNotFoundException, IllegalAccessException, InstantiationException { PluginClassLoader loader = classloaders.get(plugin); return loader.loadClass(className); } /** * Returns a plugin's dev environment if development mode is enabled for * the plugin. * * @param plugin the plugin. * @return the plugin dev environment, or <tt>null</tt> if development * mode is not enabled for the plugin. */ public PluginDevEnvironment getDevEnvironment(IPlugin plugin) { return pluginDevelopment.get(plugin); } /** * Returns the name of a plugin. The value is retrieved from the plugin.xml file * of the plugin. If the value could not be found, <tt>null</tt> will be returned. * Note that this value is distinct from the name of the plugin directory. * * @param plugin the plugin. * @return the plugin's name. */ public String getName(IPlugin plugin) { String name = getElementValue(plugin, "/plugin/name"); String pluginName = pluginDirs.get(plugin).getName(); if (name != null) { return AdminConsole.getAdminText(name, pluginName); } else { return pluginName; } } /** * Returns the description of a plugin. The value is retrieved from the plugin.xml file * of the plugin. If the value could not be found, <tt>null</tt> will be returned. * * @param plugin the plugin. * @return the plugin's description. */ public String getDescription(IPlugin plugin) { String pluginName = pluginDirs.get(plugin).getName(); return AdminConsole.getAdminText(getElementValue(plugin, "/plugin/description"), pluginName); } /** * Returns the author of a plugin. The value is retrieved from the plugin.xml file * of the plugin. If the value could not be found, <tt>null</tt> will be returned. * * @param plugin the plugin. * @return the plugin's author. */ public String getAuthor(IPlugin plugin) { return getElementValue(plugin, "/plugin/author"); } /** * Returns the version of a plugin. The value is retrieved from the plugin.xml file * of the plugin. If the value could not be found, <tt>null</tt> will be returned. * * @param plugin the plugin. * @return the plugin's version. */ public String getVersion(IPlugin plugin) { return getElementValue(plugin, "/plugin/version"); } /** * Returns the minimum server version this plugin can run within. The value is retrieved from the plugin.xml file * of the plugin. If the value could not be found, <tt>null</tt> will be returned. * * @param plugin the plugin. * @return the plugin's version. */ public String getMinServerVersion(IPlugin plugin) { return getElementValue(plugin, "/plugin/minServerVersion"); } /** * Returns the database schema key of a plugin, if it exists. The value is retrieved * from the plugin.xml file of the plugin. If the value could not be found, <tt>null</tt> * will be returned. * * @param plugin the plugin. * @return the plugin's database schema key or <tt>null</tt> if it doesn't exist. */ public String getDatabaseKey(IPlugin plugin) { return getElementValue(plugin, "/plugin/databaseKey"); } /** * Returns the database schema version of a plugin, if it exists. The value is retrieved * from the plugin.xml file of the plugin. If the value could not be found, <tt>-1</tt> * will be returned. * * @param plugin the plugin. * @return the plugin's database schema version or <tt>-1</tt> if it doesn't exist. */ public int getDatabaseVersion(IPlugin plugin) { String versionString = getElementValue(plugin, "/plugin/databaseVersion"); if (versionString != null) { try { return Integer.parseInt(versionString.trim()); } catch (NumberFormatException nfe) { Log.error(nfe.getMessage(), nfe); } } return -1; } /** * Returns the license agreement type that the plugin is governed by. The value * is retrieved from the plugin.xml file of the plugin. If the value could not be * found, {@link License#other} is returned. * * @param plugin the plugin. * @return the plugin's license agreement. */ public License getLicense(IPlugin plugin) { String licenseString = getElementValue(plugin, "/plugin/licenseType"); if (licenseString != null) { try { // Attempt to load the get the license type. We lower-case and // trim the license type to give plugin author's a break. If the // license type is not recognized, we'll log the error and default // to "other". return License.valueOf(licenseString.toLowerCase().trim()); } catch (IllegalArgumentException iae) { Log.error(iae.getMessage(), iae); } } return License.other; } /** * Returns the classloader of a plugin. * * @param plugin the plugin. * @return the classloader of the plugin. */ public PluginClassLoader getPluginClassloader(IPlugin plugin) { return classloaders.get(plugin); } /** * Returns the value of an element selected via an xpath expression from * a IPlugin's plugin.xml file. * * @param plugin the plugin. * @param xpath the xpath expression. * @return the value of the element selected by the xpath expression. */ private String getElementValue(IPlugin plugin, String xpath) { File pluginDir = pluginDirs.get(plugin); if (pluginDir == null) { return null; } try { File pluginConfig = new File(pluginDir, "plugin.xml"); if (pluginConfig.exists()) { SAXReader saxReader = new SAXReader(); saxReader.setEncoding("UTF-8"); Document pluginXML = saxReader.read(pluginConfig); Element element = (Element) pluginXML.selectSingleNode(xpath); if (element != null) { return element.getTextTrim(); } } } catch (Exception e) { Log.error(e.getMessage(), e); } return null; } /** * An enumberation for plugin license agreement types. */ public enum License { /** * The plugin is distributed using a commercial license. */ commercial, /** * The plugin is distributed using the GNU Public License (GPL). */ gpl, /** * The plugin is distributed using the Apache license. */ apache, /** * The plugin is for internal use at an organization only and is not re-distributed. */ internal, /** * The plugin is distributed under another license agreement not covered by * one of the other choices. The license agreement should be detailed in the * plugin Readme. */ other; } /** * A service that monitors the plugin directory for plugins. It periodically * checks for new plugin JAR files and extracts them if they haven't already * been extracted. Then, any new plugin directories are loaded. */ private class PluginMonitor implements Runnable { /** * Tracks if the monitor is currently running. */ private boolean running = false; /** * True if the monitor has been executed at least once. After the first iteration in {@link #run} * this variable will always be true. * */ private boolean executed = false; /** * True when it's the first time the plugin monitor process runs. This is helpful for * bootstrapping purposes. */ private boolean firstRun = true; public void run() { // If the task is already running, return. synchronized (this) { if (running) { return; } running = true; } try { running = true; // Look for extra plugin directories specified as a system property. String pluginDirs = System.getProperty("pluginDirs"); if (pluginDirs != null) { StringTokenizer st = new StringTokenizer(pluginDirs, ", "); while (st.hasMoreTokens()) { String dir = st.nextToken(); if (!devPlugins.contains(dir)) { loadPlugin(new File(dir)); devPlugins.add(dir); } } } File[] jars = pluginDirectory.listFiles(new FileFilter() { public boolean accept(File pathname) { String fileName = pathname.getName().toLowerCase(); return (fileName.endsWith(".jar") || fileName.endsWith(".war")); } }); if (jars == null) { return; } for (File jarFile : jars) { String pluginName = jarFile.getName().substring(0, jarFile.getName().length() - 4) .toLowerCase(); // See if the JAR has already been exploded. File dir = new File(pluginDirectory, pluginName); // Store the JAR/WAR file that created the plugin folder pluginFiles.put(pluginName, jarFile); // If the JAR hasn't been exploded, do so. if (!dir.exists()) { unzipPlugin(pluginName, jarFile, dir); } // See if the JAR is newer than the directory. If so, the plugin // needs to be unloaded and then reloaded. else if (jarFile.lastModified() > dir.lastModified()) { // If this is the first time that the monitor process is running, then // plugins won't be loaded yet. Therefore, just delete the directory. if (firstRun) { int count = 0; // Attempt to delete the folder for up to 5 seconds. while (!deleteDir(dir) && count < 5) { Thread.sleep(1000); } } else { unloadPlugin(pluginName); } // If the delete operation was a success, unzip the plugin. if (!dir.exists()) { unzipPlugin(pluginName, jarFile, dir); } } } File[] dirs = pluginDirectory.listFiles(new FileFilter() { public boolean accept(File pathname) { return pathname.isDirectory(); } }); // Sort the list of directories so that the "admin" plugin is always // first in the list. Arrays.sort(dirs, new Comparator<File>() { public int compare(File file1, File file2) { if (file1.getName().equals("admin")) { return -1; } else if (file2.getName().equals("admin")) { return 1; } else { return file1.compareTo(file2); } } }); // Turn the list of JAR/WAR files into a set so that we can do lookups. Set<String> jarSet = new HashSet<String>(); for (File file : jars) { jarSet.add(file.getName().toLowerCase()); } // See if any currently running plugins need to be unloaded // due to the JAR file being deleted (ignore admin plugin). // Build a list of plugins to delete first so that the plugins // keyset isn't modified as we're iterating through it. List<String> toDelete = new ArrayList<String>(); for (File pluginDir : dirs) { String pluginName = pluginDir.getName(); if (pluginName.equals("admin")) { continue; } if (!jarSet.contains(pluginName + ".jar")) { if (!jarSet.contains(pluginName + ".war")) { toDelete.add(pluginName); } } } for (String pluginName : toDelete) { unloadPlugin(pluginName); } // Load all plugins that need to be loaded. for (File dirFile : dirs) { // If the plugin hasn't already been started, start it. if (dirFile.exists() && !plugins.containsKey(dirFile.getName())) { loadPlugin(dirFile); } } // Set that at least one iteration was done. That means that "all available" plugins // have been loaded by now. if (!XmppServer.getInstance().isSetupMode()) { executed = true; } // Trigger event that plugins have been monitored firePluginsMonitored(); } catch (Throwable e) { Log.error(e.getMessage(), e); } // Finished running task. synchronized (this) { running = false; } // Process finished, so set firstRun to false (setting it multiple times doesn't hurt). firstRun = false; } /** * Unzips a plugin from a JAR file into a directory. If the JAR file * isn't a plugin, this method will do nothing. * * @param pluginName the name of the plugin. * @param file the JAR file * @param dir the directory to extract the plugin to. */ private void unzipPlugin(String pluginName, File file, File dir) { try { ZipFile zipFile = new JarFile(file); // Ensure that this JAR is a plugin. if (zipFile.getEntry("plugin.xml") == null) { return; } dir.mkdir(); // Set the date of the JAR file to the newly created folder dir.setLastModified(file.lastModified()); Log.debug("PluginManager: Extracting plugin: " + pluginName); for (Enumeration<? extends ZipEntry> e = zipFile.entries(); e.hasMoreElements();) { JarEntry entry = (JarEntry) e.nextElement(); File entryFile = new File(dir, entry.getName()); // Ignore any manifest.mf entries. if (entry.getName().toLowerCase().endsWith("manifest.mf")) { continue; } if (!entry.isDirectory()) { entryFile.getParentFile().mkdirs(); FileOutputStream out = new FileOutputStream(entryFile); InputStream zin = zipFile.getInputStream(entry); byte[] b = new byte[512]; int len; while ((len = zin.read(b)) != -1) { out.write(b, 0, len); } out.flush(); out.close(); zin.close(); } } zipFile.close(); // The lib directory of the plugin may contain Pack200 versions of the JAR // file. If so, unpack them. unpackArchives(new File(dir, "lib")); } catch (Exception e) { Log.error(e.getMessage(), e); } } /** * Converts any pack files in a directory into standard JAR files. Each * pack file will be deleted after being converted to a JAR. If no * pack files are found, this method does nothing. * * @param libDir the directory containing pack files. */ private void unpackArchives(File libDir) { // Get a list of all packed files in the lib directory. File[] packedFiles = libDir.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.endsWith(".pack"); } }); if (packedFiles == null) { // Do nothing since no .pack files were found return; } // Unpack each. for (File packedFile : packedFiles) { try { String jarName = packedFile.getName().substring(0, packedFile.getName().length() - ".pack".length()); // Delete JAR file with same name if it exists (could be due to upgrade // from old crossfire release). File jarFile = new File(libDir, jarName); if (jarFile.exists()) { jarFile.delete(); } InputStream in = new BufferedInputStream(new FileInputStream(packedFile)); JarOutputStream out = new JarOutputStream( new BufferedOutputStream(new FileOutputStream(new File(libDir, jarName)))); Pack200.Unpacker unpacker = Pack200.newUnpacker(); // Call the unpacker unpacker.unpack(in, out); in.close(); out.close(); packedFile.delete(); } catch (Exception e) { Log.error(e.getMessage(), e); } } } } /** * Deletes a directory. * * @param dir the directory to delete. * @return true if the directory was deleted. */ private boolean deleteDir(File dir) { if (dir.isDirectory()) { String[] childDirs = dir.list(); // Always try to delete JAR files first since that's what will // be under contention. We do this by always sorting the lib directory // first. List<String> children = new ArrayList<String>(Arrays.asList(childDirs)); Collections.sort(children, new Comparator<String>() { public int compare(String o1, String o2) { if (o1.equals("lib")) { return -1; } if (o2.equals("lib")) { return 1; } else { return o1.compareTo(o2); } } }); for (String file : children) { boolean success = deleteDir(new File(dir, file)); if (!success) { Log.debug("PluginManager: IPlugin removal: could not delete: " + new File(dir, file)); return false; } } } boolean deleted = !dir.exists() || dir.delete(); if (deleted) { // Remove the JAR/WAR file that created the plugin folder pluginFiles.remove(dir.getName()); } return deleted; } public void addPluginListener(IPluginListener listener) { pluginListeners.add(listener); } public void removePluginListener(IPluginListener listener) { pluginListeners.remove(listener); } public void addPluginManagerListener(IPluginManagerListener listener) { pluginManagerListeners.add(listener); if (isExecuted()) { firePluginsMonitored(); } } public void removePluginManagerListener(IPluginManagerListener listener) { pluginManagerListeners.remove(listener); } }