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

com.github.kristofa.servicepluginloader.ServicePluginLoader Maven / Gradle / Ivy

There is a newer version: 0.2
Show newest version
package com.github.kristofa.servicepluginloader;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.annotation.PreDestroy;

import org.apache.commons.lang3.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * {@link ServicePluginLoader} is used to discover and load services that are extensions/plugins to your application.
 * 

* Each plugin will have its own {@link ClassLoader} and classpath. In this way we avoid classpath collisions between * plugins. The services are loaded from external resources (jar files, directories, over the network) that are initially not * part of the classpath of our application. *

* This {@link ServicePluginLoader} uses the {@link java.util.ServiceLoader} underneath but extends it with the possibility * to define custom queryable properties for services. *

* Services are discovered and loaded lazily when calling any of the load methods for the first time. Once loaded they are * cached till close() or reload() is called. * * @author kristof * @param The type of services we want to discover and load. */ public class ServicePluginLoader { private final static Logger LOGGER = LoggerFactory.getLogger(ServicePluginLoader.class); private final static Properties EMPTY_PROPERTIES = new Properties(); private final Map>> serviceMap = new HashMap>>(); private final ServicePluginsClassPathProvider pluginsClassPathProvider; private final Class clazz; private boolean initialized = false; /** * Creates a new instance. * * @param clazz Service Plugin type. * @param pluginProvider Provides the classpath for each of our plugins. Should not be null. */ public ServicePluginLoader(final Class clazz, final ServicePluginsClassPathProvider pluginProvider) { Validate.notNull(clazz); Validate.notNull(pluginProvider); this.clazz = clazz; this.pluginsClassPathProvider = pluginProvider; } /** * Discovers and loads plugins without filtering on properties. * * @return Collection of plugins. Collection can be empty in case we can't find plugins for given type. */ public Collection> load() { return load(EMPTY_PROPERTIES); } /** * Discovers and loads plugins. * * @param properties Additional filter on plugins. Should not be null. A plugin will be returned if it has * all given properties set. If these properties are a subset of the properties defined by the plugin it will * also be returned. If empty properties object is passed in all plugins will be returned. * @return Collection of plugins. Collection can be empty in case we can't find matching plugins. */ public Collection> load(final Properties properties) { Validate.notNull(clazz); Validate.notNull(properties); synchronized (this) { if (!initialized) { try { load(clazz); } catch (final MalformedURLException e) { throw new IllegalStateException(e); } initialized = true; } } final Collection> collection = serviceMap.get(properties); if (collection != null) { return Collections.unmodifiableCollection(collection); } // See if the properties we want are a subset of 1 or more plugins. return matchSubset(properties); } /** * Removes all loaded plugins. In case plugins implement {@link Closeable} interface they will be closed prior to * removal. *

* Plugins, including any new added ones, will be loaded when calling any of the load methods. */ public void reload() { close(); } /** * Removes all loaded plugins. In case plugins implement {@link Closeable} interface they will be closed prior to * removal. *

* If you call any of the load methods after close method service plugins will be reinitialized. */ @PreDestroy public void close() { for (final Collection> collection : serviceMap.values()) { for (final ServicePlugin servicePlugin : collection) { final T plugin = servicePlugin.getPlugin(); if (plugin instanceof Closeable) { try { ((Closeable)plugin).close(); } catch (final IOException e) { LOGGER.warn("Failed to close plugin.", e); } } else { break; } } } serviceMap.clear(); initialized = false; } private Collection> matchSubset(final Properties wantedProperties) { final Collection> servicePlugins = new ArrayList>(); for (final Properties pluginProps : serviceMap.keySet()) { boolean match = true; for (final Object wantedKey : wantedProperties.keySet()) { final String foundPropertyValue = pluginProps.getProperty(wantedKey.toString()); if (foundPropertyValue == null) { match = false; break; } final String wantedValue = wantedProperties.getProperty(wantedKey.toString()); if (!wantedValue.equals(foundPropertyValue)) { match = false; break; } } if (match) { servicePlugins.addAll(serviceMap.get(pluginProps)); } } return servicePlugins; } private void load(final Class clazz) throws MalformedURLException { for (final ServicePluginClassPath extension : pluginsClassPathProvider.getPlugins()) { final URL[] urls = getArray(extension); final URLClassLoader urlClassLoader = new URLClassLoader(urls); final java.util.ServiceLoader loader = java.util.ServiceLoader.load(clazz, urlClassLoader); final Iterator iterator = loader.iterator(); while (iterator.hasNext()) { final T instance = iterator.next(); final Properties properties = loadPropertiesForClass(urlClassLoader, instance.getClass().getName()); Collection> collection = serviceMap.get(properties); if (collection == null) { collection = new ArrayList>(); serviceMap.put(properties, collection); } collection.add(new ServicePlugin(instance, properties)); } } } private URL[] getArray(final ServicePluginClassPath extension) { final URL[] urls = new URL[extension.getUrls().size()]; int i = 0; for (final URL url : extension.getUrls()) { urls[i] = url; i++; } return urls; } private Properties loadPropertiesForClass(final ClassLoader classLoader, final String className) { final Properties properties = new Properties(); final String propertiesFileName = "META-INF/services/" + className + ".properties"; final InputStream resourceAsStream = classLoader.getResourceAsStream(propertiesFileName); if (resourceAsStream == null) { return properties; } try { properties.load(resourceAsStream); } catch (final IOException e) { throw new IllegalStateException("Error when loading " + propertiesFileName, e); } return properties; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy