/********************************************************************************** * * $Id: SakaiProperties.java 105077 2012-02-24 22:54:29Z ottenhoff@longsight.com $ * *********************************************************************************** * * Copyright (c) 2007, 2008 Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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.sakaiproject.util; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import org.springframework.core.io.Resource; import org.springframework.util.CollectionUtils; import org.springframework.util.DefaultPropertiesPersister; import org.springframework.util.PropertiesPersister; /** * A configurer for "sakai.properties" files. These differ from the usual Spring default properties * files by mixing together lines which define property-value pairs and lines which define * bean property overrides. The two can be distinguished because Sakai conventionally uses * the bean name separator "@" instead of the default "." * * This class creates separate PropertyPlaceholderConfigurer and PropertyOverrideConfigurer * objects to handle bean configuration, and loads them with the input properties. * * SakaiProperties configuration supports most of the properties documented for * PropertiesFactoryBean, PropertyPlaceholderConfigurer, and PropertyOverrideConfigurer. */ public class SakaiProperties implements BeanFactoryPostProcessorCreator, InitializingBean { private static Log log = LogFactory.getLog(SakaiProperties.class); private SakaiPropertiesFactoryBean propertiesFactoryBean = new SakaiPropertiesFactoryBean(); //private PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean(); private ReversiblePropertyOverrideConfigurer propertyOverrideConfigurer = new ReversiblePropertyOverrideConfigurer(); private PropertyPlaceholderConfigurer propertyPlaceholderConfigurer = new PropertyPlaceholderConfigurer(); public SakaiProperties() { // Set defaults. propertiesFactoryBean.setIgnoreResourceNotFound(true); propertyPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(true); propertyPlaceholderConfigurer.setOrder(0); propertyOverrideConfigurer.setBeanNameAtEnd(true); propertyOverrideConfigurer.setBeanNameSeparator("@"); propertyOverrideConfigurer.setIgnoreInvalidKeys(true); } public void afterPropertiesSet() throws Exception { // Connect properties to configurers. propertiesFactoryBean.afterPropertiesSet(); propertyPlaceholderConfigurer.setProperties((Properties)propertiesFactoryBean.getObject()); propertyOverrideConfigurer.setProperties((Properties)propertiesFactoryBean.getObject()); } /* (non-Javadoc) * @see org.sakaiproject.util.BeanFactoryPostProcessorCreator#getBeanFactoryPostProcessors() */ public Collection<BeanFactoryPostProcessor> getBeanFactoryPostProcessors() { return (Arrays.asList(new BeanFactoryPostProcessor[] {propertyOverrideConfigurer, propertyPlaceholderConfigurer})); } /** * Gets the individual properties from each properties file which is read in * * @return a map of filename -> Properties */ public Map<String, Properties> getSeparateProperties() { LinkedHashMap<String, Properties> m = new LinkedHashMap<String, Properties>(); /* This doesn't work because spring always returns only the first of the properties files -AZ * very disappointing because it means we can't tell which file a property came from try { // have to use reflection to get the fields here because Spring does not expose them directly Field localPropertiesField = PropertiesLoaderSupport.class.getDeclaredField("localProperties"); Field locationsField = PropertiesLoaderSupport.class.getDeclaredField("locations"); localPropertiesField.setAccessible(true); locationsField.setAccessible(true); Properties[] localProperties = (Properties[]) localPropertiesField.get(propertiesFactoryBean); Resource[] locations = (Resource[]) locationsField.get(propertiesFactoryBean); log.info("found "+locations.length+" locations and "+localProperties.length+" props files"); for (int i = 0; i < localProperties.length; i++) { Properties p = localProperties[i]; Properties props = dereferenceProperties(p); Resource r = locations[i]; log.info("found "+p.size()+" props ("+props.size()+") in "+r.getFilename()); if (m.put(r.getFilename(), props) != null) { log.warn("SeparateProperties: Found use of 2 sakai properties files with the same name (probable data loss): "+r.getFilename()); } } } catch (Exception e) { log.warn("SeparateProperties: Failure trying to get the separate properties: "+e); m.clear(); m.put("ALL", getProperties()); } */ /* m.put("ALL", getProperties()); */ for (Entry<String, Properties> entry : propertiesFactoryBean.getLoadedProperties().entrySet()) { m.put(entry.getKey(), dereferenceProperties(entry.getValue())); } return m; } /** * INTERNAL * @return the set of properties after processing */ public Properties getProperties() { Properties rawProperties = getRawProperties(); Properties parsedProperties = dereferenceProperties(rawProperties); return parsedProperties; } /** * INTERNAL * @return the complete set of properties exactly as read from the files */ public Properties getRawProperties() { try { return (Properties)propertiesFactoryBean.getObject(); } catch (IOException e) { if (log.isWarnEnabled()) log.warn("Error collecting Sakai properties", e); return new Properties(); } } /** * Dereferences property placeholders in the given {@link Properties} * in exactly the same way the {@link BeanFactoryPostProcessor}s in this * object perform their placeholder dereferencing. Unfortunately, this * process is not readily decoupled from the act of processing a * bean factory in the Spring libraries. Hence the reflection. * * @param srcProperties a collection of name-value pairs * @return a new collection of properties. If <code>srcProperties</code> * is <code>null</code>, returns null. If <code>srcProperties</code> * is empty, returns a reference to same object. * @throws RuntimeException if any aspect of processing fails */ private Properties dereferenceProperties(Properties srcProperties) throws RuntimeException { if ( srcProperties == null ) { return null; } if ( srcProperties.isEmpty() ) { return srcProperties; } try { Properties parsedProperties = new Properties(); PropertyPlaceholderConfigurer resolver = new PropertyPlaceholderConfigurer(); resolver.setIgnoreUnresolvablePlaceholders(true); Method parseStringValue = resolver.getClass().getDeclaredMethod("parseStringValue", String.class, Properties.class, Set.class); parseStringValue.setAccessible(true); for ( Map.Entry<Object, Object> propEntry : srcProperties.entrySet() ) { String parsedPropValue = (String)parseStringValue.invoke(resolver, (String)propEntry.getValue(), srcProperties, new HashSet<Object>()); parsedProperties.setProperty((String)propEntry.getKey(), parsedPropValue); } return parsedProperties; } catch ( RuntimeException e ) { throw e; } catch ( Exception e ) { throw new RuntimeException("Failed to dereference properties", e); } } // Delegate properties loading. public void setProperties(Properties properties) { propertiesFactoryBean.setProperties(properties); } public void setPropertiesArray(Properties[] propertiesArray) { propertiesFactoryBean.setPropertiesArray(propertiesArray); } public void setLocation(Resource location) { propertiesFactoryBean.setLocation(location); } public void setLocations(Resource[] locations) { propertiesFactoryBean.setLocations(locations); } public void setFileEncoding(String encoding) { propertiesFactoryBean.setFileEncoding(encoding); } public void setIgnoreResourceNotFound(boolean ignoreResourceNotFound) { propertiesFactoryBean.setIgnoreResourceNotFound(ignoreResourceNotFound); } public void setLocalOverride(boolean localOverride) { propertiesFactoryBean.setLocalOverride(localOverride); } // Delegate PropertyPlaceholderConfigurer. public void setIgnoreUnresolvablePlaceholders(boolean ignoreUnresolvablePlaceholders) { propertyPlaceholderConfigurer.setIgnoreUnresolvablePlaceholders(ignoreUnresolvablePlaceholders); } public void setOrder(int order) { propertyPlaceholderConfigurer.setOrder(order); } public void setPlaceholderPrefix(String placeholderPrefix) { propertyPlaceholderConfigurer.setPlaceholderPrefix(placeholderPrefix); } public void setPlaceholderSuffix(String placeholderSuffix) { propertyPlaceholderConfigurer.setPlaceholderSuffix(placeholderSuffix); } public void setSearchSystemEnvironment(boolean searchSystemEnvironment) { propertyPlaceholderConfigurer.setSearchSystemEnvironment(searchSystemEnvironment); } public void setSystemPropertiesMode(int systemPropertiesMode) { propertyPlaceholderConfigurer.setSystemPropertiesMode(systemPropertiesMode); } public void setSystemPropertiesModeName(String constantName) throws IllegalArgumentException { propertyPlaceholderConfigurer.setSystemPropertiesModeName(constantName); } // Delegate PropertyOverrideConfigurer. public void setBeanNameAtEnd(boolean beanNameAtEnd) { propertyOverrideConfigurer.setBeanNameAtEnd(beanNameAtEnd); } public void setBeanNameSeparator(String beanNameSeparator) { propertyOverrideConfigurer.setBeanNameSeparator(beanNameSeparator); } public void setIgnoreInvalidKeys(boolean ignoreInvalidKeys) { propertyOverrideConfigurer.setIgnoreInvalidKeys(ignoreInvalidKeys); } /** * Blatantly stolen from the Spring classes in order to get access to the properties files as they are read in, * this could not be done by overrides because the stupid finals and private vars, this is why frameworks should * never use final and private in their code.... sigh * * @author Spring Framework * @author Aaron Zeckoski (azeckoski @ vt.edu) */ public class SakaiPropertiesFactoryBean implements FactoryBean, InitializingBean { public static final String XML_FILE_EXTENSION = ".xml"; final Log log = LogFactory.getLog(SakaiPropertiesFactoryBean.class); private Map<String, Properties> loadedProperties = new LinkedHashMap<String, Properties>(); /** * @return a map of file -> properties for everything loaded here */ public Map<String, Properties> getLoadedProperties() { return loadedProperties; } private Properties[] localProperties; private Resource[] locations; private boolean localOverride = false; private boolean ignoreResourceNotFound = false; private String fileEncoding; private PropertiesPersister propertiesPersister = new DefaultPropertiesPersister(); private boolean singleton = true; private Object singletonInstance; public final void setSingleton(boolean singleton) { // ignore this } public final boolean isSingleton() { return this.singleton; } public final void afterPropertiesSet() throws IOException { if (this.singleton) { this.singletonInstance = createInstance(); } } public final Object getObject() throws IOException { if (this.singleton) { return this.singletonInstance; } else { return createInstance(); } } @SuppressWarnings("rawtypes") public Class getObjectType() { return Properties.class; } protected Object createInstance() throws IOException { return mergeProperties(); } public void setProperties(Properties properties) { this.localProperties = new Properties[] {properties}; } public void setPropertiesArray(Properties[] propertiesArray) { // unused this.localProperties = propertiesArray; } public void setLocation(Resource location) { // unused this.locations = new Resource[] {location}; } public void setLocations(Resource[] locations) { this.locations = locations; } public void setLocalOverride(boolean localOverride) { this.localOverride = localOverride; } public void setIgnoreResourceNotFound(boolean ignoreResourceNotFound) { this.ignoreResourceNotFound = ignoreResourceNotFound; } public void setFileEncoding(String encoding) { this.fileEncoding = encoding; } public void setPropertiesPersister(PropertiesPersister propertiesPersister) { this.propertiesPersister = (propertiesPersister != null ? propertiesPersister : new DefaultPropertiesPersister()); } /** * Return a merged Properties instance containing both the loaded properties * and properties set on this FactoryBean. */ protected Properties mergeProperties() throws IOException { Properties result = new Properties(); if (this.localOverride) { // Load properties from file upfront, to let local properties override. loadProperties(result); } if (this.localProperties != null) { for (int i = 0; i < this.localProperties.length; i++) { loadedProperties.put("local"+i, this.localProperties[i]); CollectionUtils.mergePropertiesIntoMap(this.localProperties[i], result); } } if (!this.localOverride) { // Load properties from file afterwards, to let those properties override. loadProperties(result); } if (log.isInfoEnabled()) log.info("Loaded a total of "+result.size()+" properties"); return result; } /** * Load properties into the given instance. * * @param props the Properties instance to load into * @throws java.io.IOException in case of I/O errors * @see #setLocations */ protected void loadProperties(Properties props) throws IOException { if (this.locations != null) { for (int i = 0; i < this.locations.length; i++) { Resource location = this.locations[i]; if (log.isDebugEnabled()) { log.debug("Loading properties file from " + location); } InputStream is = null; try { Properties p = new Properties(); is = location.getInputStream(); if (location.getFilename().endsWith(XML_FILE_EXTENSION)) { this.propertiesPersister.loadFromXml(p, is); } else { if (this.fileEncoding != null) { this.propertiesPersister.load(p, new InputStreamReader(is, this.fileEncoding)); } else { this.propertiesPersister.load(p, is); } } if (log.isInfoEnabled()) { log.info("Loaded "+p.size()+" properties from file " + location); } loadedProperties.put(location.getFilename(), p); props.putAll(p); // merge the properties } catch (IOException ex) { if (this.ignoreResourceNotFound) { if (log.isWarnEnabled()) { log.warn("Could not load properties from " + location + ": " + ex.getMessage()); } } else { throw ex; } } finally { if (is != null) { is.close(); } } } } } } }