/*
* Copyright 2007 Ralf Joachim
*
* 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.
*
* $Id: Configuration.java 6907 2007-03-28 21:24:52Z rjoachim $
*/
package org.castor.core.util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* Abstract base class to hold Castor configuration properties.
*
* @version $Id: Configuration.java,v 1.8 2006/03/08 17:25:52 jens Exp $
* @author <a href="mailto:ralf DOT joachim AT syscon DOT eu">Ralf Joachim</a>
* @since 1.1.3
*/
public abstract class AbstractProperties {
/**
* Name of the system property that can be used to specify the location
* of user properties.
*/
private static final String USER_PROPERTIES_SYSTEM_PROPERTY =
"org.castor.user.properties.location";
/**
* The <a href="http://jakarta.apache.org/commons/logging/">Jakarta Commons
* Logging</a> instance used for all logging.
*/
private static final Log LOG = LogFactory.getLog(AbstractProperties.class);
/**
* {@link ClassLoader} to be used for all classes of Castor and its required
* libraries.
*/
private final ClassLoader _applicationClassLoader;
/**
* {@link ClassLoader} to be used for all domain objects that are
* marshalled/unmarshalled or loaded from the database.
*/
private final ClassLoader _domainClassLoader;
/**
* Parent properties.
*/
private final AbstractProperties _parent;
private final Map _map = new HashMap();
/**
* Default constructor. Application and domain class loaders will be initialized to the one
* used to load the concrete properties class. No parent properties will be set.
*/
protected AbstractProperties() {
this(null, null);
}
/**
* Construct properties that uses the specified class loaders. No parent properties will be set.
*
* @param app Classloader to be used for all classes of Castor and its required libraries.
* @param domain Classloader to be used for all domain objects.
*/
protected AbstractProperties(final ClassLoader app, final ClassLoader domain) {
_applicationClassLoader = (app != null) ? app : getClass().getClassLoader();
_domainClassLoader = (domain != null) ? domain : getClass().getClassLoader();
_parent = null;
}
/**
* Construct properties with given parent. Application and domain class loaders will be
* initialized to the ones of the parent.
*
* @param parent Parent properties.
*/
protected AbstractProperties(final AbstractProperties parent) {
_applicationClassLoader = parent.getApplicationClassLoader();
_domainClassLoader = parent.getDomainClassLoader();
_parent = parent;
}
/**
* Get classloader to be used for all classes of Castor and its required libraries.
*
* @return Classloader to be used for all classes of Castor and its required libraries.
*/
public final ClassLoader getApplicationClassLoader() {
return _applicationClassLoader;
}
/**
* Get classloader to be used for all domain objects that are marshalled/unmarshalled or
* loaded from the database.
*
* @return Classloader to be used for all domain objects.
*/
public final ClassLoader getDomainClassLoader() {
return _domainClassLoader;
}
/**
* Load module properties from default locations.
* <br/>
* First it loads default properties contained in Castor JAR. This gets overwritten
* by a properties found on Java library directory. If no properties could be found
* until that point a PropertiesException will be thrown.
*
* @param path Path to the default properties to load.
* @param filename Name of the properties file.
*/
protected void loadDefaultProperties(final String path, final String filename) {
Properties properties = new Properties();
// Get default properties from the Castor JAR.
boolean inCastorJar = loadFromClassPath(properties, path + filename);
// Get overriding properties from the Java library directory, ignore if not
// found. If found merge existing properties.
boolean inJavaLibDir = loadFromJavaHome(properties, filename);
// Couldn't find properties in Castor jar nor Java library directory.
if (!inCastorJar && !inJavaLibDir) {
throw new PropertiesException("Failed to load properties: " + filename);
}
_map.putAll(properties);
}
/**
* Load common user properties from classpath root and current working directory.
* <br/>
* First it loads default properties contained in Castor JAR. This gets overwritten
* by properties found on Java library directory. If no properties could be found
* until that point a PropertiesException will be thrown. At last overriding
* properties are loaded from root of classpath or, if that could not be found, from
* local working directory.
*
* @param filename Name of the properties file.
*/
protected void loadUserProperties(final String filename) {
Properties properties = new Properties();
// Get common properties from the classpath root, ignore if not found.
boolean userPropertiesLoaded = loadFromClassPath(properties, "/" + filename);
// If not found on classpath root, either it doesn't exist, or "." is not part of
// the classpath, try looking at local working directory.
if (!userPropertiesLoaded) {
userPropertiesLoaded = loadFromWorkingDirectory(properties, filename);
}
if (!userPropertiesLoaded) {
String property = System.getProperty(USER_PROPERTIES_SYSTEM_PROPERTY);
if (property != null && property.length() > 0) {
File file = new File(property);
if (file.exists()) {
LOG.info("Loading custom Castor properties from " + file.getAbsolutePath());
userPropertiesLoaded = loadFromFile(properties, file);
} else {
LOG.warn(file.getAbsolutePath() + " is not a valid file.");
}
}
}
_map.putAll(properties);
}
/**
* Load properties with given filename from classpath and merge them into the given properties.
*
* @param properties Properties to merge the loaded ones into.
* @param filename Name of the properties file to load from classpath.
* @return <code>true</code> if properties could be loaded, <code>false</code> otherwise.
*/
private boolean loadFromClassPath(final Properties properties, final String filename) {
InputStream classPathStream = null;
try {
URL url = getClass().getResource(filename);
if (url != null) {
classPathStream = url.openStream();
properties.load(classPathStream);
if (LOG.isDebugEnabled()) {
LOG.debug("Properties loaded from classpath: " + filename);
}
return true;
}
return false;
} catch (Exception ex) {
LOG.warn("Failed to load properties from classpath: " + filename, ex);
return false;
} finally {
if (classPathStream != null) {
try {
classPathStream.close();
} catch (IOException e) {
LOG.warn("Failed to close properties from classpath: " + filename);
}
}
}
}
/**
* Load properties with given filename from Java library directory and merge them into
* the given properties.
*
* @param properties Properties to merge the loaded ones into.
* @param filename Name of the properties file to load from Java library directory.
* @return <code>true</code> if properties could be loaded, <code>false</code> otherwise.
*/
private boolean loadFromJavaHome(final Properties properties, final String filename) {
try {
String javaHome = System.getProperty("java.home");
if (javaHome == null) { return false; }
return loadFromFile(properties, new File(new File(javaHome, "lib"), filename));
} catch (SecurityException ex) {
LOG.warn("Security policy prevented access to system property 'java.home'.", ex);
return false;
}
}
/**
* Load properties with given filename from local working directory and merge them into
* the given properties.
*
* @param properties Properties to merge the loaded ones into.
* @param filename Name of the properties file to load from local working directory.
* @return <code>true</code> if properties could be loaded, <code>false</code> otherwise.
*/
private boolean loadFromWorkingDirectory(final Properties properties, final String filename) {
return loadFromFile(properties, new File(filename));
}
/**
* Load properties with given file and merge them into the given properties.
*
* @param properties Properties to merge the loaded ones into.
* @param file Properties file to load.
* @return <code>true</code> if properties could be loaded, <code>false</code> otherwise.
*/
private boolean loadFromFile(final Properties properties, final File file) {
InputStream fileStream = null;
try {
if (file.exists() && file.canRead()) {
fileStream = new FileInputStream(file);
properties.load(fileStream);
if (LOG.isDebugEnabled()) {
LOG.debug("Properties file loaded: " + file);
}
return true;
}
return false;
} catch (SecurityException ex) {
LOG.warn("Security policy prevented access to properties file: " + file, ex);
return false;
} catch (Exception ex) {
LOG.warn("Failed to load properties file: " + file, ex);
return false;
} finally {
if (fileStream != null) {
try {
fileStream.close();
} catch (IOException e) {
LOG.warn("Failed to close properties file: " + file);
}
}
}
}
/**
* Put given value associated with given key into the properties map of this properties. If
* the properties previously associated the key to another value the previous value will be
* returned. If a mapping for the key previously exist in the parent properties only, the
* method returns <code>null</code> and not the value of the parent. This allows to distingush
* if the mapping existed in this properties or one of its parents.
* <br/>
* Putting a value in this properties does not change the value of its parent but the
* parents value isn't visible any more as it gets overwritten by this properties one.
* While this allows to redefine the value of a property it isn't allowed to undefine it.
* Therefore a <code>NullPointerException</code> will be thrown if the given value is
* <code>null</code>.
*
* @param key Key of the property to put into properties.
* @param value Value to put into properties associated with the given key..
* @return Object in this properties that previously has been associated with the given key.
*/
public final synchronized Object put(final String key, final Object value) {
if (value == null) { throw new NullPointerException(); }
return _map.put(key, value);
}
/**
* Remove any value previously associated with the given key from this properties. The value
* previously associated with the key in this properties will be returned. If a mapping
* for the key existed in the parent properties only, the method returns <code>null</code>
* and not the value of the parent. This allows to distingush if the mapping existed in this
* properties or one of its parents.
* <br/>
* Removing the value from this properties does not mean that consecutive gets return
* <code>null</code> as one of the parents may still contain a mapping for the key that
* was hidden by the mapping in this properties.
*
* @param key Key of the property to remove from properties.
* @return Object in this properties that previously has been associated with the given key.
*/
public final synchronized Object remove(final String key) {
return _map.remove(key);
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to any object value, it will be returned as is. If the property is not found,
* <code>null</code> will be returned.
*
* @param key Key of the property to get from properties.
* @return Object in this property map with the specified key value.
*/
protected synchronized Object get(final String key) {
Object value = _map.get(key);
if ((value == null) && (_parent != null)) {
value = _parent.get(key);
}
return value;
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a boolean value, it will be returned as is. For string values that are
* equal, ignore case, to 'true' or 'false', the respective boolean value will be returned. If
* the property is not found, <code>null</code> will be returned. For all other types and
* string values a PropertiesException will be thrown. This behaviour is intended for those
* usecases that need distinguish between values that are missconfigured or not specified at
* all.
*
* @param key Property key.
* @return Boolean value in this property map with the specified key value.
*/
public final Boolean getBoolean(final String key) {
Object objectValue = get(key);
if (objectValue == null) {
return null;
} else if (objectValue instanceof Boolean) {
return (Boolean) objectValue;
} else if (objectValue instanceof String) {
String stringValue = (String) objectValue;
if ("true".equalsIgnoreCase(stringValue)) {
return Boolean.TRUE;
} else if ("false".equalsIgnoreCase(stringValue)) {
return Boolean.FALSE;
}
}
Object[] args = new Object[] {key, objectValue};
String msg = "Properties value can not be converted to boolean: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args));
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a boolean value, it will be returned as is. For string values that are
* equal, ignore case, to 'true' or 'false', the respective boolean value will be returned. In
* all other cases the given default value will be returned.
*
* @param key Property key.
* @param defaultValue Default value.
* @return Boolean value in this property map with the specified key value.
*/
public final boolean getBoolean(final String key, final boolean defaultValue) {
Object objectValue = get(key);
if (objectValue instanceof Boolean) {
return ((Boolean) objectValue).booleanValue();
} else if (objectValue instanceof String) {
String stringValue = (String) objectValue;
if ("true".equalsIgnoreCase(stringValue)) {
return true;
} else if ("false".equalsIgnoreCase(stringValue)) {
return false;
}
}
return defaultValue;
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a integer value, it will be returned as is. For string values that can
* be interpreted as signed decimal integer, the respective integer value will be returned. If
* the property is not found, <code>null</code> will be returned. For all other types and
* string values a PropertiesException will be thrown. This behaviour is intended for those
* usecases that need distinguish between values that are missconfigured or not specified at
* all.
*
* @param key Property key.
* @return Integer value in this property map with the specified key value.
*/
public final Integer getInteger(final String key) {
Object objectValue = get(key);
if (objectValue == null) {
return null;
} else if (objectValue instanceof Integer) {
return (Integer) objectValue;
} else if (objectValue instanceof String) {
try {
return Integer.valueOf((String) objectValue);
} catch (NumberFormatException ex) {
Object[] args = new Object[] {key, objectValue};
String msg = "Properties value can not be converted to int: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args), ex);
}
}
Object[] args = new Object[] {key, objectValue};
String msg = "Properties value can not be converted to int: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args));
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a integer value, it will be returned as is. For string values that can
* be interpreted as signed decimal integer, the respective integer value will be returned. In
* all other cases the given default value will be returned.
*
* @param key Property key.
* @param defaultValue Default value.
* @return Integer value in this property map with the specified key value.
*/
public final int getInteger(final String key, final int defaultValue) {
Object objectValue = get(key);
if (objectValue instanceof Integer) {
return ((Integer) objectValue).intValue();
} else if (objectValue instanceof String) {
String stringValue = (String) objectValue;
try {
return Integer.parseInt(stringValue);
} catch (NumberFormatException ex) {
return defaultValue;
}
}
return defaultValue;
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a string value, it will be returned as is. If the property is not found,
* <code>null</code> will be returned. For all other types a PropertiesException will be
* thrown.
*
* @param key Property key.
* @return String value in this property map with the specified key value.
*/
public final String getString(final String key) {
Object objectValue = get(key);
if (objectValue == null) {
return null;
} else if (objectValue instanceof String) {
return (String) objectValue;
}
Object[] args = new Object[] {key, objectValue};
String msg = "Properties value is not a string: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args));
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a string value that is not empty, it will be returned as is. In all other
* cases the given default value will be returned.
*
* @param key Property key.
* @param defaultValue Default value.
* @return String value in this property map with the specified key value.
*/
public final String getString(final String key, final String defaultValue) {
Object objectValue = get(key);
if ((objectValue instanceof String) && !"".equals(objectValue)) {
return (String) objectValue;
}
return defaultValue;
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a string array, it will be returned as is. A simple string will be
* converted into a string array by splitting it into substrings at every occurence of ','
* character. If the property is not found, <code>null</code> will be returned. For all other
* types a PropertiesException will be thrown.
*
* @param key Property key.
* @return String array in this property map with the specified key value.
*/
public final String[] getStringArray(final String key) {
Object objectValue = get(key);
if (objectValue == null) {
return null;
} else if (objectValue instanceof String[]) {
return (String[]) objectValue;
} else if (objectValue instanceof String) {
return ((String) objectValue).split(",");
}
Object[] args = new Object[] {key, objectValue};
String msg = "Properties value is not a String[]: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args));
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a class, it will be returned as is. A simple string will be interpreted
* as class name of which the class will be loaded with the given class loader. If the property
* is not found, <code>null</code> will be returned. For all other types and if loading of the
* class fails a PropertiesException will be thrown.
*
* @param key Property key.
* @param loader Class loader to load classes with.
* @return Class in this property map with the specified key value.
*/
public final Class getClass(final String key, final ClassLoader loader) {
Object objectValue = get(key);
if (objectValue == null) {
return null;
} else if (objectValue instanceof Class) {
return (Class) objectValue;
} else if (objectValue instanceof String) {
String classname = (String) objectValue;
try {
return loader.loadClass(classname);
} catch (ClassNotFoundException ex) {
Object[] args = new Object[] {key, classname};
String msg = "Could not find class of properties value: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args), ex);
}
}
Object[] args = new Object[] {key, objectValue};
String msg = "Properties value is not a Class: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args));
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a class array, it will be returned as is. A simple string will be
* splitted it into substrings at every occurence of ',' character. Each of these substrings
* will interpreted as class name of which the class will be loaded with the given class
* loader. If the property is not found, <code>null</code> will be returned. For all other
* types and if loading of one of the classes fails a PropertiesException will be thrown.
*
* @param key Property key.
* @param loader Class loader to load classes with.
* @return Class array in this property map with the specified key value.
*/
public final Class[] getClassArray(final String key, final ClassLoader loader) {
Object objectValue = get(key);
if (objectValue == null) {
return null;
} else if (objectValue instanceof Class[]) {
return (Class[]) objectValue;
} else if (objectValue instanceof String) {
String[] classnames = ((String) objectValue).split(",");
Class[] classes = new Class[classnames.length];
for (int i = 0; i < classnames.length; i++) {
try {
classes[i] = loader.loadClass(classnames[i]);
} catch (ClassNotFoundException ex) {
Object[] args = new Object[] {key, new Integer(i), classnames[i]};
String msg = "Could not find class of properties value: {0}[{1}]={2}";
throw new PropertiesException(MessageFormat.format(msg, args), ex);
}
}
return classes;
}
Object[] args = new Object[] {key, objectValue};
String msg = "Properties value is not a Class[]: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args));
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to any object value, it will be returned as is. If the property is not found,
* <code>null</code> will be returned.
*
* @param key Property key.
* @return Object in this property map with the specified key value.
*/
public final Object getObject(final String key) {
return get(key);
}
/**
* Searches for the property with the specified key in this property map. If the key is not
* found in this property map, the parent property map, and its parents, recursively, are then
* checked.
* <br/>
* If the key maps to a object array, it will be returned as is. A simple string will be
* splitted it into substrings at every occurence of ',' character. Each of these substrings
* will interpreted as class name of which the class will be loaded with the given class
* loader and instantiated using its default constructor. If the property is not found,
* <code>null</code> will be returned. For all other types and if loading or instantiation of
* one of the classes fails a PropertiesException will be thrown.
*
* @param key Property key.
* @param loader Class loader to load classes with.
* @return Class array in this property map with the specified key value.
*/
public final Object[] getObjectArray(final String key, final ClassLoader loader) {
Object objectValue = get(key);
if (objectValue == null) {
return null;
} else if (objectValue instanceof Object[]) {
return (Object[]) objectValue;
} else if (objectValue instanceof String) {
List objects = new ArrayList();
String[] classnames = ((String) objectValue).split(",");
for (int i = 0; i < classnames.length; i++) {
String classname = classnames[i];
try {
if ((classname != null) && !"".equals(classname.trim())) {
classname = classname.trim();
objects.add(loader.loadClass(classname).newInstance());
}
} catch (ClassNotFoundException ex) {
Object[] args = new Object[] {key, new Integer(i), classname};
String msg = "Could not find configured class: {0}[{1}]={2}";
throw new PropertiesException(MessageFormat.format(msg, args), ex);
} catch (IllegalAccessException ex) {
Object[] args = new Object[] {key, new Integer(i), classname};
String msg = "Could not instantiate configured class: {0}[{1}]={2}";
throw new PropertiesException(MessageFormat.format(msg, args), ex);
} catch (InstantiationException ex) {
Object[] args = new Object[] {key, new Integer(i), classname};
String msg = "Could not instantiate configured class: {0}[{1}]={2}";
throw new PropertiesException(MessageFormat.format(msg, args), ex);
} catch (ExceptionInInitializerError ex) {
Object[] args = new Object[] {key, new Integer(i), classname};
String msg = "Could not instantiate configured class: {0}[{1}]={2}";
throw new PropertiesException(MessageFormat.format(msg, args), ex);
} catch (SecurityException ex) {
Object[] args = new Object[] {key, new Integer(i), classname};
String msg = "Could not instantiate configured class: {0}[{1}]={2}";
throw new PropertiesException(MessageFormat.format(msg, args), ex);
}
}
return objects.toArray();
}
Object[] args = new Object[] {key, objectValue};
String msg = "Properties value is not an Object[]: {0}={1}";
throw new PropertiesException(MessageFormat.format(msg, args));
}
}