Commit f3ed4109 authored by Matt Tucker's avatar Matt Tucker Committed by matt

Added ability to dynamically unload plugins.


git-svn-id: http://svn.igniterealtime.org/svn/repos/messenger/trunk@829 b35dd754-fafc-0310-a699-88a17e54d16e
parent a6bfd45d
...@@ -24,19 +24,19 @@ import java.io.InputStream; ...@@ -24,19 +24,19 @@ import java.io.InputStream;
import java.net.URL; import java.net.URL;
/** /**
* A model for admin tab and sidebar info. This class loads in xml definitions of the data and * A model for admin tab and sidebar info. This class loads in xml definitions of the
* produces an in-memory model.<p> * data and produces an in-memory model.<p>
* *
* This class loads its data from the <tt>admin-sidebar.xml</tt> file which is assumed to be in * This class loads its data from the <tt>admin-sidebar.xml</tt> file which is assumed
* the main application jar file. In addition, it will load files from * to be in the main application jar file. In addition, it will load files from
* <tt>META-INF/admin-sidebar.xml</tt> if they're found. This allows developers to extend the * <tt>META-INF/admin-sidebar.xml</tt> if they're found. This allows developers to
* functionality of the admin console to provide more options. See the main * extend the functionality of the admin console to provide more options. See the main
* <tt>admin-sidebar.xml</tt> file for documentation of its format.<p> * <tt>admin-sidebar.xml</tt> file for documentation of its format.
*/ */
public class AdminConsole { public class AdminConsole {
private static Element coreModel; private static Element coreModel;
private static List<Element> overrideModels; private static Map<String,Element> overrideModels;
private static Element generatedModel; private static Element generatedModel;
static { static {
...@@ -44,7 +44,7 @@ public class AdminConsole { ...@@ -44,7 +44,7 @@ public class AdminConsole {
} }
private static void init() { private static void init() {
overrideModels = new ArrayList<Element>(); overrideModels = new HashMap<String,Element>();
load(); load();
} }
...@@ -55,23 +55,35 @@ public class AdminConsole { ...@@ -55,23 +55,35 @@ public class AdminConsole {
/** /**
* Adds XML stream to the tabs/sidebar model. * Adds XML stream to the tabs/sidebar model.
* *
* @param name the name.
* @param in the XML input stream. * @param in the XML input stream.
* @throws Exception if an error occurs when parsing the XML or adding it to the model. * @throws Exception if an error occurs when parsing the XML or adding it to the model.
*/ */
public static void addModel(InputStream in) throws Exception { public static void addModel(String name, InputStream in) throws Exception {
SAXReader saxReader = new SAXReader(); SAXReader saxReader = new SAXReader();
Document doc = saxReader.read(in); Document doc = saxReader.read(in);
addModel((Element)doc.selectSingleNode("/adminconsole")); addModel(name, (Element)doc.selectSingleNode("/adminconsole"));
} }
/** /**
* Adds an &lt;adminconsole&gt; Element to the tabs/sidebar model. * Adds an &lt;adminconsole&gt; Element to the tabs/sidebar model.
* *
* @param name the name.
* @param element the Element * @param element the Element
* @throws Exception if an error occurs. * @throws Exception if an error occurs.
*/ */
public static void addModel(Element element) throws Exception { public static void addModel(String name, Element element) throws Exception {
overrideModels.add(element); overrideModels.put(name, element);
rebuildModel();
}
/**
* Removes an &lt;adminconsole&gt; Element from the tabs/sidebar model.
*
* @param name the name.
*/
public static void removeModel(String name) {
overrideModels.remove(name);
rebuildModel(); rebuildModel();
} }
...@@ -190,7 +202,7 @@ public class AdminConsole { ...@@ -190,7 +202,7 @@ public class AdminConsole {
url = (URL)e.nextElement(); url = (URL)e.nextElement();
try { try {
in = url.openStream(); in = url.openStream();
addModel(in); addModel("admin", in);
} }
finally { finally {
try { if (in != null) { in.close(); } } try { if (in != null) { in.close(); } }
...@@ -219,7 +231,7 @@ public class AdminConsole { ...@@ -219,7 +231,7 @@ public class AdminConsole {
doc.add(generatedModel); doc.add(generatedModel);
// Add in all overrides. // Add in all overrides.
for (Element element : overrideModels) { for (Element element : overrideModels.values()) {
// See if global settings are overriden. // See if global settings are overriden.
Element appName = (Element)element.selectSingleNode("//adminconsole/global/appname"); Element appName = (Element)element.selectSingleNode("//adminconsole/global/appname");
if (appName != null) { if (appName != null) {
......
...@@ -69,7 +69,6 @@ class PluginClassLoader { ...@@ -69,7 +69,6 @@ class PluginClassLoader {
urlArray[i] = (URL)urls.next(); urlArray[i] = (URL)urls.next();
} }
classLoader = new URLClassLoader(urlArray, findParentClassLoader()); classLoader = new URLClassLoader(urlArray, findParentClassLoader());
Thread.currentThread().setContextClassLoader(classLoader);
} }
/** /**
...@@ -88,6 +87,13 @@ class PluginClassLoader { ...@@ -88,6 +87,13 @@ class PluginClassLoader {
return classLoader.loadClass(name); return classLoader.loadClass(name);
} }
/**
* Destroys this class loader.
*/
public void destroy() {
classLoader = null;
}
/** /**
* Locates the best parent class loader based on context. * Locates the best parent class loader based on context.
* *
......
...@@ -27,6 +27,7 @@ import java.util.zip.ZipFile; ...@@ -27,6 +27,7 @@ import java.util.zip.ZipFile;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;
/** /**
* Loads and manages plugins. The <tt>plugins</tt> directory is monitored for any * Loads and manages plugins. The <tt>plugins</tt> directory is monitored for any
...@@ -50,7 +51,7 @@ public class PluginManager { ...@@ -50,7 +51,7 @@ public class PluginManager {
*/ */
public PluginManager(File pluginDir) { public PluginManager(File pluginDir) {
this.pluginDirectory = pluginDir; this.pluginDirectory = pluginDir;
plugins = new HashMap<String,Plugin>(); plugins = new ConcurrentHashMap<String,Plugin>();
classloaders = new HashMap<Plugin,PluginClassLoader>(); classloaders = new HashMap<Plugin,PluginClassLoader>();
} }
...@@ -75,6 +76,7 @@ public class PluginManager { ...@@ -75,6 +76,7 @@ public class PluginManager {
plugin.destroy(); plugin.destroy();
} }
plugins.clear(); plugins.clear();
classloaders.clear();
} }
/** /**
...@@ -143,7 +145,7 @@ public class PluginManager { ...@@ -143,7 +145,7 @@ public class PluginManager {
Attribute attr = (Attribute)urls.get(i); Attribute attr = (Attribute)urls.get(i);
attr.setValue("plugins/" + pluginDir.getName() + "/" + attr.getValue()); attr.setValue("plugins/" + pluginDir.getName() + "/" + attr.getValue());
} }
AdminConsole.addModel(adminElement); AdminConsole.addModel(pluginDir.getName(), adminElement);
} }
} }
else { else {
...@@ -155,6 +157,32 @@ public class PluginManager { ...@@ -155,6 +157,32 @@ public class PluginManager {
} }
} }
/**
* Unloads a plugin. The {@link Plugin#destroy()} method will be called and then
* any resources will be released.
*
* @param pluginName the name of the plugin to unload.
*/
public void unloadPlugin(String pluginName) {
Log.debug("Unloading plugin " + pluginName);
Plugin plugin = plugins.get(pluginName);
if (plugin == null) {
return;
}
File webXML = new File(pluginDirectory + File.separator + pluginName +
File.separator + "web" + File.separator + "web.xml");
if (webXML.exists()) {
AdminConsole.removeModel(pluginName);
PluginServlet.unregisterServlets(webXML);
}
PluginClassLoader classLoader = classloaders.get(plugin);
plugin.destroy();
classLoader.destroy();
plugins.remove(pluginName);
classloaders.remove(plugin);
}
public Class loadClass(String className, Plugin plugin) throws ClassNotFoundException, public Class loadClass(String className, Plugin plugin) throws ClassNotFoundException,
IllegalAccessException, InstantiationException IllegalAccessException, InstantiationException
{ {
...@@ -179,45 +207,25 @@ public class PluginManager { ...@@ -179,45 +207,25 @@ public class PluginManager {
for (int i=0; i<jars.length; i++) { for (int i=0; i<jars.length; i++) {
File jarFile = jars[i]; File jarFile = jars[i];
String jarName = jarFile.getName().substring( String pluginName = jarFile.getName().substring(
0, jarFile.getName().length()-4).toLowerCase(); 0, jarFile.getName().length()-4).toLowerCase();
// See if the JAR has already been exploded. // See if the JAR has already been exploded.
File dir = new File(pluginDirectory, jarName); File dir = new File(pluginDirectory, pluginName);
// If the JAR hasn't been exploded, do so. // If the JAR hasn't been exploded, do so.
if (!dir.exists()) { if (!dir.exists()) {
try { unzipPlugin(pluginName, jarFile, dir);
ZipFile zipFile = new JarFile(jarFile); }
// Ensure that this JAR is a plugin. // See if the JAR is newer than the directory. If so, the plugin
if (zipFile.getEntry("plugin.xml") == null) { // needs to be unloaded and then reloaded.
continue; else if (jarFile.lastModified() > dir.lastModified()) {
} unloadPlugin(pluginName);
dir.mkdir(); if (!deleteDir(dir)) {
Log.debug("Extracting plugin: " + jarName); Log.error("Error unloading plugin " + pluginName + ". " +
for (Enumeration e=zipFile.entries(); e.hasMoreElements(); ) { "You must manually delete the plugin directory.");
JarEntry entry = (JarEntry)e.nextElement(); continue;
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 = 0;
while ( (len=zin.read(b))!= -1 ) {
out.write(b,0,len);
}
out.flush();
out.close();
zin.close();
}
}
}
catch (Exception e) {
Log.error(e);
} }
// Now unzip the plugin.
unzipPlugin(pluginName, jarFile, dir);
} }
} }
...@@ -248,10 +256,89 @@ public class PluginManager { ...@@ -248,10 +256,89 @@ public class PluginManager {
loadPlugin(dirFile); loadPlugin(dirFile);
} }
} }
// Finally see if any currently running plugins need to be unloaded
// due to its JAR file being deleted (ignore admin plugin).
if (plugins.size() > jars.length + 1) {
for (String pluginName : plugins.keySet()) {
if (pluginName.equals("admin")) {
continue;
}
File file = new File(pluginDirectory, pluginName + ".jar");
if (!file.exists()) {
unloadPlugin(pluginName);
if (!deleteDir(new File(pluginDirectory, pluginName))) {
Log.error("Error unloading plugin " + pluginName + ". " +
"You must manually delete the plugin directory.");
continue;
}
}
}
}
} }
catch (Exception e) { catch (Exception e) {
Log.error(e); Log.error(e);
} }
} }
/**
* 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();
Log.debug("Extracting plugin: " + pluginName);
for (Enumeration 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 = 0;
while ( (len=zin.read(b))!= -1 ) {
out.write(b,0,len);
}
out.flush();
out.close();
zin.close();
}
}
}
catch (Exception e) {
Log.error(e);
}
}
/**
* Deletes a directory.
*/
public boolean deleteDir(File dir) {
if (dir.isDirectory()) {
String[] children = dir.list();
for (int i=0; i<children.length; i++) {
boolean success = deleteDir(new File(dir, children[i]));
if (!success) {
return false;
}
}
}
return dir.delete();
}
} }
} }
\ No newline at end of file
...@@ -97,6 +97,7 @@ public class PluginServlet extends HttpServlet { ...@@ -97,6 +97,7 @@ public class PluginServlet extends HttpServlet {
/** /**
* Registers all JSP page servlets for a plugin. * Registers all JSP page servlets for a plugin.
* *
* @param manager the plugin manager.
* @param plugin the plugin. * @param plugin the plugin.
* @param webXML the web.xml file containing JSP page names to servlet class file * @param webXML the web.xml file containing JSP page names to servlet class file
* mappings. * mappings.
...@@ -146,6 +147,41 @@ public class PluginServlet extends HttpServlet { ...@@ -146,6 +147,41 @@ public class PluginServlet extends HttpServlet {
} }
} }
/**
* Unregisters all JSP page servlets for a plugin.
*
* @param webXML the web.xml file containing JSP page names to servlet class file
* mappings.
*/
public static void unregisterServlets(File webXML) {
if (!webXML.exists()) {
Log.error("Could not unregister plugin servlets, file " + webXML.getAbsolutePath() +
" does not exist.");
return;
}
// Find the name of the plugin directory given that the webXML file
// lives in plugins/[pluginName]/web/web.xml
String pluginName = webXML.getParentFile().getParentFile().getName();
try {
SAXReader saxReader = new SAXReader();
Document doc = saxReader.read(webXML);
// Find all <servelt-mapping> entries to discover name to URL mapping.
List names = doc.selectNodes("//servlet-mapping");
for (int i=0; i<names.size(); i++) {
Element nameElement = (Element)names.get(i);
String url = nameElement.element("url-pattern").getTextTrim();
// Destroy the servlet than remove from servlets map.
HttpServlet servlet = servlets.get(pluginName + url);
servlet.destroy();
servlets.remove(pluginName + url);
servlet = null;
}
}
catch (Throwable e) {
Log.error(e);
}
}
/** /**
* Handles a request for a JSP page. It checks to see if a servlet is mapped * Handles a request for a JSP page. It checks to see if a servlet is mapped
* for the JSP URL. If one is found, request handling is passed to it. If no * for the JSP URL. If one is found, request handling is passed to it. If no
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment