/* $HeadURL:: $
* $Id$
*
* Copyright (c) 2006-2010 by Public Library of Science
* http://plos.org
* http://ambraproject.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.ambraproject.configuration;
import org.apache.commons.configuration.AbstractConfiguration;
import org.apache.commons.configuration.CombinedConfiguration;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.ConfigurationUtils;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.apache.commons.configuration.SystemConfiguration;
import org.apache.commons.configuration.XMLConfiguration;
import org.apache.commons.configuration.tree.OverrideCombiner;
import org.apache.commons.configuration.tree.UnionCombiner;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
/**
* A singleton that manages the load/unload/re-load of Configuration.<p>
*
* Configuration consists of a layered set of configuration files where configuration
* in a higher layer overrides those of the lower layers. Starting from the lowest layer,
* configuration consists of:
* <ul>
* <li><var>/global-defaults.xml</var> - A resource in this library
* <li><var>/defaults.xml</var> - A resource or resources in libraries and webapps using this lib
* <li><var>ambra.configuration.overrides</var> - If set, this defines a named resource or URL
* of a resource that is added to the configuration tree - usually supplementing
* and overriding settings in <var>/global-defaults.xml</var> and <var>/defaults.xml</var>.
* <li><var>file:/etc/ambra/ambra.xml</var> (or <var>ambra.configuration</var>) - A set of user
* overrides in <var>/etc</var>. The name of this file can be changed for webapps that use
* WebAppInitializer by changing web.xml or by setting the ambra.configuraiton system
* property.
* <li>System properties
* </ul>
*
* @author Pradeep Krishnan
* @author Eric Brown
*/
public class ConfigurationStore {
private static final Log log = LogFactory.getLog(ConfigurationStore.class);
private static final ConfigurationStore instance = new ConfigurationStore();
private CombinedConfiguration root = null;
/**
* A property used to define the location of the master set of configuration overrides.
* This is usually a xml or properties file in /etc somewhere. Note that this must be
* a URL. (For example: file:///etc/ambra/ambra.xml.)
*/
public static final String CONFIG_URL = "ambra.configuration";
/**
* A property used to define overrides. This is primarily to support something like
* a development mode. If a valid URL, the resource is found from the URL. If not a
* URL, it is treated as the name of a resource.
*/
public static final String OVERRIDES_URL = "ambra.configuration.overrides";
/**
* Default configuration overrides in /etc
*/
public static final String DEFAULT_CONFIG_URL = "file:///etc/ambra/ambra.xml";
/**
* <p>Name of resource(s) that contain defaults in a given journal</p>
*
* <p>There is one per journal.</p>
*/
public static final String JOURNAL_DIRECTORY = "/configuration/journal.xml";
public static final String DEFAULTS_RESOURCE = "ambra/configuration/defaults.xml";
/**
* The name of the global defaults that exist in this library.<p>
*
* It is assumed there is only one of these in the classpath. If somebody defines
* a second copy of this, the results are undefined. (TODO: Detect this.)
*/
public static final String GLOBAL_DEFAULTS_RESOURCE = "/ambra/configuration/global-defaults.xml";
/**
* The system variable used by Hibernates ID generator to use a prefix for unique identifiers
* This value is pulled from the ambra.xml 'config.ambra.aliases.id' node.
*/
public static final String SYSTEM_OBJECT_ID_PREFIX = "SYSTEM_OBJECT_ID_PREFIX";
/**
* Advanced usage logging
*/
public static final String ADVANCED_USAGE_LOGGING = "ambra.advancedUsageLogging";
/**
* Location of journal templates
*/
public static final String JOURNAL_TEMPLATE_DIR = "ambra.virtualJournals.templateDir";
private static final String JOURNALS = "ambra.virtualJournals.journals";
/**
* Create the singleton instance.
*/
private ConfigurationStore() {
}
/**
* Gets the singleton instance.
*
* @return Returns the only instance.
*/
public static ConfigurationStore getInstance() {
return instance;
}
/**
* Gets the current configuration root.
*
* @return Returns the currently loaded configuration root
*
* @throws RuntimeException if the configuration factory is not initialized
*/
public Configuration getConfiguration() {
if (root != null)
return root;
throw new RuntimeException("ERROR: Configuration not loaded or initialized.");
}
/**
* Overrides all existing configuration with the given configuration object
* (useful for JUnit testing!)
* @param newConfig the new configuration to test
*/
public void setConfiguration(CombinedConfiguration newConfig) {
root = newConfig;
}
/**
* Load/Reload the configuration from the factory config url.
*
* @param configURL URL to the config file for ConfigurationFactory
* @throws ConfigurationException when the config factory configuration has an error
*/
public void loadConfiguration(URL configURL) throws ConfigurationException {
root = new CombinedConfiguration(new OverrideCombiner());
// System properties override everything
root.addConfiguration(new SystemConfiguration());
// Load from ambra.configuration -- /etc/... (optional)
if (configURL != null) {
try {
root.addConfiguration(getConfigurationFromUrl(configURL));
log.info("Added URL '" + configURL + "'");
} catch (ConfigurationException ce) {
if (!(ce.getCause() instanceof FileNotFoundException))
throw ce;
log.info("Unable to open '" + configURL + "'");
}
}
// Add ambra.configuration.overrides (if defined)
String overrides = System.getProperty(OVERRIDES_URL);
if (overrides != null) {
try {
root.addConfiguration(getConfigurationFromUrl(new URL(overrides)));
log.info("Added override URL '" + overrides + "'");
} catch (MalformedURLException mue) {
// Must not be a URL, so it must be a resource
addResources(root, overrides);
}
}
CombinedConfiguration defaults = new CombinedConfiguration(new UnionCombiner());
// Add defaults.xml from classpath
addResources(defaults, DEFAULTS_RESOURCE);
// Add journal.xml from journals/journal-name/configuration/journal.xml
addJournalResources(root, defaults, JOURNAL_DIRECTORY);
root.addConfiguration(defaults);
// Add global-defaults.xml (presumably found in this jar)
addResources(root, GLOBAL_DEFAULTS_RESOURCE);
if (log.isDebugEnabled())
log.debug("Configuration dump: " + System.getProperty("line.separator") +
ConfigurationUtils.toString(root));
/**
* This prefix is needed by the AmbraIdGenerator to create prefixes for object IDs.
* Because of the way the AmbraIdGenerator class is created by hibernate, passing in values
* is very difficult. If a better method is discovered... by all means use that. Until that time
* I've created a system level property to store this prefix.
*/
String objectIDPrefix = root.getString("ambra.platform.guid-prefix");
if(objectIDPrefix == null) {
throw new RuntimeException("ambra.platform.guid-prefix node is not found in the defined configuration file.");
}
System.setProperty(SYSTEM_OBJECT_ID_PREFIX, objectIDPrefix);
}
/**
* Use the default commons configuration specified by this library.
*
* @throws ConfigurationException when the configuration can't be found.
*/
public void loadDefaultConfiguration() throws ConfigurationException {
// Allow JVM level property to override everything else
String name = System.getProperty(CONFIG_URL);
if (name == null)
name = DEFAULT_CONFIG_URL;
try {
loadConfiguration(new URL(name));
} catch (MalformedURLException e) {
throw new ConfigurationException("Invalid value of '" + name + "' for '" + CONFIG_URL +
"'. Must be a valid URL.");
}
}
/**
* Unload the current configuration.
*/
public void unloadConfiguration() {
root = null;
}
/**
* Given a URL, determine whether it represents properties or xml and load it as a
* commons-config Configuration instance.
*/
private static AbstractConfiguration getConfigurationFromUrl(URL url)
throws ConfigurationException {
if (url.getFile().endsWith("properties"))
return new PropertiesConfiguration(url);
else
return new XMLConfiguration(url);
}
/**
* Iterate over all the resources of the given name and add them to our root
* configuration.
* @param root the root configuration to add to
* @param resource the resource to add
* @throws ConfigurationException on an error in adding the new config
*/
public static void addResources(CombinedConfiguration root, String resource)
throws ConfigurationException {
Class<?> klass = ConfigurationStore.class;
if (resource.startsWith("/")) {
root.addConfiguration(getConfigurationFromUrl(klass.getResource(resource)));
log.info("Added resource '" + resource + "' to configuration");
} else {
try {
Enumeration<URL> rs = klass.getClassLoader().getResources(resource);
while (rs.hasMoreElements()) {
URL resourceUrl = rs.nextElement();
root.addConfiguration(getConfigurationFromUrl(resourceUrl));
log.info("Added resource '" + resourceUrl + "' from path '" + resource + "' to configuration");
}
} catch (IOException ioe) {
throw new Error("Unexpected error loading resources", ioe);
}
}
}
public static void addJournalResources(CombinedConfiguration root, CombinedConfiguration defaults, String path)
throws ConfigurationException {
Collection<String> journals = root.getList(JOURNALS);
String journalTemplatePath = root.getString(ConfigurationStore.JOURNAL_TEMPLATE_DIR, "/");
for (String journal : journals) {
String resourcePath = journalTemplatePath
+ (journalTemplatePath.endsWith("/") ? "journals/" : "/journals/")
+ journal
+ path;
File defaultsXmlFile = new File(resourcePath);
if (defaultsXmlFile.isFile() && defaultsXmlFile.canRead()) {
defaults.addConfiguration(new XMLConfiguration(defaultsXmlFile));
log.info("Added resource '" + resourcePath + "' to configuration");
}
}
}
}