/*==========================================================================*\ | $Id: Subsystem.java,v 1.6 2012/03/07 03:03:41 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2009 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.File; import java.io.FileInputStream; import java.io.IOException; import org.apache.log4j.Logger; import net.sf.webcat.FeatureDescriptor; import net.sf.webcat.WCServletAdaptor; import org.webcat.dbupdate.UpdateEngine; import org.webcat.dbupdate.UpdateSet; import com.webobjects.appserver.WOActionResults; import com.webobjects.appserver.WOComponent; import com.webobjects.appserver.WOContext; import com.webobjects.appserver.WORequest; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSBundle; import com.webobjects.foundation.NSData; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSPropertyListSerialization; // ------------------------------------------------------------------------- /** * The subsystem interface that defines the API used by the Core to * communicate with subsystems. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.6 $, $Date: 2012/03/07 03:03:41 $ */ public class Subsystem { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Creates a new Subsystem object. The constructor is called when * creating the subsystem, but subclasses should <b>NOT</b> include * startup actions in their constructors--only basic data initialization. * Instead, all startup actions should be placed in the subclass * {@link #init()} method, which will be called to "start up" each * subsystem <em>after</em> all subsystems have been created in the * proper order. */ public Subsystem() { // Nothing to initialize here } //~ Public Methods ........................................................ // ---------------------------------------------------------- /** * Get the short (one-word) human-readable name for this * subsystem. * * @return The short name */ public String name() { return name; } // ---------------------------------------------------------- /** * Set the short (one-word) human-readable name for this subsystem. * @param newName the name to use */ public void setName(String newName) { name = newName; } // ---------------------------------------------------------- /** * Get the FeatureDescriptor for this subsystem. * @return this subsystem's descriptor */ public FeatureDescriptor descriptor() { if (descriptor == null) { // First, look to see if there is an appropriate subsystem updater WCServletAdaptor adaptor = WCServletAdaptor.getInstance(); if (adaptor != null) { for (FeatureDescriptor sd : adaptor.subsystems()) { if (name.equals(sd.name())) { // found it! descriptor = sd; break; } } } // Otherwise, try to create one directly from properties if (descriptor == null) { log.debug("Unable to find feature descriptor for " + name() + " via adaptor. Creating one from properties."); descriptor = new FeatureDescriptor( name(), Application.configurationProperties(), false); } } return descriptor; } // ---------------------------------------------------------- /** * Clear the cached feature descriptor for this subsystem. */ public void refreshDescriptor() { descriptor = null; } // ---------------------------------------------------------- /** * Get a list of WO components that should be instantiated and presented * on the front page. * * @return The list of names, as strings */ public NSArray<String> frontPageStatusComponents() { return null; } // ---------------------------------------------------------- /** * Get a list of in-jar paths of the EOModels contained in * this subsystem's jar file. If no EOModel(s) are contained * in this subsystem, this method returns null. * * @return The list of paths, as strings */ public NSArray<String> EOModelPathsInJar() { return null; } // ---------------------------------------------------------- /** * Carry out any subsystem-specific initialization actions. This method * is called once all subsystems have been created, so any dependencies * on services provided by other subsystems are fulfilled. Subsystems * are init'ed in the same order they are created. The default * implementation calls {@link #updateDbIfNecessary()} to update * the subsystem's database tables, and then {@link #loadTabs()} to * load the subsystem's tab definitions. */ public void init() { log.debug("init() for " + name()); updateDbIfNecessary(); subsystemTabTemplate = loadTabs(); if (log.isDebugEnabled()) { log.debug("tabs for " + name() + " = " + subsystemTabTemplate); } } // ---------------------------------------------------------- /** * Determine if this subsystem has completed its initialization. * @return True if this subsystem has been initialized. */ public boolean isInitialized() { return isInitialized; } // ---------------------------------------------------------- /** * Carry out any subsystem-specific startup actions. This method is * called once all subsystems have been initialized. Subsystems * are started in the same order they are created. Subclasses should * override this method to perform custom startup actions. */ public void start() { // Subclasses should override this as necessary } // ---------------------------------------------------------- /** * Determine if this subsystem has started. * @return True if this subsystem has been started. */ public boolean hasStarted() { return hasStarted; } // ---------------------------------------------------------- /** * Access the set of parameter definitions that prescribe the * configuration interface for this subsystem. The default implementation * attempts to read the config.plist file from the subsystem's * resources directory. * @return the parameter definitions as an NSDictionary, or * null if none are found */ @SuppressWarnings("deprecation") public NSDictionary<String, Object> parameterDescriptions() { if (options == null) { File configFile = new File(myResourcesDir() + "/config.plist"); log.debug("Attempting to locate parameter descriptions in: " + configFile.getPath()); if (!configFile.exists()) { // If not found, try looking directly in the bundle, in case // the resources dir was overridden by properties (like on // the main development machine!). This is purely to support // development-mode hacks, and probably won't ever be used // in production. See the comments in myResourcesDir() // regarding the resourcePath() method being deprecated. NSBundle myBundle = myBundle(); if (myBundle != null) { configFile = new File( myBundle.resourcePath() + "/config.plist"); log.debug( "Attempting to locate parameter descriptions in: " + configFile.getPath()); } } if (configFile.exists()) { try { log.debug("loading parameter descriptions from: " + configFile.getPath()); FileInputStream in = new FileInputStream(configFile); NSData data = new NSData(in, (int)configFile.length()); @SuppressWarnings("unchecked") NSDictionary<String, Object> newOptions = (NSDictionary<String, Object>) NSPropertyListSerialization .propertyListFromData(data, "UTF-8"); options = newOptions; in.close(); } catch (java.io.IOException e) { log.error( "error reading from subsystem configuration file " + configFile.getPath(), e); } } if (log.isDebugEnabled()) { log.debug("loaded parameter descriptions for subsystem " + name() + ":\n" + options); } } return options; } // ---------------------------------------------------------- /** * Initialize the subsystem-specific session data in a newly created * session object. This method is called once by the core for * each newly created session object. * * @param s The new session object */ public void initializeSessionData(Session s) { s.tabs.mergeClonedChildren(subsystemTabTemplate); } // ---------------------------------------------------------- /** * Gets the component class that this subsystem wants to plug-in to another * page on the system. This mapping is defined in the * SubsystemFragments.plist file located in the Resources of the * subsystem. * * @param fragmentKey the unique fragment identifier * @return a component class that is plugged into the page */ public final Class<? extends WOComponent> subsystemFragmentForKey( String fragmentKey) { if (!subsystemFragmentsLoaded) { subsystemFragmentsLoaded = true; File file = new File(myResourcesDir(), SUBSYSTEM_FRAGMENTS_PLIST_FILENAME); if (!file.exists()) { return null; } try { subsystemFragments = new NSMutableDictionary <String, Class<? extends WOComponent>>(); @SuppressWarnings("unchecked") NSDictionary<String, Object> plist = (NSDictionary<String, Object>) NSPropertyListSerialization.propertyListFromData( new NSData(new FileInputStream(file), 0), "UTF-8"); for (String key : plist.allKeys()) { String className = (String) plist.objectForKey(key); try { Class<?> klass = Class.forName(className); Class<? extends WOComponent> compKlass = klass.asSubclass(WOComponent.class); subsystemFragments.setObjectForKey(compKlass, key); } catch (ClassNotFoundException e) { log.warn("The class " + className + " for the " + "subsystem fragment '" + key + "' " + "could not be found."); } catch (ClassCastException e) { log.warn("The class " + className + " for the " + "subsystem fragment '" + key + "' " + "is not a subclass of WOComponent."); } } } catch (IOException e) { subsystemFragments = null; } } if (subsystemFragments != null) { return subsystemFragments.objectForKey(fragmentKey); } else { return null; } } // ---------------------------------------------------------- /** * Generate the component definitions and bindings for a given * pre-defined information fragment, so that the result can be * plugged into other pages defined elsewhere in the system. * @param fragmentKey the identifier for the fragment to generate * (see the keys defined in {@link SubsystemFragmentCollector} * @param htmlBuffer add the html template for the subsystem's fragment * to this buffer * @param wodBuffer add the binding definitions (the .wod file contents) * for the subsystem's fragment to this buffer */ public final void collectSubsystemFragments( String fragmentKey, StringBuffer htmlBuffer, StringBuffer wodBuffer) { // Subclasses should override this as necessary } // ---------------------------------------------------------- /** * Add any subsystem-specific command-line environment variable bindings * to the given dictionary. The default implementation does nothing, * but subclasses can extend this behavior as needed. * @param env the dictionary to add environment variable bindings to; * the full set of currently available bindings are passed in. */ public void addEnvironmentBindings(NSMutableDictionary<String, String> env) { // Subclasses should override this as necessary } // ---------------------------------------------------------- /** * Add any subsystem-specific plug-in property bindings * to the given dictionary. The default implementation does nothing, * but subclasses can extend this behavior as needed. * @param properties the dictionary to add new properties to; * individual plug-in information may override these later. */ public void addPluginPropertyBindings( NSMutableDictionary<String, String> properties) { // Subclasses should override this as necessary } // ---------------------------------------------------------- /** * Handle a direct action request. The user's login session will be * passed in as well. * * @param request the request to respond to * @param session the user's session * @param context the context for this request * @return The response page or contents */ public WOActionResults handleDirectAction( WORequest request, Session session, WOContext context) { throw new RuntimeException( "invalid subsystem direct action request: " + "\n---request---\n" + request + "\n\n---session---\n" + session + "\n\n---context---\n" + context); } // ---------------------------------------------------------- /** * Get the string path name for this subsystem's Resources directory. * This is designed for use by subclasses that want to locate internal * resources for use in setting up environment variable or plug-in * property values. * * @return The Resources directory name as a string */ @SuppressWarnings("deprecation") public String myResourcesDir() { if (myResourcesDir == null) { // First, look for an overriding property, like those that // might be used for non-servlet deployment scenarios. myResourcesDir = Application.configurationProperties() .getProperty(name() + ".Resources"); } if (myResourcesDir == null) { NSBundle myBundle = myBundle(); if (myBundle != null) { // Note that the resourcePath() method is deprecated, but it // is the best way to get what we need here, so we'll use it // anyway, rather than re-implementing it. myResourcesDir = myBundle.resourcePath(); } } return myResourcesDir; } //~ Protected Methods ..................................................... // ---------------------------------------------------------- /** * Get the NSBundle for this subsystem. * * @return This subsystem's NSBundle */ protected NSBundle myBundle() { NSBundle result = NSBundle.bundleForName(name()); if (result == null && getClass() != Subsystem.class) { result = NSBundle.bundleForClass(getClass()); } if (result == null && !"webcat".equals(name())) { log.error("cannot find bundle for subsystem " + name()); } return result; } // ---------------------------------------------------------- /** * Loads this subsystem's tab definitions. The default implementation * pulls them from the subsystem's Tabs.plist resource file. * @return the loaded tab definitions, or null if the subsystem has none */ protected NSArray<TabDescriptor> loadTabs() { NSBundle bundle = myBundle(); if (bundle != null) { byte[] bytes = myBundle().bytesForResourcePath( TabDescriptor.TAB_DEFINITIONS); if (bytes != null && bytes.length > 0) { return TabDescriptor.tabsFromPropertyList(new NSData(bytes)); } } return null; } // ---------------------------------------------------------- /** * Applies any necessary database updates to the table structures using * the class returned by {@link #databaseUpdaterClass()}. Subclasses * typically do not need to override this, unless they want to hook * special behaviors before/after database updating. To change the * class containing your subsystem's database updates, override * {@link #databaseUpdaterClass()} instead. */ protected void updateDbIfNecessary() { Class<? extends UpdateSet> updaterClass = databaseUpdaterClass(); if (updaterClass != null) { try { log.debug("Applying updates for subsystem " + name() + " using " + updaterClass); // Apply any pending database updates for this subsystem UpdateEngine.instance().applyNecessaryUpdates( updaterClass.newInstance()); } catch (Exception e) { log.error( "Unable to apply updates from database updater class " + updaterClass, e); } } else { log.debug("no database updater class for subsystem " + name()); } } // ---------------------------------------------------------- /** * Returns the database update set class for this subsystem. The * default implementation takes the subsystem's class name and adds * "DatabaseUpdates" onto the end, then finds the corresponding class * by name. Subclasses can override this if they use different * conventions. The result of this method is typically used by * {@link #updateDbIfNecessary()} to apply database updates during * {@link #init()}. * @return The class for this subsystem's update set */ protected Class<? extends UpdateSet> databaseUpdaterClass() { Class<? extends UpdateSet> result = null; // Ignore subsystems that do not define their own subclasses. // Note that this also ignores Core, which has its updates applied // first by Application.initializeApplication(). if (this.getClass().equals(Subsystem.class)) { return result; } // Otherwise, try to look up the database update set based on // the class name of the subsystem itself String className = this.getClass().getName() + "DatabaseUpdates"; try { Class<?> updaterClass = DelegatingUrlClassLoader.getClassLoader() .loadClass(className); result = updaterClass.asSubclass(UpdateSet.class); } catch (ClassCastException e) { log.error("Cannot cast " + className + " to UpdateSet", e); } catch (ClassNotFoundException e) { log.debug("no class found: " + className); } catch (Exception e) { log.error("unable to load database updater class: " + className, e); } log.debug("database updater for " + this.getClass().getName() + " is " + result); return result; } // ---------------------------------------------------------- /** * Called periodically (say, daily) by the subsystem manager to give * subsystems a chance to perform periodic maintenance tasks. */ protected void performPeriodicMaintenance() { // Nothing by default } // ---------------------------------------------------------- /** * Add a file resource definition to a dictionary, overridden by an * optional user-specified value. This method is a helper for subsystems * that wish to add subsystem-specific file resources to ENV variable * definitions or plug-in properties. * @param map the dictionary to add the binding to * @param key the key to define in the map * @param userSettingKey the name of a property to look up in the * application's configuration settings; if a value is found, this value * will be bound to the key in the given map; if no value is found in * the application configuration settings, then the relativePath * will be resolved instead * @param relativePath the relative path name for the file or directory * to resolve in the current subsystem's framework * @return true if the binding was added using either the userSettingKey * or the relativePath, or false otherwise */ protected boolean addFileBinding( NSMutableDictionary<String, String> map, String key, String userSettingKey, String relativePath) { String userSetting = Application.configurationProperties() .getProperty(userSettingKey); if (userSetting != null) { map.takeValueForKey(userSetting, key); return true; } else { return addFileBinding(map, key, relativePath); } } // ---------------------------------------------------------- /** * Add a file resource definition to a dictionary. This method is a * helper for subsystems that wish to add subsystem-specific file * resources to ENV variable definitions or plug-in properties. * @param map the dictionary to add the binding to * @param key the key to define in the map * @param relativePath the relative path name for the file or directory * to resolve in the current subsystem's framework * @return true if the relative path name exists and the binding was * added, or false otherwise */ protected boolean addFileBinding( NSMutableDictionary<String, String> map, String key, String relativePath) { String rawPath = myResourcesDir() + "/" + relativePath; File file = new File(rawPath); if (file.exists()) { try { String path = file.getCanonicalPath(); map.takeValueForKey(path, key); return true; } catch (java.io.IOException e) { log.error("Attempting to get canonical path for " + rawPath + " in " + getClass().getName(), e); } } else { log.error("Cannot locate " + relativePath + " in Resources directory for " + getClass().getName()); } return false; } // ---------------------------------------------------------- /** * Called by the subsystem manager to indicate this subsystem has * been initialized--this method should <b>not</b> be called by * anything else. */ protected final void subsystemInitCompleted() { // This isn't called inside init() because we don't want this // value set until after subclasses have performed their // overridden init() actions as well (which is likely after // they call super.init()). isInitialized = true; } // ---------------------------------------------------------- /** * Called by the subsystem manager to indicate this subsystem has * been initialized--this method should <b>not</b> be called by * anything else. */ protected final void subsystemHasStarted() { // This isn't called inside start() because we don't want this // value set until after subclasses have performed their // overridden start() actions as well (which is likely after // they call super.start(), if they call it at all). hasStarted = true; } //~ Instance/static variables ............................................. private String name = getClass().getName(); private String myResourcesDir; private FeatureDescriptor descriptor; private NSDictionary<String, Object> options; private boolean isInitialized; private boolean hasStarted; private NSArray<TabDescriptor> subsystemTabTemplate; private NSMutableDictionary<String, Class<? extends WOComponent>> subsystemFragments; private boolean subsystemFragmentsLoaded; private static final String SUBSYSTEM_FRAGMENTS_PLIST_FILENAME = "SubsystemFragments.plist"; static Logger log = Logger.getLogger(Subsystem.class); }