Rationalize multi-document config file handling

Update `PropertySourceLoader` so that it no longer needs to deal with
matching multi-document files using the `spring.profile` property. The
loader now simply returns one or more `PropertSource` instances for a
given `Resource`.

All property matching now occurs in the `ConfigFileApplicationListener`.
This allows document processing logic to be contained in a single place,
and allows us to rationalize the algorithm so that negative matching
profiles are processed last.

Fixes gh-12159
This commit is contained in:
Phillip Webb 2018-02-26 12:11:08 -08:00
parent 3d8f760ea0
commit c0d79b9273
20 changed files with 412 additions and 578 deletions

View File

@ -22,7 +22,6 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.env.YamlPropertySourceLoader;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
@ -41,17 +40,16 @@ public class EnvironmentPostProcessorExample implements EnvironmentPostProcessor
public void postProcessEnvironment(ConfigurableEnvironment environment,
SpringApplication application) {
Resource path = new ClassPathResource("com/example/myapp/config.yml");
PropertySource<?> propertySource = loadYaml(path, environment);
PropertySource<?> propertySource = loadYaml(path);
environment.getPropertySources().addLast(propertySource);
}
private PropertySource<?> loadYaml(Resource path, Environment environment) {
private PropertySource<?> loadYaml(Resource path) {
if (!path.exists()) {
throw new IllegalArgumentException("Resource " + path + " does not exist");
}
try {
return this.loader.load("custom-resource", path, null,
environment::acceptsProfiles);
return this.loader.load("custom-resource", path).get(0);
}
catch (IOException ex) {
throw new IllegalStateException(

View File

@ -180,10 +180,10 @@ public class PropertiesMigrationReporterTests {
private PropertySource<?> loadPropertySource(String name, String path)
throws IOException {
ClassPathResource resource = new ClassPathResource(path);
PropertySource<?> propertySource = new PropertiesPropertySourceLoader().load(name,
resource, null, (profile) -> true);
assertThat(propertySource).isNotNull();
return propertySource;
List<PropertySource<?>> propertySources = new PropertiesPropertySourceLoader()
.load(name, resource);
assertThat(propertySources).isNotEmpty();
return propertySources.get(0);
}
private ConfigurationMetadataRepository loadRepository(String... content) {

View File

@ -20,6 +20,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
@ -27,6 +28,8 @@ import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
@ -36,6 +39,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.bind.PropertySourcesPlaceholdersResolver;
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
@ -58,6 +62,8 @@ import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
@ -101,6 +107,8 @@ public class ConfigFileApplicationListener
private static final String DEFAULT_NAMES = "application";
private static final Set<String> NO_SEARCH_NAMES = Collections.singleton(null);
/**
* The "active profiles" property name.
*/
@ -302,6 +310,8 @@ public class ConfigFileApplicationListener
private Map<Profile, MutablePropertySources> loaded;
private Map<DocumentsCacheKey, List<Document>> loadDocumentsCache = new HashMap<>();
Loader(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
this.environment = environment;
this.resourceLoader = resourceLoader == null ? new DefaultResourceLoader()
@ -318,20 +328,12 @@ public class ConfigFileApplicationListener
initializeProfiles();
while (!this.profiles.isEmpty()) {
Profile profile = this.profiles.poll();
for (String location : getSearchLocations()) {
if (!location.endsWith("/")) {
// location is a filename already, so don't search for more
// filenames
load(profile, location, null);
}
else {
for (String name : getSearchNames()) {
load(profile, location, name);
}
}
}
load(profile, this::getPositiveProfileFilter,
addToLoaded(MutablePropertySources::addLast, false));
this.processedProfiles.add(profile);
}
load(null, this::getNegativeProfileFilter,
addToLoaded(MutablePropertySources::addFirst, true));
addLoadedPropertySources();
}
@ -399,24 +401,66 @@ public class ConfigFileApplicationListener
return unprocessedActiveProfiles;
}
/**
* Load an actual property source file.
* @param profile the profile being loaded
* @param location the location of the resource
* @param name an optional name to be combined with the location
*/
private void load(Profile profile, String location, String name) {
private DocumentFilter getPositiveProfileFilter(Profile profile) {
return (Document document) -> {
if (profile == null) {
return ObjectUtils.isEmpty(document.getProfiles());
}
return ObjectUtils.containsElement(document.getProfiles(),
profile.getName())
&& this.environment.acceptsProfiles(document.getProfiles());
};
}
private DocumentFilter getNegativeProfileFilter(Profile profile) {
return (Document document) -> (profile == null
&& !ObjectUtils.isEmpty(document.getProfiles())
&& this.environment.acceptsProfiles(document.getProfiles()));
}
private DocumentConsumer addToLoaded(
BiConsumer<MutablePropertySources, PropertySource<?>> addMethod,
boolean checkForExisting) {
return (profile, document) -> {
if (checkForExisting) {
for (MutablePropertySources merged : this.loaded.values()) {
if (merged.contains(document.getPropertySource().getName())) {
return;
}
}
}
MutablePropertySources merged = this.loaded.computeIfAbsent(profile,
(k) -> new MutablePropertySources());
addMethod.accept(merged, document.getPropertySource());
};
}
private void load(Profile profile, DocumentFilterFactory filterFactory,
DocumentConsumer consumer) {
getSearchLocations().forEach((location) -> {
boolean isFolder = location.endsWith("/");
Set<String> names = (isFolder ? getSearchNames() : NO_SEARCH_NAMES);
names.forEach(
(name) -> load(location, name, profile, filterFactory, consumer));
});
}
private void load(String location, String name, Profile profile,
DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
if (!StringUtils.hasText(name)) {
for (PropertySourceLoader loader : this.propertySourceLoaders) {
if (canLoadFileExtension(loader, location)) {
load(loader, profile, location,
(profile == null ? null : profile.getName()));
load(loader, location, profile,
filterFactory.getDocumentFilter(profile), consumer);
}
}
}
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String ext : loader.getFileExtensions()) {
loadForFileExtension(loader, profile, location + name, "." + ext);
for (String fileExtension : loader.getFileExtensions()) {
String prefix = location + name;
fileExtension = "." + fileExtension;
loadForFileExtension(loader, prefix, fileExtension, profile,
filterFactory, consumer);
}
}
}
@ -427,28 +471,31 @@ public class ConfigFileApplicationListener
fileExtension));
}
private void loadForFileExtension(PropertySourceLoader loader, Profile profile,
String prefix, String ext) {
private void loadForFileExtension(PropertySourceLoader loader, String prefix,
String fileExtension, Profile profile,
DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
DocumentFilter defaultFilter = filterFactory.getDocumentFilter(null);
DocumentFilter profileFilter = filterFactory.getDocumentFilter(profile);
if (profile != null) {
// Try the profile-specific file
load(loader, profile, prefix + "-" + profile + ext, null);
// Support profile section in profile file (gh-340)
load(loader, profile, prefix + "-" + profile + ext, profile.getName());
// Try profile-specific file & profile section in profile file (gh-340)
String profileSpecificFile = prefix + "-" + profile + fileExtension;
load(loader, profileSpecificFile, profile, defaultFilter, consumer);
load(loader, profileSpecificFile, profile, profileFilter, consumer);
// Try profile specific sections in files we've already processed
for (Profile processedProfile : this.processedProfiles) {
if (processedProfile != null) {
String previouslyLoaded = prefix + "-" + processedProfile + ext;
load(loader, profile, previouslyLoaded, profile.getName());
String previouslyLoaded = prefix + "-" + processedProfile
+ fileExtension;
load(loader, previouslyLoaded, profile, profileFilter, consumer);
}
}
}
// Also try the profile-specific section (if any) of the normal file
load(loader, profile, prefix + ext,
(profile == null ? null : profile.getName()));
load(loader, prefix + fileExtension, profile, profileFilter, consumer);
}
private void load(PropertySourceLoader loader, Profile profile, String location,
String loadProfile) {
private void load(PropertySourceLoader loader, String location, Profile profile,
DocumentFilter filter, DocumentConsumer consumer) {
try {
Resource resource = this.resourceLoader.getResource(location);
String description = getDescription(location, resource);
@ -464,18 +511,25 @@ public class ConfigFileApplicationListener
this.logger.trace("Skipped empty config extension " + description);
return;
}
String name = "applicationConfig: [" + location + "]"
+ (loadProfile == null ? "" : "#" + loadProfile);
PropertySource<?> loaded = loader.load(name, resource, loadProfile,
this.environment::acceptsProfiles);
if (loaded == null) {
String name = "applicationConfig: [" + location + "]";
List<Document> documents = loadDocuments(loader, name, resource);
if (CollectionUtils.isEmpty(documents)) {
this.logger.trace("Skipped unloaded config " + description);
return;
}
handleProfileProperties(loaded);
this.loaded.computeIfAbsent(profile, (k) -> new MutablePropertySources())
.addLast(loaded);
this.logger.debug("Loaded config file " + description);
List<Document> loaded = new ArrayList<>();
for (Document document : documents) {
if (filter.match(document)) {
maybeActivateProfiles(document.getActiveProfiles());
addProfiles(document.getIncludeProfiles());
loaded.add(document);
}
}
Collections.reverse(loaded);
if (!loaded.isEmpty()) {
loaded.forEach((document) -> consumer.accept(profile, document));
this.logger.debug("Loaded config file " + description);
}
}
catch (Exception ex) {
throw new IllegalStateException("Failed to load property "
@ -483,6 +537,34 @@ public class ConfigFileApplicationListener
}
}
private List<Document> loadDocuments(PropertySourceLoader loader, String name,
Resource resource) throws IOException {
loader.load(name, resource);
DocumentsCacheKey cacheKey = new DocumentsCacheKey(loader, resource);
List<Document> documents = this.loadDocumentsCache.get(cacheKey);
if (documents == null) {
List<PropertySource<?>> loaded = loader.load(name, resource);
documents = asDocuments(loaded);
}
return documents;
}
private List<Document> asDocuments(List<PropertySource<?>> loaded) {
if (loaded == null) {
return Collections.emptyList();
}
return loaded.stream().map((propertySource) -> {
Binder binder = new Binder(
ConfigurationPropertySources.from(propertySource),
new PropertySourcesPlaceholdersResolver(this.environment));
return new Document(propertySource,
binder.bind("spring.profiles", Bindable.of(String[].class))
.orElse(null),
getProfiles(binder, ACTIVE_PROFILES_PROPERTY),
getProfiles(binder, INCLUDE_PROFILES_PROPERTY));
}).collect(Collectors.toList());
}
private String getDescription(String location, Resource resource) {
try {
if (resource != null) {
@ -495,15 +577,6 @@ public class ConfigFileApplicationListener
return String.format("'%s'", location);
}
private void handleProfileProperties(PropertySource<?> propertySource) {
Binder binder = new Binder(ConfigurationPropertySources.from(propertySource),
new PropertySourcesPlaceholdersResolver(this.environment));
Set<Profile> active = getProfiles(binder, "spring.profiles.active");
Set<Profile> include = getProfiles(binder, "spring.profiles.include");
maybeActivateProfiles(active);
addProfiles(include);
}
private Set<Profile> getProfiles(Binder binder, String name) {
return binder.bind(name, String[].class).map(this::asProfileSet)
.orElse(Collections.emptySet());
@ -638,6 +711,9 @@ public class ConfigFileApplicationListener
}
/**
* A Spring Profile that can be loaded.
*/
private static class Profile {
private final String name;
@ -685,4 +761,117 @@ public class ConfigFileApplicationListener
}
/**
* Cache key used to save loading the same document multiple times.
*/
private static class DocumentsCacheKey {
private final PropertySourceLoader loader;
private final Resource resource;
DocumentsCacheKey(PropertySourceLoader loader, Resource resource) {
this.loader = loader;
this.resource = resource;
}
@Override
public int hashCode() {
return this.loader.hashCode() * 31 + this.resource.hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
DocumentsCacheKey other = (DocumentsCacheKey) obj;
return this.loader.equals(other.loader)
&& this.resource.equals(other.resource);
}
}
/**
* A single document loaded by a {@link PropertySourceLoader}.
*/
private static class Document {
private final PropertySource<?> propertySource;
private String[] profiles;
private final Set<Profile> activeProfiles;
private final Set<Profile> includeProfiles;
Document(PropertySource<?> propertySource, String[] profiles,
Set<Profile> activeProfiles, Set<Profile> includeProfiles) {
this.propertySource = propertySource;
this.profiles = profiles;
this.activeProfiles = activeProfiles;
this.includeProfiles = includeProfiles;
}
public PropertySource<?> getPropertySource() {
return this.propertySource;
}
public String[] getProfiles() {
return this.profiles;
}
public Set<Profile> getActiveProfiles() {
return this.activeProfiles;
}
public Set<Profile> getIncludeProfiles() {
return this.includeProfiles;
}
@Override
public String toString() {
return this.propertySource.toString();
}
}
/**
* Factory used to create a {@link DocumentFilter}.
*/
@FunctionalInterface
private interface DocumentFilterFactory {
/**
* Create a filter for the given profile.
* @param profile the profile or {@code null}
* @return the filter
*/
DocumentFilter getDocumentFilter(Profile profile);
}
/**
* Filter used to restrict when a {@link Document} is loaded.
*/
@FunctionalInterface
private interface DocumentFilter {
boolean match(Document document);
}
/**
* Consumer used to handle a loaded {@link Document}.
*/
@FunctionalInterface
private interface DocumentConsumer {
void accept(Profile profile, Document document);
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright 2012-2018 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.util.function.Predicate;
import org.springframework.util.ObjectUtils;
/**
* {@link SpringProfilesDocumentMatcher} that tests if a profile is accepted.
*
* @author Phillip Webb
*/
class AcceptsProfilesDocumentMatcher extends SpringProfilesDocumentMatcher {
private final Predicate<String[]> acceptsProfiles;
AcceptsProfilesDocumentMatcher(Predicate<String[]> acceptsProfiles) {
this.acceptsProfiles = acceptsProfiles;
}
@Override
protected boolean matches(String[] profiles) {
return ObjectUtils.isEmpty(profiles) || this.acceptsProfiles.test(profiles);
}
}

View File

@ -16,9 +16,9 @@
package org.springframework.boot.env;
import java.util.LinkedHashMap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@ -53,11 +53,8 @@ class OriginTrackedYamlLoader extends YamlProcessor {
private final Resource resource;
OriginTrackedYamlLoader(Resource resource, String profileToLoad,
Predicate<String[]> acceptsProfiles) {
OriginTrackedYamlLoader(Resource resource) {
this.resource = resource;
setDocumentMatchers(new ProfileToLoadDocumentMatcher(profileToLoad),
new AcceptsProfilesDocumentMatcher(acceptsProfiles));
setResources(resource);
}
@ -70,9 +67,11 @@ class OriginTrackedYamlLoader extends YamlProcessor {
return new Yaml(constructor, representer, dumperOptions, resolver);
}
public Map<String, Object> load() {
final Map<String, Object> result = new LinkedHashMap<>();
process((properties, map) -> result.putAll(getFlattenedMap(map)));
public List<Map<String, Object>> load() {
final List<Map<String, Object>> result = new ArrayList<>();
process((properties, map) -> {
result.add(getFlattenedMap(map));
});
return result;
}

View File

@ -1,50 +0,0 @@
/*
* Copyright 2012-2018 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.util.Arrays;
import org.springframework.util.ObjectUtils;
/**
* {@link SpringProfilesDocumentMatcher} that matches a specific profile to load.
*
* @author Phillip Webb
*/
class ProfileToLoadDocumentMatcher extends SpringProfilesDocumentMatcher {
private final String profile;
ProfileToLoadDocumentMatcher(String profile) {
this.profile = profile;
}
@Override
protected boolean matches(String[] profiles) {
String[] positiveProfiles = (profiles == null ? null : Arrays.stream(profiles)
.filter(this::isPositiveProfile).toArray(String[]::new));
if (this.profile == null) {
return ObjectUtils.isEmpty(positiveProfiles);
}
return ObjectUtils.containsElement(positiveProfiles, this.profile);
}
private boolean isPositiveProfile(String profile) {
return !profile.startsWith("!");
}
}

View File

@ -17,8 +17,9 @@
package org.springframework.boot.env;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
@ -41,15 +42,14 @@ public class PropertiesPropertySourceLoader implements PropertySourceLoader {
}
@Override
public PropertySource<?> load(String name, Resource resource, String profileToLoad,
Predicate<String[]> acceptsProfiles) throws IOException {
if (profileToLoad == null) {
Map<String, ?> properties = loadProperties(resource);
if (!properties.isEmpty()) {
return new OriginTrackedMapPropertySource(name, properties);
}
public List<PropertySource<?>> load(String name, Resource resource)
throws IOException {
Map<String, ?> properties = loadProperties(resource);
if (properties.isEmpty()) {
return Collections.emptyList();
}
return null;
return Collections
.singletonList(new OriginTrackedMapPropertySource(name, properties));
}
@SuppressWarnings({ "unchecked", "rawtypes" })

View File

@ -17,7 +17,7 @@
package org.springframework.boot.env;
import java.io.IOException;
import java.util.function.Predicate;
import java.util.List;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
@ -39,17 +39,15 @@ public interface PropertySourceLoader {
String[] getFileExtensions();
/**
* Load the resource into a property source.
* @param name the name of the property source
* Load the resource into one or more property sources. Implementations may either
* return a list containing a single source, or in the case of a multi-document format
* such as yaml a source or each document in the resource.
* @param name the root name of the property source. If multiple documents are loaded
* an additional suffix should be added to the name for each source loaded.
* @param resource the resource to load
* @param profileToLoad 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.
* @param acceptsProfiles predicate to determine if a particular profile is accepted
* @return a property source or {@code null}
* @return a list property sources
* @throws IOException if the source cannot be loaded
*/
PropertySource<?> load(String name, Resource resource, String profileToLoad,
Predicate<String[]> acceptsProfiles) throws IOException;
List<PropertySource<?>> load(String name, Resource resource) throws IOException;
}

View File

@ -1,77 +0,0 @@
/*
* Copyright 2012-2018 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.util.Map;
import java.util.Properties;
import org.springframework.beans.factory.config.YamlProcessor.DocumentMatcher;
import org.springframework.beans.factory.config.YamlProcessor.MatchStatus;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import org.springframework.boot.origin.OriginTrackedValue;
/**
* Base class for {@link DocumentMatcher DocumentMatchers} that check the
* {@code spring.profiles} property.
*
* @author Phillip Webb
* @see OriginTrackedYamlLoader
*/
abstract class SpringProfilesDocumentMatcher implements DocumentMatcher {
@Override
public final MatchStatus matches(Properties properties) {
Binder binder = new Binder(
new OriginTrackedValueConfigurationPropertySource(properties));
String[] profiles = binder.bind("spring.profiles", Bindable.of(String[].class))
.orElse(null);
return (matches(profiles) ? MatchStatus.ABSTAIN : MatchStatus.NOT_FOUND);
}
protected abstract boolean matches(String[] profiles);
/**
* {@link MapConfigurationPropertySource} that deals with unwrapping
* {@link OriginTrackedValue OriginTrackedValues} from the underlying map.
*/
static class OriginTrackedValueConfigurationPropertySource
extends MapConfigurationPropertySource {
OriginTrackedValueConfigurationPropertySource(Map<?, ?> map) {
super(map);
}
@Override
public ConfigurationProperty getConfigurationProperty(
ConfigurationPropertyName name) {
ConfigurationProperty property = super.getConfigurationProperty(name);
if (property != null && property.getValue() instanceof OriginTrackedValue) {
OriginTrackedValue originTrackedValue = (OriginTrackedValue) property
.getValue();
property = new ConfigurationProperty(property.getName(),
originTrackedValue.getValue(), originTrackedValue.getOrigin());
}
return property;
}
}
}

View File

@ -17,8 +17,10 @@
package org.springframework.boot.env;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
@ -39,18 +41,23 @@ public class YamlPropertySourceLoader implements PropertySourceLoader {
}
@Override
public PropertySource<?> load(String name, Resource resource, String profileToLoad,
Predicate<String[]> acceptsProfiles) throws IOException {
public List<PropertySource<?>> load(String name, Resource resource)
throws IOException {
if (!ClassUtils.isPresent("org.yaml.snakeyaml.Yaml", null)) {
throw new IllegalStateException("Attempted to load " + name
+ " but snakeyaml was not found on the classpath");
}
Map<String, Object> source = new OriginTrackedYamlLoader(resource, profileToLoad,
acceptsProfiles).load();
if (!source.isEmpty()) {
return new OriginTrackedMapPropertySource(name, source);
List<Map<String, Object>> loaded = new OriginTrackedYamlLoader(resource).load();
if (loaded.isEmpty()) {
return Collections.emptyList();
}
return null;
List<PropertySource<?>> propertySources = new ArrayList<>(loaded.size());
for (int i = 0; i < loaded.size(); i++) {
propertySources.add(new OriginTrackedMapPropertySource(
name + (loaded.size() == 1 ? "" : " (document #" + i + ")"),
loaded.get(i)));
}
return propertySources;
}
}

View File

@ -540,8 +540,8 @@ public class ConfigFileApplicationListenerTests {
.map(org.springframework.core.env.PropertySource::getName)
.collect(Collectors.toList());
assertThat(names).contains(
"applicationConfig: [classpath:/testsetprofiles.yml]#dev",
"applicationConfig: [classpath:/testsetprofiles.yml]");
"applicationConfig: [classpath:/testsetprofiles.yml] (document #0)",
"applicationConfig: [classpath:/testsetprofiles.yml] (document #1)");
}
@Test

View File

@ -88,6 +88,48 @@ public class ConfigFileApplicationListenerYamlProfileNegationTests {
assertVersionProperty(this.context, "NOT A", "C", "B");
}
@Test
public void yamlProfileCascading() {
SpringApplication application = new SpringApplication(Config.class);
application.setWebApplicationType(WebApplicationType.NONE);
String configName = "--spring.config.name=cascadingprofiles";
this.context = application.run(configName);
assertVersionProperty(this.context, "E", "D", "C", "E", "A", "B");
assertThat(this.context.getEnvironment().getProperty("not-a")).isNull();
assertThat(this.context.getEnvironment().getProperty("not-b")).isNull();
assertThat(this.context.getEnvironment().getProperty("not-c")).isNull();
assertThat(this.context.getEnvironment().getProperty("not-d")).isNull();
assertThat(this.context.getEnvironment().getProperty("not-e")).isNull();
}
@Test
public void yamlProfileCascadingOverrideProfilesA() {
SpringApplication application = new SpringApplication(Config.class);
application.setWebApplicationType(WebApplicationType.NONE);
String configName = "--spring.config.name=cascadingprofiles";
this.context = application.run(configName, "--spring.profiles.active=A");
assertVersionProperty(this.context, "E", "C", "E", "A");
assertThat(this.context.getEnvironment().getProperty("not-a")).isNull();
assertThat(this.context.getEnvironment().getProperty("not-b")).isEqualTo("true");
assertThat(this.context.getEnvironment().getProperty("not-c")).isNull();
assertThat(this.context.getEnvironment().getProperty("not-d")).isEqualTo("true");
assertThat(this.context.getEnvironment().getProperty("not-e")).isNull();
}
@Test
public void yamlProfileCascadingOverrideProfilesB() {
SpringApplication application = new SpringApplication(Config.class);
application.setWebApplicationType(WebApplicationType.NONE);
String configName = "--spring.config.name=cascadingprofiles";
this.context = application.run(configName, "--spring.profiles.active=B");
assertVersionProperty(this.context, "E", "D", "E", "B");
assertThat(this.context.getEnvironment().getProperty("not-a")).isEqualTo("true");
assertThat(this.context.getEnvironment().getProperty("not-b")).isNull();
assertThat(this.context.getEnvironment().getProperty("not-c")).isEqualTo("true");
assertThat(this.context.getEnvironment().getProperty("not-d")).isNull();
assertThat(this.context.getEnvironment().getProperty("not-e")).isNull();
}
private void assertVersionProperty(ConfigurableApplicationContext context,
String expectedVersion, String... expectedActiveProfiles) {
assertThat(context.getEnvironment().getActiveProfiles())

View File

@ -1,72 +0,0 @@
/*
* Copyright 2012-2018 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.util.Properties;
import java.util.function.Predicate;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.config.YamlProcessor.MatchStatus;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link AcceptsProfilesDocumentMatcher}.
*
* @author Phillip Webb
*/
public class AcceptsProfilesDocumentMatcherTests {
@Mock
private Predicate<String[]> acceptsProfiles;
private AcceptsProfilesDocumentMatcher matcher;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
this.matcher = new AcceptsProfilesDocumentMatcher(this.acceptsProfiles);
}
@Test
public void matchesWhenHasNoProfilePropertyShouldReturnAbstain() {
Properties properties = new Properties();
assertThat(this.matcher.matches(properties)).isEqualTo(MatchStatus.ABSTAIN);
}
@Test
public void matchesWhenAcceptsProfileShouldReturnAbstain() {
Properties properties = new Properties();
properties.put("spring.profiles", "foo");
given(this.acceptsProfiles.test(new String[] { "foo" })).willReturn(true);
assertThat(this.matcher.matches(properties)).isEqualTo(MatchStatus.ABSTAIN);
}
@Test
public void matchesWhenDoesNotAcceptProfileShouldReturnNotFound() {
Properties properties = new Properties();
properties.put("spring.profiles", "foo");
given(this.acceptsProfiles.test(new String[] { "foo" })).willReturn(false);
assertThat(this.matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND);
}
}

View File

@ -46,7 +46,7 @@ public class NoSnakeYamlPropertySourceLoaderTests {
"Attempted to load resource but snakeyaml was not found on the classpath");
ByteArrayResource resource = new ByteArrayResource(
"foo:\n bar: spam".getBytes());
this.loader.load("resource", resource, null, (profile) -> true);
this.loader.load("resource", resource);
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.boot.env;
import java.util.List;
import java.util.Map;
import org.junit.Before;
@ -38,12 +39,12 @@ public class OriginTrackedYamlLoaderTests {
private OriginTrackedYamlLoader loader;
private Map<String, Object> result;
private List<Map<String, Object>> result;
@Before
public void setUp() {
Resource resource = new ClassPathResource("test-yaml.yml", getClass());
this.loader = new OriginTrackedYamlLoader(resource, null, (profile) -> true);
this.loader = new OriginTrackedYamlLoader(resource);
}
@Test
@ -90,15 +91,6 @@ public class OriginTrackedYamlLoaderTests {
assertThat(getLocation(education)).isEqualTo("16:12");
}
@Test
public void processWithActiveProfile() {
Resource resource = new ClassPathResource("test-yaml.yml", getClass());
this.loader = new OriginTrackedYamlLoader(resource, "development",
(profile) -> true);
Map<String, Object> result = this.loader.load();
assertThat(result.get("name").toString()).isEqualTo("Test Name");
}
@Test
public void processListOfMaps() {
OriginTrackedValue name = getValue("example.foo[0].name");
@ -129,7 +121,7 @@ public class OriginTrackedYamlLoaderTests {
if (this.result == null) {
this.result = this.loader.load();
}
return (OriginTrackedValue) this.result.get(name);
return (OriginTrackedValue) this.result.get(0).get(name);
}
private String getLocation(OriginTrackedValue value) {

View File

@ -1,80 +0,0 @@
/*
* Copyright 2012-2018 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.util.Properties;
import org.junit.Test;
import org.springframework.beans.factory.config.YamlProcessor.MatchStatus;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ProfileToLoadDocumentMatcher}.
*
* @author Phillip Webb
*/
public class ProfileToLoadDocumentMatcherTests {
@Test
public void matchesWhenProfilesIsNullAndHasNoProfilePropertiesShouldReturnAbstain() {
ProfileToLoadDocumentMatcher matcher = new ProfileToLoadDocumentMatcher(null);
Properties properties = new Properties();
assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.ABSTAIN);
}
@Test
public void matchesWhenProfileIsNullAndHasOnlyNegativeProfilePropertiesShouldReturnAbstain() {
ProfileToLoadDocumentMatcher matcher = new ProfileToLoadDocumentMatcher(null);
Properties properties = new Properties();
properties.put("spring.profiles", "!foo,!bar");
assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.ABSTAIN);
}
@Test
public void matchesWhenProfileIsNullAndHasProfilePropertyShouldReturnNotFound() {
ProfileToLoadDocumentMatcher matcher = new ProfileToLoadDocumentMatcher(null);
Properties properties = new Properties();
properties.put("spring.profiles", "!foo,!bar,baz");
assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND);
}
@Test
public void matchesWhenProfilesIsSetAndHasNoProfilePropertiesShouldReturnNotFound() {
ProfileToLoadDocumentMatcher matcher = new ProfileToLoadDocumentMatcher("bar");
Properties properties = new Properties();
assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND);
}
@Test
public void matchesWhenProfileIsSetAndHasOnlyNegativeProfilePropertiesShouldReturnNotFound() {
ProfileToLoadDocumentMatcher matcher = new ProfileToLoadDocumentMatcher("bar");
Properties properties = new Properties();
properties.put("spring.profiles", "!foo,!bar,baz");
assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND);
}
@Test
public void matchesWhenProfileIsSetAndHasProfilePropertyShouldReturnAbstain() {
ProfileToLoadDocumentMatcher matcher = new ProfileToLoadDocumentMatcher("bar");
Properties properties = new Properties();
properties.put("spring.profiles", "!foo,bar,baz");
assertThat(matcher.matches(properties)).isEqualTo(MatchStatus.ABSTAIN);
}
}

View File

@ -16,6 +16,8 @@
package org.springframework.boot.env;
import java.util.List;
import org.junit.Test;
import org.springframework.core.env.PropertySource;
@ -41,17 +43,17 @@ public class PropertiesPropertySourceLoaderTests {
@Test
public void loadProperties() throws Exception {
PropertySource<?> source = this.loader.load("test.properties",
new ClassPathResource("test-properties.properties", getClass()), null,
(profile) -> true);
List<PropertySource<?>> loaded = this.loader.load("test.properties",
new ClassPathResource("test-properties.properties", getClass()));
PropertySource<?> source = loaded.get(0);
assertThat(source.getProperty("test")).isEqualTo("properties");
}
@Test
public void loadXml() throws Exception {
PropertySource<?> source = this.loader.load("test.xml",
new ClassPathResource("test-xml.xml", getClass()), null,
(profile) -> true);
List<PropertySource<?>> loaded = this.loader.load("test.xml",
new ClassPathResource("test-xml.xml", getClass()));
PropertySource<?> source = loaded.get(0);
assertThat(source.getProperty("test")).isEqualTo("xml");
}

View File

@ -1,93 +0,0 @@
/*
* Copyright 2012-2018 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.util.Properties;
import org.junit.Test;
import org.springframework.beans.factory.config.YamlProcessor.MatchStatus;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.util.ObjectUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link SpringProfilesDocumentMatcher}.
*
* @author Phillip Webb
*/
public class SpringProfilesDocumentMatcherTests {
private TestSpringProfilesDocumentMatcher matcher = new TestSpringProfilesDocumentMatcher();
@Test
public void matchesShouldBindAgainstCommaList() {
Properties properties = new Properties();
properties.put("spring.profiles", "foo,bar");
this.matcher.matches(properties);
assertThat(this.matcher.getProfiles()).containsExactly("foo", "bar");
}
@Test
public void matchesShouldBindAgainstYamlList() {
Properties properties = new Properties();
properties.put("spring.profiles[0]", "foo");
properties.put("spring.profiles[1]", "bar");
this.matcher.matches(properties);
assertThat(this.matcher.getProfiles()).containsExactly("foo", "bar");
}
@Test
public void matchesShouldBindAgainstOriginTrackedValue() {
Properties properties = new Properties();
properties.put("spring.profiles", OriginTrackedValue.of("foo,bar"));
this.matcher.matches(properties);
assertThat(this.matcher.getProfiles()).containsExactly("foo", "bar");
}
@Test
public void matchesWhenMatchShouldReturnAbstain() {
Properties properties = new Properties();
properties.put("spring.profiles", "foo,bar");
assertThat(this.matcher.matches(properties)).isEqualTo(MatchStatus.ABSTAIN);
}
@Test
public void matchesWhenNoMatchShouldReturnNotFound() {
Properties properties = new Properties();
assertThat(this.matcher.matches(properties)).isEqualTo(MatchStatus.NOT_FOUND);
}
private static class TestSpringProfilesDocumentMatcher
extends SpringProfilesDocumentMatcher {
private String[] profiles;
@Override
protected boolean matches(String[] profiles) {
this.profiles = profiles;
return !ObjectUtils.isEmpty(profiles);
}
public String[] getProfiles() {
return this.profiles;
}
}
}

View File

@ -26,7 +26,6 @@ import org.springframework.core.env.PropertySource;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
@ -46,8 +45,7 @@ public class YamlPropertySourceLoaderTests {
public void load() throws Exception {
ByteArrayResource resource = new ByteArrayResource(
"foo:\n bar: spam".getBytes());
PropertySource<?> source = this.loader.load("resource", resource, null,
(profile) -> true);
PropertySource<?> source = this.loader.load("resource", resource).get(0);
assertThat(source).isNotNull();
assertThat(source.getProperty("foo.bar")).isEqualTo("spam");
}
@ -62,7 +60,7 @@ public class YamlPropertySourceLoaderTests {
}
ByteArrayResource resource = new ByteArrayResource(yaml.toString().getBytes());
EnumerablePropertySource<?> source = (EnumerablePropertySource<?>) this.loader
.load("resource", resource, null, (profile) -> true);
.load("resource", resource).get(0);
assertThat(source).isNotNull();
assertThat(source.getPropertyNames())
.isEqualTo(StringUtils.toStringArray(expected));
@ -75,18 +73,16 @@ public class YamlPropertySourceLoaderTests {
yaml.append("---\n");
yaml.append("foo:\n baz: wham\n");
ByteArrayResource resource = new ByteArrayResource(yaml.toString().getBytes());
PropertySource<?> source = this.loader.load("resource", resource, null,
(profile) -> true);
assertThat(source).isNotNull();
assertThat(source.getProperty("foo.bar")).isEqualTo("spam");
assertThat(source.getProperty("foo.baz")).isEqualTo("wham");
List<PropertySource<?>> loaded = this.loader.load("resource", resource);
assertThat(loaded).hasSize(2);
assertThat(loaded.get(0).getProperty("foo.bar")).isEqualTo("spam");
assertThat(loaded.get(1).getProperty("foo.baz")).isEqualTo("wham");
}
@Test
public void timestampLikeItemsDoNotBecomeDates() throws Exception {
ByteArrayResource resource = new ByteArrayResource("foo: 2015-01-28".getBytes());
PropertySource<?> source = this.loader.load("resource", resource, null,
(profile) -> true);
PropertySource<?> source = this.loader.load("resource", resource).get(0);
assertThat(source).isNotNull();
assertThat(source.getProperty("foo")).isEqualTo("2015-01-28");
}
@ -94,42 +90,13 @@ public class YamlPropertySourceLoaderTests {
@Test
public void loadOriginAware() throws Exception {
Resource resource = new ClassPathResource("test-yaml.yml", getClass());
PropertySource<?> source = this.loader.load("resource", resource, null,
(profile) -> true);
EnumerablePropertySource<?> enumerableSource = (EnumerablePropertySource<?>) source;
for (String name : enumerableSource.getPropertyNames()) {
System.out.println(name + " = " + enumerableSource.getProperty(name));
List<PropertySource<?>> loaded = this.loader.load("resource", resource);
for (PropertySource<?> source : loaded) {
EnumerablePropertySource<?> enumerableSource = (EnumerablePropertySource<?>) source;
for (String name : enumerableSource.getPropertyNames()) {
System.out.println(name + " = " + enumerableSource.getProperty(name));
}
}
}
@Test
public void loadSpecificProfile() throws Exception {
StringBuilder yaml = new StringBuilder();
yaml.append("foo:\n bar: spam\n");
yaml.append("---\n");
yaml.append("spring:\n profiles: foo\n");
yaml.append("foo:\n bar: wham\n");
ByteArrayResource resource = new ByteArrayResource(yaml.toString().getBytes());
PropertySource<?> source = this.loader.load("resource", resource, "foo",
(profile) -> true);
assertThat(source).isNotNull();
assertThat(source.getProperty("foo.bar")).isEqualTo("wham");
}
@Test
public void loadWithAcceptProfile() throws Exception {
StringBuilder yaml = new StringBuilder();
yaml.append("---\n");
yaml.append("spring:\n profiles: yay,foo\n");
yaml.append("foo:\n bar: bang\n");
yaml.append("---\n");
yaml.append("spring:\n profiles: yay,!foo\n");
yaml.append("foo:\n bar: wham\n");
ByteArrayResource resource = new ByteArrayResource(yaml.toString().getBytes());
PropertySource<?> source = this.loader.load("resource", resource, "yay",
(profiles) -> ObjectUtils.containsElement(profiles, "!foo"));
assertThat(source).isNotNull();
assertThat(source.getProperty("foo.bar")).isEqualTo("wham");
}
}

View File

@ -0,0 +1,53 @@
spring:
profiles:
active:
- A
- B
---
spring.profiles: A
spring:
profiles:
include:
- C
- E
---
spring.profiles: B
spring:
profiles:
include:
- D
- E
---
spring.profiles: E
version: E
---
spring.profiles: "!A"
not-a: true
---
spring.profiles: "!B"
not-b: true
---
spring.profiles: "!C"
not-c: true
---
spring.profiles: "!D"
not-d: true
---
spring.profiles: "!E"
not-e: true