/**
* Copyright (C) 2013 Colorado School of Mines
*
* This file is part of the Interface Software Development Kit (SDK).
*
* The InterfaceSDK is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The InterfaceSDK 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with the InterfaceSDK. If not, see <http://www.gnu.org/licenses/>.
*/
package edu.mines.acmX.exhibit.module_management;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.rmi.RemoteException;
import java.util.*;
import edu.mines.acmX.exhibit.module_management.module_executors.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import edu.mines.acmX.exhibit.module_management.loaders.ManifestLoadException;
import edu.mines.acmX.exhibit.module_management.loaders.ModuleLoadException;
import edu.mines.acmX.exhibit.module_management.loaders.ModuleLoader;
import edu.mines.acmX.exhibit.module_management.loaders.ModuleManagerManifestLoader;
import edu.mines.acmX.exhibit.module_management.loaders.ModuleManifestLoader;
import edu.mines.acmX.exhibit.module_management.metas.CheckType;
import edu.mines.acmX.exhibit.module_management.metas.DependencyType;
import edu.mines.acmX.exhibit.module_management.metas.ModuleManagerMetaData;
import edu.mines.acmX.exhibit.module_management.metas.ModuleMetaData;
import edu.mines.acmX.exhibit.module_management.modules.ModuleInterface;
/**
* TODO cleanup This class is the main entry point for the exhibit using the
* interface sdk library. This controls the lifecycle of modules and determines
* which modules can run by ensuring that they have all of their required
* dependencies. Also cycles through the next module to be run.
*
* TODO Add code to check when the directory that the modules is said to be in
* from the manifest is invalid or cannot be opened.
*
* Singleton
*
* @author Andrew DeMaria
* @author Austin Diviness
*/
public class ModuleManager implements ModuleManagerRemote {
static Logger logger = LogManager.getLogger(ModuleManager.class.getName());
private volatile Stack<ModuleExecutor> moduleStack = new Stack<>();
/**
* Singleton instance of ModuleManager This is volatile in order to be safe
* with multiple threads
*/
private static volatile ModuleManager instance = null;
// config variables
private static ModuleManagerMetaData metaData;
// core manager data variables
private ModuleInterface currentModule;
/*
* private ModuleInterface nextModule;
*/
private ModuleMetaData nextModuleMetaData;
private ModuleMetaData currentModuleMetaData;
private ModuleMetaData defaultModuleMetaData;
private boolean loadDefault;
private Map<String, ModuleMetaData> moduleConfigs;
private Scanner in;
/**
* TODO document
*
* @throws ManifestLoadException
*/
public static void configure(String moduleManifestPath)
throws ManifestLoadException {
metaData = loadModuleManagerConfig(moduleManifestPath);
}
// private static void configure(String defaultModule, String pathToModules)
// {
// logger.info("Using explicitly given configuration");
// metaData = new ModuleManagerMetaData(defaultModule, pathToModules);
// }
private ModuleManager() throws ManifestLoadException, ModuleLoadException {
if (metaData == null) {
logger.fatal("ModuleManager must be configured before you can create an instance");
throw new ManifestLoadException(
"Module Manager was not properly configured");
}
refreshModules();
try {
setDefaultModule(metaData.getDefaultModuleName());
} catch (ModuleLoadException e) {
logger.fatal("Could not load the default module");
throw e;
}
in = new Scanner(System.in);
loadDefault = true;
}
/**
* This method will refresh any modules that are not the default module. The
* default module is cached and will not be affected by this function. Any
* other modules may be affected in the following ways:
*
* a new module will be added if it meets the requirements for dependencies
*
* an exisiting module will be removed if the jar file was removed
*
* an exisiting module will be removed if the dependencies change.
*
*/
private void refreshModules() {
moduleConfigs = loadAllModuleConfigs(metaData.getPathToModules());
checkDependencies();
}
/**
* Fetches instance of ModuleManager, or creates one if it has not yet
* created.
*
* @return The single instance of ModuleManager
* @throws ManifestLoadException
* When the ModuleManager configuration is incorrect
* @throws ModuleLoadException
*/
public static ModuleManager getInstance() throws ManifestLoadException,
ModuleLoadException {
/*
* Now this is a bit tricky here. Please dont change this unless you are
* well read up on threading.
*
* The first if statement is for performance to prevent threads from
* uncessarily blocking on the nested synchronized statement.
*
* The syncronized statement itself ensures that only one thread can
* make an instance if it does not exist
*/
if (instance == null) {
synchronized (ModuleManager.class) {
if (instance == null) {
instance = new ModuleManager();
}
}
}
return instance;
}
// TODO document
/**
* Utility class to filter the jar files from other files that may exist in
* the modules' directory.
*/
private class JarFilter implements FilenameFilter {
public boolean accept(File dir, String filename) {
return filename.endsWith(".jar");
}
}
/**
* Creates a Map of all Modules found in the directory indicated by the path
* and associates each package name to the ModuleMetaData object created
* from that Module's manifest file.
*
* @param path
* The path to the directy holding modules
*
* @return A Map, where the keys are Module package names and the value is
* the meta data gathered from that module's manifest file
*/
public Map<String, ModuleMetaData> loadAllModuleConfigs(String path) {
// TODO caching
Map<String, ModuleMetaData> modConfigs = new HashMap<String, ModuleMetaData>();
logger.info("Loading jars in [" + path + "]");
File jarDir = new File(path);
File[] listOfJarFiles = jarDir.listFiles(new JarFilter());
for (File each : listOfJarFiles) {
try {
ModuleMetaData m = ModuleManifestLoader.load(each
.getCanonicalPath());
m.setJarFileName(each.getName());
modConfigs.put(m.getPackageName(), m);
} catch (ManifestLoadException e) {
logger.warn("Could not load manifest for " + each);
} catch (IOException e) {
logger.warn("Could not find manifest for " + each);
}
}
return modConfigs;
}
/**
* Loads the ModuleManager config file. TODO should really be private
*
* @param path
* Path to the ModuleManager xml config file
*/
public static ModuleManagerMetaData loadModuleManagerConfig(String path)
throws ManifestLoadException {
logger.info("Loading Module Manager config file [" + path + "]");
return ModuleManagerManifestLoader.load(path);
}
/**
* Loads an instance of ModuleInterface from the associated ModuleMetaData.
*
* @param data
* ModuleMetaData to be loaded
*
* @return loaded Module
* @throws ModuleLoadException
*/
public ModuleInterface loadModuleFromMetaData(ModuleMetaData data)
throws ModuleLoadException {
String path = (new File(metaData.getPathToModules(),
data.getJarFileName())).getPath();
return ModuleLoader.loadModule(path, data, this.getClass()
.getClassLoader());
}
/**
* Iterates through the loaded ModuleMetaData objects, removing those that
* don't have their required module dependencies.
*/
private void checkModuleDependencies() {
// First generate a new depth first search data instance
Map<String, CheckType> depthData = generateEmptyDepthFirstSeachData();
// part of a depth first search
// while there are modules that have not been checked,
// check them.
String moduleNameToCheck;
boolean isModuleOkay;
while ((moduleNameToCheck = getFirstModuleWithType(depthData,
CheckType.UNKNOWN)) != null) {
isModuleOkay = canModuleRunWithItsDependentModules(
moduleNameToCheck, depthData);
if (!isModuleOkay) {
this.moduleConfigs.remove(moduleNameToCheck);
}
}
}
/**
* This function is used internally for performing its checkDependencies
* operation
*
* @return Returns the first module name ( or key in this case ) of a module
* that has not yet been checked (CheckType.UNKNOWN). If there are
* no modules with this status then null is returned.
*/
private String getFirstModuleWithType(Map<String, CheckType> depthData,
CheckType desired) {
Iterator<String> i = depthData.keySet().iterator();
while (i.hasNext()) {
String currentKey = i.next();
CheckType current = depthData.get(currentKey);
if (current == desired) {
return currentKey;
}
}
return null;
}
/**
* Ensures that all modules have all dependencies available, including
* required modules and input services. This is a shell function because at
* one time we were thinking of checking the module's required input types
* at this point. The reason this is deferred until later (either when the
* module is set to be the next module and/or when the module is loaded is
* because the hardware may be plugged in or unplugged in between module meta
* data loading and when a module is actually loaded.
*/
public void checkDependencies() {
checkModuleDependencies();
}
/**
* This function is used internally to generate the data needed in the depth
* first search algorithm. It is not be used outside of the class.
*
* @return An empty Map of module names to their current check status
*/
private Map<String, CheckType> generateEmptyDepthFirstSeachData() {
Map<String, CheckType> depthData = new HashMap<String, CheckType>();
Iterator<String> i = this.moduleConfigs.keySet().iterator();
while (i.hasNext()) {
depthData.put(i.next(), CheckType.UNKNOWN);
}
return depthData;
}
/**
* Checks a module can run only in regards to whether or not modules it
* depends on exist. In essence this function will return false if a module
* has a required dependency and whether that required module and any of
* that referenced modules referenced modules (i.e. its recursive) do not
* exist. It will return true otherwise. Note that optional module
* dependencies are exactly that, optional and are not checked.
*
* Notice that this function essenitally performs a <a
* href="http://en.wikipedia.org/wiki/Depth-first_search"
* >DepthFirstSeach</a> when checking modules to successfully accomplish its
* goal without ending in an infinite loop.
*
* @param current
* ModuleMetaData currently being checked
*
* @param checkedModules
* Data about the current progress of the module checks
* @return true if the module dictated by the given current module name can
* run.
*/
private boolean canModuleRunWithItsDependentModules(String current,
Map<String, CheckType> checkedModules) {
// This is the main part of the DFS algorithm
checkedModules.put(current, CheckType.DIRTY);
logger.info("Checking module dependencies for " + current);
ModuleMetaData meta = moduleConfigs.get(current);
if (meta == null) {
// we ran into a module that does not exist! AHAHA... make sure
// that this module does not exist, stop processing and return false
logger.warn("One of the required modules for " + current
+ " does not exisit. Removing " + current);
checkedModules.remove(current);
moduleConfigs.remove(current); // slightly unnecessary at this point
// since we know it does not exist
return false;
}
// Now make sure that all required submodules exist
boolean moduleOkay = true;
Map<String, DependencyType> dependencies = meta.getModuleDependencies();
Iterator<String> i = dependencies.keySet().iterator();
while (i.hasNext()) {
String nextToCheck = i.next();
if (dependencies.get(nextToCheck) == DependencyType.REQUIRED) {
CheckType statusOfNextToCheck = checkedModules.get(nextToCheck);
if (statusOfNextToCheck == null
|| statusOfNextToCheck == CheckType.UNKNOWN) {
// notice that the following operator will short circuit and
// nextToCheck may be checked later (or not at all) if
// moduleOkay is already false.
// Also notice that it should be okay to call this next
// function with a module name that does not exist
moduleOkay = moduleOkay
&& canModuleRunWithItsDependentModules(nextToCheck,
checkedModules);
} else if (checkedModules.get(nextToCheck) == CheckType.DIRTY) {
// We have a circular dependency at this point and should
// not make a new call to check the nextModule
// NOTHING
}
}
}
if (!moduleOkay) {
// we ran into a module that does not have all of its dependencies!
// remove this module from our module listing
logger.warn("Removing module " + current + " from module list");
checkedModules.remove(current);
moduleConfigs.remove(current);
return false;
} else {
logger.info("Module " + current + " has all required dependencies.");
checkedModules.put(current, CheckType.CHECKED); // part of DFS
return true;
}
}
@Override
public Map<String, String> getConfigurations() {
return metaData.getConfigFiles();
}
@Override
public String getPathToModules() throws RemoteException {
return metaData.getPathToModules();
}
/**
* Main run loop of the ModuleManager. Each loops sets the next module to
* the default module specified, then runs the current module's init
* function. After, the current module is set to the next module, which will
* be the defualt module if the current module has not specified the next
* one.
*
* A note about the internal implementation: We use a semaphore
* implementation called CountDownLatch <a
* href="http://en.wikipedia.org/wiki/Semaphore_(programming)">link</a>
* which is used to block the module manager execution in run while a module
* is running. At first we were hoping to call a blocking function in the
* run function so that we could wait on a return from the module. This soon
* became unreasonable as a typical Processing Module entry point is #init
* which would not block because it would spawn new threads. This may also
* become relevant for the AWTModule.
*
* Right now it is assumed that only one module is running at a time and
* hence only one module will be activly calling the module managers run
* function.. If this ever becomes not the case we may need multiple
* countDown latches and ensure that this is syncronized along with any
* other public functions. TODO document ordering on calling hw functions
* document manifest stuff changes to the metaData general integration
* aspect
*
* @throws ModuleLoadException
*
*/
public void run() throws
ModuleLoadException {
//while (true) {
// create a new Module Executor
setupPreRuntime();
try {
runCurrentModule();
} catch (ModuleRuntimeException e) {
logger.error("Module Runtime exception occured while running " + currentModuleMetaData.getPackageName());
logger.warn("Setting default module just in case");
loadDefault = true;
}
postModuleRuntime();
//}
}
private void setupPreRuntime() throws
ModuleLoadException {
if (loadDefault) {
preModuleRuntime(defaultModuleMetaData);
} else {
try {
preModuleRuntime(nextModuleMetaData);
logger.debug("Module was set correctly!");
} catch (ModuleLoadException e) {
logger.error("Module [" + nextModuleMetaData.getPackageName()
+ "] could not be loaded");
logger.warn("Loading default module");
preModuleRuntime(defaultModuleMetaData);
}
}
commonPreRuntime();
}
private void commonPreRuntime() {
loadDefault = true;
}
private void preModuleRuntime(ModuleMetaData mmd) throws ModuleLoadException {
moduleStack.push(new ModuleFrameExecutor(mmd.getPackageName()
+ "." + mmd.getClassName(), (new File(
metaData.getPathToModules(), mmd.getJarFileName())).getPath()));
setCurrentModule(mmd);
logger.info("Loaded module " + mmd.getPackageName());
}
private void runCurrentModule() throws ModuleRuntimeException {
this.moduleStack.peek().run();
}
private void postModuleRuntime() {
// refresh modules
refreshModules();
}
/**
* Sets the default module for ModuleManager. Throws an exception if the
* default module cannot be loaded in which case the ModuleManager should
* exit.
*
* @param name
* Package name of module to be made default.
*
*/
private void setDefaultModule(String name) throws ModuleLoadException {
defaultModuleMetaData = moduleConfigs.get(name);
}
/**
* This will attempt to set the current module as the nextModule
*
* @throws ModuleLoadException
*/
private void setCurrentModule(ModuleMetaData mmd)
throws ModuleLoadException {
currentModuleMetaData = mmd;
}
/**
* Sets next module to be loaded, after the current module.
*
* @param name
* Package name of module to be loaded next.
*
* @return true if module is set, false otherwise.
*/
@Override
public boolean setNextModule(String name) {
// make a test to check that xml is checked as well even if the module
// exists
// grab the associated ModuleMetaData
// instantiate the next module using loadModuleFromMetaData
// TODO check that this method is syncronized!!!
// BE CAREFUL!!!
logger.debug("Attempting to set next module to: " + name);
// check that currentModule can set this package in question
if (!currentModuleMetaData.getOptionalAll()
&& !currentModuleMetaData.getModuleDependencies().containsKey(
name)) {
logger.debug("The next module could not be set because the current module is not allowed to run the requested module");
return false;
}
try {
nextModuleMetaData = moduleConfigs.get(name);
if (nextModuleMetaData == null) {
throw new ModuleLoadException(
"Metadata for the requested module is not available");
}
loadDefault = false;
return true;
} catch (ModuleLoadException e) {
logger.debug("The next module could not be loaded because there is no module meta data available.");
loadDefault = true; // dont necessarily need since it wasnt changed.
return false;
}
}
/**
* This function should be used by a module to get the information about its
* meta data or other modules meta datas as long as they have the permission
* to launch that module
*
* @param packageName
* @return
*/
@Override
public ModuleMetaData getModuleMetaData(String packageName) {
ModuleMetaData toReturn = moduleConfigs.get(packageName);
if (
currentModuleMetaData.getOptionalAll() ||
currentModuleMetaData.getModuleDependencies().containsKey(packageName) ||
currentModuleMetaData.getPackageName().equals(packageName) // allow module to get its own information
) {
return toReturn;
}
// TODO throw exception instead
return null;
}
/**
* This function allows for the current running module to get its own
* package name.
*
* TODO integrate this with Module Helper
*/
@Override
public String getCurrentModulePackageName() {
return currentModuleMetaData.getPackageName();
}
@Override
public String[] getAllAvailableModules() {
if (currentModuleMetaData.getOptionalAll()) {
return moduleConfigs.keySet().toArray(new String[0]);
}
return null;
}
// //////////////////////////////////////////////////
// TODO the remaing methods are for testing only! //
// //////////////////////////////////////////////////
public void setCurrentModuleMetaData(String name) {
currentModuleMetaData = moduleConfigs.get(name);
}
// USED ONLY FOR TESTING BELOW THIS COMMENT
public void setCurrentModuleMetaData(ModuleMetaData current) {
this.currentModuleMetaData = current;
}
public ModuleManagerMetaData getMetaData() {
return metaData;
}
public static void removeInstance() {
metaData = null;
instance = null;
}
public void setModuleMetaDataMap(Map<String, ModuleMetaData> m) {
this.moduleConfigs = m;
}
public Map<String, ModuleMetaData> getModuleMetaDataMap() {
return this.moduleConfigs;
}
public void setCurrentModule(ModuleInterface m) {
currentModule = m;
}
public void testSetDefaultModule(String name) throws ModuleLoadException {
setDefaultModule(name);
}
public void setMetaData(ModuleManagerMetaData data) {
metaData = data;
}
public static void createEmptyInstance() {
instance = new ModuleManager("NotUsed");
}
private ModuleManager(
String notUsedExceptToDifferentiateBetweenTheActualCTor) {
}
public String getNextModuleName() {
return this.nextModuleMetaData.getPackageName();
}
public void setNextModuleMetaData(ModuleMetaData mmd) {
this.nextModuleMetaData = mmd;
}
@Override
public ModuleMetaData getDefaultModuleMetaData() {
return defaultModuleMetaData;
}
public void setDefaultModuleMetaData(ModuleMetaData val) {
defaultModuleMetaData = val;
}
public void setDefault(boolean val) {
this.loadDefault = val;
}
public ModuleMetaData getCurrentModuleMetaData() {
return currentModuleMetaData;
}
//////////////////////////////////////////////////////////
// Scanner Interface Stuff
// * These methods are pure delegators.
//////////////////////////////////////////////////////////
@Override
public String next() throws NoSuchElementException {
return in.next();
}
@Override
public int nextInt() throws InputMismatchException, NoSuchElementException {
return in.nextInt();
}
public static void destroyCurrentModule() {
logger.debug("In DestroyCurrentModlue");
try {
ModuleManager instance = getInstance();
logger.debug("After Get instance");
while(!instance.moduleStack.empty()){
synchronized (instance.moduleStack) {
((ModuleFrameExecutor) instance.moduleStack.pop()).close();
}
}
logger.debug("After clear stack");
instance.setDefault(true);
instance.run();
} catch(ManifestLoadException e) {
e.printStackTrace();
} catch(ModuleLoadException e) {
e.printStackTrace();
}
}
public String getModulePath() {
return metaData.getPathToModules();
}
}