org.kantega.jexmec.manager.DefaultPluginManager Maven / Gradle / Ivy
package org.kantega.jexmec.manager;
/*
* Copyright 2011 Kantega AS
*
* 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.
*/
import org.kantega.jexmec.*;
import org.kantega.jexmec.events.PluginClassLoaderEvent;
import org.kantega.jexmec.events.PluginLoadingExceptionEvent;
import org.kantega.jexmec.events.PluginManagerEvent;
import org.kantega.jexmec.events.PluginRegistrationEvent;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.*;
/**
* Default implementation of PluginManager. Tracks the ClassLoaders registered by {@link PluginClassLoaderProvider}s and uses the provided
* {@link org.kantega.jexmec.PluginLoader} implementations with {@link org.kantega.jexmec.ServiceLocator}s to load plugins from the ClassLoaders.
*/
public final class DefaultPluginManager implements PluginManager
{
private final Class
pluginClass;
private final List> pluginLoaders = new ArrayList>();
private final List pluginClassLoaderProviders = new ArrayList();
private final List serviceLocators = new ArrayList();
private final List> pluginManagerListeners = Collections.synchronizedList(new ArrayList>());
private final ServiceLocator serviceLocator = new CompoundServiceLocator(serviceLocators);
private final PluginRegistry registry = new PluginRegistry(serviceLocator);
private State state = State.UNSTARTED;
private final List defaultPluginClassLoaderProviders = new ArrayList();
/**
* Creates a DefaultPluginManager managing the given type of plugins
*
* @param pluginClass the class of the plugin to manage.
* @throws IllegalArgumentException if the plugin class is not an interface
*/
public DefaultPluginManager(Class pluginClass) {
if(!pluginClass.isInterface()) {
throw new IllegalArgumentException("Plugin class needs to be an interface: " + pluginClass);
}
this.pluginClass = pluginClass;
defaultPluginClassLoaderProviders.add(new StaticPluginClassLoaderProvider(getClass().getClassLoader()));
}
/**
* Adds a {@link org.kantega.jexmec.PluginClassLoaderProvider} to the plugin manager.
*
* @param pluginClassLoaderProvider the provider to add
* @return the plugin manager
*/
public DefaultPluginManager
addPluginClassLoaderProvider(PluginClassLoaderProvider pluginClassLoaderProvider) {
state.assertEquals(State.UNSTARTED, "adding plugin class loader provider " + pluginClassLoaderProvider);
defaultPluginClassLoaderProviders.clear();
pluginClassLoaderProviders.add(pluginClassLoaderProvider);
return this;
}
/**
* Adds a plugin classloader to the plugin manager. This method calls
* {@link #addPluginClassLoaderProvider(org.kantega.jexmec.PluginClassLoaderProvider)} with
* {@link PluginClassLoaderProvider} providing the classloader
*
* @param pluginClassloader the classloader to add a provider for
* @return the plugin manager
*/
public DefaultPluginManager
addPluginClassLoader(ClassLoader pluginClassloader) {
return addPluginClassLoaderProvider(new StaticPluginClassLoaderProvider(pluginClassloader));
}
/**
* Adds a {@link org.kantega.jexmec.PluginLoader} to the plugin manager.
*
* @param pluginLoader the loader to add
* @return the plugin manager
*/
public DefaultPluginManager
addPluginLoader(PluginLoader
pluginLoader) {
state.assertEquals(State.UNSTARTED, "adding plugin loader " + pluginLoader);
pluginLoaders.add(pluginLoader);
return this;
}
/**
* Adds a {@link org.kantega.jexmec.ServiceLocator} to the plugin manager.
*
* @param serviceLocator the locator to add
* @return the plugin manager
*/
public DefaultPluginManager
addServiceLocator(ServiceLocator serviceLocator) {
state.assertEquals(State.UNSTARTED, "adding service locator " + serviceLocator);
serviceLocators.add(serviceLocator);
return this;
}
/**
* Adds a single service mapping. Shortcut for calling addServiceLocator(new StaticServiceLocator(ServiceKey, Object))
*
* @param serviceKey the key to add a service for
* @param service the service implementation
* @return the plugin manager.
*/
public DefaultPluginManager addService(ServiceKey serviceKey, T service) {
return addServiceLocator(new StaticServiceLocator(serviceKey, service));
}
/**
* Creates an instance of a services interface. The instance is implemented as
* a dynamic proxy that will look up services the service locators added to this plugin manager
*
* @param servicesClass the service class to implement
* @param the service class type
* @return an instance of the service interface
*/
public S createServicesInstance(final Class servicesClass) {
return servicesClass.cast(Proxy.newProxyInstance(DefaultPluginManager.class.getClassLoader(),
new Class[]{servicesClass},
new InvocationHandler() {
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
ServiceKey> key = objects == null ?
ServiceKey.by(method.getReturnType()) : ServiceKey.by(method.getReturnType(), ((Enum) objects[0]).name());
return serviceLocator.lookupService(key);
}
}));
}
/**
* Creates a classloader hiding resource under META-INF/services/MyPluginClass
* and META-INF/services/com.mycompany.MyPluginClassCreates an {@link Iterable} of plugins that is a dynamic proxy to {@link DefaultPluginManager#getPlugins()}.
* Adding the plugin collection as a service gives plugins access to other plugins, but without the Jexmec
* compile dependency which adding DefaultPluginManager would introduce.
*
*
* To use this feature, add an interface
* public interface Plugins extends Iterable<MyPlugin> {}
to your plugin API. The interface should not
* add any methods, it is just created for type safety. Then create an
* instance of Plugins: Plugins plugins = pluginManager.createPluginIterable(Plugins.class);
.
* Finally, add the service to DefaultPluginManager: pluginManager.addService(ServiceKey.by(PluginList.class, pluginList));
*
*
* @param pluginCollectionClass As interface extending Collection<P;> where P is your plugin class
* @param the plugin type
* @param the collection type
* @return the collection instance
*/
@SuppressWarnings("unchecked")
public > T createPluginIterable(Class pluginCollectionClass) {
return (T) Proxy.newProxyInstance(DefaultPluginManager.class.getClassLoader(), new Class[]{pluginCollectionClass}, new InvocationHandler() {
public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
return method.invoke(getPlugins(), objects);
}
});
}
/**
* Adds any default plugin loaders or plugin class loader providers.
*/
private void addDefaults() {
pluginClassLoaderProviders.addAll(defaultPluginClassLoaderProviders);
}
/**
* Starts the plugin manager.
*
* @return the plugin manager
*/
public synchronized DefaultPluginManager start() {
state.assertEquals(State.UNSTARTED, "starting plugin manager");
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.beforePluginManagerStarted(new DefaultPluginManagerEvent
(this));
}
state = State.STARTING;
addDefaults();
startPluginLoaders();
startPluginClassLoaderProviders();
state = State.STARTED;
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.afterPluginManagerStarted(new DefaultPluginManagerEvent
(this));
}
return this;
}
/**
* Stops the plugin manager.
*
* @return the plugin manager.
*/
public synchronized DefaultPluginManager
stop() {
state.assertEquals(State.STARTED, "stopping plugin manager");
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.beforePluginManagerStopped(new DefaultPluginManagerEvent
(this));
}
state = State.STOPPING;
stopPluginClassLoaderProviders();
stopPluginLoaders();
state = State.STOPPED;
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.afterPluginManagerStopped(new DefaultPluginManagerEvent
(this));
}
return this;
}
/**
* Start all plugin classloader providers
*/
private void startPluginClassLoaderProviders() {
ClassLoader parentClassLoader = new ResourceHidingClassLoader(getClass().getClassLoader(), pluginClass);
for (PluginClassLoaderProvider provider : pluginClassLoaderProviders) {
provider.start(new ProviderAwarePluginClassLoaderRegistry(provider, registry), parentClassLoader);
}
}
/**
* Starts all plugin loaders
*/
private void startPluginLoaders() {
for (PluginLoader
pluginLoader : pluginLoaders) {
pluginLoader.start();
}
}
/**
* Stops all plugin loaders.
*/
private void stopPluginLoaders() {
for (PluginLoader
pluginLoader : pluginLoaders) {
pluginLoader.stop();
}
}
/**
* Stops all plugin classloader providers.
*/
private void stopPluginClassLoaderProviders() {
for (PluginClassLoaderProvider provider : pluginClassLoaderProviders) {
provider.stop();
}
}
/**
* Return a list of the plugins currently installed in this PluginManager
* @return the list of plugins
*/
public List
getPlugins() {
state.assertEquals(State.STARTED, "getting plugins. You need to call start() *before* calling getPlugins()");
return Collections.unmodifiableList(registry.getPlugins());
}
/**
* Register a PluginManagerListener for event notification.
* @param listener the listener to add
*/
public void addPluginManagerListener(PluginManagerListener
listener) {
pluginManagerListeners.add(listener);
}
/**
* Unregister a PluginManagerListener for event notification.
* @param listener the listener to remove
*/
public void removePluginManagerListener(PluginManagerListener
listener) {
pluginManagerListeners.remove(listener);
}
/**
* Return the ClassLoader that initiated the loading of the given plugin. Note that this
* need not be the same as plugin.getClassLoader()
* @param plugin the plugin for which to return the loading plugin
* @return the initiating ClassLoader
*/
public ClassLoader getClassLoader(P plugin) {
return registry.getClassLoader(plugin);
}
/**
* A registry keeping track of plugins.
*/
private class PluginRegistry implements PluginClassLoaderProvider.Registry {
private final ServiceLocator serviceLocator;
private final Map> plugins = new LinkedHashMap>();
private final Map classLoaderMap = new HashMap
();
public PluginRegistry(ServiceLocator serviceLocator) {
this.serviceLocator = serviceLocator;
}
public void replace(final Iterable removeClassLoaders, Iterable addClassLoaders) {
PluginClassLoaderProvider provider = ProviderAwarePluginClassLoaderRegistry.getCurrentProvider();
{
// Validate that all class loaders to remove are actually registered
synchronized (this) {
for (ClassLoader loader : removeClassLoaders) {
if(!plugins.containsKey(loader)) {
throw new IllegalArgumentException("Illegal request to remove a class loader that isn't currently registered: " + loader);
}
}
}
for (ClassLoader loader : removeClassLoaders) {
for (PluginManagerListener listener : pluginManagerListeners) {
listener.beforeClassLoaderRemoved(new DefaultPluginClassLoaderEvent
(DefaultPluginManager.this, provider, loader));
}
List
pluginsRemoved;
synchronized (this) {
pluginsRemoved = plugins.get(loader);
}
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.beforePassivation(new DefaultPluginRegistrationEvent
(DefaultPluginManager.this, provider, loader, pluginsRemoved));
}
synchronized (this) {
for (P plugin : pluginsRemoved) {
classLoaderMap.remove(plugin);
}
plugins.remove(loader);
}
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.afterPassivation(new DefaultPluginRegistrationEvent
(DefaultPluginManager.this, provider, loader, pluginsRemoved));
}
unloadPluginsFromClassLoader(loader);
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.afterClassLoaderRemoved(new DefaultPluginClassLoaderEvent
(DefaultPluginManager.this, provider, loader));
}
}
}
{
for (ClassLoader loader : addClassLoaders) {
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.beforeClassLoaderAdded(new DefaultPluginClassLoaderEvent
(DefaultPluginManager.this, provider, loader));
}
final List
loadedPlugins = loadPluginsFromClassLoader(provider, loader, serviceLocator);
if(loadedPlugins.size() > 0) {
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.beforeActivation(new DefaultPluginRegistrationEvent
(DefaultPluginManager.this, provider, loader, loadedPlugins));
}
}
synchronized (this) {
for (P plugin : loadedPlugins) {
classLoaderMap.put(plugin, loader);
}
plugins.put(loader, loadedPlugins);
}
if(loadedPlugins.size() > 0) {
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.afterActivation(new DefaultPluginRegistrationEvent
(DefaultPluginManager.this, provider, loader, loadedPlugins));
}
}
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.afterClassLoaderAdded(new DefaultPluginClassLoaderEvent
(DefaultPluginManager.this, provider, loader));
}
}
}
firePluginsUpdated();
}
public void add(Iterable classLoaders) {
replace(Collections.emptySet(), classLoaders);
}
public void remove(Iterable loaders) {
replace(loaders, Collections.emptySet());
}
/**
* Load plugins from a classloader using all plugin loaders.
*
* @param provider the plugin class loader provider which requested the class loader to be added
* @param serviceLocator the service locator needed by plugin loaders
* @param classLoader the classloader to load plugins from
* @return a list of loaded plugins.
*/
private List loadPluginsFromClassLoader(PluginClassLoaderProvider provider, ClassLoader classLoader, ServiceLocator serviceLocator) {
List
plugins = new ArrayList
();
for (PluginLoader
pluginLoader : pluginLoaders) {
try {
plugins.addAll(pluginLoader.loadPlugins(pluginClass, classLoader, serviceLocator));
} catch(Throwable e) {
fireExceptionLoadingPlugins(provider, classLoader, pluginLoader, e);
}
}
return plugins;
}
/**
* Notify listeners that an exception was thrown during plugin loading.
* @param provider the plugin class loader provider which requested classloader to be added
* @param classLoader the class loader which the plugin loader was loading
* @param pluginLoader the plugin loader which loading failed
* @param e the Throwable exception or error
*/
private void fireExceptionLoadingPlugins(PluginClassLoaderProvider provider, ClassLoader classLoader, PluginLoader
pluginLoader, Throwable e) {
for(PluginManagerListener
listener : pluginManagerListeners) {
listener.pluginLoadingFailedWithException(new DefaultPluginLoadingExceptionEvent
(DefaultPluginManager.this, provider, classLoader, pluginLoader, e));
}
}
/**
* Tell all plugin loaders to unload plugins loaded from the given class loader.
*
* @param loader the ClassLoader for which all plugins should be unloaded.
*/
private void unloadPluginsFromClassLoader(ClassLoader loader) {
for (PluginLoader
pluginLoader : pluginLoaders) {
pluginLoader.unloadPlugins(loader);
}
}
/**
* Return currently installed plugins
*
* @return a list of installed plugins
*/
public List
getPlugins() {
List
plugins = new ArrayList
();
synchronized (this) {
for (List
loaderPlugins : this.plugins.values()) {
plugins.addAll(loaderPlugins);
}
}
return plugins;
}
/**
* Returns the classloader that caused the loading of the given plugin.
*
* @param plugin the plugin
* @return the classloader which the plugin was loaded from
*/
public ClassLoader getClassLoader(P plugin) {
return classLoaderMap.get(plugin);
}
private void firePluginsUpdated() {
for (PluginManagerListener
listener : pluginManagerListeners) {
listener.pluginsUpdated(registry.getPlugins());
}
}
}
/**
* A service locator that wraps a collecton of service locators. Services are looked up in
* the collection order. The first non null service is returned. If no services are found, service
* lookup returns null.
*/
private class CompoundServiceLocator implements ServiceLocator {
private final Collection serviceLocators;
public CompoundServiceLocator(Collection serviceLocators) {
this.serviceLocators = serviceLocators;
}
/**
* Gets the the set of service keys supported by the collection
* of service locators.
*
* @return the combined set of service keys
*/
public Set getServiceKeys() {
Set serviceKeys = new HashSet();
for (ServiceLocator serviceLocator : serviceLocators) {
serviceKeys.addAll(serviceLocator.getServiceKeys());
}
return serviceKeys;
}
/**
* Returns the first non-null service implementation matching the given key. If no service is
* found, null is returned.
*
* @param serviceKey the service key class to find an implementation for
* @param the type of the service
* @return the service instance or null if the service was not found.
*/
public T lookupService(ServiceKey serviceKey) {
ServiceLocator first = null;
for (Iterator i = serviceLocators.iterator(); first == null && i.hasNext();) {
ServiceLocator candidate = i.next();
first = candidate.getServiceKeys().contains(serviceKey) ? candidate : null;
}
return first != null ? first.lookupService(serviceKey) : null;
}
}
private static class StaticServiceLocator implements ServiceLocator {
private final Map services;
public StaticServiceLocator(ServiceKey serviceKey, Object service) {
this.services = Collections.singletonMap(serviceKey, service);
}
public Set getServiceKeys() {
return services.keySet();
}
public T lookupService(ServiceKey serviceKey) {
return serviceKey.getServiceType().cast(services.get(serviceKey));
}
}
private static class StaticPluginClassLoaderProvider implements PluginClassLoaderProvider {
private final List classLoaders = new ArrayList();
private Registry registry;
public StaticPluginClassLoaderProvider(ClassLoader... classLoaders) {
this.classLoaders.addAll(Arrays.asList(classLoaders));
}
public void start(Registry registry, ClassLoader parentClassLoader) {
this.registry = registry;
registry.add(classLoaders);
}
public void stop() {
registry.remove(classLoaders);
}
}
class ResourceHidingClassLoader extends ClassLoader {
private final String[] localResourcePrefixes;
/**
* Creates a ResourceHidingClassLoader hiding resources in META-INF/services/PluginName/
and
* META-INF/services/com.example.PluginName/
.
*
* @param parent the parent class loader
* @param pluginClass the plugin class to hide resources for.
*/
public ResourceHidingClassLoader(ClassLoader parent, Class pluginClass) {
super(parent);
localResourcePrefixes = new String[]{"META-INF/services/" + pluginClass.getSimpleName() + "/",
"META-INF/services/" + pluginClass.getName() + "/"};
}
@Override
public InputStream getResourceAsStream(String name) {
final URL resource = getResource(name);
try {
return resource == null ? null : resource.openStream();
} catch (IOException e) {
return null;
}
}
@Override
public URL getResource(String name) {
if (isLocalResource(name)) {
return super.findResource(name);
} else {
return super.getResource(name);
}
}
@Override
public Enumeration getResources(String name) throws IOException {
if (isLocalResource(name)) {
return super.findResources(name);
} else {
return super.getResources(name);
}
}
protected boolean isLocalResource(String name) {
for (String localResourcePrefix : localResourcePrefixes) {
if (name.startsWith(localResourcePrefix)) {
return true;
}
}
return false;
}
}
private static class ProviderAwarePluginClassLoaderRegistry implements PluginClassLoaderProvider.Registry {
private static final ThreadLocal providerThreadLocal = new ThreadLocal();
private final PluginClassLoaderProvider wrappedProvider;
private final PluginClassLoaderProvider.Registry wrappedRegistry;
public ProviderAwarePluginClassLoaderRegistry(PluginClassLoaderProvider wrappedProvider, PluginClassLoaderProvider.Registry wrappedRegistry) {
this.wrappedProvider = wrappedProvider;
this.wrappedRegistry = wrappedRegistry;
}
public void add(Iterable classLoaders) {
providerThreadLocal.set(wrappedProvider);
try {
wrappedRegistry.add(classLoaders);
} finally {
providerThreadLocal.remove();
}
}
public void remove(Iterable classLoaders) {
providerThreadLocal.set(wrappedProvider);
try {
wrappedRegistry.remove(classLoaders);
} finally {
providerThreadLocal.remove();
}
}
public void replace(Iterable removeClassLoaders, Iterable addClassLoaders) {
try {
providerThreadLocal.set(wrappedProvider);
wrappedRegistry.replace(removeClassLoaders, addClassLoaders);
} finally {
providerThreadLocal.remove();
}
}
public static PluginClassLoaderProvider getCurrentProvider() {
return providerThreadLocal.get();
}
}
private enum State {
UNSTARTED, STARTING, STARTED, STOPPING, STOPPED;
/**
* Throws an IllegalStateException if the actual state is different from the expected
state.
*
* @param expected the expected state
* @param when description of when this state assertion failed
*/
public void assertEquals(State expected, String when) {
if (this != expected) {
throw new IllegalStateException("Illegal state " + this + ". Expected " + expected + " when " + when);
}
}
}
static class DefaultPluginManagerEvent implements PluginManagerEvent
{
private final DefaultPluginManager
pluginManger;
public DefaultPluginManagerEvent(DefaultPluginManager
pluginManger) {
this.pluginManger = pluginManger;
}
public DefaultPluginManager
getPluginManager() {
return pluginManger;
}
}
static class DefaultPluginClassLoaderEvent
extends DefaultPluginManagerEvent
implements PluginClassLoaderEvent
{
private final PluginClassLoaderProvider provider;
private final ClassLoader classLoader;
public DefaultPluginClassLoaderEvent(DefaultPluginManager
pluginManger, PluginClassLoaderProvider provider, ClassLoader classLoader) {
super(pluginManger);
this.provider = provider;
this.classLoader = classLoader;
}
public PluginClassLoaderProvider getProvider() {
return provider;
}
public ClassLoader getClassLoader() {
return classLoader;
}
}
static class DefaultPluginRegistrationEvent
extends DefaultPluginClassLoaderEvent
implements PluginRegistrationEvent
{
private final List
plugins;
DefaultPluginRegistrationEvent(DefaultPluginManager
pluginManger, PluginClassLoaderProvider provider, ClassLoader classLoader, List
plugins) {
super(pluginManger, provider, classLoader);
this.plugins = plugins;
}
public List
getPlugins() {
return this.plugins;
}
}
static class DefaultPluginLoadingExceptionEvent
extends DefaultPluginClassLoaderEvent
implements PluginLoadingExceptionEvent
{
private final PluginLoader
pluginLoader;
private final Throwable throwable;
DefaultPluginLoadingExceptionEvent(DefaultPluginManager
pluginManger, PluginClassLoaderProvider provider, ClassLoader classLoader, PluginLoader
pluginLoader, Throwable throwable) {
super(pluginManger, provider, classLoader);
this.pluginLoader = pluginLoader;
this.throwable = throwable;
}
public PluginLoader
getPluginLoader() {
return pluginLoader;
}
public Throwable getThrowable() {
return throwable;
}
}
}