Commit f0bfecd3 authored by Phillip Webb's avatar Phillip Webb

Refactor PropertySource support

Locate PropertySourcesLoaders using SpringFactoriesLoader and refactor
the interface to expose file extensions and support 'profiles' within
documents.

Rework ConfigFileApplicationListener for consistent profile loading.
Profiles are now loaded in a consistent order for both profile specific
files, and contained profile documents (i.e. YAML sub-sections).

Also update ConfigFileApplicationListener so that it no longer directly
processes @ProperySource annotations. Instead the standard Spring
ConfigurationClassPostProcessor will insert @PropertySource items with
ConfigFileApplicationListener later re-ordering them.

The SpringApplication can no longer be configured using @ProperySource
annotations, however, application.properties may still be used.

Fixes gh-322
parent 06494e06
......@@ -98,7 +98,7 @@ public class VcapApplicationListener implements
private static final String VCAP_SERVICES = "VCAP_SERVICES";
// Before ConfigFileApplicationListener so values there can use these ones
private int order = ConfigFileApplicationListener.DEFAULT_CONFIG_LISTENER_ORDER - 1;;
private int order = ConfigFileApplicationListener.DEFAULT_ORDER - 1;;
private final JsonParser parser = JsonParserFactory.getJsonParser();
......
......@@ -16,40 +16,41 @@
package org.springframework.boot.config;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Queue;
import java.util.Set;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.bind.PropertySourcesPropertyValues;
import org.springframework.boot.bind.RelaxedDataBinder;
import org.springframework.boot.env.PropertySourcesLoader;
import org.springframework.boot.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.event.ApplicationPreparedEvent;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
import org.springframework.context.annotation.PropertySources;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.DigestUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
......@@ -63,254 +64,99 @@ import org.springframework.util.StringUtils;
* <li>file:./config/:</li>
* </ul>
* <p>
* Alternative locations and names can be specified using
* {@link #setSearchLocations(String[])} and {@link #setNames(String)}.
* Alternative search locations and names can be specified using
* {@link #setSearchLocations(String)} and {@link #setSearchNames(String)}.
* <p>
* Additional files will also be loaded based on active profiles. For example if a 'web'
* profile is active 'application-web.properties' and 'application-web.yml' will be
* considered.
* <p>
* The 'spring.config.name' property can be used to specify an alternative name to load or
* alternatively the 'spring.config.location' property can be used to specify an exact
* resource location.
* The 'spring.config.name' property can be used to specify an alternative name to load
* and the 'spring.config.location' property can be used to specify alternative search
* locations or specific files.
* <p>
* Configuration properties are also bound to the {@link SpringApplication}. This makes it
* possible to set {@link SpringApplication} properties dynamically, like the sources
* ("spring.main.sources" - a CSV list) the flag to indicate a web environment
* ("spring.main.web_environment=true") or the flag to switch off the banner
* ("spring.main.show_banner=false").
*
* @author Dave Syer
* @author Phillip Webb
*/
public class ConfigFileApplicationListener implements
ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
ApplicationListener<ApplicationEvent>, Ordered {
private static final String DEFAULT_PROPERTIES = "defaultProperties";
private static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
private static final String LOCATION_VARIABLE = "${spring.config.location}";
private static final String CONFIG_NAME_PROPERTY = "spring.config.name";
public static final int DEFAULT_CONFIG_LISTENER_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
private static final String CONFIG_LOCATION_PROPERTY = "spring.config.location";
private String[] searchLocations = new String[] { "classpath:/", "file:./",
"classpath:/config/", "file:./config/" };
private static final String DEFAULT_SEARCH_LOCATIONS = "classpath:/,file:./,"
+ "classpath:/config/,file:./config/";
private String names = "${spring.config.name:application}";
private static final String DEFAULT_NAMES = "application";
private int order = DEFAULT_CONFIG_LISTENER_ORDER;
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
private final ConversionService conversionService = new DefaultConversionService();
private String searchLocations;
private final Map<String, PropertySource<?>> cache = new HashMap<String, PropertySource<?>>();
private String names;
private final PropertySourceAnnotations annotations = new PropertySourceAnnotations();
private int order = DEFAULT_ORDER;
private PropertySourceLoadersFactory propertySourceLoadersFactory = new DefaultPropertySourceLoadersFactory();
private final ConversionService conversionService = new DefaultConversionService();
/**
* Binds the early {@link Environment} to the {@link SpringApplication}. This makes it
* possible to set {@link SpringApplication} properties dynamically, like the sources
* ("spring.main.sources" - a CSV list) the flag to indicate a web environment
* ("spring.main.web_environment=true") or the flag to switch off the banner
* ("spring.main.show_banner=false").
*/
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
Environment environment = event.getEnvironment();
if (environment instanceof ConfigurableEnvironment) {
configure((ConfigurableEnvironment) environment, event.getSpringApplication());
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
}
private void configure(ConfigurableEnvironment environment,
SpringApplication springApplication) {
for (Object source : springApplication.getSources()) {
this.annotations.addFromSource(source);
}
load(environment, new DefaultResourceLoader());
environment.getPropertySources().addAfter(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
new RandomValuePropertySource("random"));
int sourcesSizeBefore = springApplication.getSources().size();
// Set bean properties from the early environment
PropertyValues propertyValues = new PropertySourcesPropertyValues(
environment.getPropertySources());
RelaxedDataBinder binder = new RelaxedDataBinder(springApplication, "spring.main");
binder.setConversionService(this.conversionService);
binder.bind(propertyValues);
if (springApplication.getSources().size() > sourcesSizeBefore) {
// Configure again in case there are new @PropertySources
configure(environment, springApplication);
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
}
private void load(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
LoadCandidates candidates = new LoadCandidates(environment, resourceLoader);
PropertySource<?> defaultProperties = environment.getPropertySources().remove(
"defaultProperties");
// Load to allow a file that defines active profiles to be considered
String firstPropertySourceName = loadInitial(environment, resourceLoader,
candidates);
// Apply the active profiles (if any) from the first property source
if (environment.containsProperty(ACTIVE_PROFILES_PROPERTY)) {
activeProfilesFromProperty(environment,
environment.getProperty(ACTIVE_PROFILES_PROPERTY), true);
}
// Apply any profile additions from any source
activeProfileAdditionsFromAnySource(environment);
// Repeatedly load property sources in case additional profiles are activated
int numberOfPropertySources;
do {
numberOfPropertySources = environment.getPropertySources().size();
activeProfileAdditionsFromAnySource(environment);
loadAgain(environment, resourceLoader, candidates, firstPropertySourceName);
}
while (environment.getPropertySources().size() > numberOfPropertySources);
if (defaultProperties != null) {
environment.getPropertySources().addLast(defaultProperties);
}
}
private void activeProfileAdditionsFromAnySource(ConfigurableEnvironment environment) {
for (PropertySource<?> propertySource : environment.getPropertySources()) {
if (propertySource.containsProperty(ACTIVE_PROFILES_PROPERTY)) {
activeProfilesFromProperty(environment,
propertySource.getProperty(ACTIVE_PROFILES_PROPERTY), false);
}
}
}
private void activeProfilesFromProperty(ConfigurableEnvironment environment,
Object property, boolean addAll) {
for (String profile : StringUtils.commaDelimitedListToSet(property.toString())) {
boolean addition = profile.startsWith("+");
profile = (addition ? profile.substring(1) : profile);
if (addAll || addition) {
environment.addActiveProfile(profile);
}
}
}
};
private String loadInitial(ConfigurableEnvironment environment,
ResourceLoader resourceLoader, LoadCandidates candidates) {
String firstSourceName = null;
// Initial load allows profiles to be activated
for (String candidate : candidates) {
for (String path : StringUtils.commaDelimitedListToStringArray(environment
.resolvePlaceholders(candidate))) {
if (LOCATION_VARIABLE.equals(candidate) && !path.contains("$")) {
if (!path.contains(":")) {
path = "file:" + path;
}
path = StringUtils.cleanPath(path);
}
PropertySource<?> source = loadPropertySource(environment,
resourceLoader, path, null);
if (source != null) {
if (firstSourceName == null) {
firstSourceName = source.getName();
}
environment.getPropertySources().addLast(source);
}
}
}
return firstSourceName;
}
private void loadAgain(ConfigurableEnvironment environment,
ResourceLoader resourceLoader, LoadCandidates candidates,
String firstPropertySourceName) {
for (String profile : environment.getActiveProfiles()) {
for (String candidate : candidates) {
PropertySource<?> source = loadPropertySource(environment,
resourceLoader, candidate, profile);
addBeforeOrLast(environment, firstPropertySourceName, source);
}
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
Environment environment = event.getEnvironment();
if (environment instanceof ConfigurableEnvironment) {
onApplicationEnvironmentPreparedEvent((ConfigurableEnvironment) environment,
event.getSpringApplication());
}
}
private void addBeforeOrLast(ConfigurableEnvironment environment,
String relativePropertySourceName, PropertySource<?> source) {
if (source != null) {
MutablePropertySources propertySources = environment.getPropertySources();
// Originals go at the end so they don't override the specific profiles
if (relativePropertySourceName != null) {
propertySources.addBefore(relativePropertySourceName, source);
private void onApplicationEnvironmentPreparedEvent(
ConfigurableEnvironment environment, SpringApplication application) {
RandomValuePropertySource.addToEnvironment(environment);
try {
PropertySource<?> defaultProperties = environment.getPropertySources()
.remove(DEFAULT_PROPERTIES);
new Loader(environment).load();
if (defaultProperties != null) {
environment.getPropertySources().addLast(defaultProperties);
}
else {
propertySources.addLast(source);
}
}
}
private PropertySource<?> loadPropertySource(ConfigurableEnvironment environment,
ResourceLoader resourceLoader, String location, String profile) {
Class<?> type = this.annotations.configuration(location);
String suffix = "." + StringUtils.getFilenameExtension(location);
if (StringUtils.hasLength(profile)) {
location = location.replace(suffix, "-" + profile + suffix);
}
if (isPropertySourceAnnotationOnExcludedType(environment, profile, type, location)) {
return null;
catch (IOException ex) {
throw new IllegalStateException("Unable to load configuration files", ex);
}
Resource resource = resourceLoader.getResource(location);
String name = this.annotations.name(location);
name = (name != null ? name : location);
return getPropertySource(environment, name, resource, profile);
bindToSpringApplication(application, environment);
}
private boolean isPropertySourceAnnotationOnExcludedType(Environment environment,
String profile, Class<?> type, String location) {
if (type == null) {
// No configuration class to worry about, just a vanilla properties location
return false;
}
if (StringUtils.hasText(profile)
&& !this.annotations.getLocations().contains(location)) {
// We are looking for profile specific properties and this one isn't
// explicitly asked for in propertySourceAnnotations
return true;
}
AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(
new DefaultListableBeanFactory(), environment);
int before = reader.getRegistry().getBeanDefinitionCount();
reader.register(type);
int after = reader.getRegistry().getBeanDefinitionCount();
// Return if the configuration class was @Conditional and excluded
return (after == before);
private void bindToSpringApplication(SpringApplication application,
ConfigurableEnvironment environment) {
RelaxedDataBinder binder = new RelaxedDataBinder(application, "spring.main");
binder.setConversionService(this.conversionService);
binder.bind(new PropertySourcesPropertyValues(environment.getPropertySources()));
}
private PropertySource<?> getPropertySource(Environment environment, String name,
Resource resource, String profile) {
if (resource == null || !resource.exists()) {
return null;
}
String key = resource.getDescription() + (profile == null ? "" : "#" + profile);
if (this.cache.containsKey(key)) {
return this.cache.get(key);
}
for (PropertySourceLoader loader : this.propertySourceLoadersFactory
.getLoaders(environment)) {
if (loader.supports(resource)) {
PropertySource<?> propertySource = loader.load(name, resource);
this.cache.put(key, propertySource);
return propertySource;
}
}
throw new IllegalStateException("No supported loader found for "
+ "configuration resource: " + resource);
private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
ConfigurableApplicationContext context = event.getApplicationContext();
context.addBeanFactoryPostProcessor(new PropertySourceOrderingPostProcessor(
context));
}
public void setOrder(int order) {
......@@ -323,181 +169,233 @@ public class ConfigFileApplicationListener implements
}
/**
* Sets the names of the files that should be loaded (excluding file extension) as a
* comma separated list. Defaults to "application".
* Set the search locations that will be considered as a comma-separated list.
*/
public void setNames(String names) {
this.names = names;
public void setSearchLocations(String locations) {
Assert.hasLength(locations, "Locations must not be empty");
this.searchLocations = locations;
}
/**
* Set the search locations that will be considered.
* Sets the names of the files that should be loaded (excluding file extension) as a
* comma-separated list.
*/
public void setSearchLocations(String[] searchLocations) {
this.searchLocations = (searchLocations == null ? null : searchLocations.clone());
public void setSearchNames(String names) {
Assert.hasLength(names, "Names must not be empty");
this.names = names;
}
/**
* Set the {@link PropertySourceLoadersFactory} that will be used to create
* {@link PropertySourceLoader}s.
* {@link BeanFactoryPostProcessor} to re-order our property sources below any
* {@code @ProperySource} items added by the {@link ConfigurationClassPostProcessor}.
*/
public void setPropertySourceLoadersFactory(
PropertySourceLoadersFactory propertySourceLoaderFactory) {
this.propertySourceLoadersFactory = propertySourceLoaderFactory;
}
private class PropertySourceOrderingPostProcessor implements
BeanFactoryPostProcessor, Ordered {
/**
* Provides {@link Iterable} access to candidate property sources.
*/
private class LoadCandidates implements Iterable<String> {
private final List<String> candidates;
public LoadCandidates(ConfigurableEnvironment environment,
ResourceLoader resourceLoader) {
Set<String> candidates = new LinkedHashSet<String>();
addLoadCandidatesFromSearchLocations(environment, candidates);
candidates.add(LOCATION_VARIABLE);
// @PropertySource annotation locations go last here (eventually highest
// priority). This unfortunately isn't the same semantics as @PropertySource
// in Spring and it's hard to change that (so the property source gets added
// again in last position by Spring later in the cycle).
addLoadCandidatesFromAnnotations(environment, resourceLoader, candidates);
this.candidates = new ArrayList<String>(candidates);
Collections.reverse(this.candidates);
}
private ConfigurableApplicationContext context;
private void addLoadCandidatesFromSearchLocations(
ConfigurableEnvironment environment, Set<String> candidates) {
String[] names = StringUtils.commaDelimitedListToStringArray(environment
.resolvePlaceholders(ConfigFileApplicationListener.this.names));
for (String location : ConfigFileApplicationListener.this.searchLocations) {
for (String extension : new String[] { ".properties", ".yml" }) {
for (int i = names.length - 1; i >= 0; i--) {
candidates.add(location + names[i] + extension);
}
}
}
public PropertySourceOrderingPostProcessor(ConfigurableApplicationContext context) {
this.context = context;
}
private void addLoadCandidatesFromAnnotations(
ConfigurableEnvironment environment, ResourceLoader resourceLoader,
Set<String> candidates) {
for (String location : ConfigFileApplicationListener.this.annotations
.getLocations()) {
Resource resource = resourceLoader.getResource(environment
.resolvePlaceholders(location));
if (!ConfigFileApplicationListener.this.annotations
.ignoreResourceNotFound(location) && !resource.exists()) {
throw new IllegalStateException("Resource not found: " + location);
}
candidates.add(location);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Iterator<String> iterator() {
return this.candidates.iterator();
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
throws BeansException {
reorderSources(this.context.getEnvironment());
}
private void reorderSources(ConfigurableEnvironment environment) {
ConfigurationPropertySources.finishAndRelocate(environment
.getPropertySources());
PropertySource<?> defaultProperties = environment.getPropertySources()
.remove(DEFAULT_PROPERTIES);
if (defaultProperties != null) {
environment.getPropertySources().addLast(defaultProperties);
}
}
}
/**
* {@link PropertySource} that returns a random value for any property that starts
* with {@literal "random."}. Return a {@code byte[]} unless the property name ends
* with {@literal ".int} or {@literal ".long"}.
* Loads candidate property sources and configures the active profiles.
*/
private static class RandomValuePropertySource extends PropertySource<Random> {
private class Loader {
public RandomValuePropertySource(String name) {
super(name, new Random());
}
private final ConfigurableEnvironment environment;
@Override
public Object getProperty(String name) {
if (!name.startsWith("random.")) {
return null;
}
if (name.endsWith("int")) {
return getSource().nextInt();
}
if (name.endsWith("long")) {
return getSource().nextLong();
}
byte[] bytes = new byte[32];
getSource().nextBytes(bytes);
return DigestUtils.md5DigestAsHex(bytes);
}
private final ResourceLoader resourceLoader = new DefaultResourceLoader();
}
private PropertySourcesLoader propertiesLoader;
/**
* Holds details collected from
* {@link org.springframework.context.annotation.PropertySource} annotations.
*/
private static class PropertySourceAnnotations {
private Queue<String> profiles;
private final Collection<String> locations = new LinkedHashSet<String>();
private boolean activatedProfiles;
private final Map<String, String> names = new HashMap<String, String>();
public Loader(ConfigurableEnvironment environment) {
this.environment = environment;
}
private final Map<String, Class<?>> configs = new HashMap<String, Class<?>>();
public void load() throws IOException {
this.propertiesLoader = new PropertySourcesLoader();
this.profiles = new LinkedList<String>();
this.profiles.add(null);
this.profiles.addAll(Arrays.asList(this.environment.getActiveProfiles()));
this.activatedProfiles = false;
addActiveProfiles(this.environment.getProperty(ACTIVE_PROFILES_PROPERTY));
while (!this.profiles.isEmpty()) {
String profile = this.profiles.poll();
for (String location : getSearchLocations()) {
for (String name : getSearchNames()) {
load(location, name, profile);
}
}
}
private final Map<String, Boolean> ignores = new HashMap<String, Boolean>();
addConfigurationProperties(this.propertiesLoader.getPropertySources());
}
private void load(String location, String name, String profile)
throws IOException {
public void addFromSource(Object source) {
if (source instanceof Class<?>) {
addFromSource((Class<?>) source);
// Try to load directly from the location
PropertySource<?> locationPropertySource = load(location, profile);
// If that fails, try a search
if (locationPropertySource == null) {
for (String ext : this.propertiesLoader.getAllFileExtensions()) {
if (profile != null) {
// Try the profile specific file (with a null profile section)
load(location + name + "-" + profile + "." + ext, null);
}
// Try the profile (if any) specific section of the normal file
load(location + name + "." + ext, profile);
}
}
}
private void addFromSource(Class<?> source) {
for (org.springframework.context.annotation.PropertySource propertySource : AnnotationUtils
.getRepeatableAnnotation(source, PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
add(source, propertySource);
private PropertySource<?> load(String resourceLocation, String profile)
throws IOException {
Resource resource = this.resourceLoader.getResource(resourceLocation);
if (resource != null && resource.exists()) {
String name = "applicationConfig: " + resource.getDescription();
if (StringUtils.hasLength(profile)) {
name += " " + profile;
}
PropertySource<?> propertySource = this.propertiesLoader.load(resource,
name, profile);
if (propertySource != null) {
addActiveProfiles(propertySource
.getProperty(ACTIVE_PROFILES_PROPERTY));
}
return propertySource;
}
return null;
}
private void add(Class<?> source,
org.springframework.context.annotation.PropertySource annotation) {
this.locations.addAll(Arrays.asList(annotation.value()));
if (StringUtils.hasText(annotation.name())) {
for (String location : annotation.value()) {
this.names.put(location, annotation.name());
private void addActiveProfiles(Object property) {
String profiles = (property == null ? null : property.toString());
boolean profilesNotActivatedWhenCalled = !this.activatedProfiles;
for (String profile : asResolvedSet(profiles, null)) {
boolean addition = profile.startsWith("+");
profile = (addition ? profile.substring(1) : profile);
if (profilesNotActivatedWhenCalled || addition) {
this.profiles.add(profile);
this.environment.addActiveProfile(profile);
this.activatedProfiles = true;
}
}
for (String location : annotation.value()) {
boolean reallyIgnore = annotation.ignoreResourceNotFound();
if (this.ignores.containsKey(location)) {
// Only if they all ignore this location will it be ignored
reallyIgnore &= this.ignores.get(location);
}
public Set<String> getSearchLocations() {
Set<String> locations = new LinkedHashSet<String>();
locations.addAll(asResolvedSet(
ConfigFileApplicationListener.this.searchLocations,
DEFAULT_SEARCH_LOCATIONS));
if (this.environment.containsProperty(CONFIG_LOCATION_PROPERTY)) {
for (String path : asResolvedSet(
this.environment.getProperty(CONFIG_LOCATION_PROPERTY), null)) {
if (!path.contains("$")) {
if (!path.contains(":")) {
path = "file:" + path;
}
path = StringUtils.cleanPath(path);
}
locations.add(path);
}
this.ignores.put(location, reallyIgnore);
this.configs.put(location, source);
}
return locations;
}
public Set<String> getSearchNames() {
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
return asResolvedSet(this.environment.getProperty(CONFIG_NAME_PROPERTY),
null);
}
return asResolvedSet(ConfigFileApplicationListener.this.names, DEFAULT_NAMES);
}
private Set<String> asResolvedSet(String value, String fallback) {
List<String> list = Arrays.asList(StringUtils
.commaDelimitedListToStringArray(value != null ? this.environment
.resolvePlaceholders(value) : fallback));
Collections.reverse(list);
return new LinkedHashSet<String>(list);
}
public Class<?> configuration(String location) {
return this.configs.get(location);
private void addConfigurationProperties(MutablePropertySources sources) {
List<PropertySource<?>> reorderedSources = new ArrayList<PropertySource<?>>();
for (PropertySource<?> item : sources) {
reorderedSources.add(item);
}
Collections.reverse(reorderedSources);
this.environment.getPropertySources().addLast(
new ConfigurationPropertySources(reorderedSources));
}
public boolean ignoreResourceNotFound(String location) {
return Boolean.TRUE.equals(this.ignores.get(location));
}
/**
* Holds the configuration {@link PropertySource}s as they are loaded can relocate
* them once configuration classes have been processed.
*/
static class ConfigurationPropertySources extends PropertySource<Object> {
private static final String NAME = "applicationConfigurationProperties";
private final Collection<PropertySource<?>> sources;
public ConfigurationPropertySources(Collection<PropertySource<?>> sources) {
super(NAME);
this.sources = sources;
}
public String name(String location) {
String name = this.names.get(location);
if (name == null || Collections.frequency(this.names.values(), name) > 1) {
return null;
@Override
public Object getProperty(String name) {
for (PropertySource<?> propertySource : this.sources) {
Object value = propertySource.getProperty(name);
if (value != null) {
return value;
}
}
// Only if there is a unique name for this location
return "boot." + name;
return null;
}
public Collection<String> getLocations() {
return this.locations;
public static void finishAndRelocate(MutablePropertySources propertySources) {
ConfigurationPropertySources removed = (ConfigurationPropertySources) propertySources
.remove(ConfigurationPropertySources.NAME);
if (removed != null) {
for (PropertySource<?> propertySource : removed.sources) {
propertySources.addLast(propertySource);
}
}
}
}
}
......@@ -16,30 +16,46 @@
package org.springframework.boot.config;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.springframework.core.env.Environment;
import org.springframework.util.ClassUtils;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.util.DigestUtils;
/**
* Default implementation of {@link PropertySourceLoadersFactory}. Provides a
* {@link PropertiesPropertySourceLoader} and when possible a
* {@link YamlPropertySourceLoader}.
* {@link PropertySource} that returns a random value for any property that starts with
* {@literal "random."}. Return a {@code byte[]} unless the property name ends with
* {@literal ".int} or {@literal ".long"}.
*
* @author Dave Syer
*/
public class DefaultPropertySourceLoadersFactory implements PropertySourceLoadersFactory {
public class RandomValuePropertySource extends PropertySource<Random> {
public RandomValuePropertySource(String name) {
super(name, new Random());
}
@Override
public List<PropertySourceLoader> getLoaders(Environment environment) {
ArrayList<PropertySourceLoader> loaders = new ArrayList<PropertySourceLoader>();
loaders.add(new PropertiesPropertySourceLoader());
if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
loaders.add(YamlPropertySourceLoader.springProfileAwareLoader(environment
.getActiveProfiles()));
public Object getProperty(String name) {
if (!name.startsWith("random.")) {
return null;
}
if (name.endsWith("int")) {
return getSource().nextInt();
}
return loaders;
if (name.endsWith("long")) {
return getSource().nextLong();
}
byte[] bytes = new byte[32];
getSource().nextBytes(bytes);
return DigestUtils.md5DigestAsHex(bytes);
}
public static void addToEnvironment(ConfigurableEnvironment environment) {
environment.getPropertySources().addAfter(
StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME,
new RandomValuePropertySource("random"));
}
}
/*
* Copyright 2012-2013 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.boot.config;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import org.springframework.boot.yaml.DefaultProfileDocumentMatcher;
import org.springframework.boot.yaml.SpringProfileDocumentMatcher;
import org.springframework.boot.yaml.YamlProcessor.DocumentMatcher;
import org.springframework.boot.yaml.YamlProcessor.MatchStatus;
import org.springframework.boot.yaml.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;
/**
* Strategy to load '.yml' files into a {@link PropertySource}.
*
* @author Dave Syer
*/
public class YamlPropertySourceLoader extends PropertiesPropertySourceLoader {
private final List<DocumentMatcher> matchers;
/**
* Create a {@link YamlPropertySourceLoader} instance with the specified matchers.
* @param matchers the document matchers
*/
public YamlPropertySourceLoader(DocumentMatcher... matchers) {
this.matchers = Arrays.asList(matchers);
}
@Override
public boolean supports(Resource resource) {
return resource.getFilename().endsWith(".yml");
}
@Override
protected Properties loadProperties(final Resource resource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
if (this.matchers != null && !this.matchers.isEmpty()) {
factory.setMatchDefault(false);
factory.setDocumentMatchers(this.matchers);
}
factory.setResources(new Resource[] { resource });
return factory.getObject();
}
/**
* A property source loader that loads all properties and matches all documents.
* @return a property source loader
*/
public static YamlPropertySourceLoader matchAllLoader() {
return new YamlPropertySourceLoader();
}
/**
* A property source loader that matches documents that have no explicit profile or
* which have an explicit "spring.profiles.active" value in the current active
* profiles.
* @param activeProfiles the active profiles to match independent of file contents
* @return a property source loader
*/
public static YamlPropertySourceLoader springProfileAwareLoader(
String[] activeProfiles) {
final SpringProfileDocumentMatcher matcher = new SpringProfileDocumentMatcher();
for (String profile : activeProfiles) {
matcher.addActiveProfiles(profile);
}
return new YamlPropertySourceLoader(matcher, new DefaultProfileDocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
MatchStatus result = super.matches(properties);
if (result == MatchStatus.FOUND) {
Set<String> profiles = StringUtils.commaDelimitedListToSet(properties
.getProperty("spring.profiles.active", ""));
for (String profile : profiles) {
// allow document with no profile to set the active one
matcher.addActiveProfiles(profile);
}
}
return result;
}
});
}
}
......@@ -16,6 +16,8 @@
package org.springframework.boot.context.properties;
import java.io.IOException;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
......@@ -26,9 +28,7 @@ import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.bind.PropertiesConfigurationFactory;
import org.springframework.boot.config.PropertiesPropertySourceLoader;
import org.springframework.boot.config.PropertySourceLoader;
import org.springframework.boot.config.YamlPropertySourceLoader;
import org.springframework.boot.env.PropertySourcesLoader;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
......@@ -332,26 +332,22 @@ public class ConfigurationPropertiesBindingPostProcessor implements BeanPostProc
return this.validator;
}
private PropertySources loadPropertySources(String[] path) {
MutablePropertySources propertySources = new MutablePropertySources();
PropertySourceLoader[] loaders = {
new PropertiesPropertySourceLoader(),
YamlPropertySourceLoader.springProfileAwareLoader(this.environment
.getActiveProfiles()) };
for (String location : path) {
location = this.environment.resolvePlaceholders(location);
Resource resource = this.resourceLoader.getResource(location);
if (resource != null && resource.exists()) {
for (PropertySourceLoader loader : loaders) {
if (loader.supports(resource)) {
PropertySource<?> propertySource = loader.load(
resource.getDescription(), resource);
propertySources.addFirst(propertySource);
}
private PropertySources loadPropertySources(String[] locations) {
try {
PropertySourcesLoader loader = new PropertySourcesLoader();
for (String location : locations) {
Resource resource = this.resourceLoader.getResource(this.environment
.resolvePlaceholders(location));
for (String profile : this.environment.getActiveProfiles()) {
loader.load(resource, null, profile);
}
loader.load(resource, null, null);
}
return loader.getPropertySources();
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
return propertySources;
}
private ConversionService getDefaultConversionService() {
......
/*
* Copyright 2012-2013 the original author or authors.
* Copyright 2012-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.
......@@ -14,13 +14,10 @@
* limitations under the License.
*/
package org.springframework.boot.config;
package org.springframework.boot.env;
import java.io.IOException;
import java.util.Properties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
......@@ -30,34 +27,22 @@ import org.springframework.core.io.support.PropertiesLoaderUtils;
* Strategy to load '.properties' files into a {@link PropertySource}.
*
* @author Dave Syer
* @author Phillip Webb
*/
public class PropertiesPropertySourceLoader implements PropertySourceLoader {
private static Log logger = LogFactory.getLog(PropertiesPropertySourceLoader.class);
@Override
public boolean supports(Resource resource) {
return resource.getFilename().endsWith(".properties");
public String[] getFileExtensions() {
return new String[] { "properties" };
}
@Override
public PropertySource<?> load(String name, Resource resource) {
try {
Properties properties = loadProperties(resource);
// N.B. this is off by default unless user has supplied logback config in
// standard location
if (logger.isDebugEnabled()) {
logger.debug("Properties loaded from " + resource + ": " + properties);
}
return new PropertiesPropertySource(name, properties);
}
catch (IOException ex) {
throw new IllegalStateException("Could not load properties from " + resource,
ex);
public PropertySource<?> load(String name, Resource resource, String profile)
throws IOException {
if (profile != null) {
return null;
}
return new PropertiesPropertySource(name,
PropertiesLoaderUtils.loadProperties(resource));
}
protected Properties loadProperties(Resource resource) throws IOException {
return PropertiesLoaderUtils.loadProperties(resource);
}
}
\ No newline at end of file
}
......@@ -14,29 +14,39 @@
* limitations under the License.
*/
package org.springframework.boot.config;
package org.springframework.boot.env;
import java.io.IOException;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
/**
* Strategy interface used to load a {@link PropertySource}.
* Strategy interface located via {@link SpringFactoriesLoader} and used to load a
* {@link PropertySource}.
*
* @author Dave Syer
* @author Phillip Webb
*/
public interface PropertySourceLoader {
/**
* Returns {@code true} if the {@link Resource} is supported.
* @return if the resource is supported
* Returns the file extensions that the loader supports (excluding the '.').
*/
boolean supports(Resource resource);
String[] getFileExtensions();
/**
* Load the resource into a property source.
* @param name the name of the property source
* @return a property source
* @param resource the resource to load
* @param profile the name of the profile to load or {@code null}. The profile can be
* used to load multi-document files (such as YAML). Simple property formats should
* {@code null} when asked to load a profile.
* @return a property source or {@code null}
* @throws IOException
*/
PropertySource<?> load(String name, Resource resource);
PropertySource<?> load(String name, Resource resource, String profile)
throws IOException;
}
/*
* Copyright 2012-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.boot.env;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
/**
* Utiltiy that can be used to {@link MutablePropertySources} using
* {@link PropertySourceLoader}s.
*
* @author Phillip Webb
*/
public class PropertySourcesLoader {
private final MutablePropertySources propertySources;
private final List<PropertySourceLoader> loaders;
/**
* Create a new {@link PropertySourceLoader} instance backed by a new
* {@link MutablePropertySources}.
*/
public PropertySourcesLoader() {
this(new MutablePropertySources());
}
/**
* Create a new {@link PropertySourceLoader} instance backed by the specified
* {@link MutablePropertySources}.
* @param propertySources the destination property sources
*/
public PropertySourcesLoader(MutablePropertySources propertySources) {
Assert.notNull(propertySources, "PropertySources must not be null");
this.propertySources = propertySources;
this.loaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
null);
}
/**
* Load the specified resource (if possible) and add it as the first source.
* @param resource the source resource (may be {@code null}).
* @param name the root property name (may be {@code null}).
* @param profile a specific profile to load or {@code null} to load the default.
* @return the loaded property source or {@code null}
* @throws IOException
*/
public PropertySource<?> load(Resource resource, String name, String profile)
throws IOException {
if (resource != null && resource.exists()) {
name = generatePropertySourceName(resource, name, profile);
for (PropertySourceLoader loader : this.loaders) {
if (canLoadFileExtension(loader, resource)) {
PropertySource<?> source = loader.load(name, resource, profile);
addPropertySource(source);
return source;
}
}
}
return null;
}
private String generatePropertySourceName(Resource resource, String name,
String profile) {
if (name == null) {
name = resource.getDescription();
}
return (profile == null ? name : name + "#" + profile);
}
private boolean canLoadFileExtension(PropertySourceLoader loader, Resource resource) {
String filename = resource.getFilename().toLowerCase();
for (String extension : loader.getFileExtensions()) {
if (filename.endsWith("." + extension.toLowerCase())) {
return true;
}
}
return false;
}
private void addPropertySource(PropertySource<?> propertySource) {
if (propertySource != null) {
this.propertySources.addLast(propertySource);
}
}
/**
* Return the {@link MutablePropertySources} being loaded.
*/
public MutablePropertySources getPropertySources() {
return this.propertySources;
}
/**
* Returns all file extensions that could be loaded.
*/
public Set<String> getAllFileExtensions() {
Set<String> fileExtensions = new HashSet<String>();
for (PropertySourceLoader loader : this.loaders) {
fileExtensions.addAll(Arrays.asList(loader.getFileExtensions()));
}
return fileExtensions;
}
}
......@@ -14,26 +14,51 @@
* limitations under the License.
*/
package org.springframework.boot.config;
package org.springframework.boot.env;
import java.util.List;
import java.io.IOException;
import java.util.Properties;
import org.springframework.core.env.Environment;
import org.springframework.boot.yaml.SpringProfileDocumentMatcher;
import org.springframework.boot.yaml.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
import org.springframework.util.ClassUtils;
/**
* Factory to return {@link PropertySourceLoader}s.
* Strategy to load '.yml' files into a {@link PropertySource}.
*
* @author Dave Syer
* @see DefaultPropertySourceLoadersFactory
* @author Phillip Webb
*/
public interface PropertySourceLoadersFactory {
public class YamlPropertySourceLoader implements PropertySourceLoader {
/**
* Return a list of {@link PropertySourceLoader}s in the order that they should be
* tried.
* @param environment the source environment
* @return a list of loaders
*/
List<PropertySourceLoader> getLoaders(Environment environment);
@Override
public String[] getFileExtensions() {
return new String[] { "yml" };
}
@Override
public PropertySource<?> load(String name, Resource resource, String profile)
throws IOException {
if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
if (profile == null) {
factory.setMatchDefault(true);
factory.setDocumentMatchers(new SpringProfileDocumentMatcher());
}
else {
factory.setMatchDefault(false);
factory.setDocumentMatchers(new SpringProfileDocumentMatcher(profile));
}
factory.setResources(new Resource[] { resource });
Properties properties = factory.getObject();
if (profile == null || !properties.isEmpty()) {
return new PropertiesPropertySource(name, properties);
}
}
return null;
}
}
......@@ -37,6 +37,13 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher {
private String[] activeProfiles = new String[0];
public SpringProfileDocumentMatcher() {
}
public SpringProfileDocumentMatcher(String... profiles) {
addActiveProfiles(profiles);
}
public void addActiveProfiles(String... profiles) {
LinkedHashSet<String> set = new LinkedHashSet<String>(
Arrays.asList(this.activeProfiles));
......@@ -55,4 +62,4 @@ public class SpringProfileDocumentMatcher implements DocumentMatcher {
return new ArrayDocumentMatcher("spring.profiles", profiles).matches(properties);
}
}
\ No newline at end of file
}
......@@ -17,6 +17,8 @@
package org.springframework.boot.yaml;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
......@@ -37,7 +39,7 @@ import org.yaml.snakeyaml.Yaml;
*
* @author Dave Syer
*/
public class YamlProcessor {
public abstract class YamlProcessor {
private final Log logger = LogFactory.getLog(getClass());
......@@ -75,14 +77,15 @@ public class YamlProcessor {
* </pre>
* @param matchers a map of keys to value patterns (regular expressions)
*/
public void setDocumentMatchers(List<? extends DocumentMatcher> matchers) {
this.documentMatchers = Collections.unmodifiableList(matchers);
public void setDocumentMatchers(DocumentMatcher... matchers) {
this.documentMatchers = Collections
.unmodifiableList(new ArrayList<DocumentMatcher>(Arrays.asList(matchers)));
}
/**
* Flag indicating that a document for which all the
* {@link #setDocumentMatchers(List) document matchers} abstain will nevertheless
* match.
* {@link #setDocumentMatchers(DocumentMatcher...) document matchers} abstain will
* nevertheless match.
* @param matchDefault the flag to set (default true)
*/
public void setMatchDefault(boolean matchDefault) {
......@@ -111,10 +114,10 @@ public class YamlProcessor {
/**
* Provides an opportunity for subclasses to process the Yaml parsed from the supplied
* resources. Each resource is parsed in turn and the documents inside checked against
* the {@link #setDocumentMatchers(List) matchers}. If a document matches it is passed
* into the callback, along with its representation as Properties. Depending on the
* {@link #setResolutionMethod(ResolutionMethod)} not all of the documents will be
* parsed.
* the {@link #setDocumentMatchers(DocumentMatcher...) matchers}. If a document
* matches it is passed into the callback, along with its representation as
* Properties. Depending on the {@link #setResolutionMethod(ResolutionMethod)} not all
* of the documents will be parsed.
* @param callback a callback to delegate to once matching documents are found
*/
protected void process(MatchCallback callback) {
......@@ -172,6 +175,7 @@ public class YamlProcessor {
result.put("document", object);
return result;
}
Map<Object, Object> map = (Map<Object, Object>) object;
for (Entry<Object, Object> entry : map.entrySet()) {
Object value = entry.getValue();
......@@ -191,43 +195,42 @@ public class YamlProcessor {
}
private boolean process(Map<String, Object> map, MatchCallback callback) {
Properties properties = new Properties();
assignProperties(properties, map, null);
if (this.documentMatchers.isEmpty()) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Merging document (no matchers set)" + map);
}
callback.process(properties, map);
return true;
}
else {
boolean valueFound = false;
MatchStatus result = MatchStatus.ABSTAIN;
for (DocumentMatcher matcher : this.documentMatchers) {
MatchStatus match = matcher.matches(properties);
result = MatchStatus.getMostSpecific(match, result);
if (match == MatchStatus.FOUND) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Matched document with document matcher: "
+ properties);
}
callback.process(properties, map);
valueFound = true;
// No need to check for more matches
break;
}
}
if (result == MatchStatus.ABSTAIN && this.matchDefault) {
MatchStatus result = MatchStatus.ABSTAIN;
for (DocumentMatcher matcher : this.documentMatchers) {
MatchStatus match = matcher.matches(properties);
result = MatchStatus.getMostSpecific(match, result);
if (match == MatchStatus.FOUND) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Matched document with default matcher: " + map);
this.logger.debug("Matched document with document matcher: "
+ properties);
}
callback.process(properties, map);
return true;
}
else if (!valueFound) {
this.logger.debug("Unmatched document");
return false;
}
if (result == MatchStatus.ABSTAIN && this.matchDefault) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Matched document with default matcher: " + map);
}
callback.process(properties, map);
return true;
}
return true;
this.logger.debug("Unmatched document");
return false;
}
private void assignProperties(Properties properties, Map<String, Object> input,
......@@ -300,7 +303,21 @@ public class YamlProcessor {
* Status returned from {@link DocumentMatcher#matches(Properties)}
*/
public static enum MatchStatus {
FOUND, NOT_FOUND, ABSTAIN;
/**
* A match was found.
*/
FOUND,
/**
* No match was found.
*/
NOT_FOUND,
/**
* The matcher should not be considered.
*/
ABSTAIN;
/**
* Compare two {@link MatchStatus} items, returning the most specific status.
......
# ProperySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
# Run Participants
org.springframework.boot.SpringApplicationRunParticipant=\
org.springframework.boot.event.EventPublishingRunParticipant
......
......@@ -43,6 +43,36 @@ public class ReproTests {
assertThat(context.getEnvironment().acceptsProfiles("a"), equalTo(true));
}
@Test
public void activeProfilesWithYaml() throws Exception {
// gh-322
SpringApplication application = new SpringApplication(Config.class);
application.setWebEnvironment(false);
String configName = "--spring.config.name=activeprofilerepro";
assertVersionProperty(application.run(configName, "--spring.profiles.active=B"),
"B", "B");
assertVersionProperty(application.run(configName), "B", "B");
assertVersionProperty(application.run(configName, "--spring.profiles.active=A"),
"A", "A");
assertVersionProperty(application.run(configName, "--spring.profiles.active=C"),
"C", "C");
assertVersionProperty(
application.run(configName, "--spring.profiles.active=A,C"), "A", "A",
"C");
assertVersionProperty(
application.run(configName, "--spring.profiles.active=C,A"), "C", "C",
"A");
}
private void assertVersionProperty(ConfigurableApplicationContext context,
String expectedVersion, String... expectedActiveProfiles) {
assertThat(context.getEnvironment().getActiveProfiles(),
equalTo(expectedActiveProfiles));
assertThat("version mismatch", context.getEnvironment().getProperty("version"),
equalTo(expectedVersion));
context.close();
}
@Configuration
public static class Config {
......
......@@ -17,7 +17,6 @@
package org.springframework.boot.config;
import java.util.Arrays;
import java.util.List;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
......@@ -27,6 +26,7 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.config.ConfigFileApplicationListener.ConfigurationPropertySources;
import org.springframework.boot.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.test.EnvironmentTestUtils;
import org.springframework.context.ConfigurableApplicationContext;
......@@ -34,10 +34,9 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.SimpleCommandLinePropertySource;
import org.springframework.core.env.StandardEnvironment;
import org.springframework.core.io.Resource;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.equalTo;
......@@ -72,7 +71,7 @@ public class ConfigFileApplicationListenerTests {
@Test
public void loadPropertiesFile() throws Exception {
this.initializer.setNames("testproperties");
this.initializer.setSearchNames("testproperties");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
assertThat(property, equalTo("frompropertiesfile"));
......@@ -108,7 +107,7 @@ public class ConfigFileApplicationListenerTests {
@Test
public void loadTwoPropertiesFiles() throws Exception {
this.initializer.setNames("moreproperties,testproperties");
this.initializer.setSearchNames("moreproperties,testproperties");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
assertThat(property, equalTo("frommorepropertiesfile"));
......@@ -116,7 +115,7 @@ public class ConfigFileApplicationListenerTests {
@Test
public void loadYamlFile() throws Exception {
this.initializer.setNames("testyaml");
this.initializer.setSearchNames("testyaml");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
assertThat(property, equalTo("fromyamlfile"));
......@@ -128,7 +127,7 @@ public class ConfigFileApplicationListenerTests {
public void commandLineWins() throws Exception {
this.environment.getPropertySources().addFirst(
new SimpleCommandLinePropertySource("--my.property=fromcommandline"));
this.initializer.setNames("testproperties");
this.initializer.setSearchNames("testproperties");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
assertThat(property, equalTo("fromcommandline"));
......@@ -137,7 +136,7 @@ public class ConfigFileApplicationListenerTests {
@Test
public void systemPropertyWins() throws Exception {
System.setProperty("my.property", "fromsystem");
this.initializer.setNames("testproperties");
this.initializer.setSearchNames("testproperties");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
assertThat(property, equalTo("fromsystem"));
......@@ -145,7 +144,7 @@ public class ConfigFileApplicationListenerTests {
@Test
public void loadPropertiesThenProfileProperties() throws Exception {
this.initializer.setNames("enableprofile");
this.initializer.setSearchNames("enableprofile");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
assertThat(property, equalTo("fromprofilepropertiesfile"));
......@@ -153,7 +152,7 @@ public class ConfigFileApplicationListenerTests {
@Test
public void profilePropertiesUsedInPlaceholders() throws Exception {
this.initializer.setNames("enableprofile");
this.initializer.setSearchNames("enableprofile");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("one.more");
assertThat(property, equalTo("fromprofilepropertiesfile"));
......@@ -161,7 +160,7 @@ public class ConfigFileApplicationListenerTests {
@Test
public void yamlProfiles() throws Exception {
this.initializer.setNames("testprofiles");
this.initializer.setSearchNames("testprofiles");
this.environment.setActiveProfiles("dev");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
......@@ -172,7 +171,7 @@ public class ConfigFileApplicationListenerTests {
@Test
public void yamlSetsProfiles() throws Exception {
this.initializer.setNames("testsetprofiles");
this.initializer.setSearchNames("testsetprofiles");
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
assertThat(Arrays.asList(this.environment.getActiveProfiles()), contains("dev"));
......@@ -183,7 +182,7 @@ public class ConfigFileApplicationListenerTests {
public void yamlProfileCanBeChanged() throws Exception {
EnvironmentTestUtils.addEnvironment(this.environment,
"spring.profiles.active:prod");
this.initializer.setNames("testsetprofiles");
this.initializer.setSearchNames("testsetprofiles");
this.initializer.onApplicationEvent(this.event);
assertThat(this.environment.getActiveProfiles(), equalTo(new String[] { "prod" }));
}
......@@ -206,10 +205,11 @@ public class ConfigFileApplicationListenerTests {
this.initializer.onApplicationEvent(this.event);
String property = this.environment.getProperty("my.property");
assertThat(property, equalTo("fromspecificlocation"));
assertThat(this.environment, containsProperySource(location));
assertThat(this.environment, containsProperySource("applicationConfig: "
+ "class path resource [specificlocation.properties]"));
// The default property source is still there
assertThat(this.environment, containsProperySource("classpath:"
+ "/application.properties"));
assertThat(this.environment, containsProperySource("applicationConfig: "
+ "class path resource [application.properties]"));
assertThat(this.environment.getProperty("foo"), equalTo("bucket"));
}
......@@ -219,35 +219,8 @@ public class ConfigFileApplicationListenerTests {
EnvironmentTestUtils.addEnvironment(this.environment, "spring.config.location:"
+ location);
this.initializer.onApplicationEvent(this.event);
assertThat(this.environment, containsProperySource(location));
}
@Test
public void unsupportedResource() throws Exception {
this.initializer
.setPropertySourceLoadersFactory(new PropertySourceLoadersFactory() {
@Override
public List<PropertySourceLoader> getLoaders(Environment environment) {
return Arrays
.<PropertySourceLoader> asList(new PropertySourceLoader() {
@Override
public boolean supports(Resource resource) {
return false;
}
@Override
public org.springframework.core.env.PropertySource<?> load(
String name, Resource resource) {
return null;
}
});
}
});
this.expected.expect(IllegalStateException.class);
this.expected.expectMessage("No supported loader");
this.initializer.onApplicationEvent(this.event);
assertThat(this.environment, containsProperySource("applicationConfig: "
+ "URL [" + location + "]"));
}
@Test
......@@ -256,7 +229,8 @@ public class ConfigFileApplicationListenerTests {
EnvironmentTestUtils.addEnvironment(this.environment, "spring.config.location:"
+ location);
this.initializer.onApplicationEvent(this.event);
assertThat(this.environment, containsProperySource("file:" + location));
assertThat(this.environment, containsProperySource("applicationConfig: "
+ "URL [file:" + location + "]"));
}
@Test
......@@ -266,8 +240,8 @@ public class ConfigFileApplicationListenerTests {
ConfigurableApplicationContext context = application.run();
String property = context.getEnvironment().getProperty("my.property");
assertThat(property, equalTo("fromspecificlocation"));
assertThat(context.getEnvironment(), containsProperySource("classpath:"
+ "/specificlocation.properties"));
assertThat(context.getEnvironment(), containsProperySource("class path resource "
+ "[specificlocation.properties]"));
context.close();
}
......@@ -282,8 +256,8 @@ public class ConfigFileApplicationListenerTests {
ConfigurableApplicationContext context = application.run();
String property = context.getEnvironment().getProperty("my.property");
assertThat(property, equalTo("fromspecificlocation"));
assertThat(context.getEnvironment(), containsProperySource("classpath:"
+ "/specificlocation.properties"));
assertThat(context.getEnvironment(), containsProperySource("class path resource "
+ "[specificlocation.properties]"));
context.close();
}
......@@ -295,10 +269,7 @@ public class ConfigFileApplicationListenerTests {
ConfigurableApplicationContext context = application.run();
String property = context.getEnvironment().getProperty("my.property");
assertThat(property, equalTo("fromspecificlocation"));
// In this case "foo" should be the specificlocation.properties source, but Spring
// will have shifted it to the back of the line.
assertThat(context.getEnvironment().getPropertySources().get("boot.foo"),
notNullValue());
assertThat(context.getEnvironment(), containsProperySource("foo"));
context.close();
}
......@@ -311,10 +282,10 @@ public class ConfigFileApplicationListenerTests {
.run("--spring.profiles.active=myprofile");
String property = context.getEnvironment().getProperty("my.property");
assertThat(property, equalTo("frompropertiesfile"));
assertThat(context.getEnvironment(), containsProperySource("classpath:"
+ "/enableprofile.properties"));
assertThat(context.getEnvironment(), not(containsProperySource("classpath:"
+ "/enableprofile-myprofile.properties")));
assertThat(context.getEnvironment(), containsProperySource("class path resource "
+ "[enableprofile.properties]"));
assertThat(context.getEnvironment(), not(containsProperySource("classpath:/"
+ "enableprofile-myprofile.properties")));
context.close();
}
......@@ -339,8 +310,8 @@ public class ConfigFileApplicationListenerTests {
ConfigurableApplicationContext context = application.run();
String property = context.getEnvironment().getProperty("my.property");
assertThat(property, equalTo("frommorepropertiesfile"));
assertThat(context.getEnvironment(),
containsProperySource("classpath:/specificlocation.properties"));
assertThat(context.getEnvironment(), containsProperySource("class path resource "
+ "[specificlocation.properties]"));
context.close();
}
......@@ -352,12 +323,7 @@ public class ConfigFileApplicationListenerTests {
ConfigurableApplicationContext context = application.run();
String property = context.getEnvironment().getProperty("my.property");
assertThat(property, equalTo("frommorepropertiesfile"));
// foo is there but it is a dead rubber because the individual sources get higher
// priority (and are named after the resource locations)
assertThat(context.getEnvironment().getPropertySources().get("foo"),
notNullValue());
assertThat(context.getEnvironment(),
containsProperySource("classpath:/specificlocation.properties"));
assertThat(context.getEnvironment(), containsProperySource("foo"));
context.close();
}
......@@ -377,7 +343,6 @@ public class ConfigFileApplicationListenerTests {
private static Matcher<? super ConfigurableEnvironment> containsProperySource(
final String sourceName) {
return new TypeSafeDiagnosingMatcher<ConfigurableEnvironment>() {
@Override
public void describeTo(Description description) {
description.appendText("environment containing property source ")
......@@ -387,9 +352,12 @@ public class ConfigFileApplicationListenerTests {
@Override
protected boolean matchesSafely(ConfigurableEnvironment item,
Description mismatchDescription) {
mismatchDescription.appendText("Not matched against: ").appendValue(
MutablePropertySources sources = new MutablePropertySources(
item.getPropertySources());
return item.getPropertySources().contains(sourceName);
ConfigurationPropertySources.finishAndRelocate(sources);
mismatchDescription.appendText("Not matched against: ").appendValue(
sources);
return sources.contains(sourceName);
}
};
}
......
......@@ -36,7 +36,8 @@ import static org.junit.Assert.assertEquals;
*/
public class YamlProcessorTests {
private final YamlProcessor processor = new YamlProcessor();
private final YamlProcessor processor = new YamlProcessor() {
};
@Rule
public ExpectedException exception = ExpectedException.none();
......
......@@ -16,7 +16,6 @@
package org.springframework.boot.yaml;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
......@@ -33,7 +32,9 @@ import org.springframework.core.io.Resource;
import org.yaml.snakeyaml.Yaml;
import org.yaml.snakeyaml.scanner.ScannerException;
import static org.junit.Assert.assertEquals;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
/**
* Tests for {@link YamlPropertiesFactoryBean}.
......@@ -51,8 +52,8 @@ public class YamlPropertiesFactoryBeanTests {
factory.setResources(new Resource[] { new ByteArrayResource(
"foo: bar\nspam:\n foo: baz".getBytes()) });
Properties properties = factory.getObject();
assertEquals("bar", properties.get("foo"));
assertEquals("baz", properties.get("spam.foo"));
assertThat(properties.getProperty("foo"), equalTo("bar"));
assertThat(properties.getProperty("spam.foo"), equalTo("baz"));
}
@Test
......@@ -72,31 +73,29 @@ public class YamlPropertiesFactoryBeanTests {
new ByteArrayResource("foo: bar\nspam:\n foo: baz".getBytes()),
new ByteArrayResource("foo:\n bar: spam".getBytes()) });
Properties properties = factory.getObject();
assertEquals("bar", properties.get("foo"));
assertEquals("baz", properties.get("spam.foo"));
assertEquals("spam", properties.get("foo.bar"));
assertThat(properties.getProperty("foo"), equalTo("bar"));
assertThat(properties.getProperty("spam.foo"), equalTo("baz"));
assertThat(properties.getProperty("foo.bar"), equalTo("spam"));
}
@Test
@Ignore
// We can't fail on duplicate keys because the Map is created by the YAML library
@Ignore("We can't fail on duplicate keys because the Map is created by the YAML library")
public void testLoadResourcesWithInternalOverride() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] { new ByteArrayResource(
"foo: bar\nspam:\n foo: baz\nfoo: bucket".getBytes()) });
Properties properties = factory.getObject();
assertEquals("bar", properties.get("foo"));
assertThat(properties.getProperty("foo"), equalTo("bar"));
}
@Test
@Ignore
// We can't fail on duplicate keys because the Map is created by the YAML library
@Ignore("We can't fail on duplicate keys because the Map is created by the YAML library")
public void testLoadResourcesWithNestedInternalOverride() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] { new ByteArrayResource(
"foo:\n bar: spam\n foo: baz\nbreak: it\nfoo: bucket".getBytes()) });
Properties properties = factory.getObject();
assertEquals("spam", properties.get("foo.bar"));
assertThat(properties.getProperty("foo.bar"), equalTo("spam"));
}
@Test
......@@ -105,8 +104,8 @@ public class YamlPropertiesFactoryBeanTests {
factory.setResources(new Resource[] { new ByteArrayResource(
"foo: bar\nspam: baz\n---\nfoo: bag".getBytes()) });
Properties properties = factory.getObject();
assertEquals("bag", properties.get("foo"));
assertEquals("baz", properties.get("spam"));
assertThat(properties.getProperty("foo"), equalTo("bag"));
assertThat(properties.getProperty("spam"), equalTo("baz"));
}
@Test
......@@ -114,17 +113,16 @@ public class YamlPropertiesFactoryBeanTests {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new Resource[] { new ByteArrayResource(
"foo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes()) });
factory.setDocumentMatchers(Arrays
.<DocumentMatcher> asList(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND
: MatchStatus.NOT_FOUND;
}
}));
factory.setDocumentMatchers(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND
: MatchStatus.NOT_FOUND;
}
});
Properties properties = factory.getObject();
assertEquals("bag", properties.get("foo"));
assertEquals("bad", properties.get("spam"));
assertThat(properties.getProperty("foo"), equalTo("bag"));
assertThat(properties.getProperty("spam"), equalTo("bad"));
}
@Test
......@@ -133,21 +131,42 @@ public class YamlPropertiesFactoryBeanTests {
factory.setMatchDefault(true);
factory.setResources(new Resource[] { new ByteArrayResource(
"one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes()) });
factory.setDocumentMatchers(Arrays
.<DocumentMatcher> asList(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey("foo")) {
return MatchStatus.ABSTAIN;
}
return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND
: MatchStatus.NOT_FOUND;
}
}));
factory.setDocumentMatchers(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey("foo")) {
return MatchStatus.ABSTAIN;
}
return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND
: MatchStatus.NOT_FOUND;
}
});
Properties properties = factory.getObject();
assertEquals("bag", properties.get("foo"));
assertEquals("bad", properties.get("spam"));
assertEquals("two", properties.get("one"));
assertThat(properties.getProperty("foo"), equalTo("bag"));
assertThat(properties.getProperty("spam"), equalTo("bad"));
assertThat(properties.getProperty("one"), equalTo("two"));
}
@Test
public void testLoadResourceWithoutDefaultMatch() throws Exception {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setMatchDefault(false);
factory.setResources(new Resource[] { new ByteArrayResource(
"one: two\n---\nfoo: bar\nspam: baz\n---\nfoo: bag\nspam: bad".getBytes()) });
factory.setDocumentMatchers(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey("foo")) {
return MatchStatus.ABSTAIN;
}
return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND
: MatchStatus.NOT_FOUND;
}
});
Properties properties = factory.getObject();
assertThat(properties.getProperty("foo"), equalTo("bag"));
assertThat(properties.getProperty("spam"), equalTo("bad"));
assertThat(properties.getProperty("one"), nullValue());
}
@Test
......@@ -156,21 +175,20 @@ public class YamlPropertiesFactoryBeanTests {
factory.setMatchDefault(true);
factory.setResources(new Resource[] { new ByteArrayResource(
"one: two\n---\nfoo: bag\nspam: bad\n---\nfoo: bar\nspam: baz".getBytes()) });
factory.setDocumentMatchers(Arrays
.<DocumentMatcher> asList(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey("foo")) {
return MatchStatus.ABSTAIN;
}
return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND
: MatchStatus.NOT_FOUND;
}
}));
factory.setDocumentMatchers(new DocumentMatcher() {
@Override
public MatchStatus matches(Properties properties) {
if (!properties.containsKey("foo")) {
return MatchStatus.ABSTAIN;
}
return "bag".equals(properties.getProperty("foo")) ? MatchStatus.FOUND
: MatchStatus.NOT_FOUND;
}
});
Properties properties = factory.getObject();
assertEquals("bag", properties.get("foo"));
assertEquals("bad", properties.get("spam"));
assertEquals("two", properties.get("one"));
assertThat(properties.getProperty("foo"), equalTo("bag"));
assertThat(properties.getProperty("spam"), equalTo("bad"));
assertThat(properties.getProperty("one"), equalTo("two"));
}
@Test
......@@ -179,7 +197,7 @@ public class YamlPropertiesFactoryBeanTests {
factory.setResolutionMethod(ResolutionMethod.OVERRIDE_AND_IGNORE);
factory.setResources(new Resource[] { new ClassPathResource("no-such-file.yml") });
Properties properties = factory.getObject();
assertEquals(0, properties.size());
assertThat(properties.size(), equalTo(0));
}
@Test
......@@ -188,8 +206,8 @@ public class YamlPropertiesFactoryBeanTests {
factory.setResources(new Resource[] { new ByteArrayResource("foo: bar\nspam:"
.getBytes()) });
Properties properties = factory.getObject();
assertEquals("bar", properties.get("foo"));
assertEquals("", properties.get("spam"));
assertThat(properties.getProperty("foo"), equalTo("bar"));
assertThat(properties.getProperty("spam"), equalTo(""));
}
@Test
......@@ -198,8 +216,8 @@ public class YamlPropertiesFactoryBeanTests {
factory.setResources(new Resource[] { new ByteArrayResource("foo:\n- bar\n- baz"
.getBytes()) });
Properties properties = factory.getObject();
assertEquals("bar", properties.get("foo[0]"));
assertEquals("baz", properties.get("foo[1]"));
assertThat(properties.getProperty("foo[0]"), equalTo("bar"));
assertThat(properties.getProperty("foo[1]"), equalTo("baz"));
}
@Test
......@@ -209,10 +227,10 @@ public class YamlPropertiesFactoryBeanTests {
"foo:\n- bar:\n spam: crap\n- baz\n- one: two\n three: four"
.getBytes()) });
Properties properties = factory.getObject();
assertEquals("crap", properties.get("foo[0].bar.spam"));
assertEquals("baz", properties.get("foo[1]"));
assertEquals("two", properties.get("foo[2].one"));
assertEquals("four", properties.get("foo[2].three"));
assertThat(properties.getProperty("foo[0].bar.spam"), equalTo("crap"));
assertThat(properties.getProperty("foo[1]"), equalTo("baz"));
assertThat(properties.getProperty("foo[2].one"), equalTo("two"));
assertThat(properties.getProperty("foo[2].three"), equalTo("four"));
}
@SuppressWarnings("unchecked")
......@@ -220,8 +238,9 @@ public class YamlPropertiesFactoryBeanTests {
public void testYaml() {
Yaml yaml = new Yaml();
Map<String, ?> map = yaml.loadAs("foo: bar\nspam:\n foo: baz", Map.class);
assertEquals("bar", map.get("foo"));
assertEquals("baz", ((Map<String, Object>) map.get("spam")).get("foo"));
assertThat(map.get("foo"), equalTo((Object) "bar"));
assertThat(((Map<String, Object>) map.get("spam")).get("foo"),
equalTo((Object) "baz"));
}
}
spring.profiles.active: B
---
spring.profiles: A
version: A
---
spring.profiles: B
version: B
---
spring.profiles: C
version: C
---
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment