/**
* Copyright 2013, Big Switch Networks, Inc.
*
* 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 net.floodlightcontroller.core.module;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
import java.util.Set;
import net.floodlightcontroller.core.module.FloodlightModulePriority.Priority;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
/**
* Finds all Floodlight modules in the class path and loads/starts them.
* @author alexreimers
*
*/
public class FloodlightModuleLoader {
protected static final Logger logger =
LoggerFactory.getLogger(FloodlightModuleLoader.class);
private Map<Class<? extends IFloodlightService>,
Collection<IFloodlightModule>> serviceMap;
private Map<IFloodlightModule,
Collection<Class<? extends
IFloodlightService>>> moduleServiceMap;
private Map<String, IFloodlightModule> moduleNameMap;
private List<IFloodlightModule> loadedModuleList;
private final FloodlightModuleContext floodlightModuleContext;
protected boolean startupModules;
private static URI configFile;
public static final String COMPILED_CONF_FILE =
"floodlightdefault.properties";
public static final String FLOODLIGHT_MODULES_KEY =
"floodlight.modules";
public FloodlightModuleLoader() {
loadedModuleList = Collections.emptyList();
floodlightModuleContext = new FloodlightModuleContext(this);
startupModules = true;
}
/**
* Gets the map of modules and their names.
* @return An UNMODIFIABLE map of the modules and their names.
*/
public synchronized Map<String, IFloodlightModule> getModuleNameMap() {
if(moduleNameMap == null)
return ImmutableMap.of();
else
return Collections.unmodifiableMap(moduleNameMap);
}
/**
* Gets the list of modules in the order that they will be/were initialized
* @return An UNMODIFIABLE list of loaded modules, or null if
* not initialized.
*/
public List<IFloodlightModule> getModuleList() {
if (loadedModuleList == null)
return Collections.emptyList();
else
return Collections.unmodifiableList(loadedModuleList);
}
/**
* Return the location of the config file that was used to initialize
* floodlight. If no config file was specified (i.e. floodlight was
* configured from the default resource), then the return value is null.
* @return location of the config file or null if no config file was
* specified
*/
public static URI getConfigFileURI() {
return configFile;
}
/**
* Finds all IFloodlightModule(s) in the classpath. It creates 3 Maps.
* serviceMap -> Maps a service to a module
* moduleServiceMap -> Maps a module to all the services it provides
* moduleNameMap -> Maps the string name to the module
* @throws FloodlightModuleException If two modules are specified in the
* configuration that provide the same service.
*/
protected synchronized void findAllModules(Collection<String> mList)
throws FloodlightModuleException {
if (serviceMap != null)
return;
serviceMap = new HashMap<>();
moduleServiceMap = new HashMap<>();
moduleNameMap = new HashMap<>();
// Get all the current modules in the classpath
ClassLoader cl = Thread.currentThread().getContextClassLoader();
ServiceLoader<IFloodlightModule> moduleLoader =
ServiceLoader.load(IFloodlightModule.class, cl);
// Iterate for each module, iterate through and add it's services
Iterator<IFloodlightModule> moduleIter = moduleLoader.iterator();
while (moduleIter.hasNext()) {
IFloodlightModule m = null;
try {
m = moduleIter.next();
} catch (ServiceConfigurationError sce) {
logger.error("Could not find module: {}", sce.getMessage());
continue;
}
if (logger.isDebugEnabled()) {
logger.debug("Found module " + m.getClass().getName());
}
// Set up moduleNameMap
moduleNameMap.put(m.getClass().getCanonicalName(), m);
// Set up serviceMap
Collection<Class<? extends IFloodlightService>> servs =
m.getModuleServices();
if (servs != null) {
moduleServiceMap.put(m, servs);
for (Class<? extends IFloodlightService> s : servs) {
Collection<IFloodlightModule> mods =
serviceMap.get(s);
if (mods == null) {
mods = new ArrayList<IFloodlightModule>();
serviceMap.put(s, mods);
}
mods.add(m);
// Make sure they haven't specified duplicate modules in
// the config
int dupInConf = 0;
for (IFloodlightModule cMod : mods) {
if (mList.contains(cMod.getClass().getCanonicalName()))
dupInConf += 1;
}
if (dupInConf > 1) {
StringBuilder sb = new StringBuilder();
for (IFloodlightModule mod : mods) {
sb.append(mod.getClass().getCanonicalName());
sb.append(", ");
}
String duplicateMods = sb.toString();
String mess = "ERROR! The configuration file " +
"specifies more than one module that " +
"provides the service " +
s.getCanonicalName() +
". Please specify only ONE of the " +
"following modules in the config file: " +
duplicateMods;
throw new FloodlightModuleException(mess);
}
}
}
}
}
/**
* Loads the modules from a specified configuration file.
* @param fName The configuration file path
* @return An IFloodlightModuleContext with all the modules to be started
* @throws FloodlightModuleException
*/
public IFloodlightModuleContext loadModulesFromConfig(String fName)
throws FloodlightModuleException {
Properties prop = new Properties();
Collection<String> configMods = new ArrayList<>();
if (fName == null) {
logger.info("Loading default modules");
InputStream is = this.getClass().getClassLoader().
getResourceAsStream(COMPILED_CONF_FILE);
mergeProperties(is, null, configMods, prop);
} else {
File confFile = new File(fName);
if (! confFile.exists())
throw new FloodlightModuleConfigFileNotFoundException(fName);
logger.info("Loading modules from {}", confFile.getPath());
if (confFile.isFile()) {
mergeProperties(null, confFile,
configMods, prop);
} else {
File[] files = confFile.listFiles();
Arrays.sort(files);
for (File f : files) {
logger.debug("Loading conf.d file {}", f.getPath());
if (f.isFile() &&
f.getName().matches(".*\\.properties$")) {
mergeProperties(null, f, configMods, prop);
}
}
}
}
return loadModulesFromList(configMods, prop);
}
private void mergeProperties(InputStream is,
File confFile,
Collection<String> configMods,
Properties prop)
throws FloodlightModuleException {
try {
Properties fprop = new Properties();
if (is != null) {
fprop.load(is);
} else {
try (FileInputStream fis = new FileInputStream(confFile)) {
fprop.load(fis);
}
}
String moduleList = fprop.getProperty(FLOODLIGHT_MODULES_KEY);
if (moduleList != null) {
moduleList = moduleList.replaceAll("\\s", "");
configMods.addAll(Arrays.asList(moduleList.split(",")));
}
fprop.remove(FLOODLIGHT_MODULES_KEY);
prop.putAll(fprop);
} catch (IOException e) {
throw new FloodlightModuleException(e);
}
}
/**
* Loads modules (and their dependencies) specified in the list
* @param configMods The fully-qualified module names
* @return The ModuleContext containing all the loaded modules
* @throws FloodlightModuleException
*/
public synchronized IFloodlightModuleContext
loadModulesFromList(Collection<String> configMods,
Properties prop)
throws FloodlightModuleException {
logger.debug("Starting module loader");
findAllModules(configMods);
ArrayList<IFloodlightModule> moduleList = new ArrayList<>();
Map<Class<? extends IFloodlightService>, IFloodlightModule> moduleMap =
new HashMap<>();
HashSet<String> modsVisited = new HashSet<>();
ArrayDeque<String> modsToLoad = new ArrayDeque<>(configMods);
while (!modsToLoad.isEmpty()) {
String moduleName = modsToLoad.removeFirst();
traverseDeps(moduleName, modsToLoad,
moduleList, moduleMap, modsVisited);
}
parseConfigParameters(prop);
loadedModuleList = moduleList;
initModules(moduleList);
if(startupModules)
startupModules(moduleList);
return floodlightModuleContext;
}
private void traverseDeps(String moduleName,
Collection<String> modsToLoad,
ArrayList<IFloodlightModule> moduleList,
Map<Class<? extends IFloodlightService>,
IFloodlightModule> moduleMap,
Set<String> modsVisited)
throws FloodlightModuleException {
if (modsVisited.contains(moduleName)) return;
modsVisited.add(moduleName);
IFloodlightModule module = moduleNameMap.get(moduleName);
if (module == null) {
throw new FloodlightModuleException("Module " +
moduleName + " not found");
}
// Add its dependencies to the stack
Collection<Class<? extends IFloodlightService>> deps =
module.getModuleDependencies();
if (deps != null) {
for (Class<? extends IFloodlightService> c : deps) {
IFloodlightModule m = moduleMap.get(c);
if (m == null) {
Collection<IFloodlightModule> mods = serviceMap.get(c);
// Make sure only one module is loaded
if ((mods == null) || (mods.size() == 0)) {
throw new FloodlightModuleException("ERROR! Could not " +
"find an IFloodlightModule that provides service " +
c.toString());
} else if (mods.size() == 1) {
IFloodlightModule mod = mods.iterator().next();
traverseDeps(mod.getClass().getCanonicalName(),
modsToLoad, moduleList,
moduleMap, modsVisited);
} else {
boolean found = false;
for (IFloodlightModule moduleDep : mods) {
String d = moduleDep.getClass().getCanonicalName();
if (modsToLoad.contains(d)) {
modsToLoad.remove(d);
traverseDeps(d,
modsToLoad, moduleList,
moduleMap, modsVisited);
found = true;
break;
}
}
if (!found) {
Priority maxp = Priority.MINIMUM;
ArrayList<IFloodlightModule> curMax = new ArrayList<>();
for (IFloodlightModule moduleDep : mods) {
FloodlightModulePriority fmp =
moduleDep.getClass().
getAnnotation(FloodlightModulePriority.class);
Priority curp = Priority.NORMAL;
if (fmp != null) {
curp = fmp.value();
}
if (curp.value() > maxp.value()) {
curMax.clear();
curMax.add(moduleDep);
maxp = curp;
} else if (curp.value() == maxp.value()) {
curMax.add(moduleDep);
}
}
if (curMax.size() == 1) {
traverseDeps(curMax.get(0).
getClass().getCanonicalName(),
modsToLoad, moduleList,
moduleMap, modsVisited);
} else {
StringBuilder sb = new StringBuilder();
for (IFloodlightModule mod : curMax) {
sb.append(mod.getClass().getCanonicalName());
sb.append(", ");
}
String duplicateMods = sb.toString();
throw new FloodlightModuleException("ERROR! Found more " +
"than one (" + mods.size() + ") IFloodlightModules that provides " +
"service " + c.toString() +
". This service is required for " + moduleName +
". Please specify one of the following modules in the config: " +
duplicateMods);
}
}
}
}
}
}
// Add the module to be loaded
addModule(moduleMap, moduleList, module);
}
/**
* Add a module to the set of modules to load and register its services
* @param moduleMap the module map
* @param moduleList the module set
* @param module the module to add
*/
protected void addModule(Map<Class<? extends IFloodlightService>,
IFloodlightModule> moduleMap,
Collection<IFloodlightModule> moduleList,
IFloodlightModule module) {
Collection<Class<? extends IFloodlightService>> servs =
moduleServiceMap.get(module);
if (servs != null) {
for (Class<? extends IFloodlightService> c : servs)
moduleMap.put(c, module);
}
moduleList.add(module);
}
/**
* Allocate service implementations and then init all the modules
* @param moduleSet The set of modules to call their init function on
* @throws FloodlightModuleException If a module can not properly be loaded
*/
protected void initModules(Collection<IFloodlightModule> moduleSet)
throws FloodlightModuleException {
for (IFloodlightModule module : moduleSet) {
// Get the module's service instance(s)
Map<Class<? extends IFloodlightService>,
IFloodlightService> simpls = module.getServiceImpls();
// add its services to the context
if (simpls != null) {
for (Entry<Class<? extends IFloodlightService>,
IFloodlightService> s : simpls.entrySet()) {
if (logger.isDebugEnabled()) {
logger.debug("Setting " + s.getValue() +
" as provider for " +
s.getKey().getCanonicalName());
}
if (floodlightModuleContext.getServiceImpl(s.getKey()) == null) {
floodlightModuleContext.addService(s.getKey(),
s.getValue());
} else {
throw new FloodlightModuleException("Cannot set "
+ s.getValue()
+ " as the provider for "
+ s.getKey().getCanonicalName()
+ " because "
+ floodlightModuleContext.getServiceImpl(s.getKey())
+ " already provides it");
}
}
}
}
for (IFloodlightModule module : moduleSet) {
// init the module
if (logger.isDebugEnabled()) {
logger.debug("Initializing " +
module.getClass().getCanonicalName());
}
module.init(floodlightModuleContext);
}
}
/**
* Call each loaded module's startup method
* @param moduleSet the module set to start up
* @throws FloodlightModuleException
*/
protected void startupModules(Collection<IFloodlightModule> moduleSet)
throws FloodlightModuleException {
for (IFloodlightModule m : moduleSet) {
if (logger.isDebugEnabled()) {
logger.debug("Starting " + m.getClass().getCanonicalName());
}
m.startUp(floodlightModuleContext);
}
}
/** Tuple of floodlight module and run method */
private static class RunMethod {
private final IFloodlightModule module;
private final Method method;
public RunMethod(IFloodlightModule module, Method method) {
this.module = module;
this.method = method;
}
public void run() throws FloodlightModuleException {
try {
if (logger.isDebugEnabled()) {
logger.debug("Running {}", this);
}
method.invoke(module);
} catch (IllegalAccessException | IllegalArgumentException
| InvocationTargetException e) {
throw new FloodlightModuleException("Failed to invoke "
+ "module Run method " + this, e);
}
}
@Override
public String toString() {
return module.getClass().getCanonicalName() + "." + method;
}
}
public void runModules() throws FloodlightModuleException {
List<RunMethod> mainLoopMethods = Lists.newArrayList();
for (IFloodlightModule m : getModuleList()) {
for (Method method : m.getClass().getDeclaredMethods()) {
Run runAnnotation = method.getAnnotation(Run.class);
if (runAnnotation != null) {
RunMethod runMethod = new RunMethod(m, method);
if(runAnnotation.mainLoop()) {
mainLoopMethods.add(runMethod);
} else {
runMethod.run();
}
}
}
}
if(mainLoopMethods.size() == 1) {
mainLoopMethods.get(0).run();
} else if (mainLoopMethods.size() > 1) {
throw new FloodlightModuleException("Invalid module configuration -- "
+ "multiple run methods annotated with mainLoop detected: " + mainLoopMethods);
}
}
/**
* Parses configuration parameters for each module
* @param prop The properties file to use
*/
protected void parseConfigParameters(Properties prop) {
if (prop == null) return;
Enumeration<?> e = prop.propertyNames();
while (e.hasMoreElements()) {
String key = (String) e.nextElement();
String configValue = null;
int lastPeriod = key.lastIndexOf(".");
String moduleName = key.substring(0, lastPeriod);
String configKey = key.substring(lastPeriod + 1);
// Check to see if it's overridden on the command line
String systemKey = System.getProperty(key);
if (systemKey != null) {
configValue = systemKey;
} else {
configValue = prop.getProperty(key);
}
IFloodlightModule mod = moduleNameMap.get(moduleName);
if (mod == null) {
logger.debug("Module {} not found or loaded. " +
"Not adding configuration option {} = {}",
new Object[]{moduleName, configKey, configValue});
} else {
logger.debug("Adding configuration option {} = {} for module {}",
new Object[]{configKey, configValue, moduleName});
floodlightModuleContext.addConfigParam(mod, configKey, configValue);
}
}
}
public boolean isStartupModules() {
return startupModules;
}
public void setStartupModules(boolean startupModules) {
this.startupModules = startupModules;
}
}