/*
* 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 as published by
* the Free Software Foundation version 2 of the License.
*
* 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
package org.rhq.enterprise.server.plugin.pc;
import java.io.File;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.plugin.PluginKey;
import org.rhq.core.domain.plugin.ServerPlugin;
import org.rhq.enterprise.server.plugin.ServerPluginManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.xmlschema.ScheduledJobDefinition;
import org.rhq.enterprise.server.xmlschema.ServerPluginDescriptorMetadataParser;
import org.rhq.enterprise.server.xmlschema.ServerPluginDescriptorUtil;
import org.rhq.enterprise.server.xmlschema.generated.serverplugin.ServerPluginDescriptorType;
/**
* Provides functionality to manage plugins for a plugin container. Plugin containers
* can install their own plugin managers that are extensions to this class if they need to.
*
* Most of the methods here are protected; they are meant for the plugin container's use only.
* Usually, anything an external client needs will be done through a delegation method
* found on the plugin container.
*
* @author John Mazzitelli
*/
//TODO: need a R/W lock to make this class thread safe
public class ServerPluginManager {
private final Log log = LogFactory.getLog(this.getClass());
private final AbstractTypeServerPluginContainer parentPluginContainer;
/**
* The map of all plugin environments keyed on plugin name.
*/
private final Map<String, ServerPluginEnvironment> loadedPlugins;
/**
* Indicates which plugins are enabled and which are disabled; keyed on plugin name.
*/
private final Map<String, Boolean> enabledPlugins;
/**
* To avoid having to create contexts multiple times for the same plugin,
* contexts are cached here, keyed on plugin name.
*/
private final Map<String, ServerPluginContext> pluginContextCache;
/**
* Cached instances of objects used to initialize and shutdown individual plugins.
* Only plugins that declare their own plugin component will have objects in this cache.
* This is keyed on plugin name.
*/
private final Map<String, ServerPluginComponent> pluginComponentCache;
/**
* Creates a plugin manager for the given plugin container.
*
* @param pc the plugin manager's owning plugin container
*/
public ServerPluginManager(AbstractTypeServerPluginContainer pc) {
this.parentPluginContainer = pc;
this.loadedPlugins = new HashMap<String, ServerPluginEnvironment>();
this.pluginContextCache = new HashMap<String, ServerPluginContext>();
this.pluginComponentCache = new HashMap<String, ServerPluginComponent>();
this.enabledPlugins = new HashMap<String, Boolean>();
}
/**
* Returns the plugin container that whose plugins are managed by this manager.
*
* @return the plugin container that owns this plugin manager
*/
public AbstractTypeServerPluginContainer getParentPluginContainer() {
return this.parentPluginContainer;
}
/**
* Returns the {@link ServerPluginEnvironment}s for every plugin this manager has loaded.
* The returned collection is a copy and not backed by this manager.
*
* @return environments for all the plugins
*/
public Collection<ServerPluginEnvironment> getPluginEnvironments() {
return new ArrayList<ServerPluginEnvironment>(this.loadedPlugins.values());
}
/**
* Given a plugin name, this returns that plugin's environment.
*
* <p>The plugin's name is defined in its plugin descriptor - specifically the XML root node's "name" attribute
* (e.g. <server-plugin name="thePluginName").</p>
*
* @param pluginName the plugin whose environment is to be returned
* @return given plugin's environment
*/
public ServerPluginEnvironment getPluginEnvironment(String pluginName) {
return this.loadedPlugins.get(pluginName);
}
/**
* Initializes the plugin manager to prepare it to start loading plugins.
*
* @throws Exception if failed to initialize
*/
protected void initialize() throws Exception {
log.debug("Plugin manager initializing");
return; // no-op
}
/**
* Shuts down this manager. This should be called only after all of its plugins
* have been {@link #unloadPlugin(ServerPluginEnvironment) unloaded}.
*/
protected void shutdown() {
log.debug("Plugin manager shutting down");
if (this.loadedPlugins.size() > 0) {
log.warn("Server plugin manager is being shutdown while some plugins are still loaded: "
+ this.loadedPlugins);
}
this.loadedPlugins.clear();
this.pluginContextCache.clear();
this.pluginComponentCache.clear();
this.enabledPlugins.clear();
return;
}
/**
* Informs the plugin manager that a plugin with the given environment needs to be loaded.
* Once this method returns, the plugin's components are ready to be created and used, unless
* <code>enabled</code> is <code>false</code>, in which case the plugin will not
* be initialized.
*
* @param env the environment of the plugin to be loaded
* @param enabled <code>true</code> if the plugin should be initialized; <code>false</code> if
* the plugin's existence should be noted but it should not be initialized or started
*
* @throws Exception if the plugin manager cannot load the plugin or deems the plugin invalid.
* Typically, this method will not throw an exception unless enabled is
* <code>true</code> - loading a disabled plugin is trivial and should not
* fail or throw an exception.
*/
protected void loadPlugin(ServerPluginEnvironment env, boolean enabled) throws Exception {
String pluginName = env.getPluginKey().getPluginName();
log.debug("Loading server plugin [" + pluginName + "] from: " + env.getPluginUrl());
if (enabled) {
// tell the plugin we are loading it
ServerPluginComponent component = null;
try {
component = createServerPluginComponent(env);
} catch (Throwable t) {
throw new Exception("Plugin component failed to be created for server plugin [" + pluginName + "]", t);
}
if (component != null) {
ServerPluginContext context = getServerPluginContext(env);
log.debug("Initializing plugin component for server plugin [" + pluginName + "]");
ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(env.getPluginClassLoader());
component.initialize(context);
} catch (Throwable t) {
throw new Exception("Plugin component failed to initialize server plugin [" + pluginName + "]", t);
} finally {
Thread.currentThread().setContextClassLoader(originalContextClassLoader);
}
this.pluginComponentCache.put(pluginName, component);
}
} else {
log.info("Server plugin [" + pluginName + "] is loaded but disabled");
}
// note that we only cache it if the plugin component was successful
this.loadedPlugins.put(pluginName, env);
this.enabledPlugins.put(pluginName, Boolean.valueOf(enabled));
return;
}
protected void startPlugins() {
log.debug("Starting server plugins");
for (String pluginName : this.pluginComponentCache.keySet()) {
startPlugin(pluginName);
}
log.debug("Server plugins started.");
return;
}
protected void startPlugin(String pluginName) {
if (isPluginEnabled(pluginName)) {
ServerPluginComponent component = this.pluginComponentCache.get(pluginName);
if (component != null) {
ServerPluginEnvironment env = this.loadedPlugins.get(pluginName);
log.debug("Starting plugin component for server plugin [" + pluginName + "]");
ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader();
try {
if (env == null) {
throw new Exception("Plugin [" + pluginName + "] was never loaded");
}
Thread.currentThread().setContextClassLoader(env.getPluginClassLoader());
component.start();
} catch (Throwable t) {
log.warn("Plugin component failed to start plugin [" + pluginName + "]", t);
} finally {
Thread.currentThread().setContextClassLoader(originalContextClassLoader);
}
}
}
return;
}
protected void stopPlugins() {
log.debug("Stopping server plugins");
for (String pluginName : this.pluginComponentCache.keySet()) {
stopPlugin(pluginName);
}
log.debug("Server plugins stopped.");
return;
}
protected void stopPlugin(String pluginName) {
ServerPluginComponent component = this.pluginComponentCache.get(pluginName);
if (component != null) {
ServerPluginEnvironment env = this.loadedPlugins.get(pluginName);
log.debug("Stopping plugin component for server plugin [" + pluginName + "]");
ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader();
try {
if (env == null) {
throw new Exception("Plugin [" + pluginName + "] was never loaded");
}
Thread.currentThread().setContextClassLoader(env.getPluginClassLoader());
component.stop();
} catch (Throwable t) {
log.warn("Plugin component failed to stop plugin [" + pluginName + "]", t);
} finally {
Thread.currentThread().setContextClassLoader(originalContextClassLoader);
}
}
return;
}
/**
* Informs the plugin manager that a plugin with the given name is to be unloaded.
* The component's shutdown method will be called.
*
* @param pluginName the name of the plugin to be unloaded
*
* @throws Exception if the plugin manager cannot unload the plugin
*/
protected void unloadPlugin(String pluginName) throws Exception {
try {
ServerPluginEnvironment env = getPluginEnvironment(pluginName);
if (env == null) {
log.debug("Server plugin [" + pluginName + "] was never loaded, ignoring unload request");
return;
}
log.debug("Unloading server plugin [" + pluginName + "]");
// tell the plugin we are unloading it
ServerPluginComponent component = this.pluginComponentCache.get(pluginName);
if (component != null) {
log.debug("Shutting down plugin component for server plugin [" + pluginName + "]");
ClassLoader originalContextClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(env.getPluginClassLoader());
component.shutdown();
} catch (Throwable t) {
throw new Exception("Plugin component failed to shutdown server plugin [" + pluginName + "]", t);
} finally {
Thread.currentThread().setContextClassLoader(originalContextClassLoader);
this.pluginComponentCache.remove(pluginName);
}
}
} finally {
this.loadedPlugins.remove(pluginName);
this.enabledPlugins.remove(pluginName);
this.pluginContextCache.remove(pluginName);
}
return;
}
/**
* Informs the plugin manager that a plugin with the given name is to be unloaded.
* Once this method returns, the plugin's components should not be created or used.
*
* If <code>keepClassLoader</code> is <code>true</code>, this is the same as
* {@link #unloadPlugin(String)}.
*
* You want to keep the classloader if you are only temporarily unloading the plugin, and
* will load it back soon.
*
* Subclasses of this plugin manager class will normally not override this method; instead,
* they will typically want to override {@link #unloadPlugin(String)}.
*
* @param pluginName the name of the plugin to be unloaded
* @param keepClassLoader if <code>true</code> the classloader is not destroyed
* @throws Exception if the plugin manager cannot unload the plugin
*/
protected void unloadPlugin(String pluginName, boolean keepClassLoader) throws Exception {
try {
unloadPlugin(pluginName);
} finally {
if (!keepClassLoader) {
String pluginType = getParentPluginContainer().getSupportedServerPluginType().stringify();
PluginKey pluginKey = PluginKey.createServerPluginKey(pluginType, pluginName);
MasterServerPluginContainer master = this.parentPluginContainer.getMasterServerPluginContainer();
master.getClassLoaderManager().unloadPlugin(pluginKey);
}
}
return;
}
/**
* This will reload a plugin allowing you to enable or disable it.
* This will {@link #startPlugin(String) start the plugin component} if you enable it.
* This will {@link #stopPlugin(String) stop the plugin component} if you disable it.
* This will ensure any new plugin configuration will be re-loaded.
*
* @param pluginName the name of the loaded plugin that is to be enabled or disabled
* @param enabled <code>true</code> if you want to enable the plugin; <code>false</code>
* if you want to disable it
* @throws Exception if the plugin was never loaded before or the reload failed
*/
protected void reloadPlugin(String pluginName, boolean enabled) throws Exception {
if (enabled) {
enablePlugin(pluginName);
} else {
disablePlugin(pluginName);
}
return;
}
protected void enablePlugin(String pluginName) throws Exception {
log.info("Enabling server plugin [" + pluginName + "]");
ServerPluginEnvironment env = getPluginEnvironment(pluginName);
if (env == null) {
throw new IllegalArgumentException("Server plugin [" + pluginName + "] was never loaded, cannot enable it");
}
stopPlugin(pluginName); // under normal circumstances, we should not need to do this, but just in case the plugin is somehow already started, stop it
unloadPlugin(pluginName, true); // unloading it will clean up old data and force the plugin context to reload
env = rebuildServerPluginEnvironment(env);
try {
// reload it in the enabled state.
loadPlugin(env, true);
} catch (Exception e) {
// we've already unloaded it - so even though we failed to enable it, we need to load it back, albeit disabled
loadPlugin(env, false);
throw e;
}
startPlugin(pluginName); // since we are enabling the plugin, immediately start it
return;
}
protected void disablePlugin(String pluginName) throws Exception {
log.info("Disabling server plugin [" + pluginName + "]");
ServerPluginEnvironment env = getPluginEnvironment(pluginName);
if (env == null) {
throw new IllegalArgumentException("Server plugin [" + pluginName + "] was never loaded, cannot disable it");
}
stopPlugin(pluginName);
unloadPlugin(pluginName, true); // unloading it will clean up old data and force the plugin context to reload if we later re-enable it
env = rebuildServerPluginEnvironment(env);
loadPlugin(env, false); // re-load it in the disabled state
return;
}
protected boolean isPluginLoaded(String pluginName) {
return this.loadedPlugins.containsKey(pluginName);
}
protected boolean isPluginEnabled(String pluginName) {
return Boolean.TRUE.equals(this.enabledPlugins.get(pluginName));
}
/**
* Returns the main plugin component instance that is responsible for initializing and managing
* the plugin. This will return <code>null</code> if a plugin has not defined a plugin component.
*
* @param pluginName the name of the plugin whose plugin component is to be returned
*
* @return the plugin component instance that initialized and is managing a plugin. Will
* return <code>null</code> if the plugin has not defined a plugin component.
* <code>null</code> is also returned if the plugin is not initialized yet.
*/
protected ServerPluginComponent getServerPluginComponent(String pluginName) {
return this.pluginComponentCache.get(pluginName);
}
protected Log getLog() {
return this.log;
}
protected ServerPluginContext getServerPluginContext(ServerPluginEnvironment env) {
String pluginName = env.getPluginKey().getPluginName();
ServerPluginContext context = this.pluginContextCache.get(pluginName);
// if we already created it, return it immediately and don't create another
if (context != null) {
return context;
}
MasterServerPluginContainer masterPC = this.parentPluginContainer.getMasterServerPluginContainer();
MasterServerPluginContainerConfiguration masterConfig = masterPC.getConfiguration();
File dataDir = new File(masterConfig.getDataDirectory(), pluginName);
File tmpDir = masterConfig.getTemporaryDirectory();
Configuration pluginConfig = null;
List<ScheduledJobDefinition> schedules;
try {
ServerPlugin plugin = getPlugin(env);
pluginConfig = plugin.getPluginConfiguration();
Configuration scheduledJobsConfig = plugin.getScheduledJobsConfiguration();
schedules = ServerPluginDescriptorMetadataParser.getScheduledJobs(scheduledJobsConfig);
} catch (Exception e) {
throw new RuntimeException("Failed to get plugin config/schedules from the database", e);
}
context = new ServerPluginContext(env, dataDir, tmpDir, pluginConfig, schedules);
this.pluginContextCache.put(pluginName, context);
return context;
}
/**
* Given a plugin environment, this will rebuild a new one with up-to-date information.
* This means the descriptor will be reparsed.
*
* @param env the original environment
* @return the new environment that has been rebuild from the original but has newer data
* @throws Exception if the environment could not be rebuilt - probably due to an invalid descriptor
* in the plugin jar or the plugin jar is now missing
*/
protected ServerPluginEnvironment rebuildServerPluginEnvironment(ServerPluginEnvironment env) throws Exception {
URL url = env.getPluginUrl();
ClassLoader classLoader = env.getPluginClassLoader();
ServerPluginDescriptorType descriptor = ServerPluginDescriptorUtil.loadPluginDescriptorFromUrl(url);
ServerPluginEnvironment newEnv = new ServerPluginEnvironment(url, classLoader, descriptor);
return newEnv;
}
/**
* Given a plugin environment, return its {@link ServerPlugin} representation, which should also include
* the plugin configuration and scheduled jobs configuration.
*
* @param pluginEnv
* @return the ServerPlugin object for the given plugin
*/
protected ServerPlugin getPlugin(ServerPluginEnvironment pluginEnv) {
// get the plugin data from the database
ServerPluginManagerLocal serverPluginsManager = LookupUtil.getServerPluginManager();
ServerPlugin plugin = serverPluginsManager.getServerPlugin(pluginEnv.getPluginKey());
plugin = serverPluginsManager.getServerPluginRelationships(plugin);
return plugin;
}
/**
* This will create a new {@link ServerPluginComponent} instance for that is used to
* initialize and shutdown a particular server plugin. If there is no plugin component
* configured for the given plugin, <code>null</code> is returned.
*
* The new object will be loaded in the plugin's specific classloader.
*
* @param environment the environment in which the plugin will execute
*
* @return a new object loaded in the proper plugin classloader that can initialize/shutdown the plugin,
* or <code>null</code> if there is no plugin component to be associated with the given plugin
*
* @throws Exception if failed to create the instance
*/
protected ServerPluginComponent createServerPluginComponent(ServerPluginEnvironment environment) throws Exception {
String pluginName = environment.getPluginKey().getPluginName();
ServerPluginComponent instance = null;
ServerPluginDescriptorType descriptor = environment.getPluginDescriptor();
String className = ServerPluginDescriptorMetadataParser.getPluginComponentClassName(descriptor);
if (className != null) {
log.debug("Creating plugin component [" + className + "] for plugin [" + pluginName + "]");
instance = (ServerPluginComponent) instantiatePluginClass(environment, className);
log.debug("Plugin component created [" + instance.getClass() + "] for plugin [" + pluginName + "]");
}
return instance;
}
/**
* Loads a class with the given name within the given environment's classloader.
* The class will only be initialized if <code>initialize</code> is <code>true</code>.
*
* @param environment the environment that has the classloader where the class will be loaded
* @param className the class to load
* @param initialize whether the class must be initialized
* @return the new class that has been loaded
* @throws Exception if failed to load the class
*/
protected Class<?> loadPluginClass(ServerPluginEnvironment environment, String className, boolean initialize)
throws Exception {
ClassLoader loader = environment.getPluginClassLoader();
ServerPluginDescriptorType descriptor = environment.getPluginDescriptor();
className = ServerPluginDescriptorMetadataParser.getFullyQualifiedClassName(descriptor, className);
log.debug("Loading server plugin class [" + className + "]...");
Class<?> clazz;
try {
clazz = Class.forName(className, initialize, loader);
} catch (ClassNotFoundException e) {
throw new Exception("Could not find plugin class [" + className + "] from plugin environment ["
+ environment + "]", e);
} catch (NoClassDefFoundError e) {
throw new Exception("No class definition for plugin class [" + className + "] from plugin environment ["
+ environment + "]", e);
} catch (NullPointerException npe) {
throw new Exception("Plugin class was 'null' in plugin environment [" + environment + "]", npe);
} catch (Error e) {
// wrap error (e.g. NoClassDefFoundError) so anyone catching Exception will catch this
throw new Exception("Can not load plugin class [" + className + "] from plugin environment [" + environment
+ "]", e);
}
log.debug("Loaded server plugin class [" + clazz + "]. initialized=[" + initialize + ']');
return clazz;
}
/**
* Instantiates a class with the given name within the given environment's classloader using
* the class' no-arg constructor.
*
* @param environment the environment that has the classloader where the class will be loaded
* @param className the class to instantiate
* @return the new object that is an instance of the given class
* @throws Exception if failed to instantiate the class
*/
protected Object instantiatePluginClass(ServerPluginEnvironment environment, String className) throws Exception {
try {
Class<?> clazz = loadPluginClass(environment, className, true);
log.debug("Instantiating server plugin class [" + clazz + "]");
return clazz.newInstance();
} catch (InstantiationException e) {
throw new Exception("Could not instantiate plugin class [" + className + "] from plugin environment ["
+ environment + "]", e);
} catch (IllegalAccessException e) {
throw new Exception("Could not access plugin class [" + className + "] from plugin environment ["
+ environment + "]", e);
}
}
}