/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.factory;
import java.util.*;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Modifier;
import java.lang.reflect.InvocationTargetException;
import org.geotools.util.logging.Logging;
import org.geotools.resources.i18n.Errors;
import org.geotools.resources.i18n.ErrorKeys;
/**
* A {@linkplain FactoryRegistry factory registry} capable to creates factories if no appropriate
* instance was found in the registry.
* <p>
* This class maintains a cache of previously created factories, as {@linkplain WeakReference
* weak references}. Calls to {@link #getServiceProvider getServiceProvider} first check if a
* previously created factory can fit.
*
* @since 2.1
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux
* @author Jody Garnett
*/
public class FactoryCreator extends FactoryRegistry {
/**
* The array of classes for searching the one-argument constructor.
*/
private static final Class[] HINTS_ARGUMENT = new Class[] {Hints.class};
/**
* List of factories already created. Used as a cache.
*/
private final Map<Class<?>, List<Reference<?>>> cache = new HashMap<Class<?>, List<Reference<?>>>();
/**
* Objects under construction for each implementation class.
* Used by {@link #safeCreate} as a guard against infinite recursivity.
*/
private final Set<Class<?>> underConstruction = new HashSet<Class<?>>();
/**
* Constructs a new registry for the specified category.
*
* @param category The single category.
*
* @since 2.4
*/
public FactoryCreator(final Class<?> category) {
super(category);
}
/**
* Constructs a new registry for the specified categories.
*
* @param categories The categories.
*
* @since 2.4
*/
public FactoryCreator(final Class<?>[] categories) {
super(categories);
}
/**
* Constructs a new registry for the specified categories.
*
* @param categories The categories.
*/
public FactoryCreator(final Collection<Class<?>> categories) {
super(categories);
}
/**
* Returns the providers available in the cache. To be used by {@link FactoryRegistry}.
*/
@Override
final <T> List<Reference<T>> getCachedProviders(final Class<T> category) {
List<Reference<?>> c = cache.get(category);
if (c == null) {
c = new LinkedList<Reference<?>>();
cache.put(category, c);
}
@SuppressWarnings("unchecked")
final List<Reference<T>> cheat = (List) c;
/*
* Should be safe because we created an empty list, there is no other place where this
* list is created and from this point we can only insert elements restricted to type T.
*/
return cheat;
}
/**
* Caches the specified factory under the specified category.
*/
private <T> void cache(final Class<T> category, final T factory) {
getCachedProviders(category).add(new WeakReference<T>(factory));
}
/**
* Returns a provider for the specified category, using the specified map of hints (if any).
* If a provider matching the requirements is found in the registry, it is returned. Otherwise,
* a new provider is created and returned. This creation step is the only difference between
* this method and the {@linkplain FactoryRegistry#getServiceProvider super-class method}.
*
* @param category The category to look for.
* @param filter An optional filter, or {@code null} if none.
* @param hints A {@linkplain Hints map of hints}, or {@code null} if none.
* @param key The key to use for looking for a user-provided instance in the hints, or
* {@code null} if none.
* @return A factory for the specified category and hints (never {@code null}).
* @throws FactoryNotFoundException if no factory was found, and the specified hints don't
* provide suffisient information for creating a new factory.
* @throws FactoryRegistryException if the factory can't be created for some other reason.
*/
@Override
public <T> T getServiceProvider(final Class<T> category,
final Filter filter,
final Hints hints,
final Hints.Key key)
throws FactoryRegistryException
{
final FactoryNotFoundException notFound;
try {
return super.getServiceProvider(category, filter, hints, key);
} catch (FactoryNotFoundException exception) {
// Will be rethrown later in case of failure to create the factory.
notFound = exception;
}
/*
* No existing factory found. Creates one using reflection. First, we
* check if an implementation class was explicitly specified by the user.
*/
final Class<?>[] types;
if (hints==null || key==null) {
types = null;
} else {
final Object hint = hints.get(key);
if (hint == null) {
types = null;
} else {
if (hint instanceof Class<?>[]) {
types = (Class<?>[]) hint;
} else {
types = new Class<?>[] {(Class<?>) hint};
// Should not fails, since non-class argument should
// have been accepted by 'getServiceProvider(...)'.
}
final int length = types.length;
for (int i=0; i<length; i++) {
final Class<?> type = types[i];
if (type!=null && category.isAssignableFrom(type)) {
final int modifiers = type.getModifiers();
if (!Modifier.isAbstract(modifiers)) {
final T candidate = createSafe(category, type, hints);
if (candidate == null) {
continue;
}
if (isAcceptable(candidate, category, hints, filter)) {
cache(category, candidate);
return candidate;
}
dispose(candidate);
}
}
}
}
}
/*
* No implementation hint provided. Search the first implementation
* accepting a Hints argument. No-args constructor will be ignored.
* Note: all Factory objects should be fully constructed by now,
* since the super-class has already iterated over all factories.
*/
for (final Iterator<T> it=getUnfilteredProviders(category); it.hasNext();) {
final T factory = it.next();
final Class<?> implementation = factory.getClass();
if (types!=null && !isTypeOf(types, implementation)) {
continue;
}
if (filter!=null && !filter.filter(factory)) {
continue;
}
final T candidate;
try {
candidate = createSafe(category, implementation, hints);
} catch (FactoryNotFoundException exception) {
// The factory has a dependency which has not been found.
// Be tolerant to that kind of error.
Logging.recoverableException(LOGGER, FactoryCreator.class, "getServiceProvider", exception);
continue;
} catch (FactoryRegistryException exception) {
if (exception.getCause() instanceof NoSuchMethodException) {
// No public constructor with the expected argument.
// Try an other implementation.
continue;
} else {
// Other kind of error, probably unexpected.
// Let the exception propagates.
throw exception;
}
}
if (candidate == null) {
continue;
}
if (isAcceptable(candidate, category, hints, filter)) {
cache(category, candidate);
return candidate;
}
dispose(candidate);
}
throw notFound;
}
/**
* Returns {@code true} if the specified implementation is one of the specified types.
*/
private static boolean isTypeOf(final Class<?>[] types, final Class<?> implementation) {
for (int i=0; i<types.length; i++) {
if (types[i].isAssignableFrom(implementation)) {
return true;
}
}
return false;
}
/**
* Invokes {@link #createServiceProvider}, but checks against recursive calls. If the specified
* implementation is already under construction, returns {@code null} in order to tell to
* {@link #getServiceProvider} that it need to search for an other implementation. This is
* needed for avoiding infinite recursivity when a factory is a wrapper around an ther factory
* of the same category. For example this is the case of
* {@link org.geotools.referencing.operation.BufferedCoordinateOperationFactory}.
*/
private <T> T createSafe(final Class<T> category, final Class<?> implementation, final Hints hints) {
if (!underConstruction.add(implementation)) {
return null;
}
try {
return createServiceProvider(category, implementation, hints);
} finally {
if (!underConstruction.remove(implementation)) {
throw new AssertionError();
}
}
}
/**
* Creates a new instance of the specified factory using the specified hints.
* The default implementation tries to instantiate the given implementation class
* using the first of the following constructor found:
* <p>
* <ul>
* <li>Constructor with a single {@link Hints} argument.</li>
* <li>No-argument constructor.</li>
* </ul>
*
* @param category The category to instantiate.
* @param implementation The factory class to instantiate.
* @param hints The implementation hints.
* @return The factory.
* @throws FactoryRegistryException if the factory creation failed.
*/
protected <T> T createServiceProvider(final Class<T> category,
final Class<?> implementation,
final Hints hints)
throws FactoryRegistryException
{
Throwable cause;
try {
try {
return category.cast(implementation.getConstructor(HINTS_ARGUMENT)
.newInstance(new Object[] {hints}));
} catch (NoSuchMethodException exception) {
// Constructor do not exists or is not public. We will fallback on the no-arg one.
cause = exception;
}
try {
return category.cast(implementation.getConstructor((Class[]) null)
.newInstance((Object[]) null));
} catch (NoSuchMethodException exception) {
// No constructor accessible. Do not store the cause (we keep the one above).
}
} catch (IllegalAccessException exception) {
cause = exception; // constructor is not public (should not happen)
} catch (InstantiationException exception) {
cause = exception; // The class is abstract
} catch (InvocationTargetException exception) {
cause = exception.getCause(); // Exception in constructor
if (cause instanceof FactoryRegistryException) {
throw (FactoryRegistryException) cause;
}
}
throw new FactoryRegistryException(Errors.format(
ErrorKeys.CANT_CREATE_FACTORY_$1, implementation), cause);
}
/**
* Dispose the specified factory after. This method is invoked when a factory has been
* created, and then {@code FactoryCreator} determined that the factory doesn't meet
* user's requirements.
*/
private static void dispose(final Object factory) {
// Empty for now. This method is merely a reminder for disposal in future Geotools versions.
}
}