/*
* RHQ Management Platform
* Copyright (C) 2005-2009 Red Hat, Inc.
* All rights reserved.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License, version 2, as
* published by the Free Software Foundation, and/or the GNU Lesser
* General Public License, version 2.1, also as published by the Free
* Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License and the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU General Public License
* and the GNU Lesser General Public License along with this program;
* if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.rhq.enterprise.server.plugin.pc;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.plugin.PluginKey;
import org.rhq.enterprise.server.xmlschema.generated.serverplugin.ServerPluginDescriptorType;
/**
* Manages the classloaders created and used by the master plugin container and all plugins.
*
* @author John Mazzitelli
*/
public class ClassLoaderManager {
private final Log log = LogFactory.getLog(ClassLoaderManager.class);
/**
* Directory where temporary files can be stored. Used to extract jars embedded in plugin jars.
*/
private final File tmpDir;
/**
* Provides a map keyed on plugin keys whose values are the URLs to those plugin jars.
*/
private final Map<PluginKey, URL> pluginKeysUrls;
/**
* The parent classloader for those classloaders at the top of the classloader hierarchy.
*/
private final ClassLoader rootClassLoader;
/**
* These are the classloaders that are built for each plugin.
*
* @see #obtainServerPluginClassLoader(String)
*/
private final Map<PluginKey, ClassLoader> serverPluginClassLoaders;
/**
* Creates the object that will manage all classloaders for the plugins deployed in the server.
*
* @param plugins maps a plugin URL to that plugin's descriptor
* @param rootClassLoader the classloader at the top of the classloader hierarchy
* @param tmpDir where the classloaders can write out the jars that are embedded in the plugin jars
*/
public ClassLoaderManager(Map<URL, ? extends ServerPluginDescriptorType> plugins, ClassLoader rootClassLoader,
File tmpDir) {
this.rootClassLoader = rootClassLoader;
this.tmpDir = tmpDir;
this.serverPluginClassLoaders = new HashMap<PluginKey, ClassLoader>();
this.pluginKeysUrls = new HashMap<PluginKey, URL>(plugins.size());
for (Map.Entry<URL, ? extends ServerPluginDescriptorType> entry : plugins.entrySet()) {
loadPlugin(entry.getKey(), entry.getValue());
}
return;
}
/**
* Cleans up this object and all classloaders it has created.
*/
public synchronized void shutdown() {
for (ClassLoader doomedCL : getUniqueServerPluginClassLoaders()) {
if (doomedCL instanceof ServerPluginClassLoader) {
try {
((ServerPluginClassLoader) doomedCL).destroy();
} catch (Exception e) {
log.warn("Failed to destroy classloader: " + doomedCL, e);
}
}
}
this.serverPluginClassLoaders.clear();
return;
}
/**
* Hot-deploys a plugin into this classloader manager.
*
* @param pluginUrl location of the plugin jar file
* @param descriptor the plugin descriptor
*/
public synchronized void loadPlugin(URL pluginUrl, ServerPluginDescriptorType descriptor) {
ServerPluginType pluginType = new ServerPluginType(descriptor);
PluginKey pluginKey = PluginKey.createServerPluginKey(pluginType.stringify(), descriptor.getName());
this.pluginKeysUrls.put(pluginKey, pluginUrl);
}
/**
* Unloads the plugin identified with the current key from this classloader manager and destroys
* that plugin's classloader, if one existed.
*
* @param pluginKey identifies the plugin to be unloaded
*/
public synchronized void unloadPlugin(PluginKey pluginKey) {
this.pluginKeysUrls.remove(pluginKey);
ClassLoader unloadedCL = this.serverPluginClassLoaders.remove(pluginKey);
if (unloadedCL instanceof ServerPluginClassLoader) {
try {
((ServerPluginClassLoader) unloadedCL).destroy();
} catch (Exception e) {
log.warn("Failed to destroy classloader [" + unloadedCL + "] for plugin [" + pluginKey + "]", e);
}
}
return;
}
@Override
public String toString() {
Set<ClassLoader> classLoaders;
StringBuilder str = new StringBuilder(this.getClass().getSimpleName());
str.append(" tmp-dir=[").append(this.tmpDir).append(']');
classLoaders = getUniqueServerPluginClassLoaders();
str.append(", #plugin CLs=[").append(classLoaders.size());
classLoaders.clear(); // help out the GC, clear out the shallow copy container
str.append(']');
return str.toString();
}
/**
* Returns the classloader that should be the ancestor (i.e. top most parent) of all plugin classloaders.
*
* @return the root plugin classloader for all plugins
*/
public ClassLoader getRootClassLoader() {
return this.rootClassLoader;
}
/**
* Returns a plugin classloader (creating it if necessary).
*
* @param pluginKey the plugin whose classloader is to be created
* @return the plugin classloader
* @throws Exception
*/
public synchronized ClassLoader obtainServerPluginClassLoader(PluginKey pluginKey) throws Exception {
ClassLoader cl = this.serverPluginClassLoaders.get(pluginKey);
if (cl == null) {
URL pluginJarUrl = this.pluginKeysUrls.get(pluginKey);
if (log.isDebugEnabled()) {
log.debug("Creating classloader for plugin [" + pluginKey + "] from URL [" + pluginJarUrl + ']');
}
ClassLoader parentClassLoader = this.rootClassLoader;
cl = createClassLoader(pluginJarUrl, null, parentClassLoader);
this.serverPluginClassLoaders.put(pluginKey, cl);
}
return cl;
}
/**
* Returns the total number of plugin classloaders that have been created and managed.
* This method is here just to support a plugin container management MBean.
*
* @return number of plugin classloaders that are currently created and being used
*/
public synchronized int getNumberOfServerPluginClassLoaders() {
return this.serverPluginClassLoaders.size();
}
/**
* Returns a shallow copy of the plugin classloaders keyed on plugin key. This method is here
* just to support a plugin container management MBean.
*
* Do not use this method to obtain a plugin's classloader, instead, you want to use
* {@link #obtainServerPluginClassLoader(String)}.
*
* @return all plugin classloaders currently assigned to plugins (will never be <code>null</code>)
*/
public synchronized Map<PluginKey, ClassLoader> getServerPluginClassLoaders() {
return new HashMap<PluginKey, ClassLoader>(this.serverPluginClassLoaders);
}
private synchronized Set<ClassLoader> getUniqueServerPluginClassLoaders() {
HashSet<ClassLoader> uniqueClassLoaders = new HashSet<ClassLoader>(this.serverPluginClassLoaders.values());
return uniqueClassLoaders;
}
private ClassLoader createClassLoader(URL mainJarUrl, List<URL> additionalJars, ClassLoader parentClassLoader)
throws Exception {
ClassLoader classLoader;
if (parentClassLoader == null) {
parentClassLoader = this.getClass().getClassLoader();
}
if (mainJarUrl != null) {
// Note that we don't really care if the URL uses "file:" or not,
// we just use File to parse the name from the path.
String pluginJarName = new File(mainJarUrl.getPath()).getName();
if (additionalJars == null || additionalJars.size() == 0) {
classLoader = ServerPluginClassLoader.create(pluginJarName, mainJarUrl, true, parentClassLoader,
this.tmpDir);
} else {
List<URL> allJars = new ArrayList<URL>(additionalJars.size() + 1);
allJars.add(mainJarUrl);
allJars.addAll(additionalJars);
classLoader = ServerPluginClassLoader.create(pluginJarName, allJars.toArray(new URL[allJars.size()]),
true, parentClassLoader, this.tmpDir);
}
if (log.isDebugEnabled()) {
log.debug("Created classloader for plugin jar [" + mainJarUrl + "] with additional jars ["
+ additionalJars + "]");
}
} else {
// this is mainly to support tests
log.info("No jar URL, this should only happen in tests! If this is not a test, this is probably a bug");
classLoader = parentClassLoader;
}
return classLoader;
}
}