/* * Copyright 2014 the original author or authors. * * 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. */ package org.springframework.xd.module.options; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.config.ConfigFileApplicationListener; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import org.springframework.validation.BindException; import org.springframework.xd.module.ModuleDefinition; /** * A decorator around another {@link ModuleOptionsMetadataResolver} that will provide default values for module options * using the environment. * * <p> * Each module gets its own Environment, populated with values in the following order: * <ul> * <li>System properties and environment variables</li> * <li>Values in a properties file found at {@code $XD_MODULE_CONFIG_LOCATION/<type>/<module>/<module>.properties}. * Mappings in this file shall not use the fully qualified form, but rather the simple form * {@code <optionname>=<optionvalue>}</li> * <li>Values in a yml file found at {@code $XD_MODULE_CONFIG_LOCATION/$XD_MODULE_CONFIG_NAME}. Mappings in this file * use the fully qualified form (see below)</li> * </ul> * <p> * For each option {@code <optionname>} of a module (of type {@code <type>} and name {@code <modulename>}), this * resolver will try to read a default from {@code <type>.<modulename>.<optionname>}. * * @author Eric Bottard * @author Ilayaperumal Gopinathan */ public class EnvironmentAwareModuleOptionsMetadataResolver implements ModuleOptionsMetadataResolver, ResourceLoaderAware, EnvironmentAware { /** * Name of the configuration key that holds the location root for module configuration. */ private static final String XD_MODULE_CONFIG_LOCATION = "xd.module.config.location"; /** * Name of the configuration key that holds the base file name for global module configuration. */ private static final String XD_MODULE_CONFIG_NAME = "xd.module.config.name"; /** * The default value for key {@link #XD_MODULE_CONFIG_NAME}. */ private static final String DEFAULT_XD_MODULE_CONFIG_NAME = "modules"; /** * The name of the property source that Spring Boot will create. */ private static final String APPLICATION_CONFIGURATION_PROPERTIES = "applicationConfigurationProperties"; private ModuleOptionsMetadataResolver delegate; private String xdModuleConfigLocation; @Value("${" + XD_MODULE_CONFIG_LOCATION + ":${xd.config.home}/modules/}") public void setXdModuleConfigLocation(String xdModuleConfigLocation) { // TODO: Need to fix by removing this specific requirement as this poses explicit requirement even in windows Assert.isTrue(xdModuleConfigLocation.endsWith("/"), String.format("'%s' must end with a '/'", XD_MODULE_CONFIG_LOCATION)); this.xdModuleConfigLocation = xdModuleConfigLocation; } /** * An environment that reflects values in the {@code modules.yml} file. */ private ConfigurableEnvironment rootEnvironment; private String configName = DEFAULT_XD_MODULE_CONFIG_NAME; private ResourceLoader resourceLoader; /** * The parent environment this bean lives in. Used to know which profiles are active at the server level. */ private ConfigurableEnvironment parentEnvironment; @Value("${" + XD_MODULE_CONFIG_NAME + ":" + DEFAULT_XD_MODULE_CONFIG_NAME + "}") public void setConfigName(String configName) { boolean valid = StringUtils.hasText(configName) && !configName.endsWith(".properties") && !configName.endsWith(".yml"); Assert.isTrue(valid, String.format("'%s' should not be blank, nor end up with a file extension", XD_MODULE_CONFIG_NAME)); this.configName = configName; } public void setDelegate(ModuleOptionsMetadataResolver delegate) { this.delegate = delegate; } @Override public ModuleOptionsMetadata resolve(ModuleDefinition moduleDefinition) { ModuleOptionsMetadata wrapped = delegate.resolve(moduleDefinition); if (wrapped == null) { return null; } return new ModuleOptionsMetadataWithDefaults(wrapped, moduleDefinition); } private class ModuleOptionsMetadataWithDefaults implements ModuleOptionsMetadata { private final ModuleOptionsMetadata wrapped; private final ModuleDefinition moduleDefinition; public ModuleOptionsMetadataWithDefaults(ModuleOptionsMetadata wrapped, ModuleDefinition moduleDefinition) { this.wrapped = wrapped; this.moduleDefinition = moduleDefinition; Environment moduleEnvironment = lookupEnvironment(moduleDefinition); for (ModuleOption original : wrapped) { Object newDefault = computeDefault(original, moduleEnvironment); if (newDefault != null) { // This changes the value by side effect original.withDefaultValue(newDefault); } } } @Override public Iterator<ModuleOption> iterator() { return wrapped.iterator(); } @Override public ModuleOptions interpolate(Map<String, String> raw) throws BindException { Map<String, String> rawPlusDefaults = new HashMap<String, String>(raw); for (ModuleOption option : wrapped) { if (raw.containsKey(option.getName()) || option.getDefaultValue() == null) { continue; } rawPlusDefaults.put(option.getName(), "" + option.getDefaultValue()); } return wrapped.interpolate(rawPlusDefaults); } private Object computeDefault(ModuleOption option, Environment moduleEnvironment) { String fqKey = fullyQualifiedKey(moduleDefinition, option.getName()); return moduleEnvironment.getProperty(fqKey); } } private Environment lookupEnvironment(ModuleDefinition moduleDefinition) { // load rootEnvironment at runtime than during the startup rootEnvironment = loadPropertySources(xdModuleConfigLocation, configName); String propertySourceName = String.format("%s:%s", moduleDefinition.getType(), moduleDefinition.getName()); // Load short name values into a throwaway env String path = String.format("%s%s/%s/", xdModuleConfigLocation, moduleDefinition.getType(), moduleDefinition.getName()); ConfigurableEnvironment throwAwayEnvironment = loadPropertySources(path, moduleDefinition.getName()); EnumerablePropertySource<?> nakedPS = (EnumerablePropertySource<?>) throwAwayEnvironment.getPropertySources().get( APPLICATION_CONFIGURATION_PROPERTIES); // Now transform them to their fully qualified form Map<String, Object> values = new HashMap<String, Object>(); for (String name : nakedPS.getPropertyNames()) { values.put(fullyQualifiedKey(moduleDefinition, name), nakedPS.getProperty(name)); } EnumerablePropertySource<?> modulePS = new MapPropertySource(propertySourceName, values); ConfigurableEnvironment moduleEnvironment = new StandardEnvironment(); // Append the rootEnvironment moduleEnvironment.merge(rootEnvironment); // The global environment has been loaded by boot too and // its PS of interest was also named "applicationConfigurationProperties" moduleEnvironment.getPropertySources().addBefore(APPLICATION_CONFIGURATION_PROPERTIES, modulePS); return moduleEnvironment; } /** * Craft the "fully qualified" key for a given module option name. */ private String fullyQualifiedKey(ModuleDefinition moduleDefinition, String optionName) { return String.format("%s.%s.%s", moduleDefinition.getType(), moduleDefinition.getName(), optionName); } /** * Construct a new environment and use Spring Boot to populate its property sources using * {@link ConfigFileApplicationListener}. */ private ConfigurableEnvironment loadPropertySources(final String searchLocation, final String baseName) { final ConfigurableEnvironment environment = new StandardEnvironment(); environment.merge(parentEnvironment); new ConfigFileApplicationListener() { public void apply() { setSearchLocations(searchLocation); // We'd like to do 'setSearchNames(baseName)', but the environment property // has strong precedence and is already set for XD_CONFIG_NAME. Map<String, Object> singletonMap = Collections.singletonMap("spring.config.name", (Object) baseName); environment.getPropertySources().addFirst( new MapPropertySource("searchNamesOverride", singletonMap)); addPropertySources(environment, resourceLoader); } }.apply(); return environment; } @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void setEnvironment(Environment environment) { this.parentEnvironment = (ConfigurableEnvironment) environment; } }