From 081a7ee28cb5aa7342520d946e68a589378ed154 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 24 Aug 2020 13:04:50 -0700 Subject: [PATCH] Throw exception if import locations are not found Update config data processing code so that import locations are mandatory by default. Any import request will now throw a `ConfigDataLocationNotFoundException` if the specified import location cannot be found. For optional imports, the user can use the `optional:` prefix to indicate that the application should continue to start, even if the location does not exist. Closes gh-23032 --- .../docs/asciidoc/spring-boot-features.adoc | 41 ++++-- .../context/config/ConfigDataEnvironment.java | 4 +- .../context/config/ConfigDataImporter.java | 5 +- .../boot/context/config/ConfigDataLoader.java | 4 +- .../context/config/ConfigDataLoaders.java | 18 ++- .../context/config/ConfigDataLocation.java | 5 + .../ConfigDataLocationNotFoundException.java | 120 ++++++++++++++++ .../config/ConfigDataLocationResolver.java | 14 +- .../config/ConfigDataLocationResolvers.java | 36 ++++- .../config/ConfigFileApplicationListener.java | 15 +- .../config/ConfigTreeConfigDataLoader.java | 1 + .../ConfigTreeConfigDataLocationResolver.java | 3 +- .../config/OptionalConfigDataLocation.java | 72 ++++++++++ .../config/ResourceConfigDataLoader.java | 1 + .../config/ResourceConfigDataLocation.java | 4 + .../ResourceConfigDataLocationResolver.java | 96 +++++++++---- ...ironmentPostProcessorIntegrationTests.java | 19 ++- .../config/ConfigDataEnvironmentTests.java | 2 +- ...figDataLocationNotFoundExceptionTests.java | 135 ++++++++++++++++++ .../ConfigDataLocationResolverTests.java | 5 +- .../ConfigDataLocationResolversTests.java | 5 +- .../ConfigTreeConfigDataLoaderTests.java | 9 ++ ...igTreeConfigDataLocationResolverTests.java | 3 +- .../OptionalConfigDataLocationTests.java | 83 +++++++++++ ...sourceConfigDataLocationResolverTests.java | 28 ++-- .../config/TestConfigDataBootstrap.java | 2 +- 26 files changed, 648 insertions(+), 82 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundException.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/OptionalConfigDataLocation.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundExceptionTests.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/OptionalConfigDataLocationTests.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index 23c6319eca..0828e3461e 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -570,9 +570,11 @@ The following example shows how to specify two locations: [indent=0] ---- - $ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties + $ java -jar myproject.jar --spring.config.location=optional:classpath:/default.properties,optional:classpath:/override.properties ---- +TIP: Use the prefix `optional:` if the <> and you don't mind if they don't exist. + WARNING: `spring.config.name` and `spring.config.location` are used very early to determine which files have to be loaded. They must be defined as an environment property (typically an OS environment variable, a system property, or a command-line argument). @@ -584,22 +586,22 @@ Typical extensions that are supported out-of-the-box are `.properties`, `.yaml`, When multiple locations are specified, the later ones can override the values of earlier ones. Locations configured by using `spring.config.location` replace the default locations. -For example, if `spring.config.location` is configured with the value `classpath:/custom-config/,file:./custom-config/`, the complete set of locations considered is: +For example, if `spring.config.location` is configured with the value `optional:classpath:/custom-config/,optional:file:./custom-config/`, the complete set of locations considered is: -. `classpath:custom-config/` -. `file:./custom-config/` +. `optional:classpath:custom-config/` +. `optional:file:./custom-config/` If you prefer to add addition locations, rather than replacing them, you can use `spring.config.additional-location`. Properties loaded from additional locations can override those in the default locations. -For example, if `spring.config.additional-location` is configured with the value `classpath:/custom-config/,file:./custom-config/`, the complete the complete set of locations considered is: +For example, if `spring.config.additional-location` is configured with the value `optional:classpath:/custom-config/,optional:file:./custom-config/`, the complete the complete set of locations considered is: -. `classpath:/` -. `classpath:/config/` -. `file:./` -. `file:./config/*/` -. `file:./config/` -. `classpath:custom-config/` -. `file:./custom-config/` +. `optional:classpath:/` +. `optional:classpath:/config/` +. `optional:file:./` +. `optional:file:./config/*/` +. `optional:file:./config/` +. `optional:classpath:custom-config/` +. `optional:file:./custom-config/` This search ordering lets you specify default values in one configuration file and then selectively override those values in another. You can provide default values for your application in `application.properties` (or whatever other basename you choose with `spring.config.name`) in one of the default locations. @@ -612,6 +614,17 @@ NOTE: If your application runs in a servlet container or application server, the +[[boot-features-external-config-optional-prefix]] +==== Optional Locations +By default, when a specified config data location does not exist, Spring Boot will throw a `ConfigDataLocationNotFoundException` and your application will not start. + +If you want to specify a location, but you don't mind if it doesn't always exist, you can use the `optional:` prefix. +You can use this prefix with the `spring.config.location` and `spring.config.additional-location` properties, as well as with <> declarations. + +For example, a `spring.config.import` value of `optional:file:./myconfig.properties` allows your application to start, even if the `myconfig.properties` file is missing. + + + [[boot-features-external-config-files-wildcards]] ==== Wildcard Locations If a config file location includes the `{asterisk}` character for the last path segment, it is considered a wildcard location. @@ -662,7 +675,7 @@ For example, you might have the following in your classpath `application.propert [source,properties,indent=0] ---- spring.application.name=myapp - spring.config.import=file:./dev.properties + spring.config.import=optional:file:./dev.properties ---- This will trigger the import of a `dev.properties` file in current directory (if such a file exists). @@ -718,7 +731,7 @@ To import these properties, you can add the following to your `application.prope [source,properties,indent=0] ---- -spring.config.import=configtree:/etc/config +spring.config.import=optional:configtree:/etc/config ---- You can then access or inject `myapp.username` and `myapp.password` properties from the `Environment` in the usual way. 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 d181f9edd4..7e108fb702 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 @@ -69,8 +69,8 @@ class ConfigDataEnvironment { /** * Default search locations used if not {@link #LOCATION_PROPERTY} is found. */ - static final String[] DEFAULT_SEARCH_LOCATIONS = { "classpath:/", "classpath:/config/", "file:./", - "file:./config/*/", "file:./config/" }; + static final String[] DEFAULT_SEARCH_LOCATIONS = { "optional:classpath:/", "optional:classpath:/config/", + "optional:file:./", "optional:file:./config/*/", "optional:file:./config/" }; private static final String[] EMPTY_LOCATIONS = new String[0]; 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 59dcb82487..5368546855 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 @@ -77,7 +77,10 @@ class ConfigDataImporter { for (int i = locations.size() - 1; i >= 0; i--) { ConfigDataLocation location = locations.get(i); if (this.loadedLocations.add(location)) { - result.put(location, this.loaders.load(loaderContext, location)); + ConfigData loaded = this.loaders.load(loaderContext, location); + if (loaded != null) { + result.put(location, loaded); + } } } return Collections.unmodifiableMap(result); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoader.java index 75082703f7..921fdb8057 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoader.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoader.java @@ -54,7 +54,9 @@ public interface ConfigDataLoader { * @param location the location to load * @return the loaded config data or {@code null} if the location should be skipped * @throws IOException on IO error + * @throws ConfigDataLocationNotFoundException if the location cannot be found */ - ConfigData load(ConfigDataLoaderContext context, L location) throws IOException; + ConfigData load(ConfigDataLoaderContext context, L location) + throws IOException, ConfigDataLocationNotFoundException; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java index 43c3af693b..9aa0c07219 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLoaders.java @@ -86,9 +86,25 @@ class ConfigDataLoaders { * @throws IOException on IO error */ ConfigData load(ConfigDataLoaderContext context, L location) throws IOException { + boolean optional = location instanceof OptionalConfigDataLocation; + location = (!optional) ? location : OptionalConfigDataLocation.unwrap(location); + return load(context, optional, location); + } + + private ConfigData load(ConfigDataLoaderContext context, boolean optional, + L location) throws IOException { ConfigDataLoader loader = getLoader(context, location); this.logger.trace(LogMessage.of(() -> "Loading " + location + " using loader " + loader.getClass().getName())); - return loader.load(context, location); + try { + return loader.load(context, location); + } + catch (ConfigDataLocationNotFoundException ex) { + if (!optional) { + throw ex; + } + this.logger.trace(LogMessage.format("Skipping missing resource from optional location %s", location)); + return null; + } } @SuppressWarnings("unchecked") diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java index 8a597d70cc..137c6d6384 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocation.java @@ -27,4 +27,9 @@ package org.springframework.boot.context.config; */ public abstract class ConfigDataLocation { + /** + * Prefix used to indicate that a {@link ConfigDataLocation} is optional. + */ + public static final String OPTIONAL_PREFIX = "optional:"; + } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundException.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundException.java new file mode 100644 index 0000000000..ee52755f4e --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundException.java @@ -0,0 +1,120 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.context.config; + +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.springframework.core.io.Resource; + +/** + * Exception thrown when a config data location cannot be found. + * + * @author Phillip Webb + * @since 2.4.0 + */ +public class ConfigDataLocationNotFoundException extends ConfigDataException { + + private final ConfigDataLocation location; + + /** + * Create a new {@link ConfigDataLocationNotFoundException} instance. + * @param location the location that was not found + */ + public ConfigDataLocationNotFoundException(ConfigDataLocation location) { + this(location, null); + } + + /** + * Create a new {@link ConfigDataLocationNotFoundException} instance. + * @param location the location that was not found + * @param cause the cause of the exception + */ + public ConfigDataLocationNotFoundException(ConfigDataLocation location, Throwable cause) { + this(getMessage(location), location, cause); + } + + /** + * Create a new {@link ConfigDataLocationNotFoundException} instance. + * @param message the exception message + * @param location the location that was not found + */ + public ConfigDataLocationNotFoundException(String message, ConfigDataLocation location) { + this(message, location, null); + } + + /** + * Create a new {@link ConfigDataLocationNotFoundException} instance. + * @param message the exception message + * @param location the location that was not found + * @param cause the cause of the exception + */ + public ConfigDataLocationNotFoundException(String message, ConfigDataLocation location, Throwable cause) { + super(message, cause); + this.location = location; + } + + /** + * Return the location that could not be found. + * @return the location that could not be found. + */ + public ConfigDataLocation getLocation() { + return this.location; + } + + private static String getMessage(ConfigDataLocation location) { + return "Config data location '" + location + "' does not exist"; + } + + /** + * Throw a {@link ConfigDataLocationNotFoundException} if the specified {@link Path} + * does not exist. + * @param location the location being checked + * @param path the path to check + */ + public static void throwIfDoesNotExist(ConfigDataLocation location, Path path) { + throwIfDoesNotExist(location, Files.exists(path)); + } + + /** + * Throw a {@link ConfigDataLocationNotFoundException} if the specified {@link File} + * does not exist. + * @param location the location being checked + * @param file the file to check + */ + public static void throwIfDoesNotExist(ConfigDataLocation location, File file) { + throwIfDoesNotExist(location, file.exists()); + } + + /** + * Throw a {@link ConfigDataLocationNotFoundException} if the specified + * {@link Resource} does not exist. + * @param location the location being checked + * @param resource the resource to check + */ + public static void throwIfDoesNotExist(ConfigDataLocation location, Resource resource) { + throwIfDoesNotExist(location, resource.exists()); + } + + private static void throwIfDoesNotExist(ConfigDataLocation location, boolean exists) { + if (!exists) { + throw new ConfigDataLocationNotFoundException(location); + } + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolver.java index d123f48d29..ab18ac3dae 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolver.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolver.java @@ -60,11 +60,14 @@ public interface ConfigDataLocationResolver { * Resolve a location string into one or more {@link ConfigDataLocation} instances. * @param context the location resolver context * @param location the location that should be resolved + * @param optional if the location is optional * @return a list of resolved locations in ascending priority order. If the same key * is contained in more than one of the location, then the later source will win. - * + * @throws ConfigDataLocationNotFoundException on a non-optional location that cannot + * be found */ - List resolve(ConfigDataLocationResolverContext context, String location); + List resolve(ConfigDataLocationResolverContext context, String location, boolean optional) + throws ConfigDataLocationNotFoundException; /** * Resolve a location string into one or more {@link ConfigDataLocation} instances @@ -72,12 +75,15 @@ public interface ConfigDataLocationResolver { * from the contributed values. By default this method returns an empty list. * @param context the location resolver context * @param location the location that should be resolved + * @param optional if the location is optional * @param profiles profile information * @return a list of resolved locations in ascending priority order.If the same key is * contained in more than one of the location, then the later source will win. + * @throws ConfigDataLocationNotFoundException on a non-optional location that cannot + * be found */ - default List resolveProfileSpecific(ConfigDataLocationResolverContext context, String location, - Profiles profiles) { + default List resolveProfileSpecific(ConfigDataLocationResolverContext context, String location, boolean optional, + Profiles profiles) throws ConfigDataLocationNotFoundException { return Collections.emptyList(); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java index 37fad9b73c..bfe2991ef5 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataLocationResolvers.java @@ -19,6 +19,7 @@ package org.springframework.boot.context.config; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Supplier; import org.apache.commons.logging.Log; @@ -28,6 +29,7 @@ import org.springframework.boot.util.Instantiator; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.log.LogMessage; import org.springframework.util.StringUtils; /** @@ -39,6 +41,8 @@ import org.springframework.util.StringUtils; */ class ConfigDataLocationResolvers { + private final Log logger; + private final List> resolvers; /** @@ -61,6 +65,7 @@ class ConfigDataLocationResolvers { */ ConfigDataLocationResolvers(DeferredLogFactory logFactory, Binder binder, ResourceLoader resourceLoader, List names) { + this.logger = logFactory.getLog(getClass()); Instantiator> instantiator = new Instantiator<>(ConfigDataLocationResolver.class, (availableParameters) -> { availableParameters.add(Log.class, logFactory::getLog); @@ -106,28 +111,49 @@ class ConfigDataLocationResolvers { private List resolveAll(ConfigDataLocationResolverContext context, String location, Profiles profiles) { + boolean optional = location != null && location.startsWith(ConfigDataLocation.OPTIONAL_PREFIX); + location = (!optional) ? location : location.substring(ConfigDataLocation.OPTIONAL_PREFIX.length()); if (!StringUtils.hasText(location)) { return Collections.emptyList(); } for (ConfigDataLocationResolver resolver : getResolvers()) { if (resolver.isResolvable(context, location)) { - return resolve(resolver, context, location, profiles); + return resolve(resolver, context, optional, location, profiles); } } throw new UnsupportedConfigDataLocationException(location); } private List resolve(ConfigDataLocationResolver resolver, - ConfigDataLocationResolverContext context, String location, Profiles profiles) { - List resolved = nonNullList(resolver.resolve(context, location)); + ConfigDataLocationResolverContext context, boolean optional, String location, Profiles profiles) { + List resolved = resolve(location, optional, + () -> resolver.resolve(context, location, optional)); if (profiles == null) { return resolved; } - List profileSpecific = nonNullList( - resolver.resolveProfileSpecific(context, location, profiles)); + List profileSpecific = resolve(location, optional, + () -> resolver.resolveProfileSpecific(context, location, optional, profiles)); return merge(resolved, profileSpecific); } + private List resolve(String location, boolean optional, + Supplier> resolveAction) { + try { + List resolved = nonNullList(resolveAction.get()); + if (!resolved.isEmpty() && optional) { + resolved = OptionalConfigDataLocation.wrapAll(resolved); + } + return resolved; + } + catch (ConfigDataLocationNotFoundException ex) { + if (!optional) { + throw ex; + } + this.logger.trace(LogMessage.format("Skipping missing resource from optional location %s", location)); + return Collections.emptyList(); + } + } + @SuppressWarnings("unchecked") private List nonNullList(List list) { return (list != null) ? (List) list : Collections.emptyList(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java index cc77d3afb8..ea3b0bde5d 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java @@ -428,10 +428,23 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor, getSearchLocations().forEach((location) -> { boolean isDirectory = location.endsWith("/"); Set names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES; - names.forEach((name) -> load(location, name, profile, filterFactory, consumer)); + names.forEach((name) -> load(stripOptionalPrefix(location), name, profile, filterFactory, consumer)); }); } + /** + * Strip the optional prefix from the location. When using the legacy method, all + * locations are optional. + * @param location the location to strip + * @return the stripped location + */ + private String stripOptionalPrefix(String location) { + if (location != null && location.startsWith(ConfigDataLocation.OPTIONAL_PREFIX)) { + return location.substring(ConfigDataLocation.OPTIONAL_PREFIX.length()); + } + return location; + } + private void load(String location, String name, Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) { if (!StringUtils.hasText(name)) { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataLoader.java index 7a4bfc1adf..cd285b9097 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataLoader.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataLoader.java @@ -32,6 +32,7 @@ class ConfigTreeConfigDataLoader implements ConfigDataLoader resolve(ConfigDataLocationResolverContext context, String location) { + public List resolve(ConfigDataLocationResolverContext context, String location, + boolean optional) { ConfigTreeConfigDataLocation resolved = new ConfigTreeConfigDataLocation(location.substring(PREFIX.length())); return Collections.singletonList(resolved); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/OptionalConfigDataLocation.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/OptionalConfigDataLocation.java new file mode 100644 index 0000000000..3954f65051 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/OptionalConfigDataLocation.java @@ -0,0 +1,72 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.context.config; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@link ConfigDataLocation} wrapper used to indicate that it's optional. + * + * @author Phillip Webb + */ +class OptionalConfigDataLocation extends ConfigDataLocation { + + private ConfigDataLocation location; + + OptionalConfigDataLocation(ConfigDataLocation location) { + this.location = location; + } + + ConfigDataLocation getLocation() { + return this.location; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + OptionalConfigDataLocation other = (OptionalConfigDataLocation) obj; + return this.location.equals(other.location); + } + + @Override + public int hashCode() { + return this.location.hashCode(); + } + + @Override + public String toString() { + return this.location.toString(); + } + + static List wrapAll(List locations) { + List wrapped = new ArrayList<>(locations.size()); + locations.forEach((location) -> wrapped.add(new OptionalConfigDataLocation(location))); + return wrapped; + } + + @SuppressWarnings("unchecked") + static L unwrap(ConfigDataLocation wrapped) { + return (L) ((OptionalConfigDataLocation) wrapped).getLocation(); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLoader.java index f293e6cd70..551c916d29 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLoader.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ResourceConfigDataLoader.java @@ -30,6 +30,7 @@ class ResourceConfigDataLoader implements ConfigDataLoader resolve(ConfigDataLocationResolverContext context, String location) { - return resolve(location, getResolvables(context, location)); + public List resolve(ConfigDataLocationResolverContext context, String location, + boolean optional) { + return resolve(location, getResolvables(context, location, optional)); } @Override public List resolveProfileSpecific(ConfigDataLocationResolverContext context, - String location, Profiles profiles) { - return resolve(location, getProfileSpecificResolvables(context, location, profiles)); + String location, boolean optional, Profiles profiles) { + return resolve(location, getProfileSpecificResolvables(context, location, optional, profiles)); } - private Set getResolvables(ConfigDataLocationResolverContext context, String location) { + private Set getResolvables(ConfigDataLocationResolverContext context, String location, + boolean optional) { String resourceLocation = getResourceLocation(context, location); try { if (isDirectoryLocation(resourceLocation)) { - return getResolvablesForDirectory(resourceLocation, NO_PROFILE); + return getResolvablesForDirectory(resourceLocation, optional, NO_PROFILE); } - return getResolvablesForFile(resourceLocation, NO_PROFILE); + return getResolvablesForFile(resourceLocation, optional, NO_PROFILE); } catch (RuntimeException ex) { throw new IllegalStateException("Unable to load config data from '" + location + "'", ex); @@ -134,11 +136,11 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver getProfileSpecificResolvables(ConfigDataLocationResolverContext context, String location, - Profiles profiles) { + boolean optional, Profiles profiles) { Set resolvables = new LinkedHashSet<>(); String resourceLocation = getResourceLocation(context, location); for (String profile : profiles) { - resolvables.addAll(getResolvables(resourceLocation, profile)); + resolvables.addAll(getResolvables(resourceLocation, optional, profile)); } return resolvables; } @@ -158,31 +160,34 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver getResolvables(String resourceLocation, String profile) { + private Set getResolvables(String resourceLocation, boolean optional, String profile) { if (isDirectoryLocation(resourceLocation)) { - return getResolvablesForDirectory(resourceLocation, profile); + return getResolvablesForDirectory(resourceLocation, optional, profile); } - return getResolvablesForFile(resourceLocation, profile); + return getResolvablesForFile(resourceLocation, optional, profile); } - private Set getResolvablesForDirectory(String resourceLocation, String profile) { + private Set getResolvablesForDirectory(String directoryLocation, boolean optional, String profile) { Set resolvables = new LinkedHashSet<>(); for (String name : this.configNames) { + String rootLocation = directoryLocation + name; for (PropertySourceLoader loader : this.propertySourceLoaders) { for (String extension : loader.getFileExtensions()) { - resolvables.add(new Resolvable(resourceLocation + name, profile, extension, loader)); + Resolvable resolvable = new Resolvable(directoryLocation, rootLocation, optional, profile, + extension, loader); + resolvables.add(resolvable); } } } return resolvables; } - private Set getResolvablesForFile(String resourceLocation, String profile) { + private Set getResolvablesForFile(String fileLocation, boolean optional, String profile) { for (PropertySourceLoader loader : this.propertySourceLoaders) { - String extension = getLoadableFileExtension(loader, resourceLocation); + String extension = getLoadableFileExtension(loader, fileLocation); if (extension != null) { - String root = resourceLocation.substring(0, resourceLocation.length() - extension.length() - 1); - return Collections.singleton(new Resolvable(root, profile, extension, loader)); + String root = fileLocation.substring(0, fileLocation.length() - extension.length() - 1); + return Collections.singleton(new Resolvable(null, root, optional, profile, extension, loader)); } } throw new IllegalStateException("File extension is not known to any PropertySourceLoader. " @@ -207,9 +212,23 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver resolvables) { + for (Resolvable resolvable : resolvables) { + if (resolvable.isNonOptionalDirectory()) { + Resource resource = this.resourceLoader.getResource(resolvable.getDirectory()); + ResourceConfigDataLocation resourceLocation = createConfigResourceLocation(location, resolvable, + resource); + ConfigDataLocationNotFoundException.throwIfDoesNotExist(resourceLocation, resource); + } + } + } + private List resolve(String location, Resolvable resolvable) { if (!resolvable.isPatternLocation()) { return resolveNonPattern(location, resolvable); @@ -219,23 +238,22 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver resolveNonPattern(String location, Resolvable resolvable) { Resource resource = loadResource(resolvable.getResourceLocation()); - if (resource.exists()) { - ResourceConfigDataLocation resolved = createConfigResourceLocation(location, resolvable, resource); - return Collections.singletonList(resolved); + if (!resource.exists() && resolvable.isSkippable()) { + logSkippingResource(resolvable); + return Collections.emptyList(); } - logSkippingResource(resolvable); - return Collections.emptyList(); + return Collections.singletonList(createConfigResourceLocation(location, resolvable, resource)); } private List resolvePattern(String location, Resolvable resolvable) { validatePatternLocation(resolvable.getResourceLocation()); List resolved = new ArrayList<>(); for (Resource resource : getResourcesFromResourceLocationPattern(resolvable.getResourceLocation())) { - if (resource.exists()) { - resolved.add(createConfigResourceLocation(location, resolvable, resource)); + if (!resource.exists() && resolvable.isSkippable()) { + logSkippingResource(resolvable); } else { - logSkippingResource(resolvable); + resolved.add(createConfigResourceLocation(location, resolvable, resource)); } } return resolved; @@ -267,7 +285,7 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver this.application.run("--spring.config.location=" + location)); + } + @Test @Disabled("Disabled until spring.profiles suppport is dropped") void runWhenUsingInvalidPropertyThrowsException() { @@ -536,6 +543,12 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests { .withCauseInstanceOf(InactiveConfigDataAccessException.class); } + @Test + void runWhenHasNonOptionalImportThrowsException() { + assertThatExceptionOfType(ConfigDataLocationNotFoundException.class).isThrownBy( + () -> this.application.run("--spring.config.location=classpath:missing-appplication.properties")); + } + 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/ConfigDataEnvironmentTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java index 5757943e83..a4988661d3 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataEnvironmentTests.java @@ -198,7 +198,7 @@ class ConfigDataEnvironmentTests { } private String getConfigLocation(TestInfo info) { - return "classpath:" + info.getTestClass().get().getName().replace('.', '/') + "-" + return "optional:classpath:" + info.getTestClass().get().getName().replace('.', '/') + "-" + info.getTestMethod().get().getName() + ".properties"; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundExceptionTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundExceptionTests.java new file mode 100644 index 0000000000..8c57ae897e --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationNotFoundExceptionTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.context.config; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import org.springframework.core.io.FileSystemResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link ConfigDataLocationNotFoundException}. + * + * @author Phillip Webb + */ +class ConfigDataLocationNotFoundExceptionTests { + + private ConfigDataLocation location = mock(ConfigDataLocation.class); + + private Throwable cause = new RuntimeException(); + + private String message = "message"; + + private File exists; + + private File missing; + + @TempDir + File temp; + + @BeforeEach + void setup() throws IOException { + this.exists = new File(this.temp, "exists"); + this.missing = new File(this.temp, "missing"); + try (OutputStream out = new FileOutputStream(this.exists)) { + out.write("test".getBytes()); + } + } + + @Test + void createWithLocationCreatesInstance() { + ConfigDataLocationNotFoundException exception = new ConfigDataLocationNotFoundException(this.location); + assertThat(exception.getLocation()).isSameAs(this.location); + } + + @Test + void createWithLocationAndCauseCreatesInstance() { + ConfigDataLocationNotFoundException exception = new ConfigDataLocationNotFoundException(this.location, + this.cause); + assertThat(exception.getLocation()).isSameAs(this.location); + assertThat(exception.getCause()).isSameAs(this.cause); + } + + @Test + void createWithMessageAndLocationCreatesInstance() { + ConfigDataLocationNotFoundException exception = new ConfigDataLocationNotFoundException(this.message, + this.location, this.cause); + assertThat(exception.getLocation()).isSameAs(this.location); + assertThat(exception.getCause()).isSameAs(this.cause); + assertThat(exception.getMessage()).isEqualTo(this.message); + } + + @Test + void createWithMessageAndLocationAndCauseCreatesInstance() { + ConfigDataLocationNotFoundException exception = new ConfigDataLocationNotFoundException(this.message, + this.location); + assertThat(exception.getLocation()).isSameAs(this.location); + assertThat(exception.getMessage()).isEqualTo(this.message); + } + + @Test + void getLocationReturnsLocation() { + ConfigDataLocationNotFoundException exception = new ConfigDataLocationNotFoundException(this.location); + assertThat(exception.getLocation()).isSameAs(this.location); + } + + @Test + void throwIfDoesNotExistWhenPathExistsDoesNothing() { + ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location, this.exists.toPath()); + } + + @Test + void throwIfDoesNotExistWhenPathDoesNotExistThrowsException() { + assertThatExceptionOfType(ConfigDataLocationNotFoundException.class).isThrownBy( + () -> ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location, this.missing.toPath())); + } + + @Test + void throwIfDoesNotExistWhenFileExistsDoesNothing() { + ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location, this.exists); + + } + + @Test + void throwIfDoesNotExistWhenFileDoesNotExistThrowsException() { + assertThatExceptionOfType(ConfigDataLocationNotFoundException.class) + .isThrownBy(() -> ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location, this.missing)); + } + + @Test + void throwIfDoesNotExistWhenResourceExistsDoesNothing() { + ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location, new FileSystemResource(this.exists)); + } + + @Test + void throwIfDoesNotExistWhenResourceDoesNotExistThrowsException() { + assertThatExceptionOfType(ConfigDataLocationNotFoundException.class) + .isThrownBy(() -> ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location, + new FileSystemResource(this.missing))); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolverTests.java index d675960d73..0994ba26ce 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolverTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigDataLocationResolverTests.java @@ -37,7 +37,7 @@ class ConfigDataLocationResolverTests { @Test void resolveProfileSpecificReturnsEmptyList() { - assertThat(this.resolver.resolveProfileSpecific(this.context, null, null)).isEmpty(); + assertThat(this.resolver.resolveProfileSpecific(this.context, null, true, null)).isEmpty(); } static class TestConfigDataLocationResolver implements ConfigDataLocationResolver { @@ -48,7 +48,8 @@ class ConfigDataLocationResolverTests { } @Override - public List resolve(ConfigDataLocationResolverContext context, String location) { + public List resolve(ConfigDataLocationResolverContext context, String location, + boolean optional) { return null; } 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 9b1032bad2..b3c16b24e6 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 @@ -151,13 +151,14 @@ class ConfigDataLocationResolversTests { } @Override - public List resolve(ConfigDataLocationResolverContext context, String location) { + public List resolve(ConfigDataLocationResolverContext context, String location, + boolean optional) { return Collections.singletonList(new TestConfigDataLocation(this, location, false)); } @Override public List resolveProfileSpecific(ConfigDataLocationResolverContext context, - String location, Profiles profiles) { + String location, boolean optional, Profiles profiles) { return Collections.singletonList(new TestConfigDataLocation(this, location, true)); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLoaderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLoaderTests.java index 1999795a27..510b585a5e 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLoaderTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLoaderTests.java @@ -28,6 +28,7 @@ import org.springframework.core.env.PropertySource; import org.springframework.util.FileCopyUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.Mockito.mock; /** @@ -58,4 +59,12 @@ public class ConfigTreeConfigDataLoaderTests { assertThat(source.getProperty("hello").toString()).isEqualTo("world"); } + @Test + void loadWhenPathDoesNotExistThrowsException() { + File missing = this.directory.resolve("missing").toFile(); + ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation(missing.toString()); + assertThatExceptionOfType(ConfigDataLocationNotFoundException.class) + .isThrownBy(() -> this.loader.load(this.loaderContext, location)); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolverTests.java index 7f03545e96..ac532a1470 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolverTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolverTests.java @@ -49,7 +49,8 @@ class ConfigTreeConfigDataLocationResolverTests { @Test void resolveReturnsConfigVolumeMountLocation() { - List locations = this.resolver.resolve(this.context, "configtree:/etc/config"); + List locations = this.resolver.resolve(this.context, "configtree:/etc/config", + false); assertThat(locations.size()).isEqualTo(1); assertThat(locations).extracting(Object::toString) .containsExactly("config tree [" + new File("/etc/config").getAbsolutePath() + "]"); 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 new file mode 100644 index 0000000000..0fff868cba --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/OptionalConfigDataLocationTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2012-2020 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 + * + * https://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.context.config; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.env.PropertySourceLoader; +import org.springframework.core.io.ClassPathResource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatObject; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link OptionalConfigDataLocation}. + * + * @author Phillip Webb + */ +class OptionalConfigDataLocationTests { + + private ConfigDataLocation location; + + @BeforeEach + void setup() { + this.location = new ResourceConfigDataLocation("classpath:application.properties", + new ClassPathResource("application.properties"), mock(PropertySourceLoader.class)); + } + + @Test + void createWrapsLocation() { + OptionalConfigDataLocation optionalLocation = new OptionalConfigDataLocation(this.location); + assertThat(optionalLocation.getLocation()).isSameAs(this.location); + } + + @Test + void equalsAndHashCode() { + OptionalConfigDataLocation optionalLocation1 = new OptionalConfigDataLocation(this.location); + OptionalConfigDataLocation optionalLocation2 = new OptionalConfigDataLocation(this.location); + assertThat(optionalLocation1.hashCode()).isEqualTo(optionalLocation2.hashCode()); + assertThat(optionalLocation1).isEqualTo(optionalLocation1).isEqualTo(optionalLocation2) + .isNotEqualTo(this.location); + } + + @Test + void toStringReturnsLocationString() { + OptionalConfigDataLocation optionalLocation = new OptionalConfigDataLocation(this.location); + assertThat(optionalLocation).hasToString(this.location.toString()); + } + + @Test + void wrapAllWrapsList() { + List locations = Collections.singletonList(this.location); + List optionalLocations = OptionalConfigDataLocation.wrapAll(locations); + assertThat(optionalLocations).hasSize(1); + assertThat(optionalLocations.get(0)).isInstanceOf(OptionalConfigDataLocation.class).extracting("location") + .isSameAs(this.location); + } + + @Test + void unwrapUnwrapps() { + ConfigDataLocation optionalLocation = new OptionalConfigDataLocation(this.location); + assertThatObject(OptionalConfigDataLocation.unwrap(optionalLocation)).isSameAs(this.location); + } + +} 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 bfc65bc673..3e8bb23731 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 @@ -70,7 +70,7 @@ public class ResourceConfigDataLocationResolverTests { @Test void resolveWhenLocationIsDirectoryResolvesAllMatchingFilesInDirectory() { String location = "classpath:/configdata/properties/"; - List locations = this.resolver.resolve(this.context, location); + List locations = this.resolver.resolve(this.context, location, true); assertThat(locations.size()).isEqualTo(1); assertThat(locations).extracting(Object::toString) .containsExactly("class path resource [configdata/properties/application.properties]"); @@ -79,7 +79,7 @@ public class ResourceConfigDataLocationResolverTests { @Test void resolveWhenLocationIsFileResolvesFile() { String location = "file:src/test/resources/configdata/properties/application.properties"; - List locations = this.resolver.resolve(this.context, location); + List locations = this.resolver.resolve(this.context, location, true); assertThat(locations.size()).isEqualTo(1); assertThat(locations).extracting(Object::toString).containsExactly( filePath("src", "test", "resources", "configdata", "properties", "application.properties")); @@ -88,7 +88,7 @@ public class ResourceConfigDataLocationResolverTests { @Test void resolveWhenLocationIsFileAndNoMatchingLoaderThrowsException() { String location = "file:src/test/resources/configdata/properties/application.unknown"; - assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location)) + assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true)) .withMessageStartingWith("Unable to load config data from") .satisfies((ex) -> assertThat(ex.getCause()).hasMessageStartingWith("File extension is not known")); } @@ -96,14 +96,14 @@ public class ResourceConfigDataLocationResolverTests { @Test void resolveWhenLocationWildcardIsSpecifiedForClasspathLocationThrowsException() { String location = "classpath*:application.properties"; - assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location)) + assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true)) .withMessageContaining("Classpath wildcard patterns cannot be used as a search location"); } @Test void resolveWhenLocationWildcardIsNotBeforeLastSlashThrowsException() { String location = "file:src/test/resources/*/config/"; - assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location)) + assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true)) .withMessageStartingWith("Search location '").withMessageEndingWith("' must end with '*/'"); } @@ -119,7 +119,7 @@ public class ResourceConfigDataLocationResolverTests { @Test void resolveWhenLocationHasMultipleWildcardsThrowsException() { String location = "file:src/test/resources/config/**/"; - assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location)) + assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true)) .withMessageStartingWith("Search location '") .withMessageEndingWith("' cannot contain multiple wildcards"); } @@ -129,7 +129,7 @@ public class ResourceConfigDataLocationResolverTests { String location = "file:src/test/resources/config/*/"; this.environment.setProperty("spring.config.name", "testproperties"); this.resolver = new ResourceConfigDataLocationResolver(null, this.environmentBinder, this.resourceLoader); - List locations = this.resolver.resolve(this.context, location); + List locations = this.resolver.resolve(this.context, location, true); assertThat(locations.size()).isEqualTo(3); assertThat(locations).extracting(Object::toString) .contains(filePath("src", "test", "resources", "config", "1-first", "testproperties.properties")) @@ -142,7 +142,7 @@ public class ResourceConfigDataLocationResolverTests { String location = "file:src/test/resources/config/*/"; this.environment.setProperty("spring.config.name", "testproperties"); this.resolver = new ResourceConfigDataLocationResolver(null, this.environmentBinder, this.resourceLoader); - List locations = this.resolver.resolve(this.context, location); + List locations = this.resolver.resolve(this.context, location, true); assertThat(locations).extracting(Object::toString).containsExactly( filePath("src", "test", "resources", "config", "0-empty", "testproperties.properties"), filePath("src", "test", "resources", "config", "1-first", "testproperties.properties"), @@ -152,7 +152,7 @@ public class ResourceConfigDataLocationResolverTests { @Test void resolveWhenLocationIsWildcardFilesLoadsAllFilesThatMatch() { String location = "file:src/test/resources/config/*/testproperties.properties"; - List locations = this.resolver.resolve(this.context, location); + List locations = this.resolver.resolve(this.context, location, true); assertThat(locations.size()).isEqualTo(3); assertThat(locations).extracting(Object::toString) .contains(filePath("src", "test", "resources", "config", "1-first", "testproperties.properties")) @@ -172,7 +172,7 @@ public class ResourceConfigDataLocationResolverTests { "classpath:/configdata/properties/application.properties", parentResource, new PropertiesPropertySourceLoader()); given(this.context.getParent()).willReturn(parent); - List locations = this.resolver.resolve(this.context, location); + List locations = this.resolver.resolve(this.context, location, true); assertThat(locations.size()).isEqualTo(1); assertThat(locations).extracting(Object::toString) .contains("class path resource [configdata/properties/other.properties]"); @@ -188,7 +188,7 @@ public class ResourceConfigDataLocationResolverTests { ResourceConfigDataLocation parent = new ResourceConfigDataLocation("classpath:/config/specific.properties", parentResource, new PropertiesPropertySourceLoader()); given(this.context.getParent()).willReturn(parent); - List locations = this.resolver.resolve(this.context, location); + List locations = this.resolver.resolve(this.context, location, true); assertThat(locations.size()).isEqualTo(1); assertThat(locations).extracting(Object::toString) .contains("class path resource [config/nested/3-third/testproperties.properties]"); @@ -201,7 +201,7 @@ public class ResourceConfigDataLocationResolverTests { ResourceConfigDataLocation parent = new ResourceConfigDataLocation( "classpath:/configdata/application.properties", parentResource, new PropertiesPropertySourceLoader()); given(this.context.getParent()).willReturn(parent); - assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location)) + assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true)) .withMessageStartingWith("Unable to load config data from 'application.other'") .satisfies((ex) -> assertThat(ex.getCause()).hasMessageStartingWith("File extension is not known")); } @@ -211,7 +211,7 @@ public class ResourceConfigDataLocationResolverTests { String location = "classpath:/configdata/properties/"; Profiles profiles = mock(Profiles.class); given(profiles.iterator()).willReturn(Collections.singletonList("dev").iterator()); - List locations = this.resolver.resolveProfileSpecific(this.context, location, + List locations = this.resolver.resolveProfileSpecific(this.context, location, true, profiles); assertThat(locations.size()).isEqualTo(1); assertThat(locations).extracting(Object::toString) @@ -224,7 +224,7 @@ public class ResourceConfigDataLocationResolverTests { Profiles profiles = mock(Profiles.class); given(profiles.iterator()).willReturn(Collections.emptyIterator()); given(profiles.getActive()).willReturn(Collections.singletonList("dev")); - List locations = this.resolver.resolveProfileSpecific(this.context, location, + List locations = this.resolver.resolveProfileSpecific(this.context, location, true, profiles); assertThat(locations).isEmpty(); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java index 726ffb5b05..cb739a1b1c 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/TestConfigDataBootstrap.java @@ -41,7 +41,7 @@ class TestConfigDataBootstrap { } @Override - public List resolve(ConfigDataLocationResolverContext context, String location) { + public List resolve(ConfigDataLocationResolverContext context, String location, boolean optional) { ResolverHelper helper = context.getBootstrapRegistry().get(ResolverHelper.class, () -> new ResolverHelper(location)); return Collections.singletonList(new Location(helper));