/*==========================================================================*\ | $Id: SubsystemManager.java,v 1.6 2012/03/07 03:03:41 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2010 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT 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 Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.core; import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import net.sf.webcat.FeatureProvider; import org.apache.log4j.Logger; import com.webobjects.appserver.WOComponent; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; // ------------------------------------------------------------------------- /** * Manages the Subsystem's stored on disk. A subsystem is either a WebObjects * framework or a separate jar file that contains a framework. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.6 $, $Date: 2012/03/07 03:03:41 $ */ public class SubsystemManager { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Initialize the <code>SubsystemManager</code>. The * <code>SubsystemManager</code> will look for two properties in * the application's property settings. First, it will look for * the property "subsystem.jar.dir". If found, it will load all * jar'ed subsystems from this directory. Second, it will look * for "subsystem.unjarred.classes", which is a "|"-separated list * of subsystem classes to register--subsystems that are already on * the classpath instead of in subsystem jars. If this property is * defined, the corresponding classes will be registered as * subsystems. Either property may be undefined, in which case it * will be ignored. * * @param properties The application's property settings */ public SubsystemManager(WCProperties properties) { if (log.isDebugEnabled()) { log.debug("creating subsystem manager", new Exception("from here")); } if (properties != null) { ArrayList<String> subsystemNames = new ArrayList<String>(); // Have to look in the system properties, because that is where // all subsystem info will go, not in the config file for (Enumeration<Object> e = System.getProperties().keys(); e.hasMoreElements();) { String key = (String)e.nextElement(); if ( key.startsWith(SUBSYSTEM_KEY_PREFIX) && key.indexOf('.', SUBSYSTEM_KEY_PREFIX.length()) == -1) { String name = key.substring(SUBSYSTEM_KEY_PREFIX.length()); subsystemNames.add(name); } } addSubsystemsInOrder( subsystemNames, null, new HashMap<String, String>(), properties); } initAllSubsystems(); envp(); pluginProperties(); startAllSubsystems(); // Start up a thread to run periodic maintenance tasks every day new Thread(new Runnable() { // ---------------------------------------------------------- public void run() { performPeriodicMaintenance(); try { Thread.sleep(1000 * 60 * 60 * 24); } catch (InterruptedException e) { log.info("periodic maintenance task interrupted", e); } } }).start(); } //~ Public Methods ........................................................ // ---------------------------------------------------------- /** * Get the requested subsystem. * * @param name The name of the subsystem to get * @return The corresponding JarSubsystem */ public Subsystem subsystem(String name) { return subsystems.get(name); } // ---------------------------------------------------------- /** * Get an iterator for all loaded subsystems by name. * * @return An iterator for the names of all loaded subsystems */ public NSArray<Subsystem> subsystems() { return subsystemArray; } // ---------------------------------------------------------- /** * Add a Subsystem to the manager, given a class name. * * @param name the symbolic name to use for this subsystem * @param className the class name to load */ public void addSubsystemFromClassName(String name, String className) { log.debug("attempting to load subsystem " + name + " using class '" + className + "'"); try { addSubsystem(name, (Subsystem)DelegatingUrlClassLoader.getClassLoader() .loadClass(className).newInstance()); } catch (Exception e) { log.error("Exception loading subsystem " + name + " from class " + className + ":", e); } } // ---------------------------------------------------------- /** * Add a Subsystem to the manager. * * @param name The subsystem's name * @param s The subsystem object to add */ public void addSubsystem(String name, Subsystem s) { if (name == null) name = s.name(); if (!subsystems.containsKey(name)) { log.info("Registering subsystem " + s.name() + " as " + name); s.setName(name); subsystems.put(name, s); subsystemArray.addObject(s); clearSubsystemPropertyCache(); } else { log.error("Subsystem already registered: " + name); } } // ---------------------------------------------------------- /** * Calls {@link Subsystem#initializeSessionData(Session)} for * all registered subsystems. * @param s the session to initialize */ public void initializeSessionData(Session s) { NSArray<Subsystem> subs = subsystems(); for (int i = 0; i < subs.count(); i++) { subs.objectAtIndex(i).initializeSessionData(s); } } // ---------------------------------------------------------- /** * Collects the subsystem fragments for the specified fragment key from all * currently loaded subsystems. * * @param fragmentKey the unique identifier of the fragment * @return an array of component classes that should be plugged in for the * specified fragment */ public NSArray<Class<? extends WOComponent>> subsystemFragmentsForKey( String fragmentKey) { NSMutableArray<Class<? extends WOComponent>> fragments = new NSMutableArray<Class<? extends WOComponent>>(); for (Subsystem sub : subsystems()) { Class<? extends WOComponent> frag = sub.subsystemFragmentForKey(fragmentKey); if (frag != null) { fragments.addObject(frag); } } return fragments; } // ---------------------------------------------------------- /** * Take a list of subsystem names (typically, requirements needed to * support some feature) and determine if they are present. * @param names a list of subsystem names to look for * @return true if all of the named subsystems are installed */ public boolean subsystemsAreInstalled(NSArray<String> names) { for (String name : names) { if (subsystems.get(name) == null) { return false; } } return true; } // ---------------------------------------------------------- /** * Get the command line environment variables used for executing * external commands. * @return a dictionary of ENV bindings */ public NSDictionary<String, String> environment() { if (envCache == null) { NSMutableDictionary<String, String> env = inheritedEnvironment(); for (Subsystem sub : subsystems()) { sub.addEnvironmentBindings(env); } envCache = env; if (log.isDebugEnabled()) { log.debug("plug-in ENV = " + env); } } return envCache; } // ---------------------------------------------------------- /** * Get the command line environment variables used for executing * external commands in a form suitable for passing to * {@link Runtime#exec(String,String[])}. * @return a string array of ENV bindings, each in the form: * <i>NAME=value</i>. */ public String[] envp() { if (envpCache == null) { NSDictionary<String, String> env = environment(); ArrayList<String> envpList = new ArrayList<String>(); for (String key : env.keySet()) { String val = env.objectForKey(key).toString(); envpList.add(key + "=" + val); } String[] envp = envpList.toArray(new String[envpList.size()]); envpCache = envp; if (log.isDebugEnabled()) { log.debug("envp = " + arrayToString(envp)); } } return envpCache; } // ---------------------------------------------------------- /** * Get the plug-in property definitions to be passed to plug-ins. * @return a dictionary of plug-in properties */ public NSDictionary<String, String> pluginProperties() { if (pluginPropertiesCache == null) { NSMutableDictionary<String, String> properties = new NSMutableDictionary<String, String>(); for (Subsystem sub : subsystems()) { sub.addPluginPropertyBindings(properties); } pluginPropertiesCache = properties; if (log.isDebugEnabled()) { log.debug("plug-in properties = " + properties); } } return pluginPropertiesCache; } // ---------------------------------------------------------- /** * Clear the cache of any subsystem-provided environment definitions * or plug-in property definitions. */ public void clearSubsystemPropertyCache() { envCache = null; envpCache = null; pluginPropertiesCache = null; } // ---------------------------------------------------------- /** * Refreshes cached information about subsystems and their providers. */ public void refreshSubsystemDescriptorsAndProviders() { for (Iterator<?> i = FeatureProvider.providers().iterator(); i.hasNext();) { ((FeatureProvider)i.next()).refresh(); } if (subsystemArray != null) { for (Subsystem sub : subsystemArray) { sub.refreshDescriptor(); } } } //~ Private Methods ....................................................... // ---------------------------------------------------------- /** * Calls {@link Subsystem#init()} for all registered subsystems. */ private void initAllSubsystems() { for (Subsystem sub : subsystems()) { log.debug("initializing subsystem " + sub.name()); sub.init(); sub.subsystemInitCompleted(); log.debug("subsystem " + sub.name() + " initialized"); } } // ---------------------------------------------------------- /** * Calls {@link Subsystem#start()} for all registered subsystems. */ private void startAllSubsystems() { for (Subsystem sub : subsystems()) { log.debug("starting subsystem " + sub.name()); sub.start(); sub.subsystemHasStarted(); log.debug("subsystem " + sub.name() + " started"); } } // ---------------------------------------------------------- /** * Calls {@link Subsystem#performPeriodicMaintenance()} for all * registered subsystems. */ private void performPeriodicMaintenance() { for (Subsystem sub : subsystems()) { log.debug("periodic maintenance on subsystem " + sub.name()); try { sub.performPeriodicMaintenance(); } catch (Exception e) { log.error("Exception performing periodic maintenance on " + sub.name(), e); } catch (Error e) { log.error("Error performing periodic maintenance on " + sub.name(), e); } log.debug("subsystem " + sub.name() + " periodic maintenance finished"); } } // ---------------------------------------------------------- /** * Convert a (possibly null) comma-separated string into an array of * strings. * @param rawList the comma-separated string to split (can be null) * @return an array of the items in rawList, after separating on commas; * any whitespace surrounding commas are ignored; if rawList is null or * empty, then null is returned. */ private String[] featureList(String rawList) { String[] result = null; if (rawList != null) { rawList = rawList.trim(); if (!rawList.equals("")) { result = rawList.split("\\s*,\\s*"); } } return result; } // ---------------------------------------------------------- /** * Check to see if any of the strings in the featureList are stored * in theSet. If theSet is null, returns true. If featureList is * null, returns false. * @param featureList an array of strings to test * @param theSet a set to check in * @return true if any string in featureList is found in theSet, or if * theSet is null */ private boolean foundIn(String[] featureList, Map<String, String> theSet) { if (featureList == null) return false; if (theSet == null) return true; for (String feature : featureList) { if (theSet.containsKey(feature)) { return true; } } return false; } // ---------------------------------------------------------- /** * Check to see if all of the strings in the featureList are stored * in theSet. If featureList is null, returns false. Otherwise, if * theSet is null, returns true. * @param featureList an array of strings to test * @param theSet a set to check in * @return false if any string in featureList is missing in theSet */ private boolean missingFrom( String[] featureList, Map<String, String> theSet) { if (featureList == null) return false; if (theSet == null) return true; for (String key : featureList) { if (!theSet.containsKey(key)) { return true; } } return false; } // ---------------------------------------------------------- /** * Add all of the strings in featureList to theSet. * @param featureList an array of strings to add * @param theSet the map to add them to */ private void addTo(String[] featureList, Map<String, String> theSet) { if (featureList == null) return; for (String feature : featureList) { theSet.put(feature, feature); } } // ---------------------------------------------------------- /** * Generate a string from an array of strings. * @param array the array to convert */ private String arrayToString(String[] array) { if (array == null) return null; StringBuffer buffer = new StringBuffer(); buffer.append("[ "); for (int i = 0; i < array.length; i++) { if (i > 0) { buffer.append(", "); } buffer.append(array[i]); } buffer.append(" ]"); return buffer.toString(); } // ---------------------------------------------------------- /** * Install/load a list of subsystems, searching for dependencies and * using them to install subsystems in the correct order. Since * dependencies are fairly rare, a dumb O(n^2) algorithm is used for * the topological sort. * @param names a list of subsystem names that need to be * loaded/installed * @param pendingFeatures a map that contains the names of defined * features that have not yet been loaded, for dependency tracking * @param addedFeatures a map that contains the names of defined features * that have been installed (or at least partially installed) * @param properties The application's property settings */ private void addSubsystemsInOrder( ArrayList<String> names, Map<String, String> pendingFeatures, Map<String, String> addedFeatures, WCProperties properties) { if (names.size() == 0) return; int oldSize = names.size(); Map<String, String> incompleteFeatures = new HashMap<String, String>(); log.debug("starting subsystem list traversal: " + names); for (int i = 0; i < names.size(); i++) { String name = names.get(i); String[] depends = featureList( properties.getProperty(name + DEPENDS_SUFFIX)); String[] requires = featureList( properties.getProperty(name + REQUIRES_SUFFIX)); if (foundIn(depends, pendingFeatures) || foundIn(requires, pendingFeatures)) { if (log.isDebugEnabled()) { log.debug("skipping " + name + ": depends = " + arrayToString(depends) + "; requires = " + arrayToString(requires)); } incompleteFeatures.put(name, name); addTo(featureList( properties.getProperty(name + PROVIDES_SUFFIX)), incompleteFeatures); } else { if (log.isDebugEnabled()) { log.debug("loading " + name + ": depends = " + arrayToString(depends) + "; requires = " + arrayToString(requires)); } names.remove(i); i--; if (missingFrom(requires, addedFeatures)) { log.error("unable to load subsystem '" + name + "': one or more required subsystems are missing: " + arrayToString(requires)); } else { addedFeatures.put(name, name); addTo(featureList( properties.getProperty(name + PROVIDES_SUFFIX)), addedFeatures); String className = properties.getProperty(SUBSYSTEM_KEY_PREFIX + name); // Use a default class if no class name is specified // in the property if (className == null || className.equals("")) { className = Subsystem.class.getName(); } addSubsystemFromClassName(name, className); } } } if (oldSize == names.size()) { log.error( "cyclic or missing dependencies among subsystems detected: " + names); } else { addSubsystemsInOrder( names, incompleteFeatures, addedFeatures, properties); } } // ---------------------------------------------------------- /** * Load this application's current ENV bindings into a dictionary. * @return the current ENV bindings */ private NSMutableDictionary<String, String> inheritedEnvironment() { if (inheritedEnvCache == null) { NSMutableDictionary<String, String> env = new NSMutableDictionary<String, String>(); // Fill it up // First, try Unix command try { Process process = Runtime.getRuntime().exec( Application.isRunningOnWindows() ? "cmd /c set" : "printenv"); BufferedReader in = new BufferedReader( new InputStreamReader(process.getInputStream())); String line = in.readLine(); while (line != null) { int pos = line.indexOf('='); if (pos > 0) { String key = line.substring(0, pos); String val = line.substring(pos + 1); env.takeValueForKey(val, key); } line = in.readLine(); } } catch (java.io.IOException e) { log.error( "Error attempting to parse default ENV settings:", e); } inheritedEnvCache = env; if (log.isDebugEnabled()) { log.debug("inherited ENV = " + env); } } return inheritedEnvCache.mutableClone(); } //~ Instance/static variables ............................................. /** Map<String, JarSubsystem>: holds the loaded subsystems. */ private Map<String, Subsystem> subsystems = new HashMap<String, Subsystem>(); private NSMutableArray<Subsystem> subsystemArray = new NSMutableArray<Subsystem>(); private NSDictionary<String, String> inheritedEnvCache = null; private NSDictionary<String, String> envCache = null; private NSDictionary<String, String> pluginPropertiesCache = null; private String[] envpCache = null; private static final String SUBSYSTEM_KEY_PREFIX = "subsystem."; private static final String DEPENDS_SUFFIX = ".depends"; private static final String REQUIRES_SUFFIX = ".requires"; private static final String PROVIDES_SUFFIX = ".provides"; static Logger log = Logger.getLogger(SubsystemManager.class); }