All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.kantega.jexmec.manager.DefaultPluginManager Maven / Gradle / Ivy

There is a newer version: 2.0.0rc8
Show newest version
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; } } }