/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001-2003, ThoughtWorks, Inc. * 200 E. Randolph, 25th Floor * Chicago, IL 60601 USA * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * + Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * + Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ********************************************************************************/ package net.sourceforge.cruisecontrol.launch; import org.w3c.dom.Node; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.Set; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; public class Configuration { /* All keys used for recognizing settings */ public static final String KEY_CONFIG_FILE = "configfile"; public static final String KEY_LIBRARY_DIR = "library_dir"; public static final String KEY_PROJECTS = "projects"; public static final String KEY_ARTIFACTS = "artifacts"; public static final String KEY_LOG_DIR = "logdir"; public static final String KEY_LOG4J_CONFIG = "log4jconfig"; public static final String KEY_NO_USER_LIB = "nouserlib"; public static final String KEY_USER_LIB_DIRS = "lib"; public static final String KEY_DIST_DIR = "dist"; public static final String KEY_HOME_DIR = "home"; public static final String KEY_PRINT_HELP1 = "help"; public static final String KEY_PRINT_HELP2 = "?"; public static final String KEY_DEBUG = "debug"; public static final String KEY_RMI_PORT = "rmiport"; public static final String KEY_PORT = "port"; // deprecated, use keyJMXPort public static final String KEY_JMX_PORT = "jmxport"; public static final String KEY_WEB_PORT = "webport"; public static final String KEY_WEBAPP_PATH = "webapppath"; public static final String KEY_DASHBOARD = "dashboard"; public static final String KEY_DASHBOARD_URL = "dashboardurl"; public static final String KEY_POST_INTERVAL = "postinterval"; public static final String KEY_POST_ENABLED = "postenabled"; public static final String KEY_XLS_PATH = "xslpath"; public static final String KEY_JETTY_XML = "jettyxml"; public static final String KEY_PASSWORD = "password"; public static final String KEY_USER = "user"; public static final String KEY_CC_NAME = "ccname"; public static final String KEY_JMX_AGENT_UTIL = "agentutil"; private static final String KEY_IGNORE = "XXXX"; /** Array of default values for all the option keys */ private static final Option[] DEFAULT_OPTIONS = { new Option(KEY_CONFIG_FILE, "cruisecontrol.xml", File.class), new Option(KEY_LIBRARY_DIR, "lib", File.class), new Option(KEY_PROJECTS, "projects", File.class), new Option(KEY_ARTIFACTS, "artifacts", File.class), new Option(KEY_LOG_DIR, "logs", File.class), new Option(KEY_LOG4J_CONFIG, "log4j.properties", URL.class), new Option(KEY_NO_USER_LIB, "false", Boolean.class), new Option(KEY_USER_LIB_DIRS, "", String[].class), // File[] is not supported yet new Option(KEY_DIST_DIR, "dist", File.class), new Option(KEY_HOME_DIR, ".", File.class), new Option(KEY_PRINT_HELP1, "false", Boolean.class), new Option(KEY_PRINT_HELP2, "false", Boolean.class), new Option(KEY_DEBUG, "false", Boolean.class), new Option(KEY_RMI_PORT, "1099", Integer.class), new Option(KEY_PORT, "8000", Integer.class), new Option(KEY_JMX_PORT, "8000", Integer.class), new Option(KEY_WEB_PORT, "8080", Integer.class), new Option(KEY_WEBAPP_PATH, "/webapps/cruisecontrol", File.class), new Option(KEY_DASHBOARD, "/webapps/dashboard", File.class), new Option(KEY_DASHBOARD_URL, "http://localhost:8080/dashboard", URL.class), new Option(KEY_XLS_PATH, ".", File.class), new Option(KEY_JETTY_XML, "etc/jetty.xml", File.class), new Option(KEY_POST_INTERVAL, "5", Integer.class), new Option(KEY_POST_ENABLED, "true", Boolean.class), new Option(KEY_PASSWORD, "", String.class), new Option(KEY_USER, "", String.class), new Option(KEY_CC_NAME, "", String.class), new Option(KEY_JMX_AGENT_UTIL, "", String.class), // Just placeholders new Option(KEY_IGNORE, "", String.class), // Alternative to keyConfigFile set in Main.setUpSystemPropertiesForDashboard(). Don't know // why form from keyConfigFile is not used there ... new Option("config_file", "", String.class) }; // String used to separate items in the array-holding options public static final String ITEM_SEPARATOR = File.pathSeparator; private static Configuration config = null; //instance private final Map<Object, Option> options = new HashMap<Object, Option>(DEFAULT_OPTIONS.length); private final Set<Object> optionsSet = new HashSet<Object>(DEFAULT_OPTIONS.length / 2); // Must use "special" logger, since the log4j may not be initialized yet private static LogInterface log = new LogBuffer(); /** * Initializes a singleton instance of ConfigLoader and returns this instance. First it looks for * configuration file specified in argument -configfile. It searches the file system in this order: * <ol> * <li>If the specified path is absolute and file exists then it uses this file.</li> * <li>If the specified path is not absolute but file is directly accessible then it uses this path.</li> * <li>If the file can not be found, it tries to locate it in home directory.</li> * </ol> * Properties are initialized with default values, config file values, system property values, and * arguments values. If the same property is specified in more places it uses the one with higher * priority. Priorities from the highest: * <ol> * <li>Program arguments</li> * <li>System -D properties</li> * <li>Configuration file</li> * </ol> * * @param args the command line arguments to process * @return the initialized instance of {@link Configuration} * @throws LaunchException */ public static Configuration getInstance(final String[] args) throws LaunchException { if (config != null) { throw new LaunchException("Config was already initialized. Use getInstance()"); } return new Configuration(args); } /** * Returns a singleton instance of ConfigLoader. Use this when ConfigLoader has been already initialized. * If not, initialize it by calling {@link #getInstance(String[])}. * * @return the instance of {@link Configuration} initialized by {@link #getInstance(String[])}. * @throws IllegalStateException when not initialized */ public static Configuration getInstance() { if (config == null) { throw new IllegalStateException("You must first initialize ConfigLoader. Use getInstance(args)"); } return config; } /** * Gets the current interface through which messages are logged. It is designed for the use when * real logger is not defined yet (i.e. not set by #set ], but something is required to be logged. * * It is not recommended for extensive logging! Also, all the messages logged through this logger * appears in the context of this class (and not in the context of the class logging the message). * Redesign your algorithm to use a real logged, if "real" logging is required. */ public LogInterface getLogger() { return log; } /** It sets correct instance of "real logger" to log data through. All the data logged into the temporary * logger will be pushed to the logger just being set. Once the real logger is set, it cannot be changed. * * @param logger the real logger to log into * @throws LaunchException */ public static void setRealLog(LogInterface logger) throws LaunchException { // Ignore, if the instance is the same if (logger.equals(log)) { log.warn("Trying to set the same logger, ignoring"); return; } // Set log.flush(logger); log = logger; } /** * checks, if the given option has been set or it it holds a default value. * * @return <code>true</code> if the given option has been set, <code>false</code> if not so get * methods are going to return the hard-coded default value. */ public boolean wasOptionSet(String key) { return optionsSet.contains(key); } /** * @param key the name of the option to search for. * @return value the value of the option * @throws IllegalArgumentException when the option does not exist */ public String getOptionRaw(String key) { return getOption(options, key).val; } /** * @param key the name of the option to search for. * @return value the value of the option; gets <code>null</code> if the file does not exist * @throws IllegalArgumentException when the option does not exist or it is not a file */ public File getOptionFile(String key) { final Option opt = getOption(options, key); // must be file type if (File.class.equals(opt.type)) { final File file = findFile(opt.val); // Must be existing directory if (file != null && file.isFile()) { return file; } } // The option is not file throw new IllegalArgumentException("Option '" + key + "' = '" + opt.val + "' does not represent existing file!"); } /** * @param key the name of the option to search for. * @return value the value of the option; gets <code>null</code> if the file does not exist * @throws IllegalArgumentException when the option does not exist or it is not a directory */ public File getOptionDir(String key) { final Option opt = getOption(options, key); // must be file type if (File.class.equals(opt.type)) { final File dir = findFile(opt.val); // Must be existing directory if (dir != null && dir.isDirectory()) { return dir; } } // The option is not file throw new IllegalArgumentException("Option '" + key + "' = '" + opt.val + "' does not represent existing directory!"); } /** * @param key the name of the option to search for. * @return value the value of the option * @throws IllegalArgumentException when the option does not exist or it is no a string */ public String getOptionStr(String key) { final Option opt = getOption(options, key); // must be file type if (String.class.equals(opt.type)) { return opt.val; } // The option is not a boolean throw new IllegalArgumentException("Option '" + key + "' = '" + opt.val + "' does not represent string!"); } /** * @param key the name of the option to search for. * @return value the value of the option * @throws IllegalArgumentException when the option does not exist or it is not a boolean value */ public boolean getOptionBool(String key) { final Option opt = getOption(options, key); // must be file type if (Boolean.class.equals(opt.type)) { return Boolean.parseBoolean(opt.val); } // The option is not a boolean throw new IllegalArgumentException("Option '" + key + "' = '" + opt.val + "' does not represent boolean value!"); } /** * @param key the name of the option to search for. * @return value the value of the option * @throws IllegalArgumentException when the option does not exist or it is not an integer value */ public int getOptionInt(String key) { final Option opt = getOption(options, key); // must be file type if (Integer.class.equals(opt.type)) { return Integer.parseInt(opt.val); } // The option is not an integer value throw new IllegalArgumentException("Option '" + key + "' = '" + opt.val + "' does not represent int value!"); } /** * @param key the name of the option to search for. * @return value the value of the option * @throws IllegalArgumentException when the option does not exist or it is not a valid URL */ public URL getOptionUrl(String key) { final Option opt = getOption(options, key); // must be file type if (URL.class.equals(opt.type)) { try { final URL val = new URL(opt.val); final String protocol = val.getProtocol(); final String host = val.getHost(); // If the URL represents a file, check is as if it is a file if ("file".equalsIgnoreCase(protocol) && (host == null || host.isEmpty())) { final File file = findFile(val.getPath()); // Must be existing directory if (file == null || !file.isFile()) { throw new IllegalArgumentException("Option '" + key + "' = '" + opt.val + "' does not represent existing file!"); } } // Return the URL return val; } catch (MalformedURLException e) { // Nothing here, exception will be thrown anyway } } // The option is not an integer value throw new IllegalArgumentException("Option '" + key + "' = '" + opt.val + "' does not represent URL value!"); } /** * @param key the name of the option to search for. * @return value the value of the option * @throws IllegalArgumentException when the option does not exist or it is no a string */ public String[] getOptionStrArray(String key) { final Option opt = getOption(options, key); // must be file type if (String[].class.equals(opt.type)) { return opt.val.split(ITEM_SEPARATOR); } // The option is not a boolean throw new IllegalArgumentException("Option '" + key + "' = '" + opt.val + "' does not represent array of string!"); } /** * Constructor. It is hidden since the class can only be used as singleton, but protected to be * overidable for test purposes * * @param args the array of command line arguments passed to CC launcher * @throws LaunchException */ protected Configuration(final String[] args) throws LaunchException { final Set<Object> dummy = new HashSet<Object>(); // Initialize the configuration options with default values for (Option o : DEFAULT_OPTIONS) { options.put(o.key, o); // WARN: must put string as a key, Map<>.get() does not work for key == Option } // Override default values with command-line options. This step is used to get the // path to launcher XML (ignore marking of set attributes) parseArguments(options, dummy, args); // But remove the option from the command line arguments now, since // - it is the main XML configuration with the configuration of launcher embedded in it; the // path is already stored so overwrite would not // - it is raw launcher configuration containing path to the main XML config file; the path to // the main cruisecontrol config will be read from launcher and thus we must prevent its // re-overwrite from args for (int i = 0; i < args.length; i++) { if (("-" + KEY_CONFIG_FILE).equals(args[i])) { args[i] = "-" + KEY_IGNORE; } } // Override the values from config parseXmlConfig(options, optionsSet, options.get(KEY_CONFIG_FILE)); // Override values from properties parseProperties(options, optionsSet); // Override values from command line (to overwrite values overwritten by the config :-)) parseArguments(options, optionsSet, args); } /** * Checks, if the given file is accessible and returns absolute path to it. If the file * cannot be read (i.e. it is not set by absolute path or not being in the working directory), * the home directory of the user is tried. * * @param fname the name (+ path) to read the file from * @return absolute path to the file or <code>null</code> if it cannot be found or read */ private File findFile(final String fname) { File file = new File(fname); // If the file is not accessible (i.e. set by absolute path or in the current working directory), // try home directory of the user if (!file.isAbsolute() && !file.exists()) { final String home = System.getProperty("user.home"); log.warn("Unable to find " + file.getAbsolutePath() + ", trying home directory " + home); file = new File(home, fname); if (!file.exists()) { log.warn("Unable to find " + fname); return null; //throw new LaunchException("Unable to find " + configFile); } } log.info("Using file: " + file.getAbsolutePath()); return file; } /** * @param opts * @param set * @param xmlPath * @throws LaunchException */ private void parseXmlConfig(final Map<Object, Option> opts, final Set<Object> set, final Option xmlPath) throws LaunchException { // The path is NULL, just leave if (xmlPath == null || "".equals(xmlPath.val)) { return; } Element xmlConfig; File path = findFile(xmlPath.val); // File cannot be found, skip its reading if (path == null) { log.warn("Skipping the read of config file, using default values!"); return; } // Read the config. Use standard Java's XML tools to avoid the dependency ion an external // XML handling package (although net.sourceforge.cruisecontrol.util.Util class contains // more advanced CML parsers. Could be nice to join them ...) try { DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); xmlConfig = builder.parse(path).getDocumentElement(); } catch (Exception e) { throw new LaunchException("Failed to read XML file:" + path.getAbsolutePath(), e); } // The root element is <cruisecontrol>, find <launcher>...</launcher> section in it // and parse recursively if ("cruisecontrol".equals(xmlConfig.getNodeName())) { final Element launch = getChild(xmlConfig, "launcher"); // Not found! if (launch == null) { throw new LaunchException("No launcher configuration found in the XML config"); } // Remove the option from the <launch> element since the config is this file removeChild(launch, KEY_CONFIG_FILE); // Remove it in case that it will be set // Set the path to the file and continue with parsing the launcher element setOption(opts, set, KEY_CONFIG_FILE, path.getAbsolutePath()); xmlConfig = launch; } else { // Remove element pointing to the config file - initialize it with the default value, and it // optionally will be re-read from the values just going to be parsed for (Option opt : DEFAULT_OPTIONS) { if (KEY_CONFIG_FILE.equals(opt.key)) { opts.put(opt.key, opt); set.remove(opt.key); } } } // Parse options from <launcher>...</launcher> element for (final Node child : getChildNodes(xmlConfig)) { if (child.getNodeType() != Node.ELEMENT_NODE) { continue; } final Element elem = (Element) child; final String key = elem.getNodeName(); final String val = elem.getTextContent().trim(); setOption(opts, set, key, val); } } /* Methods for XML manipulation. They have their equivalents in org.jdom.Element object, * but since "external" org.jdompackage is not used by the launcher (to avoid the need to * define path to external jars, we have to re-implement them here. */ private static Element getChild(final Element xmlNode, final String name) { for (final Node child : getChildNodes(xmlNode)) { if (name.equals(child.getNodeName()) && child.getNodeType() == Node.ELEMENT_NODE) { return (Element) child; } } return null; } private static void removeChild(final Element xmlNode, final String name) { for (final Node child : getChildNodes(xmlNode)) { if (name.equals(child.getNodeName()) && child.getNodeType() == Node.ELEMENT_NODE) { xmlNode.removeChild(child); } } } private static Node[] getChildNodes(final Element xmlNode) { NodeList list = xmlNode.getChildNodes(); Node[] nodes = new Node[list.getLength()]; for (int i = 0; i < nodes.length; i++) { nodes[i] = list.item(i); } return nodes; } /** * * @param opts * @param args * @throws LaunchException */ private void parseArguments(final Map<Object, Option> opts, final Set<Object> set, final String[] args) throws LaunchException { String key = null; // Process the command line arguments for (String arg : args) { // boolean flags process here if (arg.startsWith("-")) { final String name = arg.substring(1); if (KEY_PRINT_HELP1.equals(name)) { setOption(opts, set, name, "true"); // continue with further processing since value can follow } if (KEY_PRINT_HELP2.equals(name)) { setOption(opts, set, name, "true"); } if (KEY_NO_USER_LIB.equals(name)) { setOption(opts, set, name, "true"); } if (KEY_DEBUG.equals(name)) { setOption(opts, set, name, "true"); } if (KEY_POST_ENABLED.equals(name)) { setOption(opts, set, name, "true"); } if (KEY_JMX_AGENT_UTIL.equals(name)) { setOption(opts, set, name, "true"); } // This is little hack for backward compatibility. If the given option appears, set its // current value again to pretend that it was set on the command line; i.e. the call of // "-webport 8585 -dashboardurl" must pretend that the dashboardurl was set as well. if (KEY_DASHBOARD_URL.equals(name)) { log.warn("Using " + arg + " without value. Try to avoid this!"); setOption(opts, set, name, getOption(opts, name).val); } } // -key value types // store the key if (arg.startsWith("-")) { key = arg.substring(1); continue; } // Store the value if (key != null) { setOption(opts, set, key, arg); continue; } // Unknown option throw new LaunchException("Unknown option " + arg); } } /** * Parses properties get by {@link System#getProperties()}, overriding already defined * values in <code>opts</code> attribute. Only properties starting with cc. are taken into the * consideration. * * Dots in the properties are replaced by underscores, so <i>cc.library.dir</i> becomes * <i>library_dir</i> */ private void parseProperties(final Map<Object, Option> opts, final Set<Object> set) throws LaunchException { final Properties props = System.getProperties(); for (String name : props.stringPropertyNames()) { if (name.startsWith("cc.")) { String key = name.substring(3).replace(".", "_"); // cc.library.dir -> library_dir setOption(opts, set, key, props.getProperty(name)); } } } /** * Creates new {@link Option} object, stores it into the map of options as well as into the set of * options being set. An object with the given key must exist in the map, otherwise {@link LaunchException} * is thrown. In this way, default (or previous) values are overwritten and the correctness of the key is * checked (i.e. all keys must have default values assigned). * * @param opts the map with options to be updated * @param set the set with options being set * @param key the name of the option * @param val the value of the option */ private static void setOption(final Map<Object, Option> opts, final Set<Object> set, final String key, String val) { Option opt = getOption(opts, key); // If the option is array and a value has already been set into it, must be treated differently if (opt.type.isArray() && set.contains(opt.key)) { // CAREFUL - the value must not contain the separator char if (val.contains(ITEM_SEPARATOR)) { throw new IllegalArgumentException("'" + ITEM_SEPARATOR + "' is not allowed in '" + key + "' = '" + val + "'"); } // Join the original options with the new option val = opt.val + ITEM_SEPARATOR + val; } opt = new Option(opt, val); opts.put(opt.key, opt); // WARN: must put string as a key, Map<>.get() does not work for key == Option set.add(opt.key); } /** * Finds the option according top the string key. * * @param opts the map with options to be searched in * @param key the name of the option to be found * @return value the option; is never <code>null</code> */ private static Option getOption(final Map<Object, Option> opts, final String key) { final Option opt = opts.get(key); //new Option(key, "", null)); // Must already be pre-filled with a default value! if (opt == null) { throw new IllegalArgumentException("Unknown option '" + key + "'"); } return opt; } /** * Single configuration option holder. It is hasheable by {@link #key} value */ private static final class Option { final String key; /** Name of the option */ final String val; /** The associated value */ final Class< ? > type; /** The class the option type belongs to */ Option(final String key, final String val, Class< ? > type) { this.key = key; this.val = val; this.type = type; } Option(final Option opt, final String val) { this.key = opt.key; this.val = val; this.type = opt.type; } /** Compares key to String or Option objects */ @Override public boolean equals(Object obj) { if (obj instanceof Option) { return key.equals(((Option) obj).key); } return key.equals(obj); } /** Gets hash code of the key */ @Override public int hashCode() { return this.key.hashCode(); } } }