/*
* Copyright 2016 Function1. All Rights Reserved.
*
* 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 tools.gsf.config;
import COM.FutureTense.Interfaces.ICS;
import javax.servlet.ServletContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Default factory producer class that creates factory classes for the scopes specified.
* <p>
* Factory classes will be looked up in a configuration file called {@link #CONFIG_FILE}.
* The format of the file is as follows: blank lines are allowed (and ignored); comments are
* allowed (and ignored). The comment indicator is (#).
* <p>
* Each functional line of the configuration file has two parts, the factory class name, and the
* scope class name. For example:
* <code>scope.class.name : factory.class.name</code>
* <p>
* Each factory class name must have a two-argument constructor. The first argument is the scope class,
* the very same class that is passed into the {@link #getFactory(Object)} method. The second
* argument is the optional delegate factory that the specified factory will defer to if it is not
* able to locate the specified object. (If no parent is specified, no delegation will occur).
* <p>
* Only one factory class per scope may be defined. If more than one is found, a configuration
* error will be thrown.
* <p>
* By default, DefaultFactoryProducer knows how to get access to the ServletContext from within the
* ICS object, and so a factory corresponding to the ServletContext scope will be used
* as the delegate for the ICS scoped factory. Users may override either of these two and the
* relationship will be preserved.
* <p>
* Conversely, this factory producer does not know the relationship between any custom scope
* and the two it knows about, and so it is unable to automatically wire delegation between
* factories involving custom scopes. To do this, it is necessary to extend this class (or
* replace it).
* <p>
* Also, this factory producer does not know how to cache factories with custom scopes, so
* new versions will be created whenever one is requested. For the known scopes, caching
* is built-in.
*
* @author Tony Field
* @since 2016-08-05
*/
public class DefaultFactoryProducer implements FactoryProducer {
private static final Logger LOG = LoggerFactory.getLogger(DefaultFactoryProducer.class);
/**
* The configuration file(s) that this class will read while looking for
* information about which factories to use
*/
protected static final String CONFIG_FILE = "META-INF/gsf-factory";
private final Map<Class, Constructor<Factory>> factoryConstructors;
/**
* Creates a new factory producer. This constructor pre-reads the configuration
* files and validates that constructors exist for the scope specified.
* <p>
* If no factory is configured for ICS or ServletContext, a default is provided.
*/
public DefaultFactoryProducer() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// read the configuration file
Map<Class, Class> conf = lookupConfiguration(classLoader);
// add defaults for known scopes
if (!conf.containsKey(ICS.class)) {
conf.put(ICS.class, IcsBackedFactory.class);
}
if (!conf.containsKey(ServletContext.class)) {
conf.put(ServletContext.class, ServletContextBackedFactory.class);
}
// get the constructors
factoryConstructors = getConstructorMap(conf);
if (LOG.isDebugEnabled()) {
LOG.debug("DefaultFactoryProducer constructor is done... factoryConstructors = " + factoryConstructors);
}
}
@Override
public Factory getFactory(Object scope) {
if (scope instanceof ICS) {
return getFactory((ICS) scope);
} else if (scope instanceof ServletContext) {
return getFactory((ServletContext) scope);
} else if (scope == null) {
throw new IllegalArgumentException("Null scope not allowed");
} else if (factoryConstructors.containsKey(scope.getClass())) {
return createFactory(scope);
} else {
throw new IllegalArgumentException("Unsupported scope: " + scope.getClass().getName());
}
}
/**
* Create a custom-scoped factory. Note that there is no caching support for a custom scope (because
* this class does not know enough about the scope to know how to cache against it).
*
* Subclasses can override this method and create their own internal cache.
* @param customScope custom scope
* @return the factory
*/
protected Factory createFactory(Object customScope) {
Constructor<Factory> con = factoryConstructors.get(customScope.getClass());
try {
return con.newInstance(customScope, null /* delegate is not knowable */);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException("Could not instantiate factory: " + e, e);
}
}
/**
* The ics object pool key for the ics-backed factory.
*/
protected static final String ICS_CONTEXT_BACKED_FACTORY = "gsf-ics-backed-factory";
/**
* Get the ics-backed factory. Once created, the factory is cached on the ICS object
* pool using {@link #ICS_CONTEXT_BACKED_FACTORY} as the key.
*
* @param ics ics context
* @return the factory, never null
*/
protected Factory getFactory(ICS ics) {
Object o = ics.GetObj(ICS_CONTEXT_BACKED_FACTORY);
if (o == null) {
o = createFactory(ics);
ics.SetObj(ICS_CONTEXT_BACKED_FACTORY, o);
}
return (Factory) o;
}
/**
* Create a new instance of the ics-backed factory.
* <p>
* This implementation uses the constructor configured in the {@link #CONFIG_FILE}. It also
* uses ICS to locate the ServletContext, and then gets the servletContext-backed factory
* which it will use as a delegate for the ics-backed factory.
*
* @param ics the ics context
* @return the factory
*/
protected Factory createFactory(ICS ics) {
ServletContext servletContext = ics.getIServlet().getServlet().getServletContext();
Factory delegate = getFactory(servletContext);
Constructor<Factory> con = factoryConstructors.get(ICS.class);
try {
return con.newInstance(ics, delegate);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException("Could not instantiate factory: " + e, e);
}
}
/**
* The servlet context attribute key for the servletContext-backed factory.
*/
protected static final String SERVLET_CONTEXT_BACKED_FACTORY = "gsf-servlet-context-backed-factory";
/**
* Get the servletContext-backed factory. Once created, the factory is cached on the servlet context
* pool using {@link #SERVLET_CONTEXT_BACKED_FACTORY} as the key.
*
* @param servletContext the servlet context
* @return the factory, never null
*/
protected Factory getFactory(ServletContext servletContext) {
Object o = servletContext.getAttribute(SERVLET_CONTEXT_BACKED_FACTORY);
if (o == null) {
o = createFactory(servletContext);
servletContext.setAttribute(SERVLET_CONTEXT_BACKED_FACTORY, o);
}
return (Factory) o;
}
/**
* Create a new instance of the servletContext-backed factory.
* <p>
* This implementation uses the constructor configured in the {@link #CONFIG_FILE}.
*
* @param servletContext the servlet context
* @return the factory, never null
*/
protected Factory createFactory(ServletContext servletContext) {
Constructor<Factory> con = factoryConstructors.get(ServletContext.class);
try {
return con.newInstance(servletContext, null);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException("Could not instantiate factory: " + e, e);
}
}
/**
* Look up the configuration for the FactoryProducer by reading the {@link #CONFIG_FILE}s found
* in the specified ClassLoader's classpath. The configuration file format is described above.
*
* @param classLoader the classloader to do the looking
* @return map containing the classes from the config file
* @throws IllegalStateException if the config file format is invalid, if more than one
* configuration setting is found for a given scope, or if the config
* file points to classes that are not found.
*/
protected static Map<Class, Class> lookupConfiguration(ClassLoader classLoader) {
Map<Class, Class> config = new HashMap<>();
Set<String> configLines = ReflectionUtils.readConfigurationResource(classLoader, CONFIG_FILE);
for (String line : configLines) {
String[] lineInfo = line.split(":");
if (lineInfo.length != 2) {
throw new IllegalStateException("Invalid configuration file format in " + CONFIG_FILE + ": " + line);
}
String scopeClassName = lineInfo[0];
String factoryClassName = lineInfo[1];
try {
Class scopeClass = classLoader.loadClass(scopeClassName);
if (config.containsKey(scopeClass)) {
throw new IllegalStateException("More than one matching configuration setting found for " +
scopeClass.getName() + " in " + CONFIG_FILE);
}
Class factoryClass = classLoader.loadClass(factoryClassName);
config.put(scopeClass, factoryClass);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Could not find class listed in configuration file: " +
CONFIG_FILE + ": " + e, e);
}
}
return config;
}
/**
* Converts the map of classes to a map containing factory constructors as the value.
* <p>
* This class will look for a constructor with the signature described above, and will
* fail if one is not found.
*
* @param config the mapping between scope and factory constructors to be used to create
* the factories
* @return a map containing factory constructors for each scope. Never null
* @throws IllegalStateException if no constructor can be found with the appropriate signature,
* or if the class specified does not implement the Factory interface.
*/
protected static Map<Class, Constructor<Factory>> getConstructorMap(Map<Class, Class> config) {
Map<Class, Constructor<Factory>> constructorMap = new HashMap<>(config.size());
for (Class scopeClass : config.keySet()) {
Class configuredFactoryClass = config.get(scopeClass);
if (Factory.class.isAssignableFrom(configuredFactoryClass)) {
@SuppressWarnings("unchecked")
Class<Factory> factoryClass = (Class<Factory>) configuredFactoryClass;
try {
Constructor<Factory> constructor = factoryClass.getConstructor(scopeClass, Factory.class);
constructorMap.put(scopeClass, constructor);
} catch (NoSuchMethodException e) {
throw new IllegalStateException("Could not locate constructor with argument " + scopeClass.getName() + " for class " + factoryClass.getName());
}
} else {
throw new IllegalStateException("Invalid configuration - class " + configuredFactoryClass.getName() + " does not implement " + Factory.class);
}
}
return constructorMap;
}
}