/* * #%L * BroadleafCommerce Common Libraries * %% * Copyright (C) 2009 - 2013 Broadleaf Commerce * %% * 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. * #L% */ package org.broadleafcommerce.common.config; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.broadleafcommerce.common.logging.SupportLogManager; import org.broadleafcommerce.common.logging.SupportLogger; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.PropertyPlaceholderConfigurer; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.StringValueResolver; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedHashSet; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; /** * A property resource configurer that chooses the property file at runtime * based on the runtime environment. * <p> * Used for choosing properties files based on the current runtime environment, * allowing for movement of the same application between multiple runtime * environments without rebuilding. * <p> * The property replacement semantics of this implementation are identical to * PropertyPlaceholderConfigurer, from which this class inherits. <code> * <pre> * <bean id="propertyConfigurator" class="frilista.framework.RuntimeEnvironmentPropertiesConfigurer"> * <property name="propertyLocation" value="/WEB-INF/runtime-properties/" /> * <property name="environments"> * <set> * <value>production</value> * <value>staging</value> * <value>integration</value> * <value>development</value> * </set> * </property> * <property name="defaultEnvironment" value="development"/> * </bean> * </code> </pre> The keys of the environment specific properties files are * compared to ensure that each property file defines the complete set of keys, * in order to avoid environment-specific failures. * <p> * An optional RuntimeEnvironmentKeyResolver implementation can be provided, * allowing for customization of how the runtime environment is determined. If * no implementation is provided, a default of * SystemPropertyRuntimeEnvironmentKeyResolver is used (which uses the system * property 'runtime.environment') * @author <a href="mailto:chris.lee.9@gmail.com">Chris Lee</a> */ public class RuntimeEnvironmentPropertiesConfigurer extends PropertyPlaceholderConfigurer implements InitializingBean { private static final Log LOG = LogFactory.getLog(RuntimeEnvironmentPropertiesConfigurer.class); protected SupportLogger logger = SupportLogManager.getLogger("UserOverride", this.getClass()); protected static final String SHARED_PROPERTY_OVERRIDE = "property-shared-override"; protected static final String PROPERTY_OVERRIDE = "property-override"; protected static Set<String> defaultEnvironments = new LinkedHashSet<String>(); protected static Set<Resource> blcPropertyLocations = new LinkedHashSet<Resource>(); protected static Set<Resource> defaultPropertyLocations = new LinkedHashSet<Resource>(); static { defaultEnvironments.add("production"); defaultEnvironments.add("staging"); defaultEnvironments.add("integrationqa"); defaultEnvironments.add("integrationdev"); defaultEnvironments.add("development"); blcPropertyLocations.add(new ClassPathResource("config/bc/")); blcPropertyLocations.add(new ClassPathResource("config/bc/admin/")); blcPropertyLocations.add(new ClassPathResource("config/bc/cms/")); blcPropertyLocations.add(new ClassPathResource("config/bc/web/")); blcPropertyLocations.add(new ClassPathResource("config/bc/fw/")); defaultPropertyLocations.add(new ClassPathResource("runtime-properties/")); } protected String defaultEnvironment = "development"; protected String determinedEnvironment = null; protected RuntimeEnvironmentKeyResolver keyResolver; protected Set<String> environments = Collections.emptySet(); protected Set<Resource> propertyLocations; protected Set<Resource> overridableProperyLocations; protected StringValueResolver stringValueResolver; public RuntimeEnvironmentPropertiesConfigurer() { super(); setIgnoreUnresolvablePlaceholders(true); // This default will get overriden by user options if present setNullValue("@null"); } public void afterPropertiesSet() throws IOException { // If no environment override has been specified, used the default environments if (environments == null || environments.size() == 0) { environments = defaultEnvironments; } // Prepend the default property locations to the specified property locations (if any) Set<Resource> combinedLocations = new LinkedHashSet<Resource>(); if (!CollectionUtils.isEmpty(overridableProperyLocations)) { combinedLocations.addAll(overridableProperyLocations); } if (!CollectionUtils.isEmpty(propertyLocations)) { combinedLocations.addAll(propertyLocations); } propertyLocations = combinedLocations; if (!environments.contains(defaultEnvironment)) { throw new AssertionError("Default environment '" + defaultEnvironment + "' not listed in environment list"); } if (keyResolver == null) { keyResolver = new SystemPropertyRuntimeEnvironmentKeyResolver(); } String environment = determineEnvironment(); ArrayList<Resource> allLocations = new ArrayList<Resource>(); /* Process configuration in the following order (later files override earlier files * common-shared.properties * [environment]-shared.properties * common.properties * [environment].properties * -Dproperty-override-shared specified value, if any * -Dproperty-override specified value, if any */ Set<Set<Resource>> testLocations = new LinkedHashSet<Set<Resource>>(); testLocations.add(propertyLocations); testLocations.add(defaultPropertyLocations); for (Resource resource : createBroadleafResource()) { if (resource.exists()) { allLocations.add(resource); } } for (Set<Resource> locations : testLocations) { for (Resource resource : createSharedCommonResource(locations)) { if (resource.exists()) { allLocations.add(resource); } } for (Resource resource : createSharedPropertiesResource(environment, locations)) { if (resource.exists()) { allLocations.add(resource); } } for (Resource resource : createCommonResource(locations)) { if (resource.exists()) { allLocations.add(resource); } } for (Resource resource : createPropertiesResource(environment, locations)) { if (resource.exists()) { allLocations.add(resource); } } } Resource sharedPropertyOverride = createSharedOverrideResource(); if (sharedPropertyOverride != null) { allLocations.add(sharedPropertyOverride); } Resource propertyOverride = createOverrideResource(); if (propertyOverride != null) { allLocations.add(propertyOverride); } Properties props = new Properties(); for (Resource resource : allLocations) { if (resource.exists()) { // We will log source-control managed properties with trace and overrides with info if (((resource.equals(sharedPropertyOverride) || resource.equals(propertyOverride))) || LOG.isTraceEnabled()) { props = new Properties(props); props.load(resource.getInputStream()); for (Entry<Object, Object> entry : props.entrySet()) { if (resource.equals(sharedPropertyOverride) || resource.equals(propertyOverride)) { logger.support("Read " + entry.getKey() + " from " + resource.getFilename()); } else { LOG.trace("Read " + entry.getKey() + " from " + resource.getFilename()); } } } } else { LOG.debug("Unable to locate resource: " + resource.getFilename()); } } setLocations(allLocations.toArray(new Resource[] {})); } protected Resource[] createSharedPropertiesResource(String environment, Set<Resource> locations) throws IOException { String fileName = environment.toString().toLowerCase() + "-shared.properties"; Resource[] resources = new Resource[locations.size()]; int index = 0; for (Resource resource : locations) { resources[index] = resource.createRelative(fileName); index++; } return resources; } protected Resource[] createBroadleafResource() throws IOException { Resource[] resources = new Resource[blcPropertyLocations.size()]; int index = 0; for (Resource resource : blcPropertyLocations) { resources[index] = resource.createRelative("common.properties"); index++; } return resources; } protected Resource[] createSharedCommonResource(Set<Resource> locations) throws IOException { Resource[] resources = new Resource[locations.size()]; int index = 0; for (Resource resource : locations) { resources[index] = resource.createRelative("common-shared.properties"); index++; } return resources; } protected Resource[] createPropertiesResource(String environment, Set<Resource> locations) throws IOException { String fileName = environment.toString().toLowerCase() + ".properties"; Resource[] resources = new Resource[locations.size()]; int index = 0; for (Resource resource : locations) { resources[index] = resource.createRelative(fileName); index++; } return resources; } protected Resource[] createCommonResource(Set<Resource> locations) throws IOException { Resource[] resources = new Resource[locations.size()]; int index = 0; for (Resource resource : locations) { resources[index] = resource.createRelative("common.properties"); index++; } return resources; } protected Resource createSharedOverrideResource() throws IOException { String path = System.getProperty(SHARED_PROPERTY_OVERRIDE); return StringUtils.isBlank(path) ? null : new FileSystemResource(path); } protected Resource createOverrideResource() throws IOException { String path = System.getProperty(PROPERTY_OVERRIDE); return StringUtils.isBlank(path) ? null : new FileSystemResource(path); } public String determineEnvironment() { if (determinedEnvironment != null) { return determinedEnvironment; } determinedEnvironment = keyResolver.resolveRuntimeEnvironmentKey(); if (determinedEnvironment == null) { LOG.warn("Unable to determine runtime environment, using default environment '" + defaultEnvironment + "'"); determinedEnvironment = defaultEnvironment; } return determinedEnvironment.toLowerCase(); } @Override protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess, Properties props) throws BeansException { super.processProperties(beanFactoryToProcess, props); stringValueResolver = new PlaceholderResolvingStringValueResolver(props); } /** * Sets the default environment name, used when the runtime environment * cannot be determined. */ public void setDefaultEnvironment(String defaultEnvironment) { this.defaultEnvironment = defaultEnvironment; } public String getDefaultEnvironment() { return defaultEnvironment; } public void setKeyResolver(RuntimeEnvironmentKeyResolver keyResolver) { this.keyResolver = keyResolver; } /** * Sets the allowed list of runtime environments */ public void setEnvironments(Set<String> environments) { this.environments = environments; } /** * Sets the directory from which to read environment-specific properties * files; note that it must end with a '/' */ public void setPropertyLocations(Set<Resource> propertyLocations) { this.propertyLocations = propertyLocations; } /** * Sets the directory from which to read environment-specific properties * files; note that it must end with a '/'. Note, these properties may be * overridden by those defined in propertyLocations and any "runtime-properties" directories * * @param overridableProperyLocations location containing overridable environment properties */ public void setOverridableProperyLocations(Set<Resource> overridableProperyLocations) { this.overridableProperyLocations = overridableProperyLocations; } private class PlaceholderResolvingStringValueResolver implements StringValueResolver { private final PropertyPlaceholderHelper helper; private final PropertyPlaceholderHelper.PlaceholderResolver resolver; public PlaceholderResolvingStringValueResolver(Properties props) { this.helper = new PropertyPlaceholderHelper("${", "}", ":", true); this.resolver = new PropertyPlaceholderConfigurerResolver(props); } public String resolveStringValue(String strVal) throws BeansException { String value = this.helper.replacePlaceholders(strVal, this.resolver); return (value.equals("") ? null : value); } } private class PropertyPlaceholderConfigurerResolver implements PropertyPlaceholderHelper.PlaceholderResolver { private final Properties props; private PropertyPlaceholderConfigurerResolver(Properties props) { this.props = props; } public String resolvePlaceholder(String placeholderName) { return RuntimeEnvironmentPropertiesConfigurer.this.resolvePlaceholder(placeholderName, props, 1); } } public StringValueResolver getStringValueResolver() { return stringValueResolver; } }