/*
* Copyright 2008, Unitils.org
*
* 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.unitils.core;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import static org.unitils.util.PropertyUtils.*;
import static org.unitils.util.ReflectionUtils.createInstanceOfType;
import java.util.*;
/**
* A class for loading unitils modules.
* <p/>
* The core names set by the {@link #PROPKEY_MODULES} property which modules will be loaded. These names can then
* be used to construct properties that define the classnames and optionally the dependencies of these modules. E.g.
* <pre><code>
* unitils.modules= a, b, c, d
* unitils.module.a.className= org.unitils.A
* unitils.module.a.runAfter= b, c
* unitils.module.b.className= org.unitils.B
* unitils.module.b.runAfter= c
* unitils.module.c.className= org.unitils.C
* unitils.module.d.enabled= false
* </code></pre>
* The above configuration will load 3 core classes A, B and C and will always perform processing in
* order C, B, A.
* <p/>
* If a circular dependency is found in the runAfter configuration, a runtime exception will be thrown.
*
* @author Filip Neven
* @author Tim Ducheyne
*/
public class ModulesLoader {
/**
* Property that contains the names of the modules that are to be loaded.
*/
public static final String PROPKEY_MODULES = "unitils.modules";
/**
* First part of all core specific properties.
*/
public static final String PROPKEY_MODULE_PREFIX = "unitils.module.";
/**
* Last part of the core specific property that specifies whether the core should be loaded.
*/
public static final String PROPKEY_MODULE_SUFFIX_ENABLED = ".enabled";
/**
* Last part of the core specific property that specifies the classname of the core.
*/
public static final String PROPKEY_MODULE_SUFFIX_CLASS_NAME = ".className";
/**
* Last part of the core specific property that specifies the names of the modules that should be run before this core.
*/
public static final String PROPKEY_MODULE_SUFFIX_RUN_AFTER = ".runAfter";
/**
* The logger instance for this class.
*/
private static Log logger = LogFactory.getLog(ModulesLoader.class);
/**
* Loads all unitils modules as described in the class javadoc.
*
* @param configuration the configuration, not null
* @return the modules, not null
*/
public List<Module> loadModules(Properties configuration) {
// get all declared modules (filter doubles)
Set<String> moduleNames = new TreeSet<String>(getStringList(PROPKEY_MODULES, configuration));
// remove all disable modules
removeDisabledModules(moduleNames, configuration);
// get all core dependencies
Map<String, List<String>> runAfters = new HashMap<String, List<String>>();
for (String moduleName : moduleNames) {
// get dependencies for core
List<String> runAfterModuleNames = getStringList(PROPKEY_MODULE_PREFIX + moduleName + PROPKEY_MODULE_SUFFIX_RUN_AFTER, configuration);
runAfters.put(moduleName, runAfterModuleNames);
}
// Count each time a core is (indirectly) used in runAfter and order by count
Map<Integer, List<String>> runAfterCounts = new TreeMap<Integer, List<String>>();
for (String moduleName : moduleNames) {
// calculate the nr of times a core is (indirectly) referenced
int count = countRunAfters(moduleName, runAfters, new HashMap<String, String>());
// store in map with count as key and a list corresponding modules as values
List<String> countModuleNames = runAfterCounts.get(count);
if (countModuleNames == null) {
countModuleNames = new ArrayList<String>();
runAfterCounts.put(count, countModuleNames);
}
countModuleNames.add(moduleName);
}
// Create core instances in the correct sequence
List<Module> result = new ArrayList<Module>();
for (List<String> moduleNameList : runAfterCounts.values()) {
List<Module> modules = createAndInitializeModules(moduleNameList, configuration);
result.addAll(modules);
}
return result;
}
/**
* Creates the modules with the given class names and calls initializes them with the given configuration.
*
* @param moduleNames the module class names, not null
* @param configuration the configuration, not null
* @return the modules, not null
*/
protected List<Module> createAndInitializeModules(List<String> moduleNames, Properties configuration) {
List<Module> result = new ArrayList<Module>();
for (String moduleName : moduleNames) {
// get module class name
String className = getString(PROPKEY_MODULE_PREFIX + moduleName + PROPKEY_MODULE_SUFFIX_CLASS_NAME, configuration);
if (!classFileExistsInClasspath(className)) {
logger.debug("Skipping module " + moduleName + ". Module class not found in classpath. Module class name: " + className);
continue;
}
try {
// create module instance
Object module = createInstanceOfType(className, true);
if (!(module instanceof Module)) {
throw new UnitilsException("Unable to load core. Module class is not of type UnitilsModule: " + className);
}
// initialize module
((Module) module).init(configuration);
result.add((Module) module);
} catch (Throwable t) {
throw new UnitilsException("An exception occured during the loading of core module " + moduleName + " with module class name " + className, t);
}
}
return result;
}
/**
* Count each time a core is (indirectly) used in runAfter and order by count.
* <p/>
* This way all modules can be ordered in such a way that all core dependencies (runAfterz) are met.
* If no such order can be found (circular dependency) a runtime exception is thrown
*
* @param moduleName the core to count, not null
* @param allRunAfters all dependencies as (moduleName, run-after moduleNames) entries, not null
* @param traversedModuleNames all moduleNames that were already counted as (moduleName, moduleName) entries, not null
* @return the count
* @throws RuntimeException if an infinite loop (circular dependency) is found
*/
private int countRunAfters(String moduleName, Map<String, List<String>> allRunAfters, Map<String, String> traversedModuleNames) {
// Check for infinite loops
if (traversedModuleNames.containsKey(moduleName)) {
throw new UnitilsException("Unable to load modules. Circular dependency found for modules: " + traversedModuleNames.keySet());
}
traversedModuleNames.put(moduleName, moduleName);
int count = 1;
List<String> runAfters = allRunAfters.get(moduleName);
if (runAfters != null) {
for (String currentModuleName : runAfters) {
// recursively count all dependencies
count += countRunAfters(currentModuleName, allRunAfters, traversedModuleNames);
}
}
traversedModuleNames.remove(moduleName);
return count;
}
/**
* Removes all modules that have a value false for the enabled property.
*
* @param moduleNames the module names, not null
* @param configuration the configuration, not null
*/
protected void removeDisabledModules(Set<String> moduleNames, Properties configuration) {
Iterator<String> moduleNameIterator = moduleNames.iterator();
while (moduleNameIterator.hasNext()) {
String moduleName = moduleNameIterator.next();
boolean enabled = getBoolean(PROPKEY_MODULE_PREFIX + moduleName + PROPKEY_MODULE_SUFFIX_ENABLED, true, configuration);
if (!enabled) {
moduleNameIterator.remove();
}
}
}
/**
* @param className The name of the class to check, not null
* @return True if the classfile exists in the classpath
*/
protected boolean classFileExistsInClasspath(String className) {
String classFileName = className.replace('.', '/') + ".class";
return getClass().getClassLoader().getResource(classFileName) != null;
}
}