/** Copyright (C) 2012 Delcyon, Inc. This program 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. This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package com.delcyon.capo; import java.io.File; import java.io.FileOutputStream; import java.util.Map.Entry; import java.util.Set; import java.util.Vector; import java.util.concurrent.ConcurrentHashMap; import java.util.logging.Level; import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.CommandLineParser; import org.apache.commons.cli.GnuParser; import org.apache.commons.cli.HelpFormatter; import org.apache.commons.cli.OptionBuilder; import org.apache.commons.cli.Options; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import com.delcyon.capo.CapoApplication.Location; import com.delcyon.capo.annotations.DefaultDocumentProvider; import com.delcyon.capo.annotations.DirectoyProvider; import com.delcyon.capo.preferences.Preference; import com.delcyon.capo.preferences.PreferenceProvider; import com.delcyon.capo.server.CapoServer; import com.delcyon.capo.xml.XPath; import com.delcyon.capo.xml.cdom.CDocument; /** * @author jeremiah */ @DirectoyProvider(preferenceName="CONFIG_DIR",preferences=Configuration.PREFERENCE.class,canUseRepository=false) @DefaultDocumentProvider(directoryPreferenceName="CONFIG_DIR",preferences=Configuration.PREFERENCE.class,name="config.xml,repository.xml") public class Configuration { private static final String CONFIG_FILENAME = "config.xml"; public enum PREFERENCE implements Preference { RETAIN("r", "retain", "overwrite values in configuration file with option from command line", null, null), DISABLE_CONFIG_AUTOSYNC("DISABLE_CONFIG_AUTOSYNC", "DISABLE_CONFIG_AUTOSYNC", "Turns off Automaticaly syncing new preferences to filesystem or creating default directories. Useful for testing only!", null,null), PORT("port", "port", "port to listen on", "2442", new String[] { "portNumber" }), SERVER_PORT("sp", "SERVER_PORT", "server port to connect to. default 2442", "2442", new String[] { "portNumber" }), SERVER_LIST("sl", "SERVER_LIST", "server addresses. comma seperated. default 127.0.0.1", "127.0.0.1", new String[] { "address" }), HELP("h", "help", "print usage", null, null), BUFFER_SIZE("bs", "BUFFER_SIZE", "Stream buffer size, default is 4096. ", "4096", new String[] { "int" }), RESOURCE_MANAGER("rm", "RESOURCE_MANAGER", "class to use as a resource manager (default is com.delcyon.capo.resourcemanager.ResourceManager)", "com.delcyon.capo.resourcemanager.ResourceManager", new String[] { "class name" }), DATA_MANAGER_ARGUMENTS("dma", "DATA_MANAGER_ARGUMENTS", "arguments to pass to data manager, like location of the file store or url of an xml server", "capo-data", new String[] { "uri" }), CAPO_DIR("CAPO_DIR", "CAPO_DIR", "main capo directory on local machine", "capo", new String[] { "dir" }), CONFIG_DIR("CONFIG_DIR", "CONFIG_DIR", "directory where main config is stored, relative to root data dir", "config", new String[] { "dir" }), STATUS_DIR("STATUS_DIR", "STATUS_DIR", "directory where status information is stored, relative to root data dir", "status", new String[] { "dir" }), WEB_DIR("WEB_DIR", "WEB_DIR", "directory where public web accessable files are stored, relative to root data dir", "public", new String[] { "dir" }), MODULE_DIR("MODULE_DIR", "MODULE_DIR", "directory where modules are stored, relative to root data dir", "modules", new String[] { "dir" }), RESOURCE_DIR("RESOURCE_DIR", "RESOURCE_DIR", "directory where misc resources are stored, relative to root data dir", "resources", new String[] { "dir" }), CONTROLLER_DIR("CONTROLLER_DIR", "CONTROLLER_DIR", "directory where controller scripts are stored, relative to root data dir", "controller", new String[] { "dir" }), RESPONSE_DIR("RESPONSE_DIR", "RESPONSE_DIR", "directory where responses to requests are stored, relative to root data dir, server only", "responses", new String[] { "dir" }), KEYSTORE("KEYSTORE", "KEYSTORE", "location of java keystore default = keystore", "keystore", new String[] { "file" }), KEYSTORE_PASSWORD("KEYSTORE_PASSWORD", "KEYSTORE_PASSWORD", "password for java keystore. default is 'password'", "password", new String[] { "password" }), CLIENT_VERIFICATION_PASSWORD("CLIENT_VERIFICATION_PASSWORD", "CLIENT_VERIFICATION_PASSWORD", "Initial password for unknown clients to use. If left blank a random password will be generated for each client. default is blank", "", new String[] { "password" }), MODE("m", "MODE", "mode to run capo in client, server, hybrid. default 'client'", "client", new String[] { "mode" }), CLIENT_MODE("cm", "CLIENT_MODE", "what kind of client connection to use, persistant or dynamic. default is dynamic", "dynamic", new String[] { "client_mode" }), STARTUP_SCRIPT("STARTUP_SCRIPT","STARTUP_SCRIPT","Capo formatted XML file, relative to CONFIG_DIR, containing controls to run at startup, before server becomes available.","startup.xml",new String[] { "file" }), UPDATE_SCRIPT("UPDATE_SCRIPT","UPDATE_SCRIPT","Capo formatted XML file, relative to CONFIG_DIR, containing controls to run an update, before client control processing.","update.xml",new String[] { "file" }), LOGGING_LEVEL("l", "LOGGING_LEVEL", "Java Logging level to use. Can be Standard Java Logging Name, or a number", "INFO", new String[] { "level" }), ; private String option; private String longOption; private boolean hasArgument; private String description; private String defaultValue; private String[] arguments; PREFERENCE(String option, String longOption, String description, String defaultValue, String[] arguments) { this.option = option; this.longOption = longOption; if (arguments != null && arguments.length > 0) { this.hasArgument = true; } else { this.hasArgument = false; } this.description = description; this.defaultValue = defaultValue; this.arguments = arguments; } @Override public String toString() { return getLongOption(); } public String getOption() { return option; } public String getLongOption() { return longOption; } public boolean hasArgument() { return hasArgument; } public String getDescription() { return description; } public String getDefaultValue() { //see if there is a default value stored in the java preferences system, //and use that otherwise use the default value if any. return Preferences.systemNodeForPackage(CapoApplication.getApplication().getClass()).get(longOption, defaultValue); } public String[] getArguments() { return arguments; } @Override public Location getLocation() { return Location.BOTH; } } private CommandLine commandLine; private Options options; private Document configDocument; private DocumentBuilder documentBuilder; private File capoConfigFile; private ConcurrentHashMap<String, String> preferenceValueHashMap = new ConcurrentHashMap<String, String>(); private ConcurrentHashMap<String, Preference> preferenceHashMap = new ConcurrentHashMap<String, Preference>(); private boolean disableAutoSync; public Configuration() throws Exception { this(new String[]{}); } @SuppressWarnings({ "unchecked", "static-access" }) public Configuration(String... programArgs) throws Exception { DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); documentBuilderFactory.setNamespaceAware(true); documentBuilder = documentBuilderFactory.newDocumentBuilder(); options = new Options(); // the enum this is a little complicated, but it gives us a nice // centralized place to put all of the system parameters // and lets us iterate of the list of options and preferences PREFERENCE[] preferences = PREFERENCE.values(); for (PREFERENCE preference : preferences) { // not the most elegant, but there is no default constructor, but // the has arguments value is always there OptionBuilder optionBuilder = OptionBuilder.hasArg(preference.hasArgument); if (preference.hasArgument == true) { String[] argNames = preference.arguments; for (String argName : argNames) { optionBuilder = optionBuilder.withArgName(argName); } } optionBuilder = optionBuilder.withDescription(preference.getDescription()); optionBuilder = optionBuilder.withLongOpt(preference.getLongOption()); options.addOption(optionBuilder.create(preference.getOption())); preferenceHashMap.put(preference.toString(), preference); } //add dynamic options Set<String> preferenceProvidersSet = CapoApplication.getAnnotationMap().get(PreferenceProvider.class.getCanonicalName()); if (preferenceProvidersSet != null) { for (String className : preferenceProvidersSet) { Class preferenceClass = Class.forName(className).getAnnotation(PreferenceProvider.class).preferences(); if (preferenceClass.isEnum()) { Object[] enumObjects = preferenceClass.getEnumConstants(); for (Object enumObject : enumObjects) { Preference preference = (Preference) enumObject; //filter out any preferences that don't belong on this server or client. if(preference.getLocation() != Location.BOTH) { if(CapoApplication.isServer() == true && preference.getLocation() == Location.CLIENT) { continue; } else if (CapoApplication.isServer() == false && preference.getLocation() == Location.SERVER) { continue; } } preferenceHashMap.put(preference.toString(), preference); boolean hasArgument = false; if (preference.getArguments() == null || preference.getArguments().length == 0) { hasArgument = false; } else { hasArgument = true; } OptionBuilder optionBuilder = OptionBuilder.hasArg(hasArgument); if (hasArgument == true) { String[] argNames = preference.getArguments(); for (String argName : argNames) { optionBuilder = optionBuilder.withArgName(argName); } } optionBuilder = optionBuilder.withDescription(preference.getDescription()); optionBuilder = optionBuilder.withLongOpt(preference.getLongOption()); options.addOption(optionBuilder.create(preference.getOption())); } } } } // create parser CommandLineParser commandLineParser = new GnuParser(); this.commandLine = commandLineParser.parse(options, programArgs); Preferences systemPreferences = Preferences.systemNodeForPackage(CapoApplication.getApplication().getClass()); String capoDirString = null; while(true) { capoDirString = systemPreferences.get(PREFERENCE.CAPO_DIR.longOption,null); if (capoDirString == null) { systemPreferences.put(PREFERENCE.CAPO_DIR.longOption, PREFERENCE.CAPO_DIR.defaultValue); capoDirString = PREFERENCE.CAPO_DIR.defaultValue; try { systemPreferences.sync(); } catch (BackingStoreException e) { //e.printStackTrace(); if (systemPreferences.isUserNode() == false) { System.err.println("Problem with System preferences, trying user's"); systemPreferences = Preferences.userNodeForPackage(CapoApplication.getApplication().getClass()); continue; } else //just bail out { throw e; } } } break; } disableAutoSync = hasOption(PREFERENCE.DISABLE_CONFIG_AUTOSYNC); File capoDirFile = new File(capoDirString); if (capoDirFile.exists() == false) { if (disableAutoSync == false) { capoDirFile.mkdirs(); } } File configDir = new File(capoDirFile,PREFERENCE.CONFIG_DIR.defaultValue); if (configDir.exists() == false) { if (disableAutoSync == false) { configDir.mkdirs(); } } if (disableAutoSync == false) { capoConfigFile = new File(configDir,CONFIG_FILENAME); if (capoConfigFile.exists() == false) { Document configDocument = CapoApplication.getDefaultDocument("config.xml"); FileOutputStream configFileOutputStream = new FileOutputStream(capoConfigFile); TransformerFactory tFactory = TransformerFactory.newInstance(); Transformer transformer = tFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.transform(new DOMSource(configDocument), new StreamResult(configFileOutputStream)); configFileOutputStream.close(); } configDocument = documentBuilder.parse(capoConfigFile); } else //going memory only, because of disabled auto sync { configDocument = CapoApplication.getDefaultDocument("config.xml"); } if(configDocument instanceof CDocument) { ((CDocument) configDocument).setSilenceEvents(true); } loadPreferences(); preferenceValueHashMap.put(PREFERENCE.CAPO_DIR.longOption, capoDirString); //print out preferences //this also has the effect of persisting all of the default values if a values doesn't already exist for (PREFERENCE preference : preferences) { if (getValue(preference) != null) { CapoApplication.logger.log(Level.CONFIG, preference.longOption+"='"+getValue(preference)+"'"); } } CapoApplication.logger.setLevel(Level.parse(getValue(PREFERENCE.LOGGING_LEVEL))); } public boolean hasOption(Preference preference) { return commandLine.hasOption(preference.getOption()); } /** * command line values take precedence over saved preferences * @param preference * @return */ public String getValue(Preference preference) { if (commandLine.hasOption(preference.getOption())) { //if the retain flag is specified persist the option or if the option exists and there is a default value, but no value in the preferences if (hasOption(PREFERENCE.RETAIN) || (getPref(preference.getLongOption(), null) == null && preference.getDefaultValue() != null)) { putPref(preference.getLongOption(), commandLine.getOptionValue(preference.getOption())); try { sync(); } catch (BackingStoreException e) { e.printStackTrace(); } } return commandLine.getOptionValue(preference.getOption()); } else { //if there isn't a preference set and there is a default value then store the default in the preferences //this is how we save the initial configuration if (getPref(preference.getLongOption(), null) == null && preference.getDefaultValue() != null) { putPref(preference.getLongOption(), preference.getDefaultValue()); try { sync(); } catch (BackingStoreException e) { e.printStackTrace(); } } return getPref(preference.getLongOption(), null); } } public void printHelp() { HelpFormatter formatter = new HelpFormatter(); formatter.setWidth(200); formatter.printHelp("CapoServer", options); } public int getIntValue(Preference preference) { return Integer.parseInt(getValue(preference)); } public boolean getBooleanValue(Preference preference) { return Boolean.parseBoolean(getValue(preference)); } public long getLongValue(Preference preference) { return Long.parseLong(getValue(preference)); } /** * Encapsulation of XML preferences * @param key * @param defaultValue * @return null, on no value * @throws BackingStoreException */ private String getPref(String key,String defaultValue) { String returnValue = defaultValue; if (preferenceValueHashMap.containsKey(key)) { returnValue = preferenceValueHashMap.get(key); } return returnValue; } private void putPref(String key, String value) { preferenceValueHashMap.put(key, value); } /** * Encapsulation of XML preferences * @param key * @param value * @throws BackingStoreException */ private void putXmlPref(String key, String value) throws BackingStoreException { try { synchronized (configDocument) { Element entryElement = (Element) XPath.selectSingleNode(configDocument, "//entry[@key = '"+key+"']"); if (entryElement != null) { entryElement.setAttribute("value", value); } else { entryElement = configDocument.createElementNS(null, "entry"); entryElement.setAttribute("key", key); entryElement.setAttribute("value", value); configDocument.getDocumentElement().appendChild(entryElement); } } } catch (Exception exception) { throw new BackingStoreException(exception.getMessage()); } } private synchronized void sync() throws BackingStoreException { try { Set<Entry<String, String>> preferenceEntrySet = preferenceValueHashMap.entrySet(); for (Entry<String, String> entry : preferenceEntrySet) { //do not persist certain preferences if (entry.getKey().equals(PREFERENCE.CLIENT_VERIFICATION_PASSWORD.getLongOption()) && CapoApplication.isServer() == false) { continue; } else { putXmlPref(entry.getKey(), entry.getValue()); } } if (disableAutoSync == false) { FileOutputStream configFileOutputStream = new FileOutputStream(capoConfigFile); TransformerFactory tFactory = TransformerFactory.newInstance(); Transformer transformer = tFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.transform(new DOMSource(configDocument), new StreamResult(configFileOutputStream)); configFileOutputStream.close(); } } catch (Exception exception) { CapoApplication.logger.log(Level.WARNING, "Couldn't sync config file", exception); throw new BackingStoreException(exception.getMessage()); } } private void loadPreferences() throws Exception { NodeList entryNodeList = XPath.selectNodes(configDocument, "//entry"); for (int nodeIndex = 0; nodeIndex < entryNodeList.getLength(); nodeIndex++) { Element entryElement = (Element) entryNodeList.item(nodeIndex); preferenceValueHashMap.put(entryElement.getAttribute("key"), entryElement.getAttribute("value")); } } @SuppressWarnings("unchecked") public Preference[] getDirectoryPreferences() { boolean isServer = CapoApplication.getApplication() instanceof CapoServer; Vector<Preference> preferenceVector = new Vector<Preference>(); Set<String> directoryProvidersSet = CapoApplication.getAnnotationMap().get(DirectoyProvider.class.getCanonicalName()); if (directoryProvidersSet != null) { for (String className : directoryProvidersSet) { try { Location location = Class.forName(className).getAnnotation(DirectoyProvider.class).location(); Class preferenceClass = Class.forName(className).getAnnotation(DirectoyProvider.class).preferences(); String preferenceName = Class.forName(className).getAnnotation(DirectoyProvider.class).preferenceName(); if (location == Location.BOTH) { preferenceVector.add((Preference)Enum.valueOf(preferenceClass, preferenceName)); } else if (isServer == true && location == Location.SERVER) { preferenceVector.add((Preference)Enum.valueOf(preferenceClass, preferenceName)); } else if (isServer == false && location == Location.CLIENT) { preferenceVector.add((Preference)Enum.valueOf(preferenceClass, preferenceName)); } } catch (ClassNotFoundException classNotFoundException) { CapoApplication.logger.log(Level.WARNING, "Error getting directory providers",classNotFoundException); } } } return preferenceVector.toArray(new Preference[]{}); } public DefaultDocumentProvider[] getDefaultDocumentProviders() { Vector<DefaultDocumentProvider> defaultDocumentProviderVector = new Vector<DefaultDocumentProvider>(); Set<String> defaultDocumentProviderSet = CapoApplication.getAnnotationMap().get(DefaultDocumentProvider.class.getCanonicalName()); for (String className : defaultDocumentProviderSet) { try { defaultDocumentProviderVector.add(Class.forName(className).getAnnotation(DefaultDocumentProvider.class)); } catch (ClassNotFoundException classNotFoundException) { CapoApplication.logger.log(Level.WARNING, "Error getting document providers",classNotFoundException); } } return defaultDocumentProviderVector.toArray(new DefaultDocumentProvider[]{}); } public Preference getPreference(String preferenceName) { return preferenceHashMap.get(preferenceName); } public void setValue(Preference preference, String value) { putPref(preference.getLongOption(), value); try { sync(); } catch (BackingStoreException e) { e.printStackTrace(); } } }