com.twelvemonkeys.util.service.ServiceRegistry Maven / Gradle / Ivy
Show all versions of common-lang Show documentation
/*
* Copyright (c) 2008, Harald Kuhr
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.twelvemonkeys.util.service;
import com.twelvemonkeys.lang.Validate;
import com.twelvemonkeys.util.FilterIterator;
import java.io.IOException;
import java.net.URL;
import java.util.*;
/**
* A registry for service provider objects.
*
* Service providers are looked up from the classpath, under the path
* {@code META-INF/services/}<full-class-name>.
*
*
* For example:
*
* {@code META-INF/services/com.company.package.spi.MyService}.
*
*
* The file should contain a list of fully-qualified concrete class names,
* one per line.
*
*
* The full-class-name represents an interface or (typically) an
* abstract class, and is the same class used as the category for this registry.
* Note that only one instance of a concrete subclass may be registered with a
* specific category at a time.
*
*
* Implementation detail: This class is a clean room implementation of
* a service registry and does not use the proprietary {@code sun.misc.Service}
* class that is referred to in the JAR File specification.
* This class should work on any Java platform.
*
*
*
* @author Harald Kuhr
* @version $Id: com/twelvemonkeys/util/service/ServiceRegistry.java#2 $
* @see RegisterableService
* @see JAR File Specification
*/
public class ServiceRegistry {
// TODO: Security issues?
// TODO: Application contexts? Probably use instance per thread group..
/**
* "META-INF/services/"
*/
public static final String SERVICES = "META-INF/services/";
// Class to CategoryRegistry mapping
private final Map, CategoryRegistry> categoryMap;
/**
* Creates a {@code ServiceRegistry} instance with a set of categories
* taken from the {@code pCategories} argument.
*
* The categories are constant during the lifetime of the registry, and may
* not be changed after initial creation.
*
*
* @param pCategories an {@code Iterator} containing
* {@code Class} objects that defines this registry's categories.
* @throws IllegalArgumentException if {@code pCategories} is {@code null}.
* @throws ClassCastException if {@code pCategories} contains anything
* but {@code Class} objects.
*/
public ServiceRegistry(final Iterator extends Class>> pCategories) {
Validate.notNull(pCategories, "categories");
Map, CategoryRegistry> map = new LinkedHashMap, CategoryRegistry>();
while (pCategories.hasNext()) {
putCategory(map, pCategories.next());
}
// NOTE: Categories are constant for the lifetime of a registry
categoryMap = Collections.unmodifiableMap(map);
}
private void putCategory(Map, CategoryRegistry> pMap, Class pCategory) {
CategoryRegistry registry = new CategoryRegistry(pCategory);
pMap.put(pCategory, registry);
}
/**
* Registers all provider implementations for this {@code ServiceRegistry}
* found in the application classpath.
*
* @throws ServiceConfigurationError if an error occurred during registration
*/
public void registerApplicationClasspathSPIs() {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Iterator> categories = categories();
while (categories.hasNext()) {
Class> category = categories.next();
try {
// Find all META-INF/services/ + name on class path
String name = SERVICES + category.getName();
Enumeration spiResources = loader.getResources(name);
while (spiResources.hasMoreElements()) {
URL resource = spiResources.nextElement();
registerSPIs(resource, category, loader);
}
}
catch (IOException e) {
throw new ServiceConfigurationError(e);
}
}
}
/**
* Registers all SPIs listed in the given resource.
*
* @param pResource the resource to load SPIs from
* @param pCategory the category class
* @param pLoader the class loader to use
*/
void registerSPIs(final URL pResource, final Class pCategory, final ClassLoader pLoader) {
Properties classNames = new Properties();
try {
classNames.load(pResource.openStream());
}
catch (IOException e) {
throw new ServiceConfigurationError(e);
}
if (!classNames.isEmpty()) {
@SuppressWarnings({"unchecked"})
CategoryRegistry registry = categoryMap.get(pCategory);
Set providerClassNames = classNames.keySet();
for (Object providerClassName : providerClassNames) {
String className = (String) providerClassName;
try {
@SuppressWarnings({"unchecked"})
Class providerClass = (Class) Class.forName(className, true, pLoader);
T provider = providerClass.newInstance();
registry.register(provider);
}
catch (ClassNotFoundException e) {
throw new ServiceConfigurationError(e);
}
catch (IllegalAccessException e) {
throw new ServiceConfigurationError(e);
}
catch (InstantiationException e) {
throw new ServiceConfigurationError(e);
}
catch (IllegalArgumentException e) {
throw new ServiceConfigurationError(e);
}
}
}
}
/**
* Returns an {@code Iterator} containing all providers in the given
* category.
*
* The iterator supports removal.
*
*
*
* NOTE: Removing a provider from the iterator, deregisters the current
* provider (as returned by the last invocation of {@code next()}) from
* {@code pCategory}, it does not remove the provider
* from other categories in the registry.
*
*
*
* @param pCategory the category class
* @return an {@code Iterator} containing all providers in the given
* category.
* @throws IllegalArgumentException if {@code pCategory} is not a valid
* category in this registry
*/
protected Iterator providers(Class pCategory) {
return getRegistry(pCategory).providers();
}
/**
* Returns an {@code Iterator} containing all categories in this registry.
*
* The iterator does not support removal.
*
*
* @return an {@code Iterator} containing all categories in this registry.
*/
protected Iterator> categories() {
return categoryMap.keySet().iterator();
}
/**
* Returns an {@code Iterator} containing all categories in this registry
* the given {@code pProvider} may be registered with.
*
* The iterator does not support removal.
*
*
* @param pProvider the provider instance
* @return an {@code Iterator} containing all categories in this registry
* the given {@code pProvider} may be registered with
*/
protected Iterator> compatibleCategories(final Object pProvider) {
return new FilterIterator>(categories(),
new FilterIterator.Filter>() {
public boolean accept(Class> pElement) {
return pElement.isInstance(pProvider);
}
});
}
/**
* Returns an {@code Iterator} containing all categories in this registry
* the given {@code pProvider} is currently registered with.
*
* The iterator supports removal.
*
*
*
* NOTE: Removing a category from the iterator, de-registers
* {@code pProvider} from the current category (as returned by the last
* invocation of {@code next()}), it does not remove the category
* itself from the registry.
*
*
*
* @param pProvider the provider instance
* @return an {@code Iterator} containing all categories in this registry
* the given {@code pProvider} may be registered with
*/
protected Iterator> containingCategories(final Object pProvider) {
// TODO: Is removal using the iterator really a good idea?
return new FilterIterator>(categories(),
new FilterIterator.Filter>() {
public boolean accept(Class> pElement) {
return getRegistry(pElement).contains(pProvider);
}
}) {
Class> current;
public Class next() {
return (current = super.next());
}
public void remove() {
if (current == null) {
throw new IllegalStateException("No current element");
}
getRegistry(current).deregister(pProvider);
current = null;
}
};
}
/**
* Gets the category registry for the given category.
*
* @param pCategory the category class
* @return the {@code CategoryRegistry} for the given category
*/
private CategoryRegistry getRegistry(final Class pCategory) {
@SuppressWarnings({"unchecked"})
CategoryRegistry registry = categoryMap.get(pCategory);
if (registry == null) {
throw new IllegalArgumentException("No such category: " + pCategory.getName());
}
return registry;
}
/**
* Registers the given provider for all categories it matches.
*
* @param pProvider the provider instance
* @return {@code true} if {@code pProvider} is now registered in
* one or more categories it was not registered in before.
* @see #compatibleCategories(Object)
*/
public boolean register(final Object pProvider) {
Iterator> categories = compatibleCategories(pProvider);
boolean registered = false;
while (categories.hasNext()) {
Class> category = categories.next();
if (registerImpl(pProvider, category) && !registered) {
registered = true;
}
}
return registered;
}
private boolean registerImpl(final Object pProvider, final Class pCategory) {
return getRegistry(pCategory).register(pCategory.cast(pProvider));
}
/**
* Registers the given provider for the given category.
*
* @param pProvider the provider instance
* @param pCategory the category class
* @return {@code true} if {@code pProvider} is now registered in
* the given category
*/
public boolean register(final T pProvider, final Class super T> pCategory) {
return registerImpl(pProvider, pCategory);
}
/**
* De-registers the given provider from all categories it's currently
* registered in.
*
* @param pProvider the provider instance
* @return {@code true} if {@code pProvider} was previously registered in
* any category and is now de-registered.
* @see #containingCategories(Object)
*/
public boolean deregister(final Object pProvider) {
Iterator> categories = containingCategories(pProvider);
boolean deregistered = false;
while (categories.hasNext()) {
Class> category = categories.next();
if (deregister(pProvider, category) && !deregistered) {
deregistered = true;
}
}
return deregistered;
}
/**
* Deregisters the given provider from the given category.
*
* @param pProvider the provider instance
* @param pCategory the category class
* @return {@code true} if {@code pProvider} was previously registered in
* the given category
*/
public boolean deregister(final Object pProvider, final Class> pCategory) {
return getRegistry(pCategory).deregister(pProvider);
}
/**
* Keeps track of each individual category.
*/
class CategoryRegistry {
private final Class category;
private final Map providers = new LinkedHashMap();
CategoryRegistry(Class pCategory) {
Validate.notNull(pCategory, "category");
category = pCategory;
}
private void checkCategory(final Object pProvider) {
if (!category.isInstance(pProvider)) {
throw new IllegalArgumentException(pProvider + " not instance of category " + category.getName());
}
}
public boolean register(final T pProvider) {
checkCategory(pProvider);
// NOTE: We only register the new instance, if we don't already have an instance of pProvider's class.
if (!contains(pProvider)) {
providers.put(pProvider.getClass(), pProvider);
processRegistration(pProvider);
return true;
}
return false;
}
void processRegistration(final T pProvider) {
if (pProvider instanceof RegisterableService) {
RegisterableService service = (RegisterableService) pProvider;
service.onRegistration(ServiceRegistry.this, category);
}
}
public boolean deregister(final Object pProvider) {
checkCategory(pProvider);
// NOTE: We remove any provider of the same class, this may or may
// not be the same instance as pProvider.
T oldProvider = providers.remove(pProvider.getClass());
if (oldProvider != null) {
processDeregistration(oldProvider);
return true;
}
return false;
}
void processDeregistration(final T pOldProvider) {
if (pOldProvider instanceof RegisterableService) {
RegisterableService service = (RegisterableService) pOldProvider;
service.onDeregistration(ServiceRegistry.this, category);
}
}
public boolean contains(final Object pProvider) {
return providers.containsKey(pProvider != null ? pProvider.getClass() : null);
}
public Iterator providers() {
// NOTE: The iterator must support removal because deregistering
// using the deregister method will result in
// ConcurrentModificationException in the iterator..
// We wrap the iterator to track deregistration right.
final Iterator iterator = providers.values().iterator();
return new Iterator() {
T current;
public boolean hasNext() {
return iterator.hasNext();
}
public T next() {
return (current = iterator.next());
}
public void remove() {
iterator.remove();
processDeregistration(current);
}
};
}
}
@SuppressWarnings({"UnnecessaryFullyQualifiedName"})
public static void main(String[] pArgs) {
abstract class Spi {}
class One extends Spi {}
class Two extends Spi {}
ServiceRegistry testRegistry = new ServiceRegistry(
Arrays.>asList(
java.nio.charset.spi.CharsetProvider.class,
java.nio.channels.spi.SelectorProvider.class,
javax.imageio.spi.ImageReaderSpi.class,
javax.imageio.spi.ImageWriterSpi.class,
Spi.class
).iterator()
);
testRegistry.registerApplicationClasspathSPIs();
One one = new One();
Two two = new Two();
testRegistry.register(one, Spi.class);
testRegistry.register(two, Spi.class);
testRegistry.deregister(one);
testRegistry.deregister(one, Spi.class);
testRegistry.deregister(two, Spi.class);
testRegistry.deregister(two);
Iterator> categories = testRegistry.categories();
System.out.println("Categories: ");
while (categories.hasNext()) {
Class> category = categories.next();
System.out.println(" " + category.getName() + ":");
Iterator> providers = testRegistry.providers(category);
Object provider = null;
while (providers.hasNext()) {
provider = providers.next();
System.out.println(" " + provider);
if (provider instanceof javax.imageio.spi.ImageReaderWriterSpi) {
System.out.println(" - " + ((javax.imageio.spi.ImageReaderWriterSpi) provider).getDescription(null));
}
// javax.imageio.spi.ImageReaderWriterSpi provider = (javax.imageio.spi.ImageReaderWriterSpi) providers.next();
// System.out.println(" " + provider);
// System.out.println(" " + provider.getVendorName());
// System.out.println(" Formats:");
//
// System.out.print(" ");
// String[] formatNames = provider.getFormatNames();
// for (int i = 0; i < formatNames.length; i++) {
// if (i != 0) {
// System.out.print(", ");
// }
// System.out.print(formatNames[i]);
// }
// System.out.println();
// Don't remove last one, it's removed later to exercise more code :-)
if (providers.hasNext()) {
providers.remove();
}
}
// Remove the last item from all categories
if (provider != null) {
Iterator containers = testRegistry.containingCategories(provider);
int count = 0;
while (containers.hasNext()) {
if (category == containers.next()) {
containers.remove();
count++;
}
}
if (count != 1) {
System.err.println("Removed " + provider + " from " + count + " categories");
}
}
// Remove all using providers iterator
providers = testRegistry.providers(category);
if (!providers.hasNext()) {
System.out.println("All providers successfully deregistered");
}
while (providers.hasNext()) {
System.err.println("Not removed: " + providers.next());
}
}
}
//*/
}