From f0bfecd375a00c4e3f1b1d1eb687a4251bb3452d Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Fri, 7 Feb 2014 11:15:51 -0800 Subject: [PATCH] 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 --- .../cloudfoundry/VcapApplicationListener.java | 2 +- .../config/ConfigFileApplicationListener.java | 592 ++++++++---------- .../DefaultPropertySourceLoadersFactory.java | 45 -- .../PropertiesPropertySourceLoader.java | 63 -- .../config/PropertySourceLoadersFactory.java | 39 -- .../config/RandomValuePropertySource.java | 61 ++ .../boot/config/YamlPropertySourceLoader.java | 105 ---- ...urationPropertiesBindingPostProcessor.java | 36 +- .../env/PropertiesPropertySourceLoader.java | 48 ++ .../{config => env}/PropertySourceLoader.java | 24 +- .../boot/env/PropertySourcesLoader.java | 128 ++++ .../boot/env/YamlPropertySourceLoader.java | 64 ++ .../yaml/SpringProfileDocumentMatcher.java | 9 +- .../boot/yaml/YamlProcessor.java | 83 ++- .../main/resources/META-INF/spring.factories | 5 + .../org/springframework/boot/ReproTests.java | 30 + .../ConfigFileApplicationListenerTests.java | 106 ++-- .../boot/yaml/YamlProcessorTests.java | 3 +- .../yaml/YamlPropertiesFactoryBeanTests.java | 147 +++-- .../src/test/resources/activeprofilerepro.yml | 11 + 20 files changed, 806 insertions(+), 795 deletions(-) delete mode 100644 spring-boot/src/main/java/org/springframework/boot/config/DefaultPropertySourceLoadersFactory.java delete mode 100644 spring-boot/src/main/java/org/springframework/boot/config/PropertiesPropertySourceLoader.java delete mode 100644 spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoadersFactory.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/config/RandomValuePropertySource.java delete mode 100644 spring-boot/src/main/java/org/springframework/boot/config/YamlPropertySourceLoader.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java rename spring-boot/src/main/java/org/springframework/boot/{config => env}/PropertySourceLoader.java (53%) create mode 100644 spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java create mode 100644 spring-boot/src/main/java/org/springframework/boot/env/YamlPropertySourceLoader.java create mode 100644 spring-boot/src/test/resources/activeprofilerepro.yml diff --git a/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/VcapApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/VcapApplicationListener.java index e586963dc7f..cf35a079e7c 100644 --- a/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/VcapApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/cloudfoundry/VcapApplicationListener.java @@ -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(); diff --git a/spring-boot/src/main/java/org/springframework/boot/config/ConfigFileApplicationListener.java b/spring-boot/src/main/java/org/springframework/boot/config/ConfigFileApplicationListener.java index 2a25d868469..68a26626252 100644 --- a/spring-boot/src/main/java/org/springframework/boot/config/ConfigFileApplicationListener.java +++ b/spring-boot/src/main/java/org/springframework/boot/config/ConfigFileApplicationListener.java @@ -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; *
  • file:./config/:
  • * *

    - * 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)}. *

    * 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. *

    - * 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. + *

    + * 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, Ordered { + ApplicationListener, 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> cache = new HashMap>(); + 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 { - - private final List candidates; - - public LoadCandidates(ConfigurableEnvironment environment, - ResourceLoader resourceLoader) { - Set candidates = new LinkedHashSet(); - 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(candidates); - Collections.reverse(this.candidates); - } + private ConfigurableApplicationContext context; - private void addLoadCandidatesFromSearchLocations( - ConfigurableEnvironment environment, Set 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 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 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 { + 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 profiles; - private final Collection locations = new LinkedHashSet(); + private boolean activatedProfiles; - private final Map names = new HashMap(); + public Loader(ConfigurableEnvironment environment) { + this.environment = environment; + } - private final Map> configs = new HashMap>(); + public void load() throws IOException { + this.propertiesLoader = new PropertySourcesLoader(); + this.profiles = new LinkedList(); + 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 ignores = new HashMap(); + 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 getSearchLocations() { + Set locations = new LinkedHashSet(); + 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 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 asResolvedSet(String value, String fallback) { + List list = Arrays.asList(StringUtils + .commaDelimitedListToStringArray(value != null ? this.environment + .resolvePlaceholders(value) : fallback)); + Collections.reverse(list); + return new LinkedHashSet(list); } - public Class configuration(String location) { - return this.configs.get(location); + private void addConfigurationProperties(MutablePropertySources sources) { + List> reorderedSources = new ArrayList>(); + 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 { + + private static final String NAME = "applicationConfigurationProperties"; + + private final Collection> sources; + + public ConfigurationPropertySources(Collection> 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 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); + } + } } + } } diff --git a/spring-boot/src/main/java/org/springframework/boot/config/DefaultPropertySourceLoadersFactory.java b/spring-boot/src/main/java/org/springframework/boot/config/DefaultPropertySourceLoadersFactory.java deleted file mode 100644 index 4dc7b97e55e..00000000000 --- a/spring-boot/src/main/java/org/springframework/boot/config/DefaultPropertySourceLoadersFactory.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.config; - -import java.util.ArrayList; -import java.util.List; - -import org.springframework.core.env.Environment; -import org.springframework.util.ClassUtils; - -/** - * Default implementation of {@link PropertySourceLoadersFactory}. Provides a - * {@link PropertiesPropertySourceLoader} and when possible a - * {@link YamlPropertySourceLoader}. - * - * @author Dave Syer - */ -public class DefaultPropertySourceLoadersFactory implements PropertySourceLoadersFactory { - - @Override - public List getLoaders(Environment environment) { - ArrayList loaders = new ArrayList(); - loaders.add(new PropertiesPropertySourceLoader()); - if (ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) { - loaders.add(YamlPropertySourceLoader.springProfileAwareLoader(environment - .getActiveProfiles())); - } - return loaders; - } - -} diff --git a/spring-boot/src/main/java/org/springframework/boot/config/PropertiesPropertySourceLoader.java b/spring-boot/src/main/java/org/springframework/boot/config/PropertiesPropertySourceLoader.java deleted file mode 100644 index ef56f0f2772..00000000000 --- a/spring-boot/src/main/java/org/springframework/boot/config/PropertiesPropertySourceLoader.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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.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; -import org.springframework.core.io.support.PropertiesLoaderUtils; - -/** - * Strategy to load '.properties' files into a {@link PropertySource}. - * - * @author Dave Syer - */ -public class PropertiesPropertySourceLoader implements PropertySourceLoader { - - private static Log logger = LogFactory.getLog(PropertiesPropertySourceLoader.class); - - @Override - public boolean supports(Resource resource) { - return resource.getFilename().endsWith(".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); - } - } - - protected Properties loadProperties(Resource resource) throws IOException { - return PropertiesLoaderUtils.loadProperties(resource); - } -} \ No newline at end of file diff --git a/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoadersFactory.java b/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoadersFactory.java deleted file mode 100644 index 456d8f97f1a..00000000000 --- a/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoadersFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.config; - -import java.util.List; - -import org.springframework.core.env.Environment; - -/** - * Factory to return {@link PropertySourceLoader}s. - * - * @author Dave Syer - * @see DefaultPropertySourceLoadersFactory - */ -public interface PropertySourceLoadersFactory { - - /** - * 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 getLoaders(Environment environment); - -} diff --git a/spring-boot/src/main/java/org/springframework/boot/config/RandomValuePropertySource.java b/spring-boot/src/main/java/org/springframework/boot/config/RandomValuePropertySource.java new file mode 100644 index 00000000000..7a33b023c4b --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/config/RandomValuePropertySource.java @@ -0,0 +1,61 @@ +/* + * 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.config; + +import java.util.Random; + +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertySource; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.DigestUtils; + +/** + * {@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 RandomValuePropertySource extends PropertySource { + + public RandomValuePropertySource(String name) { + super(name, new Random()); + } + + @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); + } + + public static void addToEnvironment(ConfigurableEnvironment environment) { + environment.getPropertySources().addAfter( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, + new RandomValuePropertySource("random")); + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/config/YamlPropertySourceLoader.java b/spring-boot/src/main/java/org/springframework/boot/config/YamlPropertySourceLoader.java deleted file mode 100644 index 7158497d390..00000000000 --- a/spring-boot/src/main/java/org/springframework/boot/config/YamlPropertySourceLoader.java +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 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 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; - } - }); - } - -} diff --git a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java index 059aa83bdc5..a7d9f2ea35c 100644 --- a/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessor.java @@ -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() { diff --git a/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java b/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java new file mode 100644 index 00000000000..dbebdab0859 --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/env/PropertiesPropertySourceLoader.java @@ -0,0 +1,48 @@ +/* + * 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 org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; +import org.springframework.core.io.Resource; +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 { + + @Override + public String[] getFileExtensions() { + return new String[] { "properties" }; + } + + @Override + public PropertySource load(String name, Resource resource, String profile) + throws IOException { + if (profile != null) { + return null; + } + return new PropertiesPropertySource(name, + PropertiesLoaderUtils.loadProperties(resource)); + } +} diff --git a/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoader.java b/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java similarity index 53% rename from spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoader.java rename to spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java index 39f0be1becd..a967fcd4a71 100644 --- a/spring-boot/src/main/java/org/springframework/boot/config/PropertySourceLoader.java +++ b/spring-boot/src/main/java/org/springframework/boot/env/PropertySourceLoader.java @@ -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; } diff --git a/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java b/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java new file mode 100644 index 00000000000..742f8245c5f --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/env/PropertySourcesLoader.java @@ -0,0 +1,128 @@ +/* + * 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 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 getAllFileExtensions() { + Set fileExtensions = new HashSet(); + for (PropertySourceLoader loader : this.loaders) { + fileExtensions.addAll(Arrays.asList(loader.getFileExtensions())); + } + return fileExtensions; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/env/YamlPropertySourceLoader.java b/spring-boot/src/main/java/org/springframework/boot/env/YamlPropertySourceLoader.java new file mode 100644 index 00000000000..2fa2a1a039e --- /dev/null +++ b/spring-boot/src/main/java/org/springframework/boot/env/YamlPropertySourceLoader.java @@ -0,0 +1,64 @@ +/* + * 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.Properties; + +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; + +/** + * Strategy to load '.yml' files into a {@link PropertySource}. + * + * @author Dave Syer + * @author Phillip Webb + */ +public class YamlPropertySourceLoader implements PropertySourceLoader { + + @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; + } + +} diff --git a/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java b/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java index 15724a9ab99..7568493274a 100644 --- a/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java +++ b/spring-boot/src/main/java/org/springframework/boot/yaml/SpringProfileDocumentMatcher.java @@ -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 set = new LinkedHashSet( 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 +} diff --git a/spring-boot/src/main/java/org/springframework/boot/yaml/YamlProcessor.java b/spring-boot/src/main/java/org/springframework/boot/yaml/YamlProcessor.java index 77db233c5e2..73b91408bfe 100644 --- a/spring-boot/src/main/java/org/springframework/boot/yaml/YamlProcessor.java +++ b/spring-boot/src/main/java/org/springframework/boot/yaml/YamlProcessor.java @@ -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 { * * @param matchers a map of keys to value patterns (regular expressions) */ - public void setDocumentMatchers(List matchers) { - this.documentMatchers = Collections.unmodifiableList(matchers); + public void setDocumentMatchers(DocumentMatcher... matchers) { + this.documentMatchers = Collections + .unmodifiableList(new ArrayList(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 map = (Map) object; for (Entry entry : map.entrySet()) { Object value = entry.getValue(); @@ -191,43 +195,42 @@ public class YamlProcessor { } private boolean process(Map 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 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. diff --git a/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot/src/main/resources/META-INF/spring.factories index 924e4d76f0e..6477d902100 100644 --- a/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot/src/main/resources/META-INF/spring.factories @@ -1,3 +1,8 @@ +# 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 diff --git a/spring-boot/src/test/java/org/springframework/boot/ReproTests.java b/spring-boot/src/test/java/org/springframework/boot/ReproTests.java index c347c0d3ffe..1323723b443 100644 --- a/spring-boot/src/test/java/org/springframework/boot/ReproTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/ReproTests.java @@ -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 { diff --git a/spring-boot/src/test/java/org/springframework/boot/config/ConfigFileApplicationListenerTests.java b/spring-boot/src/test/java/org/springframework/boot/config/ConfigFileApplicationListenerTests.java index 5a6bd80657b..d6e69f3ad91 100644 --- a/spring-boot/src/test/java/org/springframework/boot/config/ConfigFileApplicationListenerTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/config/ConfigFileApplicationListenerTests.java @@ -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 getLoaders(Environment environment) { - return Arrays - . 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 containsProperySource( final String sourceName) { return new TypeSafeDiagnosingMatcher() { - @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); } }; } diff --git a/spring-boot/src/test/java/org/springframework/boot/yaml/YamlProcessorTests.java b/spring-boot/src/test/java/org/springframework/boot/yaml/YamlProcessorTests.java index a22fe971812..14332917bcf 100644 --- a/spring-boot/src/test/java/org/springframework/boot/yaml/YamlProcessorTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/yaml/YamlProcessorTests.java @@ -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(); diff --git a/spring-boot/src/test/java/org/springframework/boot/yaml/YamlPropertiesFactoryBeanTests.java b/spring-boot/src/test/java/org/springframework/boot/yaml/YamlPropertiesFactoryBeanTests.java index 2cb6d1b9fbd..22393a06939 100644 --- a/spring-boot/src/test/java/org/springframework/boot/yaml/YamlPropertiesFactoryBeanTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/yaml/YamlPropertiesFactoryBeanTests.java @@ -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 - . 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 - . 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 - . 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 map = yaml.loadAs("foo: bar\nspam:\n foo: baz", Map.class); - assertEquals("bar", map.get("foo")); - assertEquals("baz", ((Map) map.get("spam")).get("foo")); + assertThat(map.get("foo"), equalTo((Object) "bar")); + assertThat(((Map) map.get("spam")).get("foo"), + equalTo((Object) "baz")); } } diff --git a/spring-boot/src/test/resources/activeprofilerepro.yml b/spring-boot/src/test/resources/activeprofilerepro.yml new file mode 100644 index 00000000000..3871896a3b7 --- /dev/null +++ b/spring-boot/src/test/resources/activeprofilerepro.yml @@ -0,0 +1,11 @@ +spring.profiles.active: B +--- +spring.profiles: A +version: A +--- +spring.profiles: B +version: B +--- +spring.profiles: C +version: C +---