/* * 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)); } }