/** * 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.input_services.hardware; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import edu.mines.acmX.exhibit.input_services.hardware.devicedata.DeviceDataInterface; import edu.mines.acmX.exhibit.input_services.hardware.drivers.DriverInterface; import edu.mines.acmX.exhibit.input_services.hardware.drivers.InvalidConfigurationFileException; import edu.mines.acmX.exhibit.module_management.metas.DependencyType; /** * The HardwareManager acts as a layer of communication for retrieving drivers. * Information is loaded via a manifest file which can be modified. The manager * will choose the most appropriate driver given a functionality and a specific * module's set of permissions. The manager is a singleton to reduce conflicts * between driver requests. The manager also checks whether a module's required * functionalities are available. * * The Manager is responsible for the following tasks:<br/><br/> * -Loading the HardwareManager manifest file.<br/> * -Validating the manifest file<br/> * -> Checks to see if any of the classpaths provided for interfaces or * drivers is invalid.<br/> * -> Checks to see if any functionalities given by drivers are invalid * i.e. a functionality that does not exist in the <functionalities> * tag<br/> * - Ensuring a given module meta data is able to run<br/> * -> Checks to see that all required functionalities are supported.<br/> * - Clears the driver cache so that persistance of driver data is not possible * in between module runtimes.<br/> * - Re-builds the driver cache given the currently running module meta data.<br/> * -> Checks the availabilities of drivers that are considered to support * required functionalities for the module.<br/> * - Builds drivers at runtime for functionalities considered optional for the * currently running module. <br/><br/> * * TODO Change so that if the requested driver is unavailable for a function. * then it will attempt to load the next one. * * @author Aakash Shah * @author Ryan Stauffer * * @see {@link HardwareManagerMetaData} {@link HardwareManagerManifestLoader} * */ public class HardwareManager implements HardwareManagerRemote { private static final Logger log = LogManager .getLogger(HardwareManager.class); public static final String DEFAULT_MANIFEST_PATH = "hardware_manager_manifest.xml"; private HardwareManagerMetaData metaData; private Map<String, DependencyType> currentModuleInputTypes; private volatile static HardwareManager instance = null; private static String manifest_path = DEFAULT_MANIFEST_PATH; private Map<String, List<String>> devices; // available list of devices private Map<String, DriverInterface> deviceDriverCache; private Map<String, String> configFileStore; /** * The constructor of the HardwareManager. This method loads the config * file, and ensures that the file is valid. <br/> * * @throws HardwareManagerManifestException * * @see {@link #validifyMetaData()} {@link #buildRequiredDevices()} * */ private HardwareManager() throws HardwareManagerManifestException { loadConfigFile(); validifyMetaData(); currentModuleInputTypes = new HashMap<String, DependencyType>(); devices = new HashMap<String, List<String>>(); deviceDriverCache = new HashMap<String, DriverInterface>(); configFileStore = new HashMap<String, String>(); } /** * Get's an instance of the HardwareManager * * @return A HardwareManager instance * @throws HardwareManagerManifestException */ public synchronized static HardwareManager getInstance() throws HardwareManagerManifestException { if (instance == null) { instance = new HardwareManager(); } return instance; } /** * Sets the location to load the manifest config file from. Upon doing this, * a new instance of HardwareManager will be initialized. * * @param filepath * @throws HardwareManagerManifestException * * @see {@link #HardwareManager()} */ public static void setManifestFilepath(String filepath) throws HardwareManagerManifestException { if (null == filepath) { throw new HardwareManagerManifestException("Null filepath given"); } manifest_path = filepath; instance = new HardwareManager(); } /** * Loads the configuration file. * * @throws HardwareManagerManifestException * * @see {@link HardwareManagerManifestLoader} */ private void loadConfigFile() throws HardwareManagerManifestException { metaData = HardwareManagerManifestLoader.load(manifest_path); } /** * Responsible for verifying the HardwareManagerMetaData after having been * loaded from the manifest file. This will ensure that the driver and * interface classes exist as specified from within the meta-data. Also * checks to see whether there is a disjoint between the functionalities * each device supports and interfaces mapped to it. * * @throws HardwareManagerManifestException * If the meta-data is incorrect. * * @see {@link HardwareManagerMetaData} */ private void validifyMetaData() throws HardwareManagerManifestException { // verify classes existing Collection<String> interfaces = metaData.getFunctionalities().values(); Collection<String> drivers = metaData.getDevices().values(); try { for (String i : interfaces) { Class<? extends DeviceDataInterface> cl = Class.forName(i) .asSubclass(DeviceDataInterface.class); } for (String i : drivers) { Class<? extends DriverInterface> cl = Class.forName(i) .asSubclass(DriverInterface.class); } } catch (ClassNotFoundException e) { throw new HardwareManagerManifestException( "Invalid interface/driver class"); } catch (ExceptionInInitializerError e) { throw new HardwareManagerManifestException( "Invalid interface/driver class"); } catch (Exception e) { log.error("Getting an exception"); } // check support list for being disjoint // Builds a list of all the functionalities requested by all drivers Set<String> supports = new HashSet<String>(); for (List<String> deviceSupports : metaData.getDeviceSupports() .values()) { for (String s : deviceSupports) { supports.add(s); } } // Checks to see whether the built list is a subset of all provided // functionalities. Note the exception is thrown when a functionality // may not exist in the <functionalities> tag Set<String> available = metaData.getFunctionalities().keySet(); if (!available.containsAll(supports)) { throw new HardwareManagerManifestException( "Unknown functionality supported by device"); } } /** * Gets the configuration store so that drivers may load config files if * needed. From the passed configuration store, we alter the stored map to * be not "driver name -> config path" but rather * "canonical driver path -> config path" * * @param config * A map of devices->their config files provided from the module * manager manifest. */ public void setConfigurationFileStore(Map<String, String> config) { this.configFileStore.clear(); // supportedDevices = driver->driver_path Map<String, String> supportedDevices = metaData.getDevices(); Set<String> deviceKeys = config.keySet(); for (String configName : deviceKeys) { if (supportedDevices.containsKey(configName)) { this.configFileStore.put(supportedDevices.get(configName), config.get(configName)); } } } /** * Allows a driver to figure out whether a config file was provided for the * given canonical driver path. * * @param driverPath * Canonical path to the driver */ public boolean hasConfigFile(String driverPath) { return this.configFileStore.containsKey(driverPath); } /** * * @param driverPath * Canonical path to the driver * @return location to the config file indicated in the module manager * manifest. */ public String getConfigFile(String driverPath) { return this.configFileStore.get(driverPath); } /** * Sets the currently running module. Validation checks will not occur here * as to whether the module can run. * * @param mmd * The map of functionalities and their level of dependence */ public void setRunningModulePermissions(Map<String, DependencyType> mmd) { currentModuleInputTypes = mmd; } /** * Checks to see whether the functionalities that are required by the * currently running module are supported through the HardwareManager * manifest. * * @param inputTypes * * @throws BadDeviceFunctionalityRequestException * on failure */ public void checkPermissions(Map<String, DependencyType> inputTypes) throws BadDeviceFunctionalityRequestException { if (null == inputTypes || inputTypes.isEmpty()) { return; } Set<String> functionalities = inputTypes.keySet(); for (String functionality : functionalities) { DependencyType dt = inputTypes.get(functionality); // Ignore optional ones if (dt == DependencyType.REQUIRED) { // Make sure we support this functionality if (!metaData.getFunctionalities().containsKey(functionality)) { throw new BadDeviceFunctionalityRequestException( functionality + " not supported."); } } } } /** * Calls destroy on all driver objects, then removes all cached driver * objects and rebuilds this cache based on which devices are stored from * the next running module's input types. * * @throws InvalidConfigurationFileException * Thrown if the driver requires a configuration file that was * not present on the store. * @throws BadFunctionalityRequestException */ public void resetAllDrivers() throws InvalidConfigurationFileException, BadFunctionalityRequestException { for (String driverName : deviceDriverCache.keySet()) { DriverInterface driver = deviceDriverCache.get(driverName); driver.destroy(); } devices.clear(); deviceDriverCache.clear(); buildRequiredDevices(); } /** * Goes through all the required functionalities in the module's input * types and ensures that it is available. This only builds functionalities * marked as REQUIRED in the module's manifest file. This is so that the * user may be aware when a module's required functionalities have failed * to load. * * Optional functionalities that require extra drivers to load are done at * runtime. * * Ignore the following: Loop through currentModuleInputTypes and build * the cache off that instead of looking at EVERY driver. * * Add the input-types map into checkPermissions * * Document the fact that it ONLY BUILDS required functionalities. This is * because we have already ensured that all the required ones are supported. * However, we want to provide the user the knowledge that one of their * optional functionalities has failed to initialize. * * Therefore, this function only loads required, and a special method inside * the inflateDriver method will load the optional one at runtime. * * TODO Check if a driver was loaded for the required functionality, throw * error on fail. This would indicate that a functionality considered to be * required does not have an available device supporting it. * * @throws InvalidConfigurationFileException * @throws BadFunctionalityRequestException */ public void buildRequiredDevices() throws InvalidConfigurationFileException, BadFunctionalityRequestException { Set<String> moduleFunctionalities = currentModuleInputTypes.keySet(); for (String moduleFunc : moduleFunctionalities) { if (currentModuleInputTypes.get(moduleFunc) == DependencyType.REQUIRED) { // Returns device names List<String> supportingDevices = findDeviceDriversSupporting(moduleFunc); try { buildDriverList(supportingDevices); } catch (BadFunctionalityRequestException e) { e.printStackTrace(); throw new BadFunctionalityRequestException("There were no drivers available for: " + moduleFunc ); } } } } /** * @param func The functionality requested * @return list of strings of driver names supporting the given * functionality */ private List<String> findDeviceDriversSupporting(String func) { List<String> ret = new ArrayList<String>(); // Driver Name -> List of functionalities Map<String, List<String>> deviceSupports = metaData.getDeviceSupports(); Set<String> driverNames = deviceSupports.keySet(); for (String device : driverNames) { if (deviceSupports.get(device).contains(func)) { ret.add(device); } } return ret; } /** * Given a functionality and a list of supporting driver names, detect * which drivers are available and loads them into the driver cache if not * already present. * <br/> * Furthermore, we rely on the assumption that if a device is available * then all the functionalities it supports is also available. Therefore, * we also make a call to add onto our internal map of functionalities to * available driver paths. * * @param driverNames list of driver names to check availability for * @throws InvalidConfigurationFileException * * TODO Change name of exception thrown to match context more. Something * like 'DriverLoadException' * @throws BadFunctionalityRequestException */ // instantiates and checks the availability from the provided list. private void buildDriverList(List<String> driverNames) throws InvalidConfigurationFileException, BadFunctionalityRequestException { boolean loadedAtLeastOneDriverInList = false; // Driver Name -> Driver paths Map<String, String> driverPaths = metaData.getDevices(); for (String name : driverNames) { String path = driverPaths.get(name); // Check if cache already contains this driver if (deviceDriverCache.containsKey(path)) { loadedAtLeastOneDriverInList = true; continue; } try { Class<? extends DriverInterface> cl = Class.forName(path) .asSubclass(DriverInterface.class); Constructor<? extends DriverInterface> ctor = cl .getConstructor(); DriverInterface iDriver = ctor.newInstance(); // Check whether // the device is // available if (iDriver.isAvailable()) { // Cache the driver deviceDriverCache.put(path, iDriver); addDeviceFunctionalities(name); loadedAtLeastOneDriverInList = true; } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (SecurityException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (InvocationTargetException e) { throw new InvalidConfigurationFileException(e.getMessage()); } } if(!loadedAtLeastOneDriverInList) { throw new BadFunctionalityRequestException("No driver driver in list loaded"); } } /** * Given a driver, we add all the functionalities it supports to our * internal list of available functionalities. * @param driverName driver that is available */ private void addDeviceFunctionalities(String driverName) { Map<String, List<String>> deviceSupports = metaData.getDeviceSupports(); List<String> functionalities = deviceSupports.get(driverName); for (String s : functionalities) { if (devices.containsKey(s)) { devices.get(s).add(metaData.getDevices().get(driverName)); } else { List<String> drivers = new ArrayList<String>(); drivers.add(metaData.getDevices().get(driverName)); devices.put(s, drivers); } } } /** * Constructs a driver object for a given functionality and driver path. * * @param driverPath * @param functionality * @return An instance of a driver capable of supporting that functionality * @throws BadFunctionalityRequestException * no devices support that functionality * @throws UnknownDriverRequest thrown if the driver is not * connected/in the cache */ public DeviceDataInterface inflateDriver(String driverPath, String functionality) throws BadFunctionalityRequestException, UnknownDriverRequest { String functionalityPath = getFunctionalityPath(functionality); if ("".equals(functionalityPath)) { throw new BadFunctionalityRequestException(functionality + " is unknown"); } DeviceDataInterface iDriver = null; if (!deviceDriverCache.containsKey(driverPath)) { throw new UnknownDriverRequest("Requested driver " + driverPath + " is not available/unknown"); } iDriver = (DeviceDataInterface) deviceDriverCache.get(driverPath); return iDriver; } /** * Returns the function class path for a given functionality. * * @param functionality * @return Interface path for the given functionality */ private String getFunctionalityPath(String functionality) { Map<String, String> fPaths = metaData.getFunctionalities(); if (fPaths.containsKey(functionality)) { return fPaths.get(functionality); } return ""; } /** * Returns a list of driver paths that support a given functionality. In * the event that the functionality was considered optional, attempt to * build the driver and store it into the cache and list of available * functionalities. * * @param functionality * @return list of driver paths that support the given functionality * @throws BadFunctionalityRequestException * no devices support that functionality * @throws InvalidConfigurationFileException In the event this functionality * was considered optional and failed to load correctly. * * @see {@link HardwareManager#findDeviceDriversSupporting(String)} * {@link HardwareManager#buildDriverList(List)} */ public List<String> getDevices(String functionality) throws BadFunctionalityRequestException, InvalidConfigurationFileException { // Check if the functionality was listed in our HardwareManager // manifest. if (!metaData.getFunctionalities().containsKey(functionality)) { throw new BadFunctionalityRequestException( "Bad functionality requested"); } if (!devices.containsKey(functionality)) { // Check if this functionality was considered 'OPTIONAL' if (currentModuleInputTypes.get(functionality) == DependencyType.OPTIONAL) { // Returns device names List<String> supportingDevices = findDeviceDriversSupporting(functionality); buildDriverList(supportingDevices); } else { // Ideally this exception should never be thrown. The only time // we enter this block is if dependency was required AND not in // our cache. throw new BadFunctionalityRequestException( "Bad functionality [" + functionality + "] requested"); } } // Return our list return devices.get(functionality); } /** * Allows the user to get an instance of the first available driver * supporting the specified functionality. * * @param functionality * @return An instance of a driver capable of supporting that functionality * @throws BadFunctionalityRequestException * no devices support that functionality * @throws UnknownDriverRequest * @throws InvalidConfigurationFileException */ @Override public DeviceDataInterface getInitialDriver(String functionality) throws BadFunctionalityRequestException, UnknownDriverRequest, InvalidConfigurationFileException { List<String> drivers = getDevices(functionality); // TODO check into get(0) -> underflow exception possible? return inflateDriver(drivers.get(0), functionality); } public int getNumberofDriversInCache() { return deviceDriverCache.size(); } }