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;
import java.net.URL;
/**
* A model for admin tab and sidebar info. This class loads in xml definitions of the data and
* produces an in-memory model.<p>
* A model for admin tab and sidebar info. This class loads in xml definitions of the
* 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
* 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
* functionality of the admin console to provide more options. See the main
* <tt>admin-sidebar.xml</tt> file for documentation of its format.<p>
* This class loads its data from the <tt>admin-sidebar.xml</tt> file which is assumed
* 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 functionality of the admin console to provide more options. See the main
* <tt>admin-sidebar.xml</tt> file for documentation of its format.
*/
public class AdminConsole {
private static Element coreModel;
private static List<Element> overrideModels;
private static Map<String,Element> overrideModels;
private static Element generatedModel;
static {
......@@ -44,7 +44,7 @@ public class AdminConsole {
}
private static void init() {
overrideModels = new ArrayList<Element>();
overrideModels = new HashMap<String,Element>();
load();
}
......@@ -55,23 +55,35 @@ public class AdminConsole {
/**
* Adds XML stream to the tabs/sidebar model.
*
* @param name the name.
* @param in the XML input stream.
* @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();
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.
*
* @param name the name.
* @param element the Element
* @throws Exception if an error occurs.
*/
public static void addModel(Element element) throws Exception {
overrideModels.add(element);
public static void addModel(String name, Element element) throws Exception {
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();
}
......@@ -190,7 +202,7 @@ public class AdminConsole {
url = (URL)e.nextElement();
try {
in = url.openStream();
addModel(in);
addModel("admin", in);
}
finally {
try { if (in != null) { in.close(); } }
......@@ -219,7 +231,7 @@ public class AdminConsole {
doc.add(generatedModel);
// Add in all overrides.
for (Element element : overrideModels) {
for (Element element : overrideModels.values()) {
// See if global settings are overriden.
Element appName = (Element)element.selectSingleNode("//adminconsole/global/appname");
if (appName != null) {
......
......@@ -69,7 +69,6 @@ class PluginClassLoader {
urlArray[i] = (URL)urls.next();
}
classLoader = new URLClassLoader(urlArray, findParentClassLoader());
Thread.currentThread().setContextClassLoader(classLoader);
}
/**
......@@ -88,6 +87,13 @@ class PluginClassLoader {
return classLoader.loadClass(name);
}
/**
* Destroys this class loader.
*/
public void destroy() {
classLoader = null;
}
/**
* Locates the best parent class loader based on context.
*
......
......@@ -27,6 +27,7 @@ import java.util.zip.ZipFile;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ConcurrentHashMap;
/**
* Loads and manages plugins. The <tt>plugins</tt> directory is monitored for any
......@@ -50,7 +51,7 @@ public class PluginManager {
*/
public PluginManager(File pluginDir) {
this.pluginDirectory = pluginDir;
plugins = new HashMap<String,Plugin>();
plugins = new ConcurrentHashMap<String,Plugin>();
classloaders = new HashMap<Plugin,PluginClassLoader>();
}
......@@ -75,6 +76,7 @@ public class PluginManager {
plugin.destroy();
}
plugins.clear();
classloaders.clear();
}
/**
......@@ -143,7 +145,7 @@ public class PluginManager {
Attribute attr = (Attribute)urls.get(i);
attr.setValue("plugins/" + pluginDir.getName() + "/" + attr.getValue());
}
AdminConsole.addModel(adminElement);
AdminConsole.addModel(pluginDir.getName(), adminElement);
}
}
else {
......@@ -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,
IllegalAccessException, InstantiationException
{
......@@ -179,45 +207,25 @@ public class PluginManager {
for (int i=0; i<jars.length; i++) {
File jarFile = jars[i];
String jarName = jarFile.getName().substring(
String pluginName = jarFile.getName().substring(
0, jarFile.getName().length()-4).toLowerCase();
// 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 (!dir.exists()) {
try {
ZipFile zipFile = new JarFile(jarFile);
// Ensure that this JAR is a plugin.
if (zipFile.getEntry("plugin.xml") == null) {
continue;
}
dir.mkdir();
Log.debug("Extracting plugin: " + jarName);
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);
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()) {
unloadPlugin(pluginName);
if (!deleteDir(dir)) {
Log.error("Error unloading plugin " + pluginName + ". " +
"You must manually delete the plugin directory.");
continue;
}
// Now unzip the plugin.
unzipPlugin(pluginName, jarFile, dir);
}
}
......@@ -248,10 +256,89 @@ public class PluginManager {
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) {
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 {
/**
* Registers all JSP page servlets for a plugin.
*
* @param manager the plugin manager.
* @param plugin the plugin.
* @param webXML the web.xml file containing JSP page names to servlet class file
* mappings.
......@@ -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
* 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