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
This commit is contained in:
Phillip Webb
2020-08-24 13:04:50 -07:00
parent 19558ecda7
commit 081a7ee28c
26 changed files with 648 additions and 82 deletions

View File

@@ -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 <<boot-features-external-config-optional-prefix,locations are optional>> 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 <<boot-features-external-config-files-importing, `spring.config.import`>> 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.

View File

@@ -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];

View File

@@ -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);

View File

@@ -54,7 +54,9 @@ public interface ConfigDataLoader<L extends ConfigDataLocation> {
* @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;
}

View File

@@ -86,9 +86,25 @@ class ConfigDataLoaders {
* @throws IOException on IO error
*/
<L extends ConfigDataLocation> 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 <L extends ConfigDataLocation> ConfigData load(ConfigDataLoaderContext context, boolean optional,
L location) throws IOException {
ConfigDataLoader<L> 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")

View File

@@ -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:";
}

View File

@@ -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);
}
}
}

View File

@@ -60,11 +60,14 @@ public interface ConfigDataLocationResolver<L extends ConfigDataLocation> {
* 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<L> resolve(ConfigDataLocationResolverContext context, String location);
List<L> 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<L extends ConfigDataLocation> {
* 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<L> resolveProfileSpecific(ConfigDataLocationResolverContext context, String location,
Profiles profiles) {
default List<L> resolveProfileSpecific(ConfigDataLocationResolverContext context, String location, boolean optional,
Profiles profiles) throws ConfigDataLocationNotFoundException {
return Collections.emptyList();
}

View File

@@ -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<ConfigDataLocationResolver<?>> resolvers;
/**
@@ -61,6 +65,7 @@ class ConfigDataLocationResolvers {
*/
ConfigDataLocationResolvers(DeferredLogFactory logFactory, Binder binder, ResourceLoader resourceLoader,
List<String> names) {
this.logger = logFactory.getLog(getClass());
Instantiator<ConfigDataLocationResolver<?>> instantiator = new Instantiator<>(ConfigDataLocationResolver.class,
(availableParameters) -> {
availableParameters.add(Log.class, logFactory::getLog);
@@ -106,28 +111,49 @@ class ConfigDataLocationResolvers {
private List<ConfigDataLocation> 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<ConfigDataLocation> resolve(ConfigDataLocationResolver<?> resolver,
ConfigDataLocationResolverContext context, String location, Profiles profiles) {
List<ConfigDataLocation> resolved = nonNullList(resolver.resolve(context, location));
ConfigDataLocationResolverContext context, boolean optional, String location, Profiles profiles) {
List<ConfigDataLocation> resolved = resolve(location, optional,
() -> resolver.resolve(context, location, optional));
if (profiles == null) {
return resolved;
}
List<ConfigDataLocation> profileSpecific = nonNullList(
resolver.resolveProfileSpecific(context, location, profiles));
List<ConfigDataLocation> profileSpecific = resolve(location, optional,
() -> resolver.resolveProfileSpecific(context, location, optional, profiles));
return merge(resolved, profileSpecific);
}
private List<ConfigDataLocation> resolve(String location, boolean optional,
Supplier<List<? extends ConfigDataLocation>> resolveAction) {
try {
List<ConfigDataLocation> 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 <T> List<T> nonNullList(List<? extends T> list) {
return (list != null) ? (List<T>) list : Collections.emptyList();

View File

@@ -428,10 +428,23 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
getSearchLocations().forEach((location) -> {
boolean isDirectory = location.endsWith("/");
Set<String> 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)) {

View File

@@ -32,6 +32,7 @@ class ConfigTreeConfigDataLoader implements ConfigDataLoader<ConfigTreeConfigDat
@Override
public ConfigData load(ConfigDataLoaderContext context, ConfigTreeConfigDataLocation location) throws IOException {
ConfigDataLocationNotFoundException.throwIfDoesNotExist(location, location.getPath());
Path path = location.getPath();
String name = "Config tree '" + path + "'";
ConfigTreePropertySource source = new ConfigTreePropertySource(name, path);

View File

@@ -35,7 +35,8 @@ class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationResolver
}
@Override
public List<ConfigTreeConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location) {
public List<ConfigTreeConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) {
ConfigTreeConfigDataLocation resolved = new ConfigTreeConfigDataLocation(location.substring(PREFIX.length()));
return Collections.singletonList(resolved);
}

View File

@@ -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<ConfigDataLocation> wrapAll(List<ConfigDataLocation> locations) {
List<ConfigDataLocation> wrapped = new ArrayList<>(locations.size());
locations.forEach((location) -> wrapped.add(new OptionalConfigDataLocation(location)));
return wrapped;
}
@SuppressWarnings("unchecked")
static <L extends ConfigDataLocation> L unwrap(ConfigDataLocation wrapped) {
return (L) ((OptionalConfigDataLocation) wrapped).getLocation();
}
}

View File

@@ -30,6 +30,7 @@ class ResourceConfigDataLoader implements ConfigDataLoader<ResourceConfigDataLoc
@Override
public ConfigData load(ConfigDataLoaderContext context, ResourceConfigDataLocation location) throws IOException {
ConfigDataLocationNotFoundException.throwIfDoesNotExist(location, location.getResource());
return new ConfigData(location.load());
}

View File

@@ -55,6 +55,10 @@ class ResourceConfigDataLocation extends ConfigDataLocation {
this.propertySourceLoader = propertySourceLoader;
}
Resource getResource() {
return this.resource;
}
String getLocation() {
return this.name;
}

View File

@@ -110,23 +110,25 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
}
@Override
public List<ResourceConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location) {
return resolve(location, getResolvables(context, location));
public List<ResourceConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) {
return resolve(location, getResolvables(context, location, optional));
}
@Override
public List<ResourceConfigDataLocation> 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<Resolvable> getResolvables(ConfigDataLocationResolverContext context, String location) {
private Set<Resolvable> 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<R
}
private Set<Resolvable> getProfileSpecificResolvables(ConfigDataLocationResolverContext context, String location,
Profiles profiles) {
boolean optional, Profiles profiles) {
Set<Resolvable> 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<R
return resourceLocation;
}
private Set<Resolvable> getResolvables(String resourceLocation, String profile) {
private Set<Resolvable> 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<Resolvable> getResolvablesForDirectory(String resourceLocation, String profile) {
private Set<Resolvable> getResolvablesForDirectory(String directoryLocation, boolean optional, String profile) {
Set<Resolvable> 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<Resolvable> getResolvablesForFile(String resourceLocation, String profile) {
private Set<Resolvable> 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<R
for (Resolvable resolvable : resolvables) {
resolved.addAll(resolve(location, resolvable));
}
if (resolved.isEmpty()) {
assertNonOptionalDirectories(location, resolvables);
}
return resolved;
}
private void assertNonOptionalDirectories(String location, Set<Resolvable> 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<ResourceConfigDataLocation> resolve(String location, Resolvable resolvable) {
if (!resolvable.isPatternLocation()) {
return resolveNonPattern(location, resolvable);
@@ -219,23 +238,22 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
private List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> resolvePattern(String location, Resolvable resolvable) {
validatePatternLocation(resolvable.getResourceLocation());
List<ResourceConfigDataLocation> 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<R
String fileName = resourceLocationPattern.substring(resourceLocationPattern.lastIndexOf("/") + 1);
Resource directoryResource = loadResource(directoryPath);
if (!directoryResource.exists()) {
return EMPTY_RESOURCES;
return new Resource[] { directoryResource };
}
File directory = getDirectory(resourceLocationPattern, directoryResource);
File[] subDirectories = directory.listFiles(File::isDirectory);
@@ -311,16 +329,38 @@ class ResourceConfigDataLocationResolver implements ConfigDataLocationResolver<R
*/
private static class Resolvable {
private final String directory;
private final String resourceLocation;
private final boolean optional;
private final String profile;
private final PropertySourceLoader loader;
Resolvable(String rootLocation, String profile, String extension, PropertySourceLoader loader) {
Resolvable(String directory, String rootLocation, boolean optional, String profile, String extension,
PropertySourceLoader loader) {
String profileSuffix = (StringUtils.hasText(profile)) ? "-" + profile : "";
this.directory = directory;
this.resourceLocation = rootLocation + profileSuffix + "." + extension;
this.optional = optional;
this.profile = profile;
this.loader = loader;
}
boolean isNonOptionalDirectory() {
return !this.optional && this.directory != null;
}
String getDirectory() {
return this.directory;
}
boolean isSkippable() {
return this.optional || this.directory != null || this.profile != null;
}
boolean isPatternLocation() {
return this.resourceLocation.contains("*");
}

View File

@@ -163,7 +163,7 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
@Test
void runWhenOneCustomLocationDoesNotExistLoadsOthers() {
ConfigurableApplicationContext context = this.application.run(
"--spring.config.location=classpath:application.properties,classpath:testproperties.properties,classpath:nonexistent.properties");
"--spring.config.location=classpath:application.properties,classpath:testproperties.properties,optional:classpath:nonexistent.properties");
String property = context.getEnvironment().getProperty("the.property");
assertThat(property).isEqualTo("frompropertiesfile");
}
@@ -503,11 +503,18 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
}
@Test
void runWhenConfigLocationHasUnknownDirectoryContinuesToLoad() {
String location = "classpath:application.unknown/";
void runWhenConfigLocationHasOptionalMissingDirectoryContinuesToLoad() {
String location = "optional:classpath:application.unknown/";
this.application.run("--spring.config.location=" + location);
}
@Test
void runWhenConfigLocationHasNonOptionalMissingDirectoryThrowsException() {
String location = "classpath:application.unknown/";
assertThatExceptionOfType(ConfigDataLocationNotFoundException.class)
.isThrownBy(() -> 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<ConfigurableEnvironment> matchingPropertySource(final String sourceName) {
return new Condition<ConfigurableEnvironment>("environment containing property source " + sourceName) {

View File

@@ -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";
}

View File

@@ -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)));
}
}

View File

@@ -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<ConfigDataLocation> {
@@ -48,7 +48,8 @@ class ConfigDataLocationResolverTests {
}
@Override
public List<ConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location) {
public List<ConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) {
return null;
}

View File

@@ -151,13 +151,14 @@ class ConfigDataLocationResolversTests {
}
@Override
public List<TestConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location) {
public List<TestConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) {
return Collections.singletonList(new TestConfigDataLocation(this, location, false));
}
@Override
public List<TestConfigDataLocation> resolveProfileSpecific(ConfigDataLocationResolverContext context,
String location, Profiles profiles) {
String location, boolean optional, Profiles profiles) {
return Collections.singletonList(new TestConfigDataLocation(this, location, true));
}

View File

@@ -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));
}
}

View File

@@ -49,7 +49,8 @@ class ConfigTreeConfigDataLocationResolverTests {
@Test
void resolveReturnsConfigVolumeMountLocation() {
List<ConfigTreeConfigDataLocation> locations = this.resolver.resolve(this.context, "configtree:/etc/config");
List<ConfigTreeConfigDataLocation> 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() + "]");

View File

@@ -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<ConfigDataLocation> locations = Collections.singletonList(this.location);
List<ConfigDataLocation> 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);
}
}

View File

@@ -70,7 +70,7 @@ public class ResourceConfigDataLocationResolverTests {
@Test
void resolveWhenLocationIsDirectoryResolvesAllMatchingFilesInDirectory() {
String location = "classpath:/configdata/properties/";
List<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location);
List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location);
List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location);
List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location);
List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location);
List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location);
List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location);
List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> locations = this.resolver.resolveProfileSpecific(this.context, location,
List<ResourceConfigDataLocation> 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<ResourceConfigDataLocation> locations = this.resolver.resolveProfileSpecific(this.context, location,
List<ResourceConfigDataLocation> locations = this.resolver.resolveProfileSpecific(this.context, location, true,
profiles);
assertThat(locations).isEmpty();
}

View File

@@ -41,7 +41,7 @@ class TestConfigDataBootstrap {
}
@Override
public List<Location> resolve(ConfigDataLocationResolverContext context, String location) {
public List<Location> resolve(ConfigDataLocationResolverContext context, String location, boolean optional) {
ResolverHelper helper = context.getBootstrapRegistry().get(ResolverHelper.class,
() -> new ResolverHelper(location));
return Collections.singletonList(new Location(helper));