package org.wikibrain.conf; import com.typesafe.config.Config; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.reflect.ConstructorUtils; import org.apache.commons.lang3.tuple.Pair; import org.clapper.util.classutil.*; import org.wikibrain.utils.JvmUtils; import java.io.Closeable; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Binds together providers for a collection of components. A component is uniquely * identified by two elements: * * 1. A superclass or interface (e.g. DataSource). * 2. A name (e.g. 'foo'). * * So there can be multiple instances of the same component type as long as they are * uniquely named. * * The configurator scans the class path for all classes that extend * org.wikibrain.conf.Provider. This configurator will instantiate the provider and ask it * what class it provides (Provider.getType) and what path of the configuration it handles * along (Provider.getPath()). * * For example, let's say that there are two different providers for DataSource: * MySqlDataSourceProvider and H2DataSourceProvider. Both their Provider.getType() * methods must return javax.sql.DataSource and both their Provider.getPath() methods * must return "dao.dataSource". * * Given the following config: * * ... some top-level elements... * * 'dao' : * 'dataSource' : { * 'foo' : { ... config params for foo impl ... }, * 'bar' : { ... config params for bar impl ... }, * } * * If a client requests the local page dao named 'foo', the configurator iterates * through the two providers passing '{ ... config params for foo .. }' until * a provider accepts and generates the requested DataSource. * * A special optional key named 'default' has a value corresponding to the name of * implementation that should be used for the version of get() that does not take * a name. For example, if the dataSource hashtable above had entry 'default' : 'bar', * the 'bar' entry would be used by default if no name was supplied to the get() * method. * * All generated components are considered singletons. Once a named component is * generated once, it is cached and reused for future requests. */ public class Configurator implements Cloneable { private static final Logger LOG = LoggerFactory.getLogger(Configurator.class); public static final int MAX_FILE_SIZE = 8 * 1024 * 1024; // 8MB private final Configuration conf; /** * A collection of providers for a particular type of component (e.g. LocalPageDao). */ private class ProviderSet { Class type = null; // component's superclass or interface String path; // path for config elements of the components List<Provider> providers; // list of providers for the component. ProviderSet(Class klass, String path) { this.type = klass; this.path = path; this.providers = new ArrayList<Provider>(); } } public Configuration getConf() { return conf; } /** * Providers for each component. */ private final Map<Class, ProviderSet> providers = new HashMap<Class, ProviderSet>(); /** * Named instances of each component. */ private final Map<Class, Map<String, Object>> components = new HashMap<Class, Map<String, Object>>(); /** * Constructs a new configuration object with the specified configuration. * @param conf */ public Configurator(Configuration conf) throws ConfigurationException { this.conf = conf; registerProviders(); } /** * Registers all class that extend Providers * @throws ConfigurationException */ private void registerProviders() throws ConfigurationException { // map from class names to file they are registered in. Set<String> visited = new HashSet<String>(); // files already scanned Map<String, File> registered = new HashMap<String, File>(); String classRegEx = System.getProperty("wikibrain.classRegEx", "org\\.wikibrain\\.*"); for (File file : JvmUtils.getClassPathAsList()) { LOG.debug("considering classpath entry " + file); String canonical = FilenameUtils.normalize(file.getAbsolutePath()); if (visited.contains(canonical)) { LOG.debug("skipping looking for providers in duplicate classpath entry " + canonical); continue; } visited.add(canonical); ClassFinder finder = new ClassFinder(); finder.add(file); ClassFilter filter = new AndClassFilter(new RegexClassFilter(classRegEx), new ProviderFilter()); Collection<ClassInfo> foundClasses = new ArrayList<ClassInfo>(); finder.findClasses (foundClasses,filter); for (ClassInfo classInfo : foundClasses) { if (registered.containsKey(classInfo.getClassName())) { LOG.debug("class " + classInfo.getClassName() + " found in " + file + " but previously found in " + registered.get(classInfo.getClassName()) + ". Skipping!"); } else { LOG.debug("registering component " + classInfo); registerProvider(classInfo.getClassName()); registered.put(classInfo.getClassName(), file); } } } int total = 0; for (Class c : providers.keySet()) { ProviderSet pset = providers.get(c); total += pset.providers.size(); LOG.debug("installed " + pset.providers.size() + " configurators for " + pset.type); } LOG.info("configurator installed " + total + " providers for " + providers.size() + " classes"); } /** * Instantiates providers for the component. * @param providerClass The name of the provider that should be instaniated * @return * @throws ConfigurationException */ private void registerProvider(String providerClass) throws ConfigurationException { try { Class<Provider> klass = (Class<Provider>) Class.forName(providerClass); Provider provider = ConstructorUtils.invokeConstructor(klass, this, conf); Class type = provider.getType(); String path = provider.getPath(); ProviderSet pset = providers.get(type); if (pset == null) { pset = new ProviderSet(type, path); providers.put(type, pset); components.put(type, new HashMap<String, Object>()); } if (pset.type != type) { throw new IllegalStateException(); } if (!ObjectUtils.equals(pset.path, path)) { throw new ConfigurationException( "inconsistent component path declared for provider " + klass + " that provides type " + type + " expected path " + pset.path + ", found path " + path); } pset.providers.add(provider); } catch (ClassNotFoundException e) { throw new ConfigurationException("error when loading provider " + providerClass, e); } catch (InvocationTargetException e) { throw new ConfigurationException("error when loading provider " + providerClass, e); } catch (NoSuchMethodException e) { throw new ConfigurationException("error when loading provider " + providerClass, e); } catch (InstantiationException e) { throw new ConfigurationException("error when loading provider " + providerClass, e); } catch (IllegalAccessException e) { throw new ConfigurationException("error when loading provider " + providerClass, e); } } /** * @see #get(Class, String, java.util.Map) * @param klass * @param name * @param <T> * @return * @throws ConfigurationException */ public <T> T get(Class<T> klass, String name) throws ConfigurationException { return get(klass, name, null); } /** * Get a component with a single runtime parameter * @see #get(Class, String, java.util.Map) * @param klass * @param name * @param runtimeKey * @param runtimeValue * @param <T> * @return * @throws ConfigurationException */ public <T> T get(Class<T> klass, String name, String runtimeKey, String runtimeValue) throws ConfigurationException { Map<String, String> runtimeParams = new HashMap<String, String>(); runtimeParams.put(runtimeKey, runtimeValue); return get(klass, name, runtimeParams); } /** * Get a specific named instance of the component with the specified class. * * @param klass The generic interface or superclass, not the specific implementation. * @param name The name of the class as it appears in the config file. If name is null, * the configurator tries to guess by looking for a "default" entry in * the config that provides the name for a default implementation or, if * there is exactly one implementation returning it. Otherwise, if name is * null it throws an error. * @param runtimeParams Parameters to be passed to the provider that affect component creation. * The identity of a component includes the runtime parameters, so * two components with the same klass and name, but different runtimeParams * will be cached independently. * @return The requested component. */ public <T> T get(Class<T> klass, String name, Map<String, String> runtimeParams) throws ConfigurationException { name = resolveComponentName(klass, name); Config config = getConfig(klass, name); Map<String, Object> cache = components.get(klass); String key = makeCacheKey(name, runtimeParams); synchronized (cache) { if (cache.containsKey(key)) { return (T) cache.get(key); } else { Pair<Provider, T> pair = constructInternal(klass, name, config, runtimeParams); if (pair.getLeft().getScope() == Provider.Scope.SINGLETON) { cache.put(key, pair.getRight()); } return pair.getRight(); } } } /** * Returns a unique string for the name and params * @param name * @param runtimeParams * @return */ private String makeCacheKey(String name, Map<String, String> runtimeParams) { String key = name; if (runtimeParams != null) { List<String> runtimeKeys = new ArrayList<String>(runtimeParams.keySet()); Collections.sort(runtimeKeys); StringBuffer buffer = new StringBuffer(); for (String k : runtimeKeys) { buffer.append("|"); buffer.append(k); buffer.append("="); buffer.append(runtimeParams.get(k)); } key += buffer.toString(); } return key; } /** * If the component name is "default" or null, return the name of the default implementation of the compoenent. * Otherwise, return the specified name. * @param klass * @param name * @return */ public String resolveComponentName(Class klass, String name) throws ConfigurationException { if (!providers.containsKey(klass)) { throw new ConfigurationException("No registered providers for components with class " + klass); } ProviderSet pset = providers.get(klass); // If name is "default", treat it as null for default option if (name != null && name.equalsIgnoreCase("default")) { name = null; } // If name is null, check to see if there is a default entry or only one option. if (name == null) { if (!conf.get().hasPath(pset.path)) { throw new ConfigurationException("Configuration path " + pset.path + " does not exist"); } Config config = conf.get().getConfig(pset.path); if (config.hasPath("default")) { name = config.getString("default"); } else if (config.root().keySet().size() == 1) { name = config.root().keySet().iterator().next(); } else { throw new IllegalArgumentException( "Ambiguous request for nameless component with type " + klass + " the configuration dictionary at path " + pset.path + " must either have a 'default' key specifying the name " + " of the default implementation or exactly one element. " + "Available provider implementations are: " + Arrays.toString(pset.providers.toArray()) ); } } return name; } /** * Returns the config object associated with the given class and name. * @param klass The generic interface or superclass, not the specific implementation. * @param name The name of the class as it appears in the config file. If name is null, * the configurator tries to guess by looking for a "default" entry in * the config that provides the name for a default implementation or, if * there is exactly one implementation returning it. Otherwise, if name is * null it throws an error. * @return The requested config object. * @throws ConfigurationException */ public Config getConfig(Class klass, String name) throws ConfigurationException { if (!providers.containsKey(klass)) { throw new ConfigurationException("No registered providers for components with class " + klass); } ProviderSet pset = providers.get(klass); name = resolveComponentName(klass, name); String path = pset.path + "." + name; if (!conf.get().hasPath(path)) { throw new ConfigurationException("Configuration path " + path + " does not exist"); } return conf.get().getConfig(path); } /** * Constructs an instance of the specified class with the passed * in config. This bypasses the cache and the configuration object. * * * @param klass The class being created. * @param name An arbitrary name for the object. Can be null. * @param conf The configuration for the object. * @param runtimeParams * @return The object */ public <T> T construct(Class<T> klass, String name, Config conf, Map<String, String> runtimeParams) throws ConfigurationException { return constructInternal(klass, name, conf, runtimeParams).getRight(); } private <T> Pair<Provider, T> constructInternal(Class<T> klass, String name, Config conf, Map<String, String> runtimeParams) throws ConfigurationException { if (!providers.containsKey(klass)) { throw new ConfigurationException("No registered providers for components with class " + klass); } List<Provider> pset = providers.get(klass).providers; for (Provider p : pset) { Object o = p.get(name, conf, runtimeParams); if (o != null) { return Pair.of(p, (T) o); } } throw new ConfigurationException( "None of the " + pset.size() + " providers claimed ownership of component " + "with class " + klass + ", name '" + name + "' and configuration '" + conf + "'" ); } /** * Get a specific named instance of the component with the specified class. * This method can only be used when there is exactly one instance of the component. * * @param klass The generic interface or superclass, not the specific implementation. * @return The requested component. */ public <T> T get(Class<T> klass) throws ConfigurationException { return get(klass, null); } /** * Tries to close all open components, clears the components map. */ public void close() { for (Map<String, Object> implementations : components.values() ) { for (Object obj : implementations.values()) { if (obj instanceof Closeable) { try { ((java.io.Closeable) obj).close(); } catch (IOException e) { LOG.error("closing component " + obj + " failed:", e); } } } } components.clear(); } }