Add parent origin support to config data

Allow `ConfigDataLocationResolvers` to access `Origin` information for
locations so that they can be used as a parent origin of loaded items.

The `ResourceConfigData...` classes have been reworked so that loaded
`PropertySources` include the parent origin.

See gh-23018
pull/23127/head
Phillip Webb 4 years ago
parent 960651c15a
commit 3c1e141aef

@ -28,6 +28,7 @@ import java.util.stream.StreamSupport;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
@ -137,6 +138,10 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
return (this.properties != null) ? this.properties.getImports() : Collections.emptyList();
}
Origin getImportOrigin(String importLocation) {
return (this.properties != null) ? this.properties.getImportOrigin(importLocation) : null;
}
/**
* Return true if this contributor has imports that have not yet been processed in the
* given phase.

@ -39,6 +39,7 @@ import org.springframework.boot.context.properties.source.ConfigurationPropertyN
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.env.BootstrapRegistry;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.boot.origin.Origin;
import org.springframework.core.log.LogMessage;
import org.springframework.util.ObjectUtils;
@ -267,6 +268,11 @@ class ConfigDataEnvironmentContributors implements Iterable<ConfigDataEnvironmen
return this.contributors.getBootstrapRegistry();
}
@Override
public Origin getLocationOrigin(String location) {
return this.contributor.getImportOrigin(location);
}
}
private class InactiveSourceChecker implements BindHandler {

@ -19,6 +19,7 @@ package org.springframework.boot.context.config;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.BootstrapRegistry;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.origin.Origin;
/**
* Context provided to {@link ConfigDataLocationResolver} methods.
@ -50,4 +51,11 @@ public interface ConfigDataLocationResolverContext {
*/
BootstrapRegistry getBootstrapRegistry();
/**
* Return the {@link Origin} of a location that's being resolved.
* @param location the location being resolved
* @return the {@link Origin} of the location or {@code null}
*/
Origin getLocationOrigin(String location);
}

@ -16,8 +16,11 @@
package org.springframework.boot.context.config;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.springframework.boot.cloud.CloudPlatform;
@ -25,9 +28,11 @@ import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.bind.BoundPropertiesTrackingBindHandler;
import org.springframework.boot.context.properties.bind.Name;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.origin.Origin;
import org.springframework.util.ObjectUtils;
/**
@ -40,6 +45,8 @@ class ConfigDataProperties {
private static final ConfigurationPropertyName NAME = ConfigurationPropertyName.of("spring.config");
private static final ConfigurationPropertyName IMPORT_NAME = ConfigurationPropertyName.of("spring.config.import");
private static final ConfigurationPropertyName LEGACY_PROFILES_NAME = ConfigurationPropertyName
.of("spring.profiles");
@ -51,14 +58,28 @@ class ConfigDataProperties {
private final Activate activate;
private final Map<ConfigurationPropertyName, ConfigurationProperty> boundProperties;
/**
* Create a new {@link ConfigDataProperties} instance.
* @param imports the imports requested
* @param activate the activate properties
*/
ConfigDataProperties(@Name("import") List<String> imports, Activate activate) {
this(imports, activate, Collections.emptyList());
}
private ConfigDataProperties(List<String> imports, Activate activate, List<ConfigurationProperty> boundProperties) {
this.imports = (imports != null) ? imports : Collections.emptyList();
this.activate = activate;
this.boundProperties = mapByName(boundProperties);
}
private Map<ConfigurationPropertyName, ConfigurationProperty> mapByName(
List<ConfigurationProperty> boundProperties) {
Map<ConfigurationPropertyName, ConfigurationProperty> result = new LinkedHashMap<>();
boundProperties.forEach((property) -> result.put(property.getName(), property));
return Collections.unmodifiableMap(result);
}
/**
@ -69,6 +90,21 @@ class ConfigDataProperties {
return this.imports;
}
/**
* Return the {@link Origin} of a given import location.
* @param importLocation the import location to check
* @return the origin of the import or {@code null}
*/
Origin getImportOrigin(String importLocation) {
int index = this.imports.indexOf(importLocation);
if (index == -1) {
return null;
}
ConfigurationProperty bound = this.boundProperties.get(IMPORT_NAME.append("[" + index + "]"));
bound = (bound != null) ? bound : this.boundProperties.get(IMPORT_NAME);
return (bound != null) ? bound.getOrigin() : null;
}
/**
* Return {@code true} if the properties indicate that the config data property source
* is active for the given activation context.
@ -94,6 +130,10 @@ class ConfigDataProperties {
return new ConfigDataProperties(this.imports, new Activate(this.activate.onCloudPlatform, legacyProfiles));
}
ConfigDataProperties withBoundProperties(List<ConfigurationProperty> boundProperties) {
return new ConfigDataProperties(this.imports, this.activate, boundProperties);
}
/**
* Factory method used to create {@link ConfigDataProperties} from the given
* {@link Binder}.
@ -104,13 +144,16 @@ class ConfigDataProperties {
LegacyProfilesBindHandler legacyProfilesBindHandler = new LegacyProfilesBindHandler();
String[] legacyProfiles = binder.bind(LEGACY_PROFILES_NAME, BINDABLE_STRING_ARRAY, legacyProfilesBindHandler)
.orElse(null);
ConfigDataProperties properties = binder.bind(NAME, BINDABLE_PROPERTIES).orElse(null);
List<ConfigurationProperty> boundProperties = new ArrayList<>();
ConfigDataProperties properties = binder
.bind(NAME, BINDABLE_PROPERTIES, new BoundPropertiesTrackingBindHandler(boundProperties::add))
.orElse(null);
if (!ObjectUtils.isEmpty(legacyProfiles)) {
properties = (properties != null)
? properties.withLegacyProfiles(legacyProfiles, legacyProfilesBindHandler.getProperty())
: new ConfigDataProperties(null, new Activate(null, legacyProfiles));
}
return properties;
return (properties != null) ? properties.withBoundProperties(boundProperties) : null;
}
/**

@ -20,6 +20,8 @@ import java.io.IOException;
import java.util.List;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginTrackedResource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.FileUrlResource;
@ -38,20 +40,25 @@ class ResourceConfigDataLocation extends ConfigDataLocation {
private final Resource resource;
private final Origin origin;
private final PropertySourceLoader propertySourceLoader;
/**
* Create a new {@link ResourceConfigDataLocation} instance.
* @param name the source location
* @param resource the underlying resource
* @param origin the origin of the resource
* @param propertySourceLoader the loader that should be used to load the resource
*/
ResourceConfigDataLocation(String name, Resource resource, PropertySourceLoader propertySourceLoader) {
ResourceConfigDataLocation(String name, Resource resource, Origin origin,
PropertySourceLoader propertySourceLoader) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(resource, "Resource must not be null");
Assert.notNull(propertySourceLoader, "PropertySourceLoader must not be null");
this.name = name;
this.resource = resource;
this.origin = origin;
this.propertySourceLoader = propertySourceLoader;
}
@ -64,7 +71,8 @@ class ResourceConfigDataLocation extends ConfigDataLocation {
}
List<PropertySource<?>> load() throws IOException {
return this.propertySourceLoader.load(this.name, this.resource);
Resource resource = OriginTrackedResource.of(this.resource, this.origin);
return this.propertySourceLoader.load(this.name, resource);
}
@Override

@ -32,6 +32,7 @@ import org.apache.commons.logging.Log;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.origin.Origin;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.core.io.FileSystemResource;
@ -126,12 +127,13 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
private Set<Resolvable> getResolvables(ConfigDataLocationResolverContext context, String location,
boolean optional) {
Origin origin = context.getLocationOrigin(location);
String resourceLocation = getResourceLocation(context, location);
try {
if (isDirectoryLocation(resourceLocation)) {
return getResolvablesForDirectory(resourceLocation, optional, NO_PROFILE);
return getResolvablesForDirectory(resourceLocation, optional, NO_PROFILE, origin);
}
return getResolvablesForFile(resourceLocation, optional, NO_PROFILE);
return getResolvablesForFile(resourceLocation, optional, NO_PROFILE, origin);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Unable to load config data from '" + location + "'", ex);
@ -140,10 +142,11 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
private Set<Resolvable> getProfileSpecificResolvables(ConfigDataLocationResolverContext context, String location,
boolean optional, Profiles profiles) {
Origin origin = context.getLocationOrigin(location);
Set<Resolvable> resolvables = new LinkedHashSet<>();
String resourceLocation = getResourceLocation(context, location);
for (String profile : profiles) {
resolvables.addAll(getResolvables(resourceLocation, optional, profile));
resolvables.addAll(getResolvables(resourceLocation, optional, profile, origin));
}
return resolvables;
}
@ -163,21 +166,22 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
return resourceLocation;
}
private Set<Resolvable> getResolvables(String resourceLocation, boolean optional, String profile) {
private Set<Resolvable> getResolvables(String resourceLocation, boolean optional, String profile, Origin origin) {
if (isDirectoryLocation(resourceLocation)) {
return getResolvablesForDirectory(resourceLocation, optional, profile);
return getResolvablesForDirectory(resourceLocation, optional, profile, origin);
}
return getResolvablesForFile(resourceLocation, optional, profile);
return getResolvablesForFile(resourceLocation, optional, profile, origin);
}
private Set<Resolvable> getResolvablesForDirectory(String directoryLocation, boolean optional, String profile) {
private Set<Resolvable> getResolvablesForDirectory(String directoryLocation, boolean optional, String profile,
Origin origin) {
Set<Resolvable> resolvables = new LinkedHashSet<>();
for (String name : this.configNames) {
String rootLocation = directoryLocation + name;
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String extension : loader.getFileExtensions()) {
Resolvable resolvable = new Resolvable(directoryLocation, rootLocation, optional, profile,
extension, loader);
extension, origin, loader);
resolvables.add(resolvable);
}
}
@ -185,7 +189,8 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
return resolvables;
}
private Set<Resolvable> getResolvablesForFile(String fileLocation, boolean optional, String profile) {
private Set<Resolvable> getResolvablesForFile(String fileLocation, boolean optional, String profile,
Origin origin) {
Matcher extensionHintMatcher = EXTENSION_HINT_PATTERN.matcher(fileLocation);
boolean extensionHintLocation = extensionHintMatcher.matches();
if (extensionHintLocation) {
@ -196,7 +201,7 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
if (extension != null) {
String root = fileLocation.substring(0, fileLocation.length() - extension.length() - 1);
return Collections.singleton(new Resolvable(null, root, optional, profile,
(!extensionHintLocation) ? extension : null, loader));
(!extensionHintLocation) ? extension : null, origin, loader));
}
}
throw new IllegalStateException("File extension is not known to any PropertySourceLoader. "
@ -230,7 +235,7 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
private void assertNonOptionalDirectories(String location, Set<Resolvable> resolvables) {
for (Resolvable resolvable : resolvables) {
if (resolvable.isNonOptionalDirectory()) {
Resource resource = this.resourceLoader.getResource(resolvable.getDirectory());
Resource resource = loadResource(resolvable.getDirectory());
ResourceConfigDataLocation resourceLocation = createConfigResourceLocation(location, resolvable,
resource);
ConfigDataLocationNotFoundException.throwIfDoesNotExist(resourceLocation, resource);
@ -276,7 +281,7 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
Resource resource) {
String name = String.format("Resource config '%s' imported via location \"%s\"",
resolvable.getResourceLocation(), location);
return new ResourceConfigDataLocation(name, resource, resolvable.getLoader());
return new ResourceConfigDataLocation(name, resource, resolvable.getOrigin(), resolvable.getLoader());
}
private void validatePatternLocation(String resourceLocation) {
@ -346,16 +351,19 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
private final String profile;
private Origin origin;
private final PropertySourceLoader loader;
Resolvable(String directory, String rootLocation, boolean optional, String profile, String extension,
PropertySourceLoader loader) {
Origin origin, PropertySourceLoader loader) {
String profileSuffix = (StringUtils.hasText(profile)) ? "-" + profile : "";
this.directory = directory;
this.resourceLocation = rootLocation + profileSuffix + ((extension != null) ? "." + extension : "");
this.optional = optional;
this.profile = profile;
this.loader = loader;
this.origin = origin;
}
boolean isNonOptionalDirectory() {
@ -378,6 +386,10 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
return this.resourceLocation;
}
Origin getOrigin() {
return this.origin;
}
PropertySourceLoader getLoader() {
return this.loader;
}

@ -19,6 +19,7 @@ package org.springframework.boot.context.config;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -35,7 +36,14 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindException;
import org.springframework.boot.context.properties.bind.BindHandler;
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.origin.Origin;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.ConfigurableEnvironment;
@ -577,6 +585,29 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
.run("--spring.config.location=classpath:application-include-profiles-in-profile-specific.properties"));
}
@Test
void runWhenImportingIncludesParentOrigin() {
ConfigurableApplicationContext context = this.application
.run("--spring.config.location=classpath:application-import-with-placeholder.properties");
Binder binder = Binder.get(context.getEnvironment());
List<ConfigurationProperty> properties = new ArrayList<>();
BindHandler bindHandler = new BindHandler() {
@Override
public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context,
Object result) {
properties.add(context.getConfigurationProperty());
return result;
}
};
binder.bind("my.value", Bindable.of(String.class), bindHandler);
assertThat(properties).hasSize(1);
Origin origin = properties.get(0).getOrigin();
assertThat(origin.toString()).contains("application-import-with-placeholder-imported");
assertThat(origin.getParent().toString()).contains("application-import-with-placeholder");
}
private Condition<ConfigurableEnvironment> matchingPropertySource(final String sourceName) {
return new Condition<ConfigurableEnvironment>("environment containing property source " + sourceName) {

@ -189,6 +189,28 @@ class ConfigDataPropertiesTests {
.isThrownBy(() -> ConfigDataProperties.get(binder));
}
@Test
void getImportOriginWhenCommaListReturnsOrigin() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource();
source.put("spring.config.import", "one,two,three");
Binder binder = new Binder(source);
ConfigDataProperties properties = ConfigDataProperties.get(binder);
assertThat(properties.getImportOrigin("two"))
.hasToString("\"spring.config.import\" from property source \"source\"");
}
@Test
void getImportOriginWhenBracketListReturnsOrigin() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource();
source.put("spring.config.import[0]", "one");
source.put("spring.config.import[1]", "two");
source.put("spring.config.import[2]", "three");
Binder binder = new Binder(source);
ConfigDataProperties properties = ConfigDataProperties.get(binder);
assertThat(properties.getImportOrigin("two"))
.hasToString("\"spring.config.import[1]\" from property source \"source\"");
}
private Profiles createTestProfiles() {
MockEnvironment environment = new MockEnvironment();
environment.setActiveProfiles("a", "b", "c");

@ -41,7 +41,7 @@ class OptionalConfigDataLocationTests {
@BeforeEach
void setup() {
this.location = new ResourceConfigDataLocation("classpath:application.properties",
new ClassPathResource("application.properties"), mock(PropertySourceLoader.class));
new ClassPathResource("application.properties"), null, mock(PropertySourceLoader.class));
}
@Test

@ -43,7 +43,7 @@ public class ResourceConfigDataLoaderTests {
@Test
void loadWhenLocationResultsInMultiplePropertySourcesAddsAllToConfigData() throws IOException {
ResourceConfigDataLocation location = new ResourceConfigDataLocation("application.yml",
new ClassPathResource("configdata/yaml/application.yml"), new YamlPropertySourceLoader());
new ClassPathResource("configdata/yaml/application.yml"), null, new YamlPropertySourceLoader());
ConfigData configData = this.loader.load(this.loaderContext, location);
assertThat(configData.getPropertySources().size()).isEqualTo(2);
PropertySource<?> source1 = configData.getPropertySources().get(0);
@ -57,7 +57,7 @@ public class ResourceConfigDataLoaderTests {
@Test
void loadWhenPropertySourceIsEmptyAddsNothingToConfigData() throws IOException {
ResourceConfigDataLocation location = new ResourceConfigDataLocation("testproperties.properties",
new ClassPathResource("config/0-empty/testproperties.properties"),
new ClassPathResource("config/0-empty/testproperties.properties"), null,
new PropertiesPropertySourceLoader());
ConfigData configData = this.loader.load(this.loaderContext, location);
assertThat(configData.getPropertySources().size()).isEqualTo(0);

@ -170,7 +170,7 @@ public class ResourceConfigDataLocationResolverTests {
this.resourceLoader);
ClassPathResource parentResource = new ClassPathResource("configdata/properties/application.properties");
ResourceConfigDataLocation parent = new ResourceConfigDataLocation(
"classpath:/configdata/properties/application.properties", parentResource,
"classpath:/configdata/properties/application.properties", parentResource, null,
new PropertiesPropertySourceLoader());
given(this.context.getParent()).willReturn(parent);
List<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location, true);
@ -187,7 +187,7 @@ public class ResourceConfigDataLocationResolverTests {
this.resourceLoader);
ClassPathResource parentResource = new ClassPathResource("config/specific.properties");
ResourceConfigDataLocation parent = new ResourceConfigDataLocation("classpath:/config/specific.properties",
parentResource, new PropertiesPropertySourceLoader());
parentResource, null, new PropertiesPropertySourceLoader());
given(this.context.getParent()).willReturn(parent);
List<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location, true);
assertThat(locations.size()).isEqualTo(1);
@ -200,7 +200,8 @@ public class ResourceConfigDataLocationResolverTests {
String location = "application.other";
ClassPathResource parentResource = new ClassPathResource("configdata/application.properties");
ResourceConfigDataLocation parent = new ResourceConfigDataLocation(
"classpath:/configdata/application.properties", parentResource, new PropertiesPropertySourceLoader());
"classpath:/configdata/application.properties", parentResource, null,
new PropertiesPropertySourceLoader());
given(this.context.getParent()).willReturn(parent);
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true))
.withMessageStartingWith("Unable to load config data from 'application.other'")

@ -38,36 +38,36 @@ public class ResourceConfigDataLocationTests {
private final Resource resource = mock(Resource.class);
private final PropertySourceLoader propertySource = mock(PropertySourceLoader.class);
private final PropertySourceLoader propertySourceLoader = mock(PropertySourceLoader.class);
@Test
void constructorWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceConfigDataLocation(null, this.resource, this.propertySource))
.isThrownBy(() -> new ResourceConfigDataLocation(null, this.resource, null, this.propertySourceLoader))
.withMessage("Name must not be null");
}
@Test
void constructorWhenResourceIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceConfigDataLocation(this.location, null, this.propertySource))
.isThrownBy(() -> new ResourceConfigDataLocation(this.location, null, null, this.propertySourceLoader))
.withMessage("Resource must not be null");
}
@Test
void constructorWhenLoaderIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceConfigDataLocation(this.location, this.resource, null))
.isThrownBy(() -> new ResourceConfigDataLocation(this.location, this.resource, null, null))
.withMessage("PropertySourceLoader must not be null");
}
@Test
void equalsWhenResourceIsTheSameReturnsTrue() {
Resource resource = new ClassPathResource("config/");
ResourceConfigDataLocation location = new ResourceConfigDataLocation("my-location", resource,
this.propertySource);
ResourceConfigDataLocation other = new ResourceConfigDataLocation("other-location", resource,
this.propertySource);
ResourceConfigDataLocation location = new ResourceConfigDataLocation("my-location", resource, null,
this.propertySourceLoader);
ResourceConfigDataLocation other = new ResourceConfigDataLocation("other-location", resource, null,
this.propertySourceLoader);
assertThat(location).isEqualTo(other);
}
@ -75,10 +75,10 @@ public class ResourceConfigDataLocationTests {
void equalsWhenResourceIsDifferentReturnsFalse() {
Resource resource1 = new ClassPathResource("config/");
Resource resource2 = new ClassPathResource("configdata/");
ResourceConfigDataLocation location = new ResourceConfigDataLocation("my-location", resource1,
this.propertySource);
ResourceConfigDataLocation other = new ResourceConfigDataLocation("other-location", resource2,
this.propertySource);
ResourceConfigDataLocation location = new ResourceConfigDataLocation("my-location", resource1, null,
this.propertySourceLoader);
ResourceConfigDataLocation other = new ResourceConfigDataLocation("other-location", resource2, null,
this.propertySourceLoader);
assertThat(location).isNotEqualTo(other);
}

Loading…
Cancel
Save