/*
* JBoss, Home of Professional Open Source
* Copyright 2009, Red Hat Middleware LLC, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* 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.
*/
package org.jboss.shrinkwrap.impl.base;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.logging.Logger;
import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.ArchiveFormat;
import org.jboss.shrinkwrap.api.Assignable;
import org.jboss.shrinkwrap.api.ClassLoaderSearchUtilDelegator;
import org.jboss.shrinkwrap.api.ConfigurationBuilder;
import org.jboss.shrinkwrap.api.ExtensionLoader;
import org.jboss.shrinkwrap.api.UnknownExtensionTypeException;
import org.jboss.shrinkwrap.api.UnknownExtensionTypeExceptionDelegator;
/**
* ServiceExtensionLoader
*
* This class is the default strategy to load extensions when an instance of {@link ExtensionLoader} is not provided to
* the {@link ConfigurationBuilder} and the {@link ConfigurationBuilder#build()} method is invoked. If the
* {@link ConfigurationBuilder} doesn't provide any {@link ClassLoader}, {@link ConfigurationBuilder#build()} defaults
* to a one-element collection holding the TCCL. The {@link ServiceExtensionLoader#classLoaders} are used to find the
* provider-configuration file for the extension to be loaded in META-INF/services/. This provider-configuration file is
* used to make an instance of the SPI implementation and cached in {@link ServiceExtensionLoader#cache}.
*
* @author <a href="mailto:aslak@conduct.no">Aslak Knutsen</a>
* @author <a href="mailto:ken@glxn.net">Ken Gullaksen</a>
* @version $Revision: $
*/
public class ServiceExtensionLoader implements ExtensionLoader {
// -------------------------------------------------------------------------------------||
// Class Members ----------------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* Logger
*/
@SuppressWarnings("unused")
private static final Logger log = Logger.getLogger(ServiceExtensionLoader.class.getName());
// -------------------------------------------------------------------------------------||
// Instance Members -------------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
private Map<Class<?>, Class<?>> cache = new HashMap<Class<?>, Class<?>>();
private Map<Class<?>, ExtensionWrapper> extensionMappings = new HashMap<Class<?>, ExtensionWrapper>();
/**
* ClassLoader used for loading extensions
*/
private final Iterable<ClassLoader> classLoaders;
// -------------------------------------------------------------------------------------||
// Constructor ------------------------------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* Creates a new instance, using the specified {@link ClassLoader}s to create extensions
*
* @param classLoaders
* @throws IllegalArgumentException
* If the {@link ClassLoader} is not specified
*/
public ServiceExtensionLoader(final Iterable<ClassLoader> classLoaders) throws IllegalArgumentException {
if (classLoaders == null) {
throw new IllegalArgumentException("ClassLoader must be specified");
}
this.classLoaders = classLoaders;
}
// -------------------------------------------------------------------------------------||
// Required Implementations - ExtensionLoader -----------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* {@inheritDoc}
*
* @see org.jboss.shrinkwrap.api.ExtensionLoader#load(java.lang.Class, org.jboss.shrinkwrap.api.Archive)
*/
@Override
public <T extends Assignable> T load(Class<T> extensionClass, Archive<?> baseArchive)
throws UnknownExtensionTypeException {
if (isCached(extensionClass)) {
return createFromCache(extensionClass, baseArchive);
}
T object = createFromLoadExtension(extensionClass, baseArchive);
addToCache(extensionClass, object.getClass());
return object;
}
// -------------------------------------------------------------------------------------||
// Internal Helper Methods - Cache ----------------------------------------------------||
// -------------------------------------------------------------------------------------||
boolean isCached(Class<?> extensionClass) {
return cache.containsKey(extensionClass);
}
private <T extends Assignable> T createFromCache(Class<T> extensionClass, Archive<?> archive) {
Class<T> extensionImplClass = getFromCache(extensionClass);
return createExtension(extensionImplClass, archive);
}
void addToCache(Class<?> extensionClass, Class<?> extensionImplClass) {
cache.put(extensionClass, extensionImplClass);
}
@SuppressWarnings("unchecked")
<T extends Assignable> Class<T> getFromCache(Class<T> extensionClass) {
return (Class<T>) cache.get(extensionClass);
}
// -------------------------------------------------------------------------------------||
// Internal Helper Methods - Override -------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* {@inheritDoc}
*
* @see org.jboss.shrinkwrap.api.ExtensionLoader#addOverride(java.lang.Class, java.lang.Class)
*/
public <T extends Assignable> ServiceExtensionLoader addOverride(final Class<T> extensionClass,
final Class<? extends T> extensionImplClass) {
addToCache(extensionClass, extensionImplClass);
return this;
}
/**
* {@inheritDoc}
*
* @see org.jboss.shrinkwrap.api.ExtensionLoader#getExtensionFromExtensionMapping(java.lang.Class)
*/
public <T extends Assignable> String getExtensionFromExtensionMapping(final Class<T> type) {
ExtensionWrapper extensionWrapper = extensionMappings.get(type);
if (extensionWrapper == null) {
loadExtensionMapping(type);
}
extensionWrapper = extensionMappings.get(type);
if (extensionWrapper == null) {
throw UnknownExtensionTypeExceptionDelegator.newExceptionInstance(type);
}
return extensionWrapper.getProperty("extension");
}
/**
* {@inheritDoc}
*
* @see org.jboss.shrinkwrap.api.ExtensionLoader#getArchiveFormatFromExtensionMapping(java.lang.Class)
*/
public <T extends Archive<T>> ArchiveFormat getArchiveFormatFromExtensionMapping(final Class<T> type) {
ExtensionWrapper extensionWrapper = extensionMappings.get(type);
if (extensionWrapper == null) {
loadExtensionMapping(type);
}
extensionWrapper = extensionMappings.get(type);
if (extensionWrapper == null) {
throw UnknownExtensionTypeExceptionDelegator.newExceptionInstance(type);
}
String archiveFormat = extensionWrapper.getProperty("archiveFormat");
return ArchiveFormat.valueOf(archiveFormat);
}
/**
* Check to see if a specific extension interface is beeing overloaded
*
* @param extensionClass
* The ExtensionType interface class
* @return true if found
*/
public boolean isOverriden(Class<?> extensionClass) {
return isCached(extensionClass);
}
// -------------------------------------------------------------------------------------||
// Internal Helper Methods - Loading --------------------------------------------------||
// -------------------------------------------------------------------------------------||
/**
* Creates a new instance of a <code>extensionClass</code> implementation. The implementation class is found in a
* provider-configuration file in META-INF/services/
*
* @param <T>
* @param extensionClass
* @param archive
* @return an instance of the <code>extensionClass</code>' implementation.
*/
private <T extends Assignable> T createFromLoadExtension(Class<T> extensionClass, Archive<?> archive) {
ExtensionWrapper extensionWrapper = loadExtensionMapping(extensionClass);
if (extensionWrapper == null) {
throw new RuntimeException("Failed to load ExtensionMapping");
}
Class<T> extensionImplClass = loadExtension(extensionWrapper);
if (!extensionClass.isAssignableFrom(extensionImplClass)) {
throw new RuntimeException("Found extension impl class " + extensionImplClass.getName()
+ " not assignable to extension interface " + extensionClass.getName());
}
return createExtension(extensionImplClass, archive);
}
/**
* Loads the implementation class hold in {@link ExtensionWrapper#implementingClassName}
*
* @param <T>
* @param extensionWrapper
* @return
*/
private <T extends Assignable> Class<T> loadExtension(ExtensionWrapper extensionWrapper) {
return loadExtensionClass(extensionWrapper.implementingClassName);
}
/**
* Finds the SPI configuration, wraps it into a {@link ExtensionWrapper} and loads it to
* {@link ServiceExtensionLoader#extensionMappings}.
*
* @param <T>
* @param extensionClass
* @return
*/
private <T extends Assignable> ExtensionWrapper loadExtensionMapping(Class<T> extensionClass) {
final InputStream extensionStream = findExtensionImpl(extensionClass);
ExtensionWrapper extensionWrapper = loadExtensionWrapper(extensionStream, extensionClass);
this.extensionMappings.put(extensionClass, extensionWrapper);
return extensionWrapper;
}
/**
* Iterates through the classloaders to load the provider-configuration file for <code>extensionClass</code> in
* META-INF/services/ using its binary name.
*
* @param <T>
* @param extensionClass
* SPI type for which the configuration file is looked for
* @return An {@link InputStream} representing <code>extensionClass</code>'s configuration file
* @throws RuntimeException
* if it doesn't find a provider-configuration file for <code>extensionClass</code>
* @throws UnknownExtensionTypeExceptionDelegator
*/
private <T extends Assignable> InputStream findExtensionImpl(final Class<T> extensionClass) {
try {
// Add all extension impls found in all CLs
for (final ClassLoader cl : this.getClassLoaders()) {
final InputStream stream = cl.getResourceAsStream("META-INF/services/" + extensionClass.getName());
if (stream != null) {
return stream;
}
}
// None found
throw new RuntimeException("No extension implementation found for " + extensionClass.getName()
+ ", please verify classpath or add a extensionOverride");
} catch (Exception e) {
throw UnknownExtensionTypeExceptionDelegator.newExceptionInstance(extensionClass);
}
}
/**
* Wraps the provider-configuration file <code>extensionStream</code>, the SPI <code>extensionClass</code> and its
* implementation class name into a {@link ExtensionWrapper} instance.
*
* @param <T>
* @param extensionStream
* - a bytes stream representation of the provider-configuration file
* @param extensionClass
* - SPI type
* @return a {@link ExtensionWrapper} instance
*/
private <T extends Assignable> ExtensionWrapper loadExtensionWrapper(final InputStream extensionStream,
Class<T> extensionClass) {
Properties properties = new Properties();
try {
properties.load(extensionStream);
} catch (IOException e) {
throw new RuntimeException("Could not open stream for extensionURL " + extensionStream, e);
}
String implementingClassName = (String) properties.get("implementingClassName");
if (implementingClassName == null) {
throw new RuntimeException("Property implementingClassName is not present in " + extensionStream);
}
final Map<String, String> map = new HashMap<String, String>(properties.size());
final Enumeration<Object> keys = properties.keys();
while (keys.hasMoreElements()) {
final String key = (String) keys.nextElement();
final String value = (String) properties.get(key);
map.put(key, value);
}
return new ExtensionWrapper(implementingClassName, map, extensionClass);
}
/**
* Delegates class loading of <code>extensionClassName</code> to
* {@link ClassLoaderSearchUtilDelegator#findClassFromClassLoaders(String, Iterable)} passing the
* <code>extensionClassName</code> and the instance's <code>classLoaders</code>.
*
* @param <T>
* @param extensionClassName
* @return
*/
@SuppressWarnings("unchecked")
private <T extends Assignable> Class<T> loadExtensionClass(String extensionClassName) {
try {
return (Class<T>) ClassLoaderSearchUtilDelegator.findClassFromClassLoaders(extensionClassName,
getClassLoaders());
} catch (final ClassNotFoundException e) {
throw new RuntimeException("Could not load class " + extensionClassName, e);
}
}
/**
* Creates an instance of <code>extensionImplClass</code> using <code>archive</code> as the parameter for its
* one-argument list constructor.
*
* @param <T>
* @param extensionImplClass
* @param archive
* @return
*/
private <T extends Assignable> T createExtension(Class<T> extensionImplClass, Archive<?> archive) {
T extension;
Constructor<T> extensionImplConstructor = findConstructor(extensionImplClass);
@SuppressWarnings("unchecked")
Class<T> constructorArg = (Class<T>) extensionImplConstructor.getParameterTypes()[0];
try {
if (constructorArg.isInstance(archive)) {
extension = extensionImplConstructor.newInstance(archive);
} else {
extension = extensionImplConstructor.newInstance(load(constructorArg, archive));
}
} catch (InstantiationException e) {
throw new ExtensionLoadingException("Failed to instantiate class of type " + archive.getClass()
+ ". The underlying class can not be abstract.", e);
} catch (IllegalAccessException e) {
throw new ExtensionLoadingException("Failed to instantiate class of type " + archive.getClass()
+ ". The underlying constructor is inaccessible.", e);
} catch (InvocationTargetException e) {
throw new ExtensionLoadingException("Failed to instantiate class of type " + archive.getClass()
+ ". The underlying constructor threw an exception.", e);
}
return extension;
}
/**
* Finds a constructor with a one-argument list's element which implements {@link Archive}.
*
* @param <T>
* @param extensionImplClass
* - Implementation of {@link Assignable} with a one-argument list's element which implements
* {@link Archive}.
* @return
*/
@SuppressWarnings("unchecked")
private <T extends Assignable> Constructor<T> findConstructor(Class<T> extensionImplClass) {
Constructor<?>[] constructors = SecurityActions.getConstructors(extensionImplClass);
for (Constructor<?> constructor : constructors) {
Class<?>[] parameters = constructor.getParameterTypes();
if (parameters.length != 1) {
continue;
}
Class<?> parameter = parameters[0];
if (Archive.class.isAssignableFrom(parameter)) {
return (Constructor<T>) constructor;
}
}
throw new ExtensionLoadingException("No constructor with a single argument of type " + Archive.class.getName()
+ " could be found");
}
private Iterable<ClassLoader> getClassLoaders() {
return this.classLoaders;
}
}