diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java index b9f28ea082..053b0f64f0 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java @@ -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 boundProperties; + /** * Create a new {@link ConfigDataProperties} instance. * @param imports the imports requested * @param activate the activate properties */ ConfigDataProperties(@Name("import") List imports, Activate activate) { + this(imports, activate, Collections.emptyList()); + } + + private ConfigDataProperties(List imports, Activate activate, List boundProperties) { this.imports = (imports != null) ? imports : Collections.emptyList(); this.activate = activate; + this.boundProperties = mapByName(boundProperties); + } + + private Map mapByName( + List boundProperties) { + Map 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 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 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; } /** diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLocation.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLocation.java index 3210767eb8..4c42c2eb5a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLocation.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLocation.java @@ -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> 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 diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLocationResolver.java index 763dcee10e..241eee7145 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLocationResolver.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLocationResolver.java @@ -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 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 getProfileSpecificResolvables(ConfigDataLocationResolverContext context, String location, boolean optional, Profiles profiles) { + Origin origin = context.getLocationOrigin(location); Set 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 getResolvables(String resourceLocation, boolean optional, String profile) { + private Set 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 getResolvablesForDirectory(String directoryLocation, boolean optional, String profile) { + private Set getResolvablesForDirectory(String directoryLocation, boolean optional, String profile, + Origin origin) { Set 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 getResolvablesForFile(String fileLocation, boolean optional, String profile) { + private Set 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 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 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 matchingPropertySource(final String sourceName) { return new Condition("environment containing property source " + sourceName) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java index f3674ff2d9..ba68719b4c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataPropertiesTests.java @@ -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"); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/OptionalConfigDataLocationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/OptionalConfigDataLocationTests.java index 0fff868cba..0216337f2d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/OptionalConfigDataLocationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/OptionalConfigDataLocationTests.java @@ -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 diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLoaderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLoaderTests.java index 6d254e446b..2168e2810b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLoaderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLoaderTests.java @@ -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); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLocationResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLocationResolverTests.java index 936ce9a8dd..6ed35ee72b 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLocationResolverTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLocationResolverTests.java @@ -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 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 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'") diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLocationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLocationTests.java index 16e3f431c9..538f73b6fc 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLocationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ResourceConfigDataLocationTests.java @@ -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); }