/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
package org.dspace.servicemanager.config;
import java.io.*;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.*;
import java.util.Map.Entry;
import org.dspace.constants.Constants;
import org.dspace.servicemanager.ServiceConfig;
import org.dspace.services.ConfigurationService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.SimpleTypeConverter;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
/**
* The central DSpace configuration service.
* This is effectively immutable once the config has loaded.
*
* @author Aaron Zeckoski (azeckoski @ gmail.com)
* @author Kevin Van de Velde (kevin at atmire dot com)
* @author Mark Diggory (mdiggory at atmire dot com)
*/
public final class DSpaceConfigurationService implements ConfigurationService {
private static final Logger log = LoggerFactory.getLogger(DSpaceConfigurationService.class);
public static final String DSPACE_WEB_CONTEXT_PARAM = "dspace-config";
public static final String DSPACE = "dspace";
public static final String EXT_CONFIG = "cfg";
public static final String DOT_CONFIG = "." + EXT_CONFIG;
public static final String DSPACE_PREFIX = "dspace.";
public static final String DSPACE_HOME = DSPACE + ".dir";
public static final String DEFAULT_CONFIGURATION_FILE_NAME = "dspace-defaults" + DOT_CONFIG;
public static final String DEFAULT_DSPACE_CONFIG_PATH = "config/" + DEFAULT_CONFIGURATION_FILE_NAME;
public static final String DSPACE_CONFIG_PATH = "config/" + DSPACE + DOT_CONFIG;
public static final String DSPACE_MODULES_CONFIG_PATH = "config" + File.separator + "modules";
protected transient Map<String, Map<String, ServiceConfig>> serviceNameConfigs;
public static final String DSPACE_CONFIG_ADDON = "dspace/config-*";
public DSpaceConfigurationService() {
// init and load up current config settings
loadInitialConfig(null);
}
public DSpaceConfigurationService(String providedHome) {
loadInitialConfig(providedHome);
}
/* (non-Javadoc)
* @see org.dspace.services.ConfigurationService#getAllProperties()
*/
@Override
public Map<String, String> getAllProperties() {
Map<String, String> props = new LinkedHashMap<String, String>();
// for (Entry<String, DSpaceConfig> config : configuration.entrySet()) {
// props.put(config.getKey(), config.getValue().getValue());
// }
for (DSpaceConfig config : configuration.values()) {
props.put(config.getKey(), config.getValue());
}
return props;
}
/* (non-Javadoc)
* @see org.dspace.services.ConfigurationService#getProperties()
*/
@Override
public Properties getProperties() {
Properties props = new Properties();
for (DSpaceConfig config : configuration.values()) {
props.put(config.getKey(), config.getValue());
}
return props;
}
/* (non-Javadoc)
* @see org.dspace.services.ConfigurationService#getProperty(java.lang.String)
*/
@Override
public String getProperty(String name) {
DSpaceConfig config = configuration.get(name);
String value = null;
if (config != null) {
value = config.getValue();
}
return value;
}
/* (non-Javadoc)
* @see org.dspace.services.ConfigurationService#getPropertyAsType(java.lang.String, java.lang.Class)
*/
@Override
public <T> T getPropertyAsType(String name, Class<T> type) {
String value = getProperty(name);
return convert(value, type);
}
/* (non-Javadoc)
* @see org.dspace.services.ConfigurationService#getPropertyAsType(java.lang.String, java.lang.Object)
*/
@Override
public <T> T getPropertyAsType(String name, T defaultValue) {
return getPropertyAsType(name, defaultValue, false);
}
/* (non-Javadoc)
* @see org.dspace.services.ConfigurationService#getPropertyAsType(java.lang.String, java.lang.Object, boolean)
*/
@SuppressWarnings("unchecked")
@Override
public <T> T getPropertyAsType(String name, T defaultValue, boolean setDefaultIfNotFound) {
String value = getProperty(name);
T property = null;
if (defaultValue == null) {
property = null; // just return null when default value is null
} else if (value == null) {
property = defaultValue; // just return the default value if nothing is currently set
// also set the default value as the current stored value
if (setDefaultIfNotFound) {
setProperty(name, defaultValue);
}
} else {
// something is already set so we convert the stored value to match the type
property = (T)convert(value, defaultValue.getClass());
}
return property;
}
// config loading methods
/* (non-Javadoc)
* @see org.dspace.services.ConfigurationService#setProperty(java.lang.String, java.lang.Object)
*/
@Override
public boolean setProperty(String name, Object value) {
if (name == null) {
throw new IllegalArgumentException("name cannot be null for setting configuration");
}
boolean changed = false;
if (value == null) {
changed = this.configuration.remove(name) != null;
log.info("Cleared the configuration setting for name ("+name+")");
} else {
SimpleTypeConverter converter = new SimpleTypeConverter();
String sVal = (String)converter.convertIfNecessary(value, String.class);
changed = loadConfig(name, sVal);
}
return changed;
}
// INTERNAL loading methods
public List<DSpaceConfig> getConfiguration() {
return new ArrayList<DSpaceConfig>( configuration.values() );
}
/**
* Get all configs that start with the given value.
* @param prefix a string which the configs to return must start with
* @return the list of all configs that start with the given string
*/
public List<DSpaceConfig> getConfigsByPrefix(String prefix) {
List<DSpaceConfig> configs = new ArrayList<DSpaceConfig>();
if (prefix != null && prefix.length() > 0) {
for (DSpaceConfig config : configuration.values()) {
if (config.getKey().startsWith(prefix)) {
configs.add(config);
}
}
}
return configs;
}
protected Map<String, DSpaceConfig> configuration = Collections.synchronizedMap(new LinkedHashMap<String, DSpaceConfig>());
/**
* @return a map of the service name configurations that are known for fast resolution
*/
public Map<String, Map<String, ServiceConfig>> getServiceNameConfigs() {
return serviceNameConfigs;
}
public void setConfiguration(Map<String, DSpaceConfig> configuration) {
if (configuration == null) {
throw new IllegalArgumentException("configuration cannot be null");
}
this.configuration = configuration;
replaceVariables(this.configuration);
// refresh the configs
serviceNameConfigs = makeServiceNameConfigs();
}
/**
* Load a series of properties into the configuration.
* Checks to see if the settings exist or are changed and only loads
* changes. Clears out existing ones depending on the setting.
*
* @param properties a map of key -> value strings
* @param clear if true then clears the existing configuration settings first
* @return the list of changed configuration names
*/
public String[] loadConfiguration(Map<String, String> properties, boolean clear) {
if (properties == null) {
throw new IllegalArgumentException("properties cannot be null");
}
// transform to configs and call load
ArrayList<DSpaceConfig> dspaceConfigs = new ArrayList<DSpaceConfig>();
for (Entry<String, String> entry : properties.entrySet()) {
String key = entry.getKey();
if (key != null && ! "".equals(key)) {
String val = entry.getValue();
if (val != null && ! "".equals(val)) {
dspaceConfigs.add( new DSpaceConfig(entry.getKey(), entry.getValue()) );
}
}
}
return loadConfiguration(dspaceConfigs, clear);
}
/**
* Load up a bunch of {@link DSpaceConfig}s into the configuration.
* Checks to see if the settings exist or are changed and only
* loads changes. Clears out existing ones depending on the setting.
*
* @param dspaceConfigs a list of {@link DSpaceConfig} objects
* @param clear if true then clears the existing configuration settings first
* @return the list of changed configuration names
*/
public String[] loadConfiguration(List<DSpaceConfig> dspaceConfigs, boolean clear) {
ArrayList<String> changed = new ArrayList<String>();
if (clear) {
this.configuration.clear();
}
for (DSpaceConfig config : dspaceConfigs) {
String key = config.getKey();
boolean same = true;
if (clear) {
// all are new
same = false;
} else {
if (this.configuration.containsKey(key)) {
if (this.configuration.get(key).equals(config)) {
// this one has changed
same = false;
}
} else {
// this one is new
same = false;
}
}
if (!same) {
changed.add(key);
this.configuration.put(key, config);
}
}
if (changed.size() > 0) {
replaceVariables(this.configuration);
// refresh the configs
serviceNameConfigs = makeServiceNameConfigs();
}
return changed.toArray(new String[changed.size()]);
}
/**
* Loads an additional config setting into the system.
* @param key
* @param value
* @return true if the config is new or changed
*/
public boolean loadConfig(String key, String value) {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
// update replacements and add
boolean changed = replaceAndAddConfig( new DSpaceConfig(key, value) );
if (changed) {
// refresh the configs
serviceNameConfigs = makeServiceNameConfigs();
}
return changed;
}
/**
* Clears the configuration settings.
*/
public void clear() {
this.configuration.clear();
this.serviceNameConfigs.clear();
log.info("Cleared all configuration settings");
}
// loading from files code
/**
* Loads up the default initial configuration from the DSpace configuration
* files in the file home and on the classpath. Order:
* <ol>
* <li>Create {@code serverId} from local host name if available.</li>
* <li>Create {@code dspace.testing = false}.
* <li>Determine the value of {@code dspace.dir} and add to configuration.</li>
* <li>Load {@code classpath:config/dspace_defaults.cfg}.</li>
* <li>Copy system properties with names beginning "dspace." <em>except</em>
* {@code dspace.dir}, removing the "dspace." prefix from the name.</li>
* <li>Load all {@code classpath:dspace/config-*.cfg} using whatever
* matched "*" as module prefix.</li>
* <li>Load all {@code ${dspace.dir}/config/modules/*.cfg} using whatever
* matched "*" as module prefix.</li>
* <li>Load {@code classpath:dspace.cfg}.</li>
* <li>Load from the path in the system property {@code dspace.configuration}
* if defined, or {@code ${dspace.dir}/config/dspace.cfg}.</li>
* <li>Perform variable substitutions throughout the assembled configuration.</li>
* </ol>
*
* <p>The initial value of {@code dspace.dir} will be:</p>
* <ol>
* <li>the value of the system property {@code dspace.dir} if defined;</li>
* <li>else the value of {@code providedHome} if not null;</li>
* <li>else the servlet container's home + "/dspace/" if defined (see {@link getCatalina()});</li>
* <li>else the user's home directory if defined;</li>
* <li>else "/".
* </ol>
*
* @param providedHome DSpace home directory, or null.
*/
public void loadInitialConfig(String providedHome) {
Map<String, String> configMap = new LinkedHashMap<String, String>();
// load default settings
try {
String defaultServerId = InetAddress.getLocalHost().getHostName();
configMap.put("serverId", defaultServerId);
} catch (UnknownHostException e) {
// oh well
}
// default is testing mode off
configMap.put(Constants.DSPACE_TESTING_MODE, "false");
// now we load the settings from properties files
String homePath = System.getProperty(DSPACE_HOME);
// now we load from the provided parameter if its not null
if (providedHome != null && homePath == null) {
homePath = providedHome;
}
if (homePath == null) {
String catalina = getCatalina();
if (catalina != null) {
homePath = catalina + File.separatorChar + DSPACE + File.separatorChar;
}
}
if (homePath == null) {
homePath = System.getProperty("user.home");
}
if (homePath == null) {
homePath = "/";
}
// make sure it's set properly
//System.setProperty(DSPACE_HOME, homePath);
configMap.put(DSPACE_HOME, homePath);
// LOAD the internal defaults
Properties defaultProps = readPropertyResource(DEFAULT_DSPACE_CONFIG_PATH);
if (defaultProps.size() <= 0) {
// failed to load defaults!
throw new RuntimeException("Failed to load default dspace config properties: " + DEFAULT_DSPACE_CONFIG_PATH);
}
pushPropsToMap(configMap, defaultProps);
// load all properties from the system which begin with the prefix
Properties systemProps = System.getProperties();
for (Object o : systemProps.keySet()) {
String key = (String) o;
if (key != null
&& ! key.equals(DSPACE_HOME)) {
try {
if (key.startsWith(DSPACE_PREFIX)) {
String propName = key.substring(DSPACE_PREFIX.length());
String propVal = systemProps.getProperty(key);
log.info("Loading system property as config: "+propName+"=>"+propVal);
configMap.put(propName, propVal);
}
} catch (RuntimeException e) {
log.error("Failed to properly get config value from system property: " + o, e);
}
}
}
// Collect values from all the properties files: the later ones loaded override settings from prior.
//Find any addon config files found in the config dir in our jars
try {
PathMatchingResourcePatternResolver patchMatcher = new PathMatchingResourcePatternResolver();
Resource[] resources = patchMatcher.getResources("classpath*:" + DSPACE_CONFIG_ADDON + DOT_CONFIG);
for (Resource resource : resources) {
String prefix = resource.getFilename().substring(0, resource.getFilename().lastIndexOf(".")).replaceFirst("config-", "");
pushPropsToMap(configMap, prefix, readPropertyStream(resource.getInputStream()));
}
}catch (Exception e){
log.error("Failed to retrieve properties from classpath: " + e.getMessage(), e);
}
//Attempt to load up all the config files in the modules directory
try{
File modulesDirectory = new File(homePath + File.separator + DSPACE_MODULES_CONFIG_PATH + File.separator);
if(modulesDirectory.exists()){
try{
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(modulesDirectory.toURI().toURL().toString() + "*" + DOT_CONFIG);
if(resources != null){
for(Resource resource : resources){
String prefix = resource.getFilename().substring(0, resource.getFilename().lastIndexOf("."));
pushPropsToMap(configMap, prefix, readPropertyStream(resource.getInputStream()));
}
}
}catch (IOException e){
log.error("Error while loading the modules properties from:" + modulesDirectory.getAbsolutePath());
}
}else{
log.info("Failed to load the modules properties since (" + homePath + File.separator + DSPACE_MODULES_CONFIG_PATH + "): Does not exist");
}
}catch (IllegalArgumentException e){
//This happens if we don't have a modules directory
log.error("Error while loading the module properties since (" + homePath + File.separator + DSPACE_MODULES_CONFIG_PATH + "): is not a valid directory", e);
}
// attempt to load from the current classloader also (works for commandline config sitting on classpath
pushPropsToMap(configMap, readPropertyResource(DSPACE + DOT_CONFIG));
// read all the known files from the home path that are properties files
String configPath = System.getProperty("dspace.configuration");
if (null == configPath)
{
configPath = homePath + File.separatorChar + DSPACE_CONFIG_PATH;
}
pushPropsToMap(configMap, readPropertyFile(configPath));
// TODO: still use this local file loading?
// pushPropsToMap(configMap, readPropertyFile(homePath + File.separatorChar + "local" + DOT_PROPERTIES));
// pushPropsToMap(configMap, readPropertyResource(DSPACE + DOT_PROPERTIES));
// pushPropsToMap(configMap, readPropertyResource("local" + DOT_PROPERTIES));
// pushPropsToMap(configMap, readPropertyResource("webapp" + DOT_PROPERTIES));
// now push all of these into the config service store
loadConfiguration(configMap, true);
log.info("Started up configuration service and loaded "+configMap.size()+" settings");
}
/**
* Adds in this DSConfig and then updates the config by checking for
* replacements everywhere else.
* @param dsConfig a DSConfig to update the value of and then add in to the main config
* @return true if the config changed or is new
*/
protected boolean replaceAndAddConfig(DSpaceConfig dsConfig) {
DSpaceConfig newConfig = null;
String key = dsConfig.getKey();
if (dsConfig.getValue().contains("${")) {
String value = dsConfig.getValue();
int start = -1;
while ((start = value.indexOf("${")) > -1) {
int end = value.indexOf('}', start);
if (end > -1) {
String newKey = value.substring(start+2, end);
if (newKey.equals(key)) {
log.warn("Found circular reference for key ("+newKey+") in config value: " + value);
break;
}
DSpaceConfig dsc = this.configuration.get(newKey);
if (dsc == null) {
log.warn("Could not find key ("+newKey+") for replacement in value: " + value);
break;
}
String newVal = dsc.getValue();
value = value.replace("${"+newKey+"}", newVal);
newConfig = new DSpaceConfig(key, value);
} else {
log.warn("Found '${' but could not find a closing '}' in the value: " + value);
break;
}
}
}
// add the config
if (this.configuration.containsKey(key) && this.configuration.get(key).equals(dsConfig)) {
return false; // SHORT CIRCUIT
}
// config changed or new
this.configuration.put(key, newConfig != null ? newConfig : dsConfig);
// update replacements
replaceVariables(this.configuration);
return true;
}
/**
* This will replace the ${key} with the value from the matching key
* if it exists. Logs a warning if the key does not exist.
* Goes through and updates the replacements for the the entire
* configuration and updates any replaced values.
*/
protected void replaceVariables(Map<String, DSpaceConfig> dsConfiguration) {
for (Entry<String, DSpaceConfig> entry : dsConfiguration.entrySet()) {
if (entry.getValue().getValue().contains("${")) {
String value = entry.getValue().getValue();
int start = -1;
while ((start = value.indexOf("${")) > -1) {
int end = value.indexOf('}', start);
if (end > -1) {
String newKey = value.substring(start+2, end);
DSpaceConfig dsc = dsConfiguration.get(newKey);
if (dsc == null) {
log.warn("Could not find key ("+newKey+") for replacement in value: " + value);
break;
}
String newVal = dsc.getValue();
String oldValue = value;
value = value.replace("${"+newKey+"}", newVal);
if (value.equals(oldValue)) {
log.warn("No change after variable replacement -- is "
+ newKey + " = " + newVal +
" a circular reference?");
break;
}
entry.setValue( new DSpaceConfig(entry.getValue().getKey(), value) );
} else {
log.warn("Found '${' but could not find a closing '}' in the value: " + value);
break;
}
}
}
}
}
protected Properties readPropertyFile(String filePathName) {
Properties props = new Properties();
InputStream is = null;
try {
File f = new File(filePathName);
if (f.exists()) {
is = new FileInputStream(f);
props.load(is);
log.info("Loaded "+props.size()+" config properties from file: " + f);
}
else
{
log.info("Failed to load config properties from file ("+filePathName+"): Does not exist");
}
} catch (Exception e) {
log.warn("Failed to load config properties from file ("+filePathName+"): " + e.getMessage(), e);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException ioe) {
// Ignore exception on close
}
}
}
return props;
}
protected Properties readPropertyResource(String resourcePathName) {
Properties props = new Properties();
try {
ClassPathResource resource = new ClassPathResource(resourcePathName);
if (resource.exists()) {
props.load(resource.getInputStream());
log.info("Loaded "+props.size()+" config properties from resource: " + resource);
}
} catch (Exception e) {
log.warn("Failed to load config properties from resource ("+resourcePathName+"): " + e.getMessage(), e);
}
return props;
}
protected Properties readPropertyFile(File propertyFile) {
Properties props = new Properties();
try{
if(propertyFile.exists()){
props.load(new FileInputStream(propertyFile));
log.info("Loaded"+props.size() + " config properties from file: " + propertyFile.getName());
}
} catch (Exception e){
log.warn("Failed to load config properties from file (" + propertyFile.getName() + ": "+ e.getMessage(), e);
}
return props;
}
protected Properties readPropertyStream(InputStream propertyStream){
Properties props = new Properties();
try{
props.load(propertyStream);
log.info("Loaded"+props.size() + " config properties from stream");
} catch (Exception e){
log.warn("Failed to load config properties from stream: " + e.getMessage(), e);
}
return props;
}
protected void pushPropsToMap(Map<String, String> map, Properties props) {
pushPropsToMap(map, null, props);
}
protected void pushPropsToMap(Map<String, String> map, String prefix, Properties props) {
for (Entry<Object, Object> entry : props.entrySet()) {
String key = entry.getKey().toString();
if(prefix != null){
key = prefix + "." + key;
}
map.put(key, entry.getValue() == null ? "" : entry.getValue().toString());
}
}
/**
* This simply attempts to find the servlet container home for tomcat.
* @return the path to the servlet container home OR null if it cannot be found
*/
protected String getCatalina() {
String catalina = System.getProperty("catalina.base");
if (catalina == null) {
catalina = System.getProperty("catalina.home");
}
return catalina;
}
@Override
public String toString() {
return "Config:" + DSPACE_HOME + ":size=" + configuration.size();
}
/**
* Constructs service name configs map for fast lookup of service
* configurations.
* @return the map of config service settings
*/
public Map<String, Map<String, ServiceConfig>> makeServiceNameConfigs() {
Map<String, Map<String, ServiceConfig>> serviceNameConfigs = new HashMap<String, Map<String,ServiceConfig>>();
for (DSpaceConfig dsConfig : getConfiguration()) {
String beanName = dsConfig.getBeanName();
if (beanName != null) {
Map<String, ServiceConfig> map = null;
if (serviceNameConfigs.containsKey(beanName)) {
map = serviceNameConfigs.get(beanName);
} else {
map = new HashMap<String, ServiceConfig>();
serviceNameConfigs.put(beanName, map);
}
map.put(beanName, new ServiceConfig(dsConfig));
}
}
return serviceNameConfigs;
}
private <T> T convert(String value, Class<T> type) {
SimpleTypeConverter converter = new SimpleTypeConverter();
if (value != null) {
if (type.isArray()) {
String[] values = value.split(",");
return (T)converter.convertIfNecessary(values, type);
}
if (type.isAssignableFrom(String.class)) {
return (T)value;
}
} else {
if (boolean.class.equals(type)) {
return (T)Boolean.FALSE;
} else if (int.class.equals(type) || long.class.equals(type)) {
return (T)converter.convertIfNecessary(0, type);
}
}
return (T)converter.convertIfNecessary(value, type);
}
}