From 0e3ef4071efbb9d6756f19cedc604880892ad830 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 17 May 2021 21:27:52 -0700 Subject: [PATCH] Allow optional ConfigDataLocationResolver results Update `ConfigData` so that it signal if is considered optional. This update allows `ConfigDataLocationResolvers` to return results that behave in the same way as `optional:` prefixed locations without the user themselves needing to prefix the location string. Closes gh-25894 --- .../context/config/ConfigDataEnvironment.java | 12 +++-- .../context/config/ConfigDataImporter.java | 26 +++++++--- .../context/config/ConfigDataResource.java | 24 +++++++++- ...ironmentPostProcessorIntegrationTests.java | 47 +++++++++++++++++++ .../ConfigDataLocationResolversTests.java | 35 ++++++++++++-- .../test/resources/META-INF/spring.factories | 2 + 6 files changed, 131 insertions(+), 15 deletions(-) diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java index 0e04b1ce69..0744d8a93b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java @@ -233,7 +233,8 @@ class ConfigDataEnvironment { contributors = processWithoutProfiles(contributors, importer, activationContext); activationContext = withProfiles(contributors, activationContext); contributors = processWithProfiles(contributors, importer, activationContext); - applyToEnvironment(contributors, activationContext, importer.getLoadedLocations()); + applyToEnvironment(contributors, activationContext, importer.getLoadedLocations(), + importer.getOptionalLocations()); } private ConfigDataEnvironmentContributors processInitial(ConfigDataEnvironmentContributors contributors, @@ -322,9 +323,10 @@ class ConfigDataEnvironment { } private void applyToEnvironment(ConfigDataEnvironmentContributors contributors, - ConfigDataActivationContext activationContext, Set loadedLocations) { + ConfigDataActivationContext activationContext, Set loadedLocations, + Set optionalLocations) { checkForInvalidProperties(contributors); - checkMandatoryLocations(contributors, activationContext, loadedLocations); + checkMandatoryLocations(contributors, activationContext, loadedLocations, optionalLocations); MutablePropertySources propertySources = this.environment.getPropertySources(); this.logger.trace("Applying config data environment contributions"); for (ConfigDataEnvironmentContributor contributor : contributors) { @@ -359,7 +361,8 @@ class ConfigDataEnvironment { } private void checkMandatoryLocations(ConfigDataEnvironmentContributors contributors, - ConfigDataActivationContext activationContext, Set loadedLocations) { + ConfigDataActivationContext activationContext, Set loadedLocations, + Set optionalLocations) { Set mandatoryLocations = new LinkedHashSet<>(); for (ConfigDataEnvironmentContributor contributor : contributors) { if (contributor.isActive(activationContext)) { @@ -372,6 +375,7 @@ class ConfigDataEnvironment { } } mandatoryLocations.removeAll(loadedLocations); + mandatoryLocations.removeAll(optionalLocations); if (!mandatoryLocations.isEmpty()) { for (ConfigDataLocation mandatoryLocation : mandatoryLocations) { this.notFoundAction.handle(this.logger, new ConfigDataLocationNotFoundException(mandatoryLocation)); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataImporter.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataImporter.java index 4369880f29..0970b4f6a6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataImporter.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataImporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -51,6 +51,8 @@ class ConfigDataImporter { private final Set loadedLocations = new HashSet<>(); + private final Set optionalLocations = new HashSet<>(); + /** * Create a new {@link ConfigDataImporter} instance. * @param logFactory the log factory @@ -103,7 +105,7 @@ class ConfigDataImporter { return this.resolvers.resolve(locationResolverContext, location, profiles); } catch (ConfigDataNotFoundException ex) { - handle(ex, location); + handle(ex, location, null); return Collections.emptyList(); } } @@ -115,6 +117,9 @@ class ConfigDataImporter { ConfigDataResolutionResult candidate = candidates.get(i); ConfigDataLocation location = candidate.getLocation(); ConfigDataResource resource = candidate.getResource(); + if (resource.isOptional()) { + this.optionalLocations.add(location); + } if (this.loaded.contains(resource)) { this.loadedLocations.add(location); } @@ -128,26 +133,33 @@ class ConfigDataImporter { } } catch (ConfigDataNotFoundException ex) { - handle(ex, location); + handle(ex, location, resource); } } } return Collections.unmodifiableMap(result); } - private void handle(ConfigDataNotFoundException ex, ConfigDataLocation location) { + private void handle(ConfigDataNotFoundException ex, ConfigDataLocation location, ConfigDataResource resource) { if (ex instanceof ConfigDataResourceNotFoundException) { ex = ((ConfigDataResourceNotFoundException) ex).withLocation(location); } - getNotFoundAction(location).handle(this.logger, ex); + getNotFoundAction(location, resource).handle(this.logger, ex); } - private ConfigDataNotFoundAction getNotFoundAction(ConfigDataLocation location) { - return (!location.isOptional()) ? this.notFoundAction : ConfigDataNotFoundAction.IGNORE; + private ConfigDataNotFoundAction getNotFoundAction(ConfigDataLocation location, ConfigDataResource resource) { + if (location.isOptional() || (resource != null && resource.isOptional())) { + return ConfigDataNotFoundAction.IGNORE; + } + return this.notFoundAction; } Set getLoadedLocations() { return this.loadedLocations; } + Set getOptionalLocations() { + return this.optionalLocations; + } + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResource.java index e16dbd4b0b..45505150b8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataResource.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2021 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. @@ -27,4 +27,26 @@ package org.springframework.boot.context.config; */ public abstract class ConfigDataResource { + private final boolean optional; + + /** + * Create a new non-optional {@link ConfigDataResource} instance. + */ + public ConfigDataResource() { + this(false); + } + + /** + * Create a new {@link ConfigDataResource} instance. + * @param optional if the resource is optional + * @since 2.4.6 + */ + protected ConfigDataResource(boolean optional) { + this.optional = optional; + } + + boolean isOptional() { + return this.optional; + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java index 6f11c70ff6..9db066c874 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessorIntegrationTests.java @@ -46,6 +46,7 @@ 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.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.ConfigurableEnvironment; @@ -581,6 +582,12 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests { + StringUtils.cleanPath(location.getAbsolutePath()))); } + @Test + void runWhenResolvedIsOptionalDoesNotThrowException() { + ApplicationContext context = this.application.run("--spring.config.location=test:optionalresult"); + assertThat(context.getEnvironment().containsProperty("spring")).isFalse(); + } + @Test @Disabled("Disabled until spring.profiles suppport is dropped") void runWhenUsingInvalidPropertyThrowsException() { @@ -752,4 +759,44 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests { } + static class LocationResolver implements ConfigDataLocationResolver { + + @Override + public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { + return location.hasPrefix("test:"); + + } + + @Override + public List resolve(ConfigDataLocationResolverContext context, + ConfigDataLocation location) + throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException { + return Collections.singletonList(new TestConfigDataResource(location)); + } + + } + + static class Loader implements ConfigDataLoader { + + @Override + public ConfigData load(ConfigDataLoaderContext context, TestConfigDataResource resource) + throws IOException, ConfigDataResourceNotFoundException { + if (resource.isOptional()) { + return null; + } + MapPropertySource propertySource = new MapPropertySource("loaded", + Collections.singletonMap("spring", "boot")); + return new ConfigData(Collections.singleton(propertySource)); + } + + } + + static class TestConfigDataResource extends ConfigDataResource { + + TestConfigDataResource(ConfigDataLocation location) { + super(location.toString().contains("optionalresult")); + } + + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolversTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolversTests.java index abc12408f4..354e8a25d2 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolversTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolversTests.java @@ -168,8 +168,27 @@ class ConfigDataLocationResolversTests { .satisfies((ex) -> assertThat(ex.getLocation()).isEqualTo(location)); } + @Test + void resolveWhenOptional() { + ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext, + this.binder, this.resourceLoader, Arrays.asList(OptionalResourceTestResolver.class.getName())); + ConfigDataLocation location = ConfigDataLocation.of("OptionalResourceTestResolver:test"); + List resolved = resolvers.resolve(this.context, location, null); + assertThat(resolved.get(0).getResource().isOptional()).isTrue(); + } + static class TestResolver implements ConfigDataLocationResolver { + private final boolean optionalResource; + + TestResolver() { + this(false); + } + + TestResolver(boolean optionalResource) { + this.optionalResource = optionalResource; + } + @Override public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { String name = getClass().getName(); @@ -181,13 +200,13 @@ class ConfigDataLocationResolversTests { public List resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException { - return Collections.singletonList(new TestConfigDataResource(this, location, false)); + return Collections.singletonList(new TestConfigDataResource(this.optionalResource, this, location, false)); } @Override public List resolveProfileSpecific(ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) throws ConfigDataLocationNotFoundException { - return Collections.singletonList(new TestConfigDataResource(this, location, true)); + return Collections.singletonList(new TestConfigDataResource(this.optionalResource, this, location, true)); } } @@ -250,6 +269,14 @@ class ConfigDataLocationResolversTests { } + static class OptionalResourceTestResolver extends TestResolver { + + OptionalResourceTestResolver() { + super(true); + } + + } + static class TestConfigDataResource extends ConfigDataResource { private final TestResolver resolver; @@ -258,7 +285,9 @@ class ConfigDataLocationResolversTests { private final boolean profileSpecific; - TestConfigDataResource(TestResolver resolver, ConfigDataLocation location, boolean profileSpecific) { + TestConfigDataResource(boolean optional, TestResolver resolver, ConfigDataLocation location, + boolean profileSpecific) { + super(optional); this.resolver = resolver; this.location = location; this.profileSpecific = profileSpecific; diff --git a/spring-boot-project/spring-boot/src/test/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/test/resources/META-INF/spring.factories index b8acdbc769..0ab92a3d7b 100644 --- a/spring-boot-project/spring-boot/src/test/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/test/resources/META-INF/spring.factories @@ -3,9 +3,11 @@ org.springframework.boot.context.config.TestPropertySourceLoader1,\ org.springframework.boot.context.config.TestPropertySourceLoader2 org.springframework.boot.context.config.ConfigDataLocationResolver=\ +org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessorIntegrationTests.LocationResolver,\ org.springframework.boot.context.config.TestConfigDataBootstrap.LocationResolver,\ org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessorImportCombinedWithProfileSpecificIntegrationTests.LocationResolver org.springframework.boot.context.config.ConfigDataLoader=\ +org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessorIntegrationTests.Loader,\ org.springframework.boot.context.config.TestConfigDataBootstrap.Loader,\ org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessorImportCombinedWithProfileSpecificIntegrationTests.Loader