Improve ConfigData processing code

Refactor `ConfigData` processing code to make it less awkward to
follow.

Prior to this commit the `ConfigDataLocationResolver` would take a
String location and return a `ConfigDataLocation` instance. This was
a little confusing since sometimes we would refer to `location` as the
String value, and sometimes it would be the typed instance. We also
had nowhere sensible to put the `optional:` prefix logic and we needed
to pass a `boolean` parameter to a number of methods. The recently
introduced `Orgin` support also didn't have a good home.

To solve this, `ConfigDataLocation` has been renamed to
`ConfigDataResource`. This frees up `ConfigDataLocation` to be used
as a richer `location` type that holds the String value, the `Orgin`
and provides a home for the `optional:` logic.

This commit also cleans up a few other areas of the code, including
renaming `ResourceConfigData...` to `StandardConfigData...`. It also
introduces a new exception hierarchy for `ConfigDataNotFoundExceptions`.

Closes gh-23711
pull/23706/head
Phillip Webb 4 years ago
parent f89b99bdbc
commit 1cf9fc107e

@ -663,7 +663,7 @@ You can use this prefix with the `spring.config.location` and `spring.config.add
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.
If you want to ignore all `ConfigDataLocationNotFoundExceptions` and always continue to start your application, you can use the `spring.config.on-location-not-found` property.
If you want to ignore all `ConfigDataLocationNotFoundExceptions` and always continue to start your application, you can use the `spring.config.on-not-found` property.
Set the value to `ignore` using `SpringApplication.setDefaultProperties(...)` or with a system/environment variable.

@ -29,9 +29,9 @@ import org.springframework.core.env.PropertySource;
import org.springframework.util.Assert;
/**
* Configuration data that has been loaded from an external {@link ConfigDataLocation
* location} and may ultimately contribute {@link PropertySource property sources} to
* Spring's {@link Environment}.
* Configuration data that has been loaded from a {@link ConfigDataResource} and may
* ultimately contribute {@link PropertySource property sources} to Spring's
* {@link Environment}.
*
* @author Phillip Webb
* @author Madhura Bhave

@ -48,10 +48,10 @@ import org.springframework.util.StringUtils;
* Wrapper around a {@link ConfigurableEnvironment} that can be used to import and apply
* {@link ConfigData}. Configures the initial set of
* {@link ConfigDataEnvironmentContributors} by wrapping property sources from the Spring
* {@link Environment} and adding the initial set of imports.
* {@link Environment} and adding the initial set of locations.
* <p>
* The initial imports can be influenced via the {@link #LOCATION_PROPERTY},
* {@value #ADDITIONAL_LOCATION_PROPERTY} and {@value #IMPORT_PROPERTY} properties. If not
* The initial locations can be influenced via the {@link #LOCATION_PROPERTY},
* {@value #ADDITIONAL_LOCATION_PROPERTY} and {@value #IMPORT_PROPERTY} properties. If no
* explicit properties are set, the {@link #DEFAULT_SEARCH_LOCATIONS} will be used.
*
* @author Phillip Webb
@ -76,28 +76,41 @@ class ConfigDataEnvironment {
/**
* Property used to determine what action to take when a
* {@code ConfigDataLocationNotFoundException} is thrown.
* @see ConfigDataLocationNotFoundAction
* {@code ConfigDataNotFoundAction} is thrown.
* @see ConfigDataNotFoundAction
*/
static final String ON_LOCATION_NOT_FOUND_PROPERTY = "spring.config.on-location-not-found";
static final String ON_NOT_FOUND_PROPERTY = "spring.config.on-not-found";
/**
* Default search locations used if not {@link #LOCATION_PROPERTY} is found.
*/
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];
static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
static {
List<ConfigDataLocation> locations = new ArrayList<>();
locations.add(ConfigDataLocation.of("optional:classpath:/"));
locations.add(ConfigDataLocation.of("optional:classpath:/config/"));
locations.add(ConfigDataLocation.of("optional:file:./"));
locations.add(ConfigDataLocation.of("optional:file:./config/*/"));
locations.add(ConfigDataLocation.of("optional:file:./config/"));
DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
};
private static final ConfigDataLocation[] EMPTY_LOCATIONS = new ConfigDataLocation[0];
private static final ConfigurationPropertyName INCLUDE_PROFILES = ConfigurationPropertyName
.of(Profiles.INCLUDE_PROFILES_PROPERTY_NAME);
private static final Bindable<ConfigDataLocation[]> CONFIG_DATA_LOCATION_ARRAY = Bindable
.of(ConfigDataLocation[].class);
private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);
private final DeferredLogFactory logFactory;
private final Log logger;
private final ConfigDataNotFoundAction notFoundAction;
private final ConfigurableBootstrapContext bootstrapContext;
private final ConfigurableEnvironment environment;
@ -122,25 +135,21 @@ class ConfigDataEnvironment {
ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
Binder binder = Binder.get(environment);
UseLegacyConfigProcessingException.throwIfRequested(binder);
ConfigDataLocationNotFoundAction locationNotFoundAction = binder
.bind(ON_LOCATION_NOT_FOUND_PROPERTY, ConfigDataLocationNotFoundAction.class)
.orElse(ConfigDataLocationNotFoundAction.FAIL);
this.logFactory = logFactory;
this.logger = logFactory.getLog(getClass());
this.notFoundAction = binder.bind(ON_NOT_FOUND_PROPERTY, ConfigDataNotFoundAction.class)
.orElse(ConfigDataNotFoundAction.FAIL);
this.bootstrapContext = bootstrapContext;
this.environment = environment;
this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, locationNotFoundAction, binder,
resourceLoader);
this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
this.additionalProfiles = additionalProfiles;
this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext, locationNotFoundAction);
this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext);
this.contributors = createContributors(binder);
}
protected ConfigDataLocationResolvers createConfigDataLocationResolvers(DeferredLogFactory logFactory,
ConfigurableBootstrapContext bootstrapContext, ConfigDataLocationNotFoundAction locationNotFoundAction,
Binder binder, ResourceLoader resourceLoader) {
return new ConfigDataLocationResolvers(logFactory, bootstrapContext, locationNotFoundAction, binder,
resourceLoader);
ConfigurableBootstrapContext bootstrapContext, Binder binder, ResourceLoader resourceLoader) {
return new ConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
}
private ConfigDataEnvironmentContributors createContributors(Binder binder) {
@ -172,23 +181,26 @@ class ConfigDataEnvironment {
private List<ConfigDataEnvironmentContributor> getInitialImportContributors(Binder binder) {
List<ConfigDataEnvironmentContributor> initialContributors = new ArrayList<>();
addInitialImportContributors(initialContributors, bindLocations(binder, IMPORT_PROPERTY, EMPTY_LOCATIONS));
addInitialImportContributors(initialContributors,
binder.bind(IMPORT_PROPERTY, String[].class).orElse(EMPTY_LOCATIONS));
bindLocations(binder, ADDITIONAL_LOCATION_PROPERTY, EMPTY_LOCATIONS));
addInitialImportContributors(initialContributors,
binder.bind(ADDITIONAL_LOCATION_PROPERTY, String[].class).orElse(EMPTY_LOCATIONS));
addInitialImportContributors(initialContributors,
binder.bind(LOCATION_PROPERTY, String[].class).orElse(DEFAULT_SEARCH_LOCATIONS));
bindLocations(binder, LOCATION_PROPERTY, DEFAULT_SEARCH_LOCATIONS));
return initialContributors;
}
private ConfigDataLocation[] bindLocations(Binder binder, String propertyName, ConfigDataLocation[] other) {
return binder.bind(propertyName, CONFIG_DATA_LOCATION_ARRAY).orElse(other);
}
private void addInitialImportContributors(List<ConfigDataEnvironmentContributor> initialContributors,
String[] locations) {
ConfigDataLocation[] locations) {
for (int i = locations.length - 1; i >= 0; i--) {
initialContributors.add(createInitialImportContributor(locations[i]));
}
}
private ConfigDataEnvironmentContributor createInitialImportContributor(String location) {
private ConfigDataEnvironmentContributor createInitialImportContributor(ConfigDataLocation location) {
this.logger.trace(LogMessage.format("Adding initial config data import from location '%s'", location));
return ConfigDataEnvironmentContributor.ofInitialImport(location);
}
@ -198,7 +210,8 @@ class ConfigDataEnvironment {
* {@link Environment}.
*/
void processAndApply() {
ConfigDataImporter importer = new ConfigDataImporter(this.resolvers, this.loaders);
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
this.loaders);
this.bootstrapContext.register(Binder.class, InstanceSupplier
.from(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE)));
ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);

@ -28,7 +28,6 @@ import java.util.stream.StreamSupport;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.core.env.Environment;
import org.springframework.core.env.PropertySource;
@ -52,7 +51,7 @@ import org.springframework.core.env.PropertySource;
*/
class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironmentContributor> {
private final ConfigDataLocation location;
private final ConfigDataResource resource;
private final PropertySource<?> propertySource;
@ -69,7 +68,7 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
/**
* Create a new {@link ConfigDataEnvironmentContributor} instance.
* @param kind the contributor kind
* @param location the location that contributed the data or {@code null}
* @param resource the resource that contributed the data or {@code null}
* @param propertySource the property source for the data or {@code null}
* @param configurationPropertySource the configuration property source for the data
* or {@code null}
@ -77,11 +76,11 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
* @param ignoreImports if import properties should be ignored
* @param children the children of this contributor at each {@link ImportPhase}
*/
ConfigDataEnvironmentContributor(Kind kind, ConfigDataLocation location, PropertySource<?> propertySource,
ConfigDataEnvironmentContributor(Kind kind, ConfigDataResource resource, PropertySource<?> propertySource,
ConfigurationPropertySource configurationPropertySource, ConfigDataProperties properties,
boolean ignoreImports, Map<ImportPhase, List<ConfigDataEnvironmentContributor>> children) {
this.kind = kind;
this.location = location;
this.resource = resource;
this.properties = properties;
this.propertySource = propertySource;
this.configurationPropertySource = configurationPropertySource;
@ -107,11 +106,11 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
}
/**
* Return the location that contributed this instance.
* @return the location or {@code null}
* Return the resource that contributed this instance.
* @return the resource or {@code null}
*/
ConfigDataLocation getLocation() {
return this.location;
ConfigDataResource getResource() {
return this.resource;
}
/**
@ -134,14 +133,10 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
* Return any imports requested by this contributor.
* @return the imports
*/
List<String> getImports() {
List<ConfigDataLocation> getImports() {
return (this.properties != null) ? this.properties.getImports() : Collections.emptyList();
}
Origin getImportOrigin(String importLocation) {
return (this.properties != null) ? this.properties.getImportOrigin(importLocation) : null;
}
/**
* Return true if this contributor has imports that have not yet been processed in the
* given phase.
@ -184,13 +179,19 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
return new ContributorIterator();
}
/**
* Create an new {@link ConfigDataEnvironmentContributor} with bound
* {@link ConfigDataProperties}.
* @param binder the binder to use
* @return a new contributor instance
*/
ConfigDataEnvironmentContributor withBoundProperties(Binder binder) {
UseLegacyConfigProcessingException.throwIfRequested(binder);
ConfigDataProperties properties = ConfigDataProperties.get(binder);
if (this.ignoreImports) {
properties = properties.withoutImports();
}
return new ConfigDataEnvironmentContributor(Kind.BOUND_IMPORT, this.location, this.propertySource,
return new ConfigDataEnvironmentContributor(Kind.BOUND_IMPORT, this.resource, this.propertySource,
this.configurationPropertySource, properties, this.ignoreImports, null);
}
@ -205,7 +206,7 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
List<ConfigDataEnvironmentContributor> children) {
Map<ImportPhase, List<ConfigDataEnvironmentContributor>> updatedChildren = new LinkedHashMap<>(this.children);
updatedChildren.put(importPhase, children);
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.propertySource,
return new ConfigDataEnvironmentContributor(this.kind, this.resource, this.propertySource,
this.configurationPropertySource, this.properties, this.ignoreImports, updatedChildren);
}
@ -230,7 +231,7 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
}
updatedChildren.put(importPhase, Collections.unmodifiableList(updatedContributors));
});
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.propertySource,
return new ConfigDataEnvironmentContributor(this.kind, this.resource, this.propertySource,
this.configurationPropertySource, this.properties, this.ignoreImports, updatedChildren);
}
@ -249,11 +250,11 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
* Factory method to create a {@link Kind#INITIAL_IMPORT initial import} contributor.
* This contributor is used to trigger initial imports of additional contributors. It
* does not contribute any properties itself.
* @param importLocation the initial import location (with placeholder resolved)
* @param initialImport the initial import location (with placeholders resolved)
* @return a new {@link ConfigDataEnvironmentContributor} instance
*/
static ConfigDataEnvironmentContributor ofInitialImport(String importLocation) {
List<String> imports = Collections.singletonList(importLocation);
static ConfigDataEnvironmentContributor ofInitialImport(ConfigDataLocation initialImport) {
List<ConfigDataLocation> imports = Collections.singletonList(initialImport);
ConfigDataProperties properties = new ConfigDataProperties(imports, null);
return new ConfigDataEnvironmentContributor(Kind.INITIAL_IMPORT, null, null, null, properties, false, null);
}
@ -274,17 +275,17 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
* Factory method to create an {@link Kind#UNBOUND_IMPORT unbound import} contributor.
* This contributor has been actively imported from another contributor and may itself
* import further contributors later.
* @param location the location of imported config data
* @param resource the condig data resource
* @param configData the config data
* @param propertySourceIndex the index of the property source that should be used
* @return a new {@link ConfigDataEnvironmentContributor} instance
*/
static ConfigDataEnvironmentContributor ofUnboundImport(ConfigDataLocation location, ConfigData configData,
static ConfigDataEnvironmentContributor ofUnboundImport(ConfigDataResource resource, ConfigData configData,
int propertySourceIndex) {
PropertySource<?> propertySource = configData.getPropertySources().get(propertySourceIndex);
ConfigurationPropertySource configurationPropertySource = ConfigurationPropertySource.from(propertySource);
boolean ignoreImports = configData.getOptions().contains(ConfigData.Option.IGNORE_IMPORTS);
return new ConfigDataEnvironmentContributor(Kind.UNBOUND_IMPORT, location, propertySource,
return new ConfigDataEnvironmentContributor(Kind.UNBOUND_IMPORT, resource, propertySource,
configurationPropertySource, null, ignoreImports, null);
}

@ -64,9 +64,9 @@ class ConfigDataEnvironmentContributorPlaceholdersResolver implements Placeholde
Object value = (propertySource != null) ? propertySource.getProperty(placeholder) : null;
if (value != null && !contributor.isActive(this.activationContext)) {
if (this.failOnResolveFromInactiveContributor) {
ConfigDataResource resource = contributor.getResource();
Origin origin = OriginLookup.getOrigin(propertySource, placeholder);
throw new InactiveConfigDataAccessException(propertySource, contributor.getLocation(), placeholder,
origin);
throw new InactiveConfigDataAccessException(propertySource, resource, placeholder, origin);
}
value = null;
}

@ -39,7 +39,6 @@ import org.springframework.boot.context.properties.bind.PlaceholdersResolver;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.boot.origin.Origin;
import org.springframework.core.log.LogMessage;
import org.springframework.util.ObjectUtils;
@ -114,12 +113,12 @@ class ConfigDataEnvironmentContributors implements Iterable<ConfigDataEnvironmen
ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
result, contributor, activationContext);
ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
List<String> imports = contributor.getImports();
List<ConfigDataLocation> imports = contributor.getImports();
this.logger.trace(LogMessage.format("Processing imports %s", imports));
Map<ConfigDataLocation, ConfigData> imported = importer.resolveAndLoad(activationContext,
Map<ConfigDataResource, ConfigData> imported = importer.resolveAndLoad(activationContext,
locationResolverContext, loaderContext, imports);
this.logger.trace(LogMessage.of(() -> imported.isEmpty() ? "Nothing imported" : "Imported "
+ imported.size() + " location " + ((imported.size() != 1) ? "s" : "") + imported.keySet()));
+ imported.size() + " resource " + ((imported.size() != 1) ? "s" : "") + imported.keySet()));
ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
asContributors(imported));
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
@ -148,7 +147,7 @@ class ConfigDataEnvironmentContributors implements Iterable<ConfigDataEnvironmen
return contributor.isActive(activationContext) && contributor.hasUnprocessedImports(importPhase);
}
private List<ConfigDataEnvironmentContributor> asContributors(Map<ConfigDataLocation, ConfigData> imported) {
private List<ConfigDataEnvironmentContributor> asContributors(Map<ConfigDataResource, ConfigData> imported) {
List<ConfigDataEnvironmentContributor> contributors = new ArrayList<>(imported.size() * 5);
imported.forEach((location, data) -> {
for (int i = data.getPropertySources().size() - 1; i >= 0; i--) {
@ -259,8 +258,8 @@ class ConfigDataEnvironmentContributors implements Iterable<ConfigDataEnvironmen
}
@Override
public ConfigDataLocation getParent() {
return this.contributor.getLocation();
public ConfigDataResource getParent() {
return this.contributor.getResource();
}
@Override
@ -268,11 +267,6 @@ class ConfigDataEnvironmentContributors implements Iterable<ConfigDataEnvironmen
return this.contributors.getBootstrapContext();
}
@Override
public Origin getLocationOrigin(String location) {
return this.contributor.getImportOrigin(location);
}
}
private class InactiveSourceChecker implements BindHandler {

@ -53,9 +53,9 @@ public class ConfigDataEnvironmentPostProcessor implements EnvironmentPostProces
/**
* Property used to determine what action to take when a
* {@code ConfigDataLocationNotFoundException} is thrown.
* @see ConfigDataLocationNotFoundAction
* @see ConfigDataNotFoundAction
*/
public static final String ON_LOCATION_NOT_FOUND_PROPERTY = ConfigDataEnvironment.ON_LOCATION_NOT_FOUND_PROPERTY;
public static final String ON_LOCATION_NOT_FOUND_PROPERTY = ConfigDataEnvironment.ON_NOT_FOUND_PROPERTY;
private final DeferredLogFactory logFactory;

@ -17,6 +17,7 @@
package org.springframework.boot.context.config;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
@ -24,9 +25,13 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.springframework.boot.logging.DeferredLogFactory;
/**
* Imports {@link ConfigData} by {@link ConfigDataLocationResolver resolving} and
* {@link ConfigDataLoader loading} imports. {@link ConfigDataLocation locations} are
* {@link ConfigDataLoader loading} locations. {@link ConfigDataResource resources} are
* tracked to ensure that they are not imported multiple times.
*
* @author Phillip Webb
@ -34,20 +39,29 @@ import java.util.Set;
*/
class ConfigDataImporter {
private final Log logger;
private final ConfigDataLocationResolvers resolvers;
private final ConfigDataLoaders loaders;
private final Set<ConfigDataLocation> loadedLocations = new HashSet<>();
private final ConfigDataNotFoundAction notFoundAction;
private final Set<ConfigDataResource> loaded = new HashSet<>();
/**
* Create a new {@link ConfigDataImporter} instance.
* @param logFactory the log factory
* @param notFoundAction the action to take when a location cannot be found
* @param resolvers the config data location resolvers
* @param loaders the config data loaders
*/
ConfigDataImporter(ConfigDataLocationResolvers resolvers, ConfigDataLoaders loaders) {
ConfigDataImporter(DeferredLogFactory logFactory, ConfigDataNotFoundAction notFoundAction,
ConfigDataLocationResolvers resolvers, ConfigDataLoaders loaders) {
this.logger = logFactory.getLog(getClass());
this.resolvers = resolvers;
this.loaders = loaders;
this.notFoundAction = notFoundAction;
}
/**
@ -59,31 +73,70 @@ class ConfigDataImporter {
* @param locations the locations to resolve
* @return a map of the loaded locations and data
*/
Map<ConfigDataLocation, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext,
Map<ConfigDataResource, ConfigData> resolveAndLoad(ConfigDataActivationContext activationContext,
ConfigDataLocationResolverContext locationResolverContext, ConfigDataLoaderContext loaderContext,
List<String> locations) {
List<ConfigDataLocation> locations) {
try {
Profiles profiles = (activationContext != null) ? activationContext.getProfiles() : null;
return load(loaderContext, this.resolvers.resolveAll(locationResolverContext, locations, profiles));
List<ConfigDataResolutionResult> resolved = resolve(locationResolverContext, profiles, locations);
return load(loaderContext, resolved);
}
catch (IOException ex) {
throw new IllegalStateException("IO error on loading imports from " + locations, ex);
}
}
private Map<ConfigDataLocation, ConfigData> load(ConfigDataLoaderContext loaderContext,
List<ConfigDataLocation> locations) throws IOException {
Map<ConfigDataLocation, ConfigData> result = new LinkedHashMap<>();
for (int i = locations.size() - 1; i >= 0; i--) {
ConfigDataLocation location = locations.get(i);
if (this.loadedLocations.add(location)) {
ConfigData loaded = this.loaders.load(loaderContext, location);
if (loaded != null) {
result.put(location, loaded);
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext locationResolverContext,
Profiles profiles, List<ConfigDataLocation> locations) {
List<ConfigDataResolutionResult> resolved = new ArrayList<>(locations.size());
for (ConfigDataLocation location : locations) {
resolved.addAll(resolve(locationResolverContext, profiles, location));
}
return Collections.unmodifiableList(resolved);
}
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext locationResolverContext,
Profiles profiles, ConfigDataLocation location) {
try {
return this.resolvers.resolve(locationResolverContext, location, profiles);
}
catch (ConfigDataNotFoundException ex) {
handle(ex, location);
return Collections.emptyList();
}
}
private Map<ConfigDataResource, ConfigData> load(ConfigDataLoaderContext loaderContext,
List<ConfigDataResolutionResult> candidates) throws IOException {
Map<ConfigDataResource, ConfigData> result = new LinkedHashMap<>();
for (int i = candidates.size() - 1; i >= 0; i--) {
ConfigDataResolutionResult candidate = candidates.get(i);
ConfigDataLocation location = candidate.getLocation();
ConfigDataResource resource = candidate.getResource();
if (this.loaded.add(resource)) {
try {
ConfigData loaded = this.loaders.load(loaderContext, resource);
if (loaded != null) {
result.put(resource, loaded);
}
}
catch (ConfigDataNotFoundException ex) {
handle(ex, location);
}
}
}
return Collections.unmodifiableMap(result);
}
private void handle(ConfigDataNotFoundException ex, ConfigDataLocation location) {
if (ex instanceof ConfigDataResourceNotFoundException) {
ex = ((ConfigDataResourceNotFoundException) ex).withLocation(location);
}
getNotFoundAction(location).handle(this.logger, ex);
}
private ConfigDataNotFoundAction getNotFoundAction(ConfigDataLocation location) {
return (!location.isOptional()) ? this.notFoundAction : ConfigDataNotFoundAction.IGNORE;
}
}

@ -25,8 +25,8 @@ import org.springframework.boot.BootstrapRegistry;
import org.springframework.boot.ConfigurableBootstrapContext;
/**
* Strategy class that can be used used to load {@link ConfigData} instances from a
* {@link ConfigDataLocation location}. Implementations should be added as a
* Strategy class that can be used used to load {@link ConfigData} for a given
* {@link ConfigDataResource}. Implementations should be added as a
* {@code spring.factories} entries. The following constructor parameter types are
* supported:
* <ul>
@ -36,34 +36,34 @@ import org.springframework.boot.ConfigurableBootstrapContext;
* ({@link BootstrapContext} or {@link BootstrapRegistry} may also be used).</li>
* </ul>
* <p>
* Multiple loaders cannot claim the same location.
* Multiple loaders cannot claim the same resource.
*
* @param <L> the location type
* @param <R> the resource type
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.4.0
*/
public interface ConfigDataLoader<L extends ConfigDataLocation> {
public interface ConfigDataLoader<R extends ConfigDataResource> {
/**
* Returns if the specified location can be loaded by this instance.
* Returns if the specified resource can be loaded by this instance.
* @param context the loader context
* @param location the location to check.
* @return if the location is supported by this loader
* @param resource the resource to check.
* @return if the resource is supported by this loader
*/
default boolean isLoadable(ConfigDataLoaderContext context, L location) {
default boolean isLoadable(ConfigDataLoaderContext context, R resource) {
return true;
}
/**
* Load {@link ConfigData} for the given location.
* Load {@link ConfigData} for the given resource.
* @param context the loader context
* @param location the location to load
* @param resource the resource 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
* @throws ConfigDataResourceNotFoundException if the resource cannot be found
*/
ConfigData load(ConfigDataLoaderContext context, L location)
throws IOException, ConfigDataLocationNotFoundException;
ConfigData load(ConfigDataLoaderContext context, R resource)
throws IOException, ConfigDataResourceNotFoundException;
}

@ -43,37 +43,28 @@ class ConfigDataLoaders {
private final Log logger;
private final ConfigDataLocationNotFoundAction locationNotFoundAction;
private final List<ConfigDataLoader<?>> loaders;
private final List<Class<?>> locationTypes;
private final List<Class<?>> resourceTypes;
/**
* Create a new {@link ConfigDataLoaders} instance.
* @param logFactory the deferred log factory
* @param bootstrapContext the bootstrap context
* @param locationNotFoundAction the action to take if a
* {@link ConfigDataLocationNotFoundException} is thrown
*/
ConfigDataLoaders(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ConfigDataLocationNotFoundAction locationNotFoundAction) {
this(logFactory, bootstrapContext, locationNotFoundAction,
SpringFactoriesLoader.loadFactoryNames(ConfigDataLoader.class, null));
ConfigDataLoaders(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext) {
this(logFactory, bootstrapContext, SpringFactoriesLoader.loadFactoryNames(ConfigDataLoader.class, null));
}
/**
* Create a new {@link ConfigDataLoaders} instance.
* @param logFactory the deferred log factory
* @param bootstrapContext the bootstrap context
* @param locationNotFoundAction the action to take if a
* {@link ConfigDataLocationNotFoundException} is thrown
* @param names the {@link ConfigDataLoader} class names instantiate
*/
ConfigDataLoaders(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ConfigDataLocationNotFoundAction locationNotFoundAction, List<String> names) {
List<String> names) {
this.logger = logFactory.getLog(getClass());
this.locationNotFoundAction = locationNotFoundAction;
Instantiator<ConfigDataLoader<?>> instantiator = new Instantiator<>(ConfigDataLoader.class,
(availableParameters) -> {
availableParameters.add(Log.class, logFactory::getLog);
@ -82,69 +73,52 @@ class ConfigDataLoaders {
availableParameters.add(BootstrapRegistry.class, bootstrapContext);
});
this.loaders = instantiator.instantiate(names);
this.locationTypes = getLocationTypes(this.loaders);
this.resourceTypes = getResourceTypes(this.loaders);
}
private List<Class<?>> getLocationTypes(List<ConfigDataLoader<?>> loaders) {
List<Class<?>> locationTypes = new ArrayList<>(loaders.size());
private List<Class<?>> getResourceTypes(List<ConfigDataLoader<?>> loaders) {
List<Class<?>> resourceTypes = new ArrayList<>(loaders.size());
for (ConfigDataLoader<?> loader : loaders) {
locationTypes.add(getLocationType(loader));
resourceTypes.add(getResourceType(loader));
}
return Collections.unmodifiableList(locationTypes);
return Collections.unmodifiableList(resourceTypes);
}
private Class<?> getLocationType(ConfigDataLoader<?> loader) {
private Class<?> getResourceType(ConfigDataLoader<?> loader) {
return ResolvableType.forClass(loader.getClass()).as(ConfigDataLoader.class).resolveGeneric();
}
/**
* Load {@link ConfigData} using the first appropriate {@link ConfigDataLoader}.
* @param <L> the config data location type
* @param <R> the resource type
* @param context the loader context
* @param location the location to load
* @param resource the resource to load
* @return the loaded {@link ConfigData}
* @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()));
try {
return loader.load(context, location);
}
catch (ConfigDataLocationNotFoundException ex) {
if (optional) {
this.logger.trace(LogMessage.format("Skipping missing resource from optional location %s", location));
return null;
}
this.locationNotFoundAction.handle(this.logger, location, ex);
return null;
}
<R extends ConfigDataResource> ConfigData load(ConfigDataLoaderContext context, R resource) throws IOException {
ConfigDataLoader<R> loader = getLoader(context, resource);
this.logger.trace(LogMessage.of(() -> "Loading " + resource + " using loader " + loader.getClass().getName()));
return loader.load(context, resource);
}
@SuppressWarnings("unchecked")
private <L extends ConfigDataLocation> ConfigDataLoader<L> getLoader(ConfigDataLoaderContext context, L location) {
ConfigDataLoader<L> result = null;
private <R extends ConfigDataResource> ConfigDataLoader<R> getLoader(ConfigDataLoaderContext context, R resource) {
ConfigDataLoader<R> result = null;
for (int i = 0; i < this.loaders.size(); i++) {
ConfigDataLoader<?> candidate = this.loaders.get(i);
if (this.locationTypes.get(i).isInstance(location)) {
ConfigDataLoader<L> loader = (ConfigDataLoader<L>) candidate;
if (loader.isLoadable(context, location)) {
if (this.resourceTypes.get(i).isInstance(resource)) {
ConfigDataLoader<R> loader = (ConfigDataLoader<R>) candidate;
if (loader.isLoadable(context, resource)) {
if (result != null) {
throw new IllegalStateException("Multiple loaders found for location " + location + " ["
throw new IllegalStateException("Multiple loaders found for resource '" + resource + "' ["
+ candidate.getClass().getName() + "," + result.getClass().getName() + "]");
}
result = loader;
}
}
}
Assert.state(result != null, () -> "No loader found for location '" + location + "'");
Assert.state(result != null, () -> "No loader found for resource '" + resource + "'");
return result;
}

@ -16,20 +16,131 @@
package org.springframework.boot.context.config;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginProvider;
import org.springframework.util.StringUtils;
/**
* A location from which {@link ConfigData} can be loaded. Implementations must implement
* a valid {@link #equals(Object) equals}, {@link #hashCode() hashCode} and
* {@link #toString() toString} methods.
* A user specified location that can be {@link ConfigDataLocationResolver resolved} to
* one or {@link ConfigDataResource config data resources}. A {@link ConfigDataLocation}
* is a simple wrapper around a {@link String} value. The exact format of the value will
* depend on the underlying technology, but is usually a URL like syntax consisting of a
* prefix and path. For example, {@code crypt:somehost/somepath}.
* <p>
* Locations can be mandatory or {@link #isOptional() optional}. Optional locations are
* prefixed with {@code optional:}.
*
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.4.0
*/
public abstract class ConfigDataLocation {
public final class ConfigDataLocation implements OriginProvider {
/**
* Prefix used to indicate that a {@link ConfigDataLocation} is optional.
* Prefix used to indicate that a {@link ConfigDataResource} is optional.
*/
public static final String OPTIONAL_PREFIX = "optional:";
private final boolean optional;
private final String value;
private final Origin origin;
private ConfigDataLocation(boolean optional, String value, Origin origin) {
this.value = value;
this.optional = optional;
this.origin = origin;
}
/**
* Return the the location is optional and should ignore
* {@link ConfigDataNotFoundException}.
* @return if the location is optional
*/
public boolean isOptional() {
return this.optional;
}
/**
* Return the value of the location (always excluding any user specified
* {@code optional:} prefix.
* @return the location value
*/
public String getValue() {
return this.value;
}
/**
* Return if {@link #getValue()} has the specified prefix.
* @param prefix the prefix to check
* @return if the value has the prefix
*/
public boolean hasPrefix(String prefix) {
return this.value.startsWith(prefix);
}
/**
* Return {@link #getValue()} with the specified prefix removed. If the location does
* not have the given prefix then the {@link #getValue()} is returned unchanged.
* @param prefix the prefix to check
* @return the value with the prefix removed
*/
public String getNonPrefixedValue(String prefix) {
if (hasPrefix(prefix)) {
return this.value.substring(prefix.length());
}
return this.value;
}
@Override
public Origin getOrigin() {
return this.origin;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ConfigDataLocation other = (ConfigDataLocation) obj;
return this.value.equals(other.value);
}
@Override
public int hashCode() {
return this.value.hashCode();
}
@Override
public String toString() {
return (!this.optional) ? this.value : OPTIONAL_PREFIX + this.value;
}
/**
* Create a new {@link ConfigDataLocation} with a specific {@link Origin}.
* @param origin the orgin to set
* @return a new {@link ConfigDataLocation} instance.
*/
ConfigDataLocation withOrigin(Origin origin) {
return new ConfigDataLocation(this.optional, this.value, origin);
}
/**
* Factory method to create a new {@link ConfigDataLocation} from a string.
* @param location the location string
* @return a {@link ConfigDataLocation} instance or {@code null} if no location was
* provided
*/
public static ConfigDataLocation of(String location) {
boolean optional = location != null && location.startsWith(OPTIONAL_PREFIX);
String value = (!optional) ? location : location.substring(OPTIONAL_PREFIX.length());
if (!StringUtils.hasText(value)) {
return null;
}
return new ConfigDataLocation(optional, value, null);
}
}

@ -0,0 +1,68 @@
/*
* 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.List;
import org.springframework.boot.context.properties.bind.AbstractBindHandler;
import org.springframework.boot.context.properties.bind.BindContext;
import org.springframework.boot.context.properties.bind.BindHandler;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.origin.Origin;
/**
* {@link BindHandler} to set the {@link Origin} of bound {@link ConfigDataLocation}
* objects.
*
* @author Phillip Webb
*/
class ConfigDataLocationBindHandler extends AbstractBindHandler {
@Override
@SuppressWarnings("unchecked")
public Object onSuccess(ConfigurationPropertyName name, Bindable<?> target, BindContext context, Object result) {
if (result instanceof ConfigDataLocation) {
return withOrigin(context, (ConfigDataLocation) result);
}
if (result instanceof List) {
List<Object> list = (List<Object>) result;
for (int i = 0; i < list.size(); i++) {
Object element = list.get(i);
if (element instanceof ConfigDataLocation) {
list.set(i, withOrigin(context, (ConfigDataLocation) element));
}
}
}
if (result instanceof ConfigDataLocation[]) {
ConfigDataLocation[] locations = (ConfigDataLocation[]) result;
for (int i = 0; i < locations.length; i++) {
locations[i] = withOrigin(context, locations[i]);
}
}
return result;
}
private ConfigDataLocation withOrigin(BindContext context, ConfigDataLocation result) {
if (result.getOrigin() != null) {
return result;
}
Origin origin = Origin.from(context.getConfigurationProperty());
return result.withOrigin(origin);
}
}

@ -16,25 +16,23 @@
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;
import org.springframework.boot.origin.Origin;
import org.springframework.util.Assert;
/**
* Exception thrown when a config data location cannot be found.
* {@link ConfigDataNotFoundException} thrown when a {@link ConfigDataLocation} cannot be
* found.
*
* @author Phillip Webb
* @since 2.4.0
*/
public class ConfigDataLocationNotFoundException extends ConfigDataException {
public class ConfigDataLocationNotFoundException extends ConfigDataNotFoundException {
private final ConfigDataLocation location;
/**
* Create a new {@link ConfigDataLocationNotFoundException} instance.
* @param location the location that was not found
* @param location the location that could not be found
*/
public ConfigDataLocationNotFoundException(ConfigDataLocation location) {
this(location, null);
@ -42,79 +40,39 @@ public class ConfigDataLocationNotFoundException extends ConfigDataException {
/**
* Create a new {@link ConfigDataLocationNotFoundException} instance.
* @param location the location that was not found
* @param cause the cause of the exception
* @param location the location that could not be found
* @param cause the exception cause
*/
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);
super(getMessage(location), cause);
Assert.notNull(location, "Location must not be null");
this.location = location;
}
/**
* Return the location that could not be found.
* @return the location that could not be found.
* @return the location
*/
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));
@Override
public Origin getOrigin() {
return Origin.from(this.location);
}
/**
* 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());
@Override
public String getReferenceDescription() {
return getReferenceDescription(this.location);
}
/**
* 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 String getMessage(ConfigDataLocation location) {
return String.format("Config data %s cannot be found", getReferenceDescription(location));
}
private static void throwIfDoesNotExist(ConfigDataLocation location, boolean exists) {
if (!exists) {
throw new ConfigDataLocationNotFoundException(location);
}
private static String getReferenceDescription(ConfigDataLocation location) {
return String.format("location '%s'", location);
}
}

@ -31,9 +31,10 @@ import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
/**
* Strategy interface used to resolve {@link ConfigDataLocation locations} from a String
* based location address. Implementations should be added as a {@code spring.factories}
* entries. The following constructor parameter types are supported:
* Strategy interface used to resolve {@link ConfigDataLocation locations} into one or
* more {@link ConfigDataResource resources}. Implementations should be added as a
* {@code spring.factories} entries. The following constructor parameter types are
* supported:
* <ul>
* <li>{@link Log} - if the resolver needs deferred logging</li>
* <li>{@link Binder} - if the resolver needs to obtain values from the initial
@ -47,12 +48,12 @@ import org.springframework.core.io.ResourceLoader;
* Resolvers may implement {@link Ordered} or use the {@link Order @Order} annotation. The
* first resolver that supports the given location will be used.
*
* @param <L> the location type
* @param <R> the location type
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.4.0
*/
public interface ConfigDataLocationResolver<L extends ConfigDataLocation> {
public interface ConfigDataLocationResolver<R extends ConfigDataResource> {
/**
* Returns if the specified location address can be resolved by this resolver.
@ -60,35 +61,34 @@ public interface ConfigDataLocationResolver<L extends ConfigDataLocation> {
* @param location the location to check.
* @return if the location is supported by this resolver
*/
boolean isResolvable(ConfigDataLocationResolverContext context, String location);
boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location);
/**
* Resolve a location string into one or more {@link ConfigDataLocation} instances.
* Resolve a {@link ConfigDataLocation} into one or more {@link ConfigDataResource}
* 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.
* @return a list of {@link ConfigDataResource resources} in ascending priority order.
* @throws ConfigDataLocationNotFoundException on a non-optional location that cannot
* be found
* @throws ConfigDataResourceNotFoundException if a resolved resource cannot be found
*/
List<L> resolve(ConfigDataLocationResolverContext context, String location, boolean optional)
throws ConfigDataLocationNotFoundException;
List<R> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location)
throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException;
/**
* Resolve a location string into one or more {@link ConfigDataLocation} instances
* based on available profiles. This method is called once profiles have been deduced
* from the contributed values. By default this method returns an empty list.
* Resolve a {@link ConfigDataLocation} into one or more {@link ConfigDataResource}
* instances based on available profiles. This method is called once profiles have
* been deduced 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.
* @return a list of resolved locations in ascending priority order.
* @throws ConfigDataLocationNotFoundException on a non-optional location that cannot
* be found
*/
default List<L> resolveProfileSpecific(ConfigDataLocationResolverContext context, String location, boolean optional,
default List<R> resolveProfileSpecific(ConfigDataLocationResolverContext context, ConfigDataLocation location,
Profiles profiles) throws ConfigDataLocationNotFoundException {
return Collections.emptyList();
}

@ -19,7 +19,6 @@ package org.springframework.boot.context.config;
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.origin.Origin;
/**
* Context provided to {@link ConfigDataLocationResolver} methods.
@ -38,11 +37,11 @@ public interface ConfigDataLocationResolverContext {
Binder getBinder();
/**
* Provides access to the parent location that triggered the resolve or {@code null}
* if there is no available parent.
* Provides access to the parent {@link ConfigDataResource} that triggered the resolve
* or {@code null} if there is no available parent.
* @return the parent location
*/
ConfigDataLocation getParent();
ConfigDataResource getParent();
/**
* Provides access to the {@link ConfigurableBootstrapContext} shared across all
@ -51,11 +50,4 @@ public interface ConfigDataLocationResolverContext {
*/
ConfigurableBootstrapContext getBootstrapContext();
/**
* Return the {@link Origin} of a location that's being resolved.
* @param location the location being resolved
* @return the {@link Origin} of the location or {@code null}
*/
Origin getLocationOrigin(String location);
}

@ -32,8 +32,6 @@ 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;
/**
* A collection of {@link ConfigDataLocationResolver} instances loaded via
@ -44,24 +42,18 @@ import org.springframework.util.StringUtils;
*/
class ConfigDataLocationResolvers {
private final Log logger;
private final ConfigDataLocationNotFoundAction locationNotFoundAction;
private final List<ConfigDataLocationResolver<?>> resolvers;
/**
* Create a new {@link ConfigDataLocationResolvers} instance.
* @param logFactory a {@link DeferredLogFactory} used to inject {@link Log} instances
* @param bootstrapContext the bootstrap context
* @param locationNotFoundAction the action to take if a
* {@link ConfigDataLocationNotFoundException} is thrown
* @param binder a binder providing values from the initial {@link Environment}
* @param resourceLoader {@link ResourceLoader} to load resource locations
*/
ConfigDataLocationResolvers(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ConfigDataLocationNotFoundAction locationNotFoundAction, Binder binder, ResourceLoader resourceLoader) {
this(logFactory, bootstrapContext, locationNotFoundAction, binder, resourceLoader,
Binder binder, ResourceLoader resourceLoader) {
this(logFactory, bootstrapContext, binder, resourceLoader,
SpringFactoriesLoader.loadFactoryNames(ConfigDataLocationResolver.class, null));
}
@ -69,17 +61,12 @@ class ConfigDataLocationResolvers {
* Create a new {@link ConfigDataLocationResolvers} instance.
* @param logFactory a {@link DeferredLogFactory} used to inject {@link Log} instances
* @param bootstrapContext the bootstrap context
* @param locationNotFoundAction the action to take if a
* {@link ConfigDataLocationNotFoundException} is thrown
* @param binder {@link Binder} providing values from the initial {@link Environment}
* @param resourceLoader {@link ResourceLoader} to load resource locations
* @param names the {@link ConfigDataLocationResolver} class names
*/
ConfigDataLocationResolvers(DeferredLogFactory logFactory, ConfigurableBootstrapContext bootstrapContext,
ConfigDataLocationNotFoundAction locationNotFoundAction, Binder binder, ResourceLoader resourceLoader,
List<String> names) {
this.logger = logFactory.getLog(getClass());
this.locationNotFoundAction = locationNotFoundAction;
Binder binder, ResourceLoader resourceLoader, List<String> names) {
Instantiator<ConfigDataLocationResolver<?>> instantiator = new Instantiator<>(ConfigDataLocationResolver.class,
(availableParameters) -> {
availableParameters.add(Log.class, logFactory::getLog);
@ -94,10 +81,10 @@ class ConfigDataLocationResolvers {
private List<ConfigDataLocationResolver<?>> reorder(List<ConfigDataLocationResolver<?>> resolvers) {
List<ConfigDataLocationResolver<?>> reordered = new ArrayList<>(resolvers.size());
ResourceConfigDataLocationResolver resourceResolver = null;
StandardConfigDataLocationResolver resourceResolver = null;
for (ConfigDataLocationResolver<?> resolver : resolvers) {
if (resolver instanceof ResourceConfigDataLocationResolver) {
resourceResolver = (ResourceConfigDataLocationResolver) resolver;
if (resolver instanceof StandardConfigDataLocationResolver) {
resourceResolver = (StandardConfigDataLocationResolver) resolver;
}
else {
reordered.add(resolver);
@ -109,67 +96,38 @@ class ConfigDataLocationResolvers {
return Collections.unmodifiableList(reordered);
}
/**
* Resolve all location strings using the most appropriate
* {@link ConfigDataLocationResolver}.
* @param context the location resolver context
* @param locations the locations to resolve
* @param profiles the current profiles or {@code null}
* @return the resolved locations
*/
List<ConfigDataLocation> resolveAll(ConfigDataLocationResolverContext context, List<String> locations,
List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location,
Profiles profiles) {
List<ConfigDataLocation> resolved = new ArrayList<>(locations.size());
for (String location : locations) {
resolved.addAll(resolveAll(context, location, profiles));
}
return resolved;
}
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)) {
if (location == null) {
return Collections.emptyList();
}
for (ConfigDataLocationResolver<?> resolver : getResolvers()) {
if (resolver.isResolvable(context, location)) {
return resolve(resolver, context, optional, location, profiles);
return resolve(resolver, context, location, profiles);
}
}
throw new UnsupportedConfigDataLocationException(location);
}
private List<ConfigDataLocation> resolve(ConfigDataLocationResolver<?> resolver,
ConfigDataLocationResolverContext context, boolean optional, String location, Profiles profiles) {
List<ConfigDataLocation> resolved = resolve(location, optional,
() -> resolver.resolve(context, location, optional));
private List<ConfigDataResolutionResult> resolve(ConfigDataLocationResolver<?> resolver,
ConfigDataLocationResolverContext context, ConfigDataLocation location, Profiles profiles) {
List<ConfigDataResolutionResult> resolved = resolve(location, () -> resolver.resolve(context, location));
if (profiles == null) {
return resolved;
}
List<ConfigDataLocation> profileSpecific = resolve(location, optional,
() -> resolver.resolveProfileSpecific(context, location, optional, profiles));
List<ConfigDataResolutionResult> profileSpecific = resolve(location,
() -> resolver.resolveProfileSpecific(context, location, 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) {
this.logger.trace(LogMessage.format("Skipping missing resource from optional location %s", location));
return Collections.emptyList();
}
this.locationNotFoundAction.handle(this.logger, location, ex);
return Collections.emptyList();
private List<ConfigDataResolutionResult> resolve(ConfigDataLocation location,
Supplier<List<? extends ConfigDataResource>> resolveAction) {
List<ConfigDataResource> resources = nonNullList(resolveAction.get());
List<ConfigDataResolutionResult> resolved = new ArrayList<>(resources.size());
for (ConfigDataResource resource : resources) {
resolved.add(new ConfigDataResolutionResult(location, resource));
}
return resolved;
}
@SuppressWarnings("unchecked")

@ -21,12 +21,12 @@ import org.apache.commons.logging.Log;
import org.springframework.core.log.LogMessage;
/**
* Action to take when an uncaught {@link ConfigDataLocationNotFoundException} is thrown.
* Action to take when an uncaught {@link ConfigDataNotFoundException} is thrown.
*
* @author Phillip Webb
* @since 2.4.0
*/
public enum ConfigDataLocationNotFoundAction {
public enum ConfigDataNotFoundAction {
/**
* Throw the exception to fail startup.
@ -34,7 +34,7 @@ public enum ConfigDataLocationNotFoundAction {
FAIL {
@Override
void handle(Log logger, Object location, ConfigDataLocationNotFoundException ex) {
void handle(Log logger, ConfigDataNotFoundException ex) {
throw ex;
}
@ -46,19 +46,17 @@ public enum ConfigDataLocationNotFoundAction {
IGNORE {
@Override
void handle(Log logger, Object location, ConfigDataLocationNotFoundException ex) {
logger.trace(LogMessage.format("Ignoring missing resource from location %s", location));
void handle(Log logger, ConfigDataNotFoundException ex) {
logger.trace(LogMessage.format("Ignoring missing config data %s", ex.getReferenceDescription()));
}
};
/**
* Handle the given exception.
* @param logger the logger used for output
* @param location the location being checked (a {@link ConfigDataLocation} or
* {@code String})
* @param logger the logger used for output {@code ConfigDataLocation})
* @param ex the exception to handle
*/
abstract void handle(Log logger, Object location, ConfigDataLocationNotFoundException ex);
abstract void handle(Log logger, ConfigDataNotFoundException ex);
}

@ -0,0 +1,44 @@
/*
* 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 org.springframework.boot.origin.OriginProvider;
/**
* {@link ConfigDataNotFoundException} thrown when a {@link ConfigData} cannot be found.
*
* @author Phillip Webb
* @since 2.4.0
*/
public abstract class ConfigDataNotFoundException extends ConfigDataException implements OriginProvider {
/**
* Create a new {@link ConfigDataNotFoundException} instance.
* @param message the exception message
* @param cause the exception cause
*/
ConfigDataNotFoundException(String message, Throwable cause) {
super(message, cause);
}
/**
* Return a description of actual referenced item that could not be found.
* @return a description of the referenced items
*/
public abstract String getReferenceDescription();
}

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

@ -0,0 +1,44 @@
/*
* 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;
/**
* Result returned from {@link ConfigDataLocationResolvers} containing both the
* {@link ConfigDataResource} and the original {@link ConfigDataLocation}.
*
* @author Phillip Webb
*/
class ConfigDataResolutionResult {
private final ConfigDataLocation location;
private final ConfigDataResource resource;
ConfigDataResolutionResult(ConfigDataLocation location, ConfigDataResource resource) {
this.location = location;
this.resource = resource;
}
ConfigDataLocation getLocation() {
return this.location;
}
ConfigDataResource getResource() {
return this.resource;
}
}

@ -16,23 +16,15 @@
package org.springframework.boot.context.config;
import java.io.IOException;
import org.springframework.core.io.Resource;
/**
* {@link ConfigDataLoader} for {@link Resource} backed locations.
* A single resource from which {@link ConfigData} can be loaded. Implementations must
* implement a valid {@link #equals(Object) equals}, {@link #hashCode() hashCode} and
* {@link #toString() toString} methods.
*
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.4.0
*/
public class ResourceConfigDataLoader implements ConfigDataLoader<ResourceConfigDataLocation> {
@Override
public ConfigData load(ConfigDataLoaderContext context, ResourceConfigDataLocation location) throws IOException {
ConfigDataLocationNotFoundException.throwIfDoesNotExist(location, location.getResource());
return new ConfigData(location.load());
}
public abstract class ConfigDataResource {
}

@ -0,0 +1,148 @@
/*
* 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.boot.origin.Origin;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* {@link ConfigDataNotFoundException} thrown when a {@link ConfigDataResource} cannot be
* found.
*
* @author Phillip Webb
* @since 2.4.0
*/
public class ConfigDataResourceNotFoundException extends ConfigDataNotFoundException {
private final ConfigDataResource resource;
private final ConfigDataLocation location;
/**
* Create a new {@link ConfigDataResourceNotFoundException} instance.
* @param resource the resource that could not be found
*/
public ConfigDataResourceNotFoundException(ConfigDataResource resource) {
this(resource, null);
}
/**
* Create a new {@link ConfigDataResourceNotFoundException} instance.
* @param resource the resource that could not be found
* @param cause the exception cause
*/
public ConfigDataResourceNotFoundException(ConfigDataResource resource, Throwable cause) {
this(resource, null, cause);
}
private ConfigDataResourceNotFoundException(ConfigDataResource resource, ConfigDataLocation location,
Throwable cause) {
super(getMessage(resource, location), cause);
Assert.notNull(resource, "Resource must not be null");
this.resource = resource;
this.location = location;
}
/**
* Return the resource that could not be found.
* @return the resource
*/
public ConfigDataResource getResource() {
return this.resource;
}
/**
* Return the original location that was resolved to determine the resource.
* @return the location or {@code null} if no location is availble
*/
public ConfigDataLocation getLocation() {
return this.location;
}
@Override
public Origin getOrigin() {
return Origin.from(this.location);
}
@Override
public String getReferenceDescription() {
return getReferenceDescription(this.resource, this.location);
}
/**
* Create a new {@link ConfigDataResourceNotFoundException} instance with a location.
* @param location the location to set
* @return a new {@link ConfigDataResourceNotFoundException} instance
*/
ConfigDataResourceNotFoundException withLocation(ConfigDataLocation location) {
return new ConfigDataResourceNotFoundException(this.resource, location, getCause());
}
private static String getMessage(ConfigDataResource resource, ConfigDataLocation location) {
return String.format("Config data %s cannot be found", getReferenceDescription(resource, location));
}
private static String getReferenceDescription(ConfigDataResource resource, ConfigDataLocation location) {
String description = String.format("resource '%s'", resource);
if (location != null) {
description += String.format(" via location '%s'", location);
}
return description;
}
/**
* Throw a {@link ConfigDataNotFoundException} if the specified {@link Path} does not
* exist.
* @param resource the config data resource
* @param pathToCheck the path to check
*/
public static void throwIfDoesNotExist(ConfigDataResource resource, Path pathToCheck) {
throwIfDoesNotExist(resource, Files.exists(pathToCheck));
}
/**
* Throw a {@link ConfigDataNotFoundException} if the specified {@link File} does not
* exist.
* @param resource the config data resource
* @param fileToCheck the file to check
*/
public static void throwIfDoesNotExist(ConfigDataResource resource, File fileToCheck) {
throwIfDoesNotExist(resource, fileToCheck.exists());
}
/**
* Throw a {@link ConfigDataNotFoundException} if the specified {@link Resource} does
* not exist.
* @param resource the config data resource
* @param resourceToCheck the resource to check
*/
public static void throwIfDoesNotExist(ConfigDataResource resource, Resource resourceToCheck) {
throwIfDoesNotExist(resource, resourceToCheck.exists());
}
private static void throwIfDoesNotExist(ConfigDataResource resource, boolean exists) {
if (!exists) {
throw new ConfigDataResourceNotFoundException(resource);
}
}
}

@ -426,25 +426,13 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private void load(Profile profile, DocumentFilterFactory filterFactory, DocumentConsumer consumer) {
getSearchLocations().forEach((location) -> {
String nonOptionalLocation = ConfigDataLocation.of(location).getValue();
boolean isDirectory = location.endsWith("/");
Set<String> names = isDirectory ? getSearchNames() : NO_SEARCH_NAMES;
names.forEach((name) -> load(stripOptionalPrefix(location), name, profile, filterFactory, consumer));
names.forEach((name) -> load(nonOptionalLocation, 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)) {
@ -552,9 +540,9 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
}
}
private String getLocationName(String location, Resource resource) {
if (!location.contains("*")) {
return location;
private String getLocationName(String locationReference, Resource resource) {
if (!locationReference.contains("*")) {
return locationReference;
}
if (resource instanceof FileSystemResource) {
return ((FileSystemResource) resource).getPath();
@ -562,24 +550,24 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
return resource.getDescription();
}
private Resource[] getResources(String location) {
private Resource[] getResources(String locationReference) {
try {
if (location.contains("*")) {
return getResourcesFromPatternLocation(location);
if (locationReference.contains("*")) {
return getResourcesFromPatternLocationReference(locationReference);
}
return new Resource[] { this.resourceLoader.getResource(location) };
return new Resource[] { this.resourceLoader.getResource(locationReference) };
}
catch (Exception ex) {
return EMPTY_RESOURCES;
}
}
private Resource[] getResourcesFromPatternLocation(String location) throws IOException {
String directoryPath = location.substring(0, location.indexOf("*/"));
private Resource[] getResourcesFromPatternLocationReference(String locationReference) throws IOException {
String directoryPath = locationReference.substring(0, locationReference.indexOf("*/"));
Resource resource = this.resourceLoader.getResource(directoryPath);
File[] files = resource.getFile().listFiles(File::isDirectory);
if (files != null) {
String fileName = location.substring(location.lastIndexOf("/") + 1);
String fileName = locationReference.substring(locationReference.lastIndexOf("/") + 1);
Arrays.sort(files, FILE_COMPARATOR);
return Arrays.stream(files).map((file) -> file.listFiles((dir, name) -> name.equals(fileName)))
.filter(Objects::nonNull).flatMap((Function<File[], Stream<File>>) Arrays::stream)
@ -622,7 +610,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
}).collect(Collectors.toList());
}
private StringBuilder getDescription(String prefix, String location, Resource resource, Profile profile) {
private StringBuilder getDescription(String prefix, String locationReference, Resource resource,
Profile profile) {
StringBuilder result = new StringBuilder(prefix);
try {
if (resource != null) {
@ -630,12 +619,12 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
result.append("'");
result.append(uri);
result.append("' (");
result.append(location);
result.append(locationReference);
result.append(")");
}
}
catch (IOException ex) {
result.append(location);
result.append(locationReference);
}
if (profile != null) {
result.append(" for profile ");

@ -29,12 +29,13 @@ import org.springframework.boot.env.ConfigTreePropertySource;
* @author Phillip Webb
* @since 2.4.0
*/
public class ConfigTreeConfigDataLoader implements ConfigDataLoader<ConfigTreeConfigDataLocation> {
public class ConfigTreeConfigDataLoader implements ConfigDataLoader<ConfigTreeConfigDataResource> {
@Override
public ConfigData load(ConfigDataLoaderContext context, ConfigTreeConfigDataLocation location) throws IOException {
ConfigDataLocationNotFoundException.throwIfDoesNotExist(location, location.getPath());
Path path = location.getPath();
public ConfigData load(ConfigDataLoaderContext context, ConfigTreeConfigDataResource resource)
throws IOException, ConfigDataResourceNotFoundException {
Path path = resource.getPath();
ConfigDataResourceNotFoundException.throwIfDoesNotExist(resource, path);
String name = "Config tree '" + path + "'";
ConfigTreePropertySource source = new ConfigTreePropertySource(name, path);
return new ConfigData(Collections.singletonList(source));

@ -26,19 +26,19 @@ import java.util.List;
* @author Phillip Webb
* @since 2.4.0
*/
public class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationResolver<ConfigTreeConfigDataLocation> {
public class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationResolver<ConfigTreeConfigDataResource> {
private static final String PREFIX = "configtree:";
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
return location.startsWith(PREFIX);
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
return location.hasPrefix(PREFIX);
}
@Override
public List<ConfigTreeConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) {
ConfigTreeConfigDataLocation resolved = new ConfigTreeConfigDataLocation(location.substring(PREFIX.length()));
public List<ConfigTreeConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location) {
ConfigTreeConfigDataResource resolved = new ConfigTreeConfigDataResource(location.getNonPrefixedValue(PREFIX));
return Collections.singletonList(resolved);
}

@ -24,18 +24,18 @@ import org.springframework.boot.env.ConfigTreePropertySource;
import org.springframework.util.Assert;
/**
* {@link ConfigDataLocation} backed by a config tree directory.
* {@link ConfigDataResource} backed by a config tree directory.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 2.4.0
* @see ConfigTreePropertySource
*/
public class ConfigTreeConfigDataLocation extends ConfigDataLocation {
public class ConfigTreeConfigDataResource extends ConfigDataResource {
private final Path path;
ConfigTreeConfigDataLocation(String path) {
ConfigTreeConfigDataResource(String path) {
Assert.notNull(path, "Path must not be null");
this.path = Paths.get(path).toAbsolutePath();
}
@ -52,7 +52,7 @@ public class ConfigTreeConfigDataLocation extends ConfigDataLocation {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ConfigTreeConfigDataLocation other = (ConfigTreeConfigDataLocation) obj;
ConfigTreeConfigDataResource other = (ConfigTreeConfigDataResource) obj;
return Objects.equals(this.path, other.path);
}

@ -35,7 +35,7 @@ public class InactiveConfigDataAccessException extends ConfigDataException {
private final PropertySource<?> propertySource;
private final ConfigDataLocation location;
private final ConfigDataResource location;
private final String propertyName;
@ -44,12 +44,12 @@ public class InactiveConfigDataAccessException extends ConfigDataException {
/**
* Create a new {@link InactiveConfigDataAccessException} instance.
* @param propertySource the inactive property source
* @param location the {@link ConfigDataLocation} of the property source or
* @param location the {@link ConfigDataResource} of the property source or
* {@code null} if the source was not loaded from {@link ConfigData}.
* @param propertyName the name of the property
* @param origin the origin or the property or {@code null}
*/
InactiveConfigDataAccessException(PropertySource<?> propertySource, ConfigDataLocation location,
InactiveConfigDataAccessException(PropertySource<?> propertySource, ConfigDataResource location,
String propertyName, Origin origin) {
super(getMessage(propertySource, location, propertyName, origin), null);
this.propertySource = propertySource;
@ -58,7 +58,7 @@ public class InactiveConfigDataAccessException extends ConfigDataException {
this.origin = origin;
}
private static String getMessage(PropertySource<?> propertySource, ConfigDataLocation location, String propertyName,
private static String getMessage(PropertySource<?> propertySource, ConfigDataResource location, String propertyName,
Origin origin) {
StringBuilder message = new StringBuilder("Inactive property source '");
message.append(propertySource.getName());
@ -86,11 +86,11 @@ public class InactiveConfigDataAccessException extends ConfigDataException {
}
/**
* Return the {@link ConfigDataLocation} of the property source or {@code null} if the
* Return the {@link ConfigDataResource} of the property source or {@code null} if the
* source was not loaded from {@link ConfigData}.
* @return the config data location or {@code null}
*/
public ConfigDataLocation getLocation() {
public ConfigDataResource getLocation() {
return this.location;
}
@ -121,7 +121,7 @@ public class InactiveConfigDataAccessException extends ConfigDataException {
ConfigurationProperty property = (source != null) ? source.getConfigurationProperty(name) : null;
if (property != null) {
PropertySource<?> propertySource = contributor.getPropertySource();
ConfigDataLocation location = contributor.getLocation();
ConfigDataResource location = contributor.getResource();
throw new InactiveConfigDataAccessException(propertySource, location, name.toString(),
property.getOrigin());
}

@ -47,10 +47,10 @@ public class InvalidConfigDataPropertyException extends ConfigDataException {
private final ConfigurationPropertyName replacement;
private final ConfigDataLocation location;
private final ConfigDataResource location;
InvalidConfigDataPropertyException(ConfigurationProperty property, ConfigurationPropertyName replacement,
ConfigDataLocation location) {
ConfigDataResource location) {
super(getMessage(property, replacement, location), null);
this.property = property;
this.replacement = replacement;
@ -66,11 +66,11 @@ public class InvalidConfigDataPropertyException extends ConfigDataException {
}
/**
* Return the {@link ConfigDataLocation} of the invalid property or {@code null} if
* Return the {@link ConfigDataResource} of the invalid property or {@code null} if
* the source was not loaded from {@link ConfigData}.
* @return the config data location or {@code null}
*/
public ConfigDataLocation getLocation() {
public ConfigDataResource getLocation() {
return this.location;
}
@ -97,14 +97,14 @@ public class InvalidConfigDataPropertyException extends ConfigDataException {
WARNING.forEach((invalid, replacement) -> {
ConfigurationProperty property = propertySource.getConfigurationProperty(invalid);
if (property != null) {
logger.warn(getMessage(property, replacement, contributor.getLocation()));
logger.warn(getMessage(property, replacement, contributor.getResource()));
}
});
}
}
private static String getMessage(ConfigurationProperty property, ConfigurationPropertyName replacement,
ConfigDataLocation location) {
ConfigDataResource location) {
StringBuilder message = new StringBuilder("Property '");
message.append(property.getName());
if (location != null) {

@ -1,72 +0,0 @@
/*
* 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();
}
}

@ -1,423 +0,0 @@
/*
* 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.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.origin.Origin;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
* {@link ConfigDataLocationResolver} for standard locations.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 2.4.0
*/
public class ResourceConfigDataLocationResolver
implements ConfigDataLocationResolver<ResourceConfigDataLocation>, Ordered {
private static final String PREFIX = "resource:";
static final String CONFIG_NAME_PROPERTY = "spring.config.name";
private static final String[] DEFAULT_CONFIG_NAMES = { "application" };
private static final Resource[] EMPTY_RESOURCES = {};
private static final Comparator<File> FILE_COMPARATOR = Comparator.comparing(File::getAbsolutePath);
private static final Pattern URL_PREFIX = Pattern.compile("^([a-zA-Z][a-zA-Z0-9*]*?:)(.*$)");
private static final Pattern EXTENSION_HINT_PATTERN = Pattern.compile("^(.*)\\[(\\.\\w+)\\](?!\\[)$");
private static final String NO_PROFILE = null;
private final Log logger;
private final List<PropertySourceLoader> propertySourceLoaders;
private final String[] configNames;
private final ResourceLoader resourceLoader;
/**
* Create a new {@link ResourceConfigDataLocationResolver} instance.
* @param logger the logger to use
* @param binder a binder backed by the initial {@link Environment}
* @param resourceLoader a {@link ResourceLoader} used to load resources
*/
public ResourceConfigDataLocationResolver(Log logger, Binder binder, ResourceLoader resourceLoader) {
this.logger = logger;
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
this.configNames = getConfigNames(binder);
this.resourceLoader = resourceLoader;
}
private String[] getConfigNames(Binder binder) {
String[] configNames = binder.bind(CONFIG_NAME_PROPERTY, String[].class).orElse(DEFAULT_CONFIG_NAMES);
for (String configName : configNames) {
validateConfigName(configName);
}
return configNames;
}
private void validateConfigName(String name) {
Assert.state(!name.contains("*"), () -> "Config name '" + name + "' cannot contain '*'");
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
return true;
}
@Override
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, boolean optional, Profiles profiles) {
return resolve(location, getProfileSpecificResolvables(context, location, optional, profiles));
}
private Set<Resolvable> getResolvables(ConfigDataLocationResolverContext context, String location,
boolean optional) {
Origin origin = context.getLocationOrigin(location);
String resourceLocation = getResourceLocation(context, location);
try {
if (isDirectoryLocation(resourceLocation)) {
return getResolvablesForDirectory(resourceLocation, optional, NO_PROFILE, origin);
}
return getResolvablesForFile(resourceLocation, optional, NO_PROFILE, origin);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Unable to load config data from '" + location + "'", ex);
}
}
private Set<Resolvable> getProfileSpecificResolvables(ConfigDataLocationResolverContext context, String location,
boolean optional, Profiles profiles) {
Origin origin = context.getLocationOrigin(location);
Set<Resolvable> resolvables = new LinkedHashSet<>();
String resourceLocation = getResourceLocation(context, location);
for (String profile : profiles) {
resolvables.addAll(getResolvables(resourceLocation, optional, profile, origin));
}
return resolvables;
}
private String getResourceLocation(ConfigDataLocationResolverContext context, String location) {
String resourceLocation = (location.startsWith(PREFIX)) ? location.substring(PREFIX.length()) : location;
boolean isAbsolute = resourceLocation.startsWith("/") || URL_PREFIX.matcher(resourceLocation).matches();
if (isAbsolute) {
return resourceLocation;
}
ConfigDataLocation parent = context.getParent();
if (parent instanceof ResourceConfigDataLocation) {
String parentLocation = ((ResourceConfigDataLocation) parent).getLocation();
String parentDirectory = parentLocation.substring(0, parentLocation.lastIndexOf("/") + 1);
return parentDirectory + resourceLocation;
}
return resourceLocation;
}
private Set<Resolvable> getResolvables(String resourceLocation, boolean optional, String profile, Origin origin) {
if (isDirectoryLocation(resourceLocation)) {
return getResolvablesForDirectory(resourceLocation, optional, profile, origin);
}
return getResolvablesForFile(resourceLocation, optional, profile, origin);
}
private Set<Resolvable> getResolvablesForDirectory(String directoryLocation, boolean optional, String profile,
Origin origin) {
Set<Resolvable> resolvables = new LinkedHashSet<>();
for (String name : this.configNames) {
String rootLocation = directoryLocation + name;
for (PropertySourceLoader loader : this.propertySourceLoaders) {
for (String extension : loader.getFileExtensions()) {
Resolvable resolvable = new Resolvable(directoryLocation, rootLocation, optional, profile,
extension, origin, loader);
resolvables.add(resolvable);
}
}
}
return resolvables;
}
private Set<Resolvable> getResolvablesForFile(String fileLocation, boolean optional, String profile,
Origin origin) {
Matcher extensionHintMatcher = EXTENSION_HINT_PATTERN.matcher(fileLocation);
boolean extensionHintLocation = extensionHintMatcher.matches();
if (extensionHintLocation) {
fileLocation = extensionHintMatcher.group(1) + extensionHintMatcher.group(2);
}
for (PropertySourceLoader loader : this.propertySourceLoaders) {
String extension = getLoadableFileExtension(loader, fileLocation);
if (extension != null) {
String root = fileLocation.substring(0, fileLocation.length() - extension.length() - 1);
return Collections.singleton(new Resolvable(null, root, optional, profile,
(!extensionHintLocation) ? extension : null, origin, loader));
}
}
throw new IllegalStateException("File extension is not known to any PropertySourceLoader. "
+ "If the location is meant to reference a directory, it must end in '/'");
}
private String getLoadableFileExtension(PropertySourceLoader loader, String resourceLocation) {
for (String fileExtension : loader.getFileExtensions()) {
if (StringUtils.endsWithIgnoreCase(resourceLocation, fileExtension)) {
return fileExtension;
}
}
return null;
}
private boolean isDirectoryLocation(String resourceLocation) {
return resourceLocation.endsWith("/");
}
private List<ResourceConfigDataLocation> resolve(String location, Set<Resolvable> resolvables) {
List<ResourceConfigDataLocation> resolved = new ArrayList<>();
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 = loadResource(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);
}
return resolvePattern(location, resolvable);
}
private List<ResourceConfigDataLocation> resolveNonPattern(String location, Resolvable resolvable) {
Resource resource = loadResource(resolvable.getResourceLocation());
if (!resource.exists() && resolvable.isSkippable()) {
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() && resolvable.isSkippable()) {
logSkippingResource(resolvable);
}
else {
resolved.add(createConfigResourceLocation(location, resolvable, resource));
}
}
return resolved;
}
private void logSkippingResource(Resolvable resolvable) {
this.logger.trace(LogMessage.format("Skipping missing resource location %s", resolvable.getResourceLocation()));
}
private ResourceConfigDataLocation createConfigResourceLocation(String location, Resolvable resolvable,
Resource resource) {
String name = String.format("Resource config '%s' imported via location \"%s\"",
resolvable.getResourceLocation(), location);
return new ResourceConfigDataLocation(name, resource, resolvable.getOrigin(), resolvable.getLoader());
}
private void validatePatternLocation(String resourceLocation) {
Assert.state(!resourceLocation.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX),
"Classpath wildcard patterns cannot be used as a search location");
Assert.state(StringUtils.countOccurrencesOf(resourceLocation, "*") == 1,
() -> "Search location '" + resourceLocation + "' cannot contain multiple wildcards");
String directoryPath = resourceLocation.substring(0, resourceLocation.lastIndexOf("/") + 1);
Assert.state(directoryPath.endsWith("*/"),
() -> "Search location '" + resourceLocation + "' must end with '*/'");
}
private Resource[] getResourcesFromResourceLocationPattern(String resourceLocationPattern) {
String directoryPath = resourceLocationPattern.substring(0, resourceLocationPattern.indexOf("*/"));
String fileName = resourceLocationPattern.substring(resourceLocationPattern.lastIndexOf("/") + 1);
Resource directoryResource = loadResource(directoryPath);
if (!directoryResource.exists()) {
return new Resource[] { directoryResource };
}
File directory = getDirectory(resourceLocationPattern, directoryResource);
File[] subDirectories = directory.listFiles(File::isDirectory);
if (subDirectories == null) {
return EMPTY_RESOURCES;
}
Arrays.sort(subDirectories, FILE_COMPARATOR);
List<Resource> resources = new ArrayList<>();
FilenameFilter filter = (dir, name) -> name.equals(fileName);
for (File subDirectory : subDirectories) {
File[] files = subDirectory.listFiles(filter);
if (files != null) {
Arrays.stream(files).map(FileSystemResource::new).forEach(resources::add);
}
}
return resources.toArray(EMPTY_RESOURCES);
}
private Resource loadResource(String location) {
location = StringUtils.cleanPath(location);
if (!ResourceUtils.isUrl(location)) {
location = ResourceUtils.FILE_URL_PREFIX + location;
}
return this.resourceLoader.getResource(location);
}
private File getDirectory(String patternLocation, Resource resource) {
try {
File directory = resource.getFile();
Assert.state(directory.isDirectory(), () -> "'" + directory + "' is not a directory");
return directory;
}
catch (Exception ex) {
throw new IllegalStateException(
"Unable to load config data resource from pattern '" + patternLocation + "'", ex);
}
}
/**
* A resource location that could be resolved by this resolver.
*/
private static class Resolvable {
private final String directory;
private final String resourceLocation;
private final boolean optional;
private final String profile;
private Origin origin;
private final PropertySourceLoader loader;
Resolvable(String directory, String rootLocation, boolean optional, String profile, String extension,
Origin origin, PropertySourceLoader loader) {
String profileSuffix = (StringUtils.hasText(profile)) ? "-" + profile : "";
this.directory = directory;
this.resourceLocation = rootLocation + profileSuffix + ((extension != null) ? "." + extension : "");
this.optional = optional;
this.profile = profile;
this.loader = loader;
this.origin = origin;
}
boolean isNonOptionalDirectory() {
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("*");
}
String getResourceLocation() {
return this.resourceLocation;
}
Origin getOrigin() {
return this.origin;
}
PropertySourceLoader getLoader() {
return this.loader;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
Resolvable other = (Resolvable) obj;
return this.resourceLocation.equals(other.resourceLocation);
}
@Override
public int hashCode() {
return this.resourceLocation.hashCode();
}
@Override
public String toString() {
return this.resourceLocation;
}
}
}

@ -0,0 +1,49 @@
/*
* 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.IOException;
import java.util.List;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginTrackedResource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.Resource;
/**
* {@link ConfigDataLoader} for {@link Resource} backed locations.
*
* @author Phillip Webb
* @author Madhura Bhave
* @since 2.4.0
*/
public class StandardConfigDataLoader implements ConfigDataLoader<StandardConfigDataResource> {
@Override
public ConfigData load(ConfigDataLoaderContext context, StandardConfigDataResource resource)
throws IOException, ConfigDataNotFoundException {
ConfigDataResourceNotFoundException.throwIfDoesNotExist(resource, resource.getResource());
StandardConfigDataReference reference = resource.getReference();
Resource originTrackedResource = OriginTrackedResource.of(resource.getResource(),
Origin.from(reference.getConfigDataLocation()));
String name = String.format("Config resource '%s' via location '%s'", reference.getResourceLocation(),
reference.getConfigDataLocation());
List<PropertySource<?>> propertySources = reference.getPropertySourceLoader().load(name, originTrackedResource);
return new ConfigData(propertySources);
}
}

@ -0,0 +1,343 @@
/*
* 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.FilenameFilter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.logging.Log;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
/**
* {@link ConfigDataLocationResolver} for standard locations.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 2.4.0
*/
public class StandardConfigDataLocationResolver
implements ConfigDataLocationResolver<StandardConfigDataResource>, Ordered {
private static final String PREFIX = "resource:";
static final String CONFIG_NAME_PROPERTY = "spring.config.name";
private static final String[] DEFAULT_CONFIG_NAMES = { "application" };
private static final Resource[] EMPTY_RESOURCES = {};
private static final Comparator<File> FILE_COMPARATOR = Comparator.comparing(File::getAbsolutePath);
private static final Pattern URL_PREFIX = Pattern.compile("^([a-zA-Z][a-zA-Z0-9*]*?:)(.*$)");
private static final Pattern EXTENSION_HINT_PATTERN = Pattern.compile("^(.*)\\[(\\.\\w+)\\](?!\\[)$");
private static final String NO_PROFILE = null;
private final Log logger;
private final List<PropertySourceLoader> propertySourceLoaders;
private final String[] configNames;
private final ResourceLoader resourceLoader;
/**
* Create a new {@link StandardConfigDataLocationResolver} instance.
* @param logger the logger to use
* @param binder a binder backed by the initial {@link Environment}
* @param resourceLoader a {@link ResourceLoader} used to load resources
*/
public StandardConfigDataLocationResolver(Log logger, Binder binder, ResourceLoader resourceLoader) {
this.logger = logger;
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
this.configNames = getConfigNames(binder);
this.resourceLoader = resourceLoader;
}
private String[] getConfigNames(Binder binder) {
String[] configNames = binder.bind(CONFIG_NAME_PROPERTY, String[].class).orElse(DEFAULT_CONFIG_NAMES);
for (String configName : configNames) {
validateConfigName(configName);
}
return configNames;
}
private void validateConfigName(String name) {
Assert.state(!name.contains("*"), () -> "Config name '" + name + "' cannot contain '*'");
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
return true;
}
@Override
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location) throws ConfigDataNotFoundException {
return resolve(getReferences(context, location));
}
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
ConfigDataLocation configDataLocation) {
String resourceLocation = getResourceLocation(context, configDataLocation);
try {
if (isDirectory(resourceLocation)) {
return getReferencesForDirectory(configDataLocation, resourceLocation, NO_PROFILE);
}
return getReferencesForFile(configDataLocation, resourceLocation, NO_PROFILE);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Unable to load config data from '" + configDataLocation + "'", ex);
}
}
@Override
public List<StandardConfigDataResource> resolveProfileSpecific(ConfigDataLocationResolverContext context,
ConfigDataLocation location, Profiles profiles) {
return resolve(getProfileSpecificReferences(context, location, profiles));
}
private Set<StandardConfigDataReference> getProfileSpecificReferences(ConfigDataLocationResolverContext context,
ConfigDataLocation configDataLocation, Profiles profiles) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
String resourceLocation = getResourceLocation(context, configDataLocation);
for (String profile : profiles) {
references.addAll(getReferences(configDataLocation, resourceLocation, profile));
}
return references;
}
private String getResourceLocation(ConfigDataLocationResolverContext context,
ConfigDataLocation configDataLocation) {
String resourceLocation = configDataLocation.getNonPrefixedValue(PREFIX);
boolean isAbsolute = resourceLocation.startsWith("/") || URL_PREFIX.matcher(resourceLocation).matches();
if (isAbsolute) {
return resourceLocation;
}
ConfigDataResource parent = context.getParent();
if (parent instanceof StandardConfigDataResource) {
String parentResourceLocation = ((StandardConfigDataResource) parent).getReference().getResourceLocation();
String parentDirectory = parentResourceLocation.substring(0, parentResourceLocation.lastIndexOf("/") + 1);
return parentDirectory + resourceLocation;
}
return resourceLocation;
}
private Set<StandardConfigDataReference> getReferences(ConfigDataLocation configDataLocation,
String resourceLocation, String profile) {
if (isDirectory(resourceLocation)) {
return getReferencesForDirectory(configDataLocation, resourceLocation, profile);
}
return getReferencesForFile(configDataLocation, resourceLocation, profile);
}
private Set<StandardConfigDataReference> getReferencesForDirectory(ConfigDataLocation configDataLocation,
String directory, String profile) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
for (String name : this.configNames) {
for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {
for (String extension : propertySourceLoader.getFileExtensions()) {
StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation,
directory, directory + name, profile, extension, propertySourceLoader);
references.add(reference);
}
}
}
return references;
}
private Set<StandardConfigDataReference> getReferencesForFile(ConfigDataLocation configDataLocation, String file,
String profile) {
Matcher extensionHintMatcher = EXTENSION_HINT_PATTERN.matcher(file);
boolean extensionHintLocation = extensionHintMatcher.matches();
if (extensionHintLocation) {
file = extensionHintMatcher.group(1) + extensionHintMatcher.group(2);
}
for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {
String extension = getLoadableFileExtension(propertySourceLoader, file);
if (extension != null) {
String root = file.substring(0, file.length() - extension.length() - 1);
StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, null, root,
profile, (!extensionHintLocation) ? extension : null, propertySourceLoader);
return Collections.singleton(reference);
}
}
throw new IllegalStateException("File extension is not known to any PropertySourceLoader. "
+ "If the location is meant to reference a directory, it must end in '/'");
}
private String getLoadableFileExtension(PropertySourceLoader loader, String file) {
for (String fileExtension : loader.getFileExtensions()) {
if (StringUtils.endsWithIgnoreCase(file, fileExtension)) {
return fileExtension;
}
}
return null;
}
private boolean isDirectory(String resourceLocation) {
return resourceLocation.endsWith("/");
}
private List<StandardConfigDataResource> resolve(Set<StandardConfigDataReference> references) {
List<StandardConfigDataResource> resolved = new ArrayList<>();
for (StandardConfigDataReference reference : references) {
resolved.addAll(resolve(reference));
}
if (resolved.isEmpty()) {
assertNonOptionalDirectories(references);
}
return resolved;
}
private void assertNonOptionalDirectories(Set<StandardConfigDataReference> references) {
for (StandardConfigDataReference reference : references) {
if (reference.isNonOptionalDirectory()) {
assertDirectoryExists(reference);
}
}
}
private void assertDirectoryExists(StandardConfigDataReference reference) {
Resource resource = loadResource(reference.getDirectory());
StandardConfigDataResource configDataResource = new StandardConfigDataResource(reference, resource);
ConfigDataResourceNotFoundException.throwIfDoesNotExist(configDataResource, resource);
}
private List<StandardConfigDataResource> resolve(StandardConfigDataReference reference) {
if (!reference.isPatternLocation()) {
return resolveNonPattern(reference);
}
return resolvePattern(reference);
}
private List<StandardConfigDataResource> resolveNonPattern(StandardConfigDataReference reference) {
Resource resource = loadResource(reference.getResourceLocation());
if (!resource.exists() && reference.isSkippable()) {
logSkippingResource(reference);
return Collections.emptyList();
}
return Collections.singletonList(createConfigResourceLocation(reference, resource));
}
private List<StandardConfigDataResource> resolvePattern(StandardConfigDataReference reference) {
validatePatternLocation(reference.getResourceLocation());
List<StandardConfigDataResource> resolved = new ArrayList<>();
for (Resource resource : getResourcesFromResourceLocationPattern(reference.getResourceLocation())) {
if (!resource.exists() && reference.isSkippable()) {
logSkippingResource(reference);
}
else {
resolved.add(createConfigResourceLocation(reference, resource));
}
}
return resolved;
}
private void logSkippingResource(StandardConfigDataReference reference) {
this.logger.trace(LogMessage.format("Skipping missing resource %s", reference));
}
private StandardConfigDataResource createConfigResourceLocation(StandardConfigDataReference reference,
Resource resource) {
return new StandardConfigDataResource(reference, resource);
}
private void validatePatternLocation(String resourceLocation) {
Assert.state(!resourceLocation.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX),
"Classpath wildcard patterns cannot be used as a search location");
Assert.state(StringUtils.countOccurrencesOf(resourceLocation, "*") == 1,
() -> "Search location '" + resourceLocation + "' cannot contain multiple wildcards");
String directoryPath = resourceLocation.substring(0, resourceLocation.lastIndexOf("/") + 1);
Assert.state(directoryPath.endsWith("*/"),
() -> "Search location '" + resourceLocation + "' must end with '*/'");
}
private Resource[] getResourcesFromResourceLocationPattern(String resourceLocationPattern) {
String directoryPath = resourceLocationPattern.substring(0, resourceLocationPattern.indexOf("*/"));
String fileName = resourceLocationPattern.substring(resourceLocationPattern.lastIndexOf("/") + 1);
Resource directoryResource = loadResource(directoryPath);
if (!directoryResource.exists()) {
return new Resource[] { directoryResource };
}
File directory = getDirectory(resourceLocationPattern, directoryResource);
File[] subDirectories = directory.listFiles(File::isDirectory);
if (subDirectories == null) {
return EMPTY_RESOURCES;
}
Arrays.sort(subDirectories, FILE_COMPARATOR);
List<Resource> resources = new ArrayList<>();
FilenameFilter filter = (dir, name) -> name.equals(fileName);
for (File subDirectory : subDirectories) {
File[] files = subDirectory.listFiles(filter);
if (files != null) {
Arrays.stream(files).map(FileSystemResource::new).forEach(resources::add);
}
}
return resources.toArray(EMPTY_RESOURCES);
}
private Resource loadResource(String location) {
location = StringUtils.cleanPath(location);
if (!ResourceUtils.isUrl(location)) {
location = ResourceUtils.FILE_URL_PREFIX + location;
}
return this.resourceLoader.getResource(location);
}
private File getDirectory(String patternLocation, Resource resource) {
try {
File directory = resource.getFile();
Assert.state(directory.isDirectory(), () -> "'" + directory + "' is not a directory");
return directory;
}
catch (Exception ex) {
throw new IllegalStateException(
"Unable to load config data resource from pattern '" + patternLocation + "'", ex);
}
}
}

@ -0,0 +1,111 @@
/*
* 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 org.springframework.boot.env.PropertySourceLoader;
import org.springframework.util.StringUtils;
/**
* An reference expanded from the original {@link ConfigDataLocation} that can ultimately
* be resolved to one or more {@link StandardConfigDataResource resources}.
*
* @author Phillip Webb
*/
class StandardConfigDataReference {
private final ConfigDataLocation configDataLocation;
private final String resourceLocation;
private final String directory;
private final String profile;
private final PropertySourceLoader propertySourceLoader;
/**
* Create a new {@link StandardConfigDataReference} instance.
* @param configDataLocation the original location passed to the resolver
* @param directory the directory of the resource or {@code null} if the reference is
* to a file
* @param root the root of the resource location
* @param profile the profile being loaded
* @param extension the file extension for the resource
* @param propertySourceLoader the property source loader that should be used for this
* reference
*/
StandardConfigDataReference(ConfigDataLocation configDataLocation, String directory, String root, String profile,
String extension, PropertySourceLoader propertySourceLoader) {
this.configDataLocation = configDataLocation;
String profileSuffix = (StringUtils.hasText(profile)) ? "-" + profile : "";
this.resourceLocation = root + profileSuffix + ((extension != null) ? "." + extension : "");
this.directory = directory;
this.profile = profile;
this.propertySourceLoader = propertySourceLoader;
}
ConfigDataLocation getConfigDataLocation() {
return this.configDataLocation;
}
String getResourceLocation() {
return this.resourceLocation;
}
boolean isNonOptionalDirectory() {
return !this.configDataLocation.isOptional() && this.directory != null;
}
String getDirectory() {
return this.directory;
}
boolean isSkippable() {
return this.configDataLocation.isOptional() || this.directory != null || this.profile != null;
}
boolean isPatternLocation() {
return this.resourceLocation.contains("*");
}
PropertySourceLoader getPropertySourceLoader() {
return this.propertySourceLoader;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
StandardConfigDataReference other = (StandardConfigDataReference) obj;
return this.resourceLocation.equals(other.resourceLocation);
}
@Override
public int hashCode() {
return this.resourceLocation.hashCode();
}
@Override
public String toString() {
return this.resourceLocation;
}
}

@ -17,63 +17,43 @@
package org.springframework.boot.context.config;
import java.io.IOException;
import java.util.List;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginTrackedResource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.FileUrlResource;
import org.springframework.core.io.Resource;
import org.springframework.util.Assert;
/**
* {@link ConfigDataLocation} backed by a {@link Resource}.
* {@link ConfigDataResource} backed by a {@link Resource}.
*
* @author Madhura Bhave
* @author Phillip Webb
* @since 2.4.0
*/
public class ResourceConfigDataLocation extends ConfigDataLocation {
public class StandardConfigDataResource extends ConfigDataResource {
private final String name;
private final StandardConfigDataReference reference;
private final Resource resource;
private final Origin origin;
private final PropertySourceLoader propertySourceLoader;
/**
* Create a new {@link ResourceConfigDataLocation} instance.
* @param name the source location
* Create a new {@link StandardConfigDataResource} instance.
* @param reference the resource reference
* @param resource the underlying resource
* @param origin the origin of the resource
* @param propertySourceLoader the loader that should be used to load the resource
*/
ResourceConfigDataLocation(String name, Resource resource, Origin origin,
PropertySourceLoader propertySourceLoader) {
Assert.notNull(name, "Name must not be null");
StandardConfigDataResource(StandardConfigDataReference reference, Resource resource) {
Assert.notNull(reference, "Reference must not be null");
Assert.notNull(resource, "Resource must not be null");
Assert.notNull(propertySourceLoader, "PropertySourceLoader must not be null");
this.name = name;
this.reference = reference;
this.resource = resource;
this.origin = origin;
this.propertySourceLoader = propertySourceLoader;
}
Resource getResource() {
return this.resource;
}
String getLocation() {
return this.name;
StandardConfigDataReference getReference() {
return this.reference;
}
List<PropertySource<?>> load() throws IOException {
Resource resource = OriginTrackedResource.of(this.resource, this.origin);
return this.propertySourceLoader.load(this.name, resource);
Resource getResource() {
return this.resource;
}
@Override
@ -84,7 +64,7 @@ public class ResourceConfigDataLocation extends ConfigDataLocation {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
ResourceConfigDataLocation other = (ResourceConfigDataLocation) obj;
StandardConfigDataResource other = (StandardConfigDataResource) obj;
return this.resource.equals(other.resource);
}

@ -25,22 +25,22 @@ package org.springframework.boot.context.config;
*/
public class UnsupportedConfigDataLocationException extends ConfigDataException {
private final String location;
private final ConfigDataLocation location;
/**
* Create a new {@link UnsupportedConfigDataLocationException} instance.
* @param location the unsupported location
*/
UnsupportedConfigDataLocationException(String location) {
UnsupportedConfigDataLocationException(ConfigDataLocation location) {
super("Unsupported config data location '" + location + "'", null);
this.location = location;
}
/**
* Return the unsupported location.
* @return the unsupported location
* Return the unsupported location reference.
* @return the unsupported location reference
*/
public String getLocation() {
public ConfigDataLocation getLocation() {
return this.location;
}

@ -12,12 +12,12 @@ org.springframework.boot.env.YamlPropertySourceLoader
# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.ResourceConfigDataLocationResolver
org.springframework.boot.context.config.StandardConfigDataLocationResolver
# ConfigData Loaders
org.springframework.boot.context.config.ConfigDataLoader=\
org.springframework.boot.context.config.ConfigTreeConfigDataLoader,\
org.springframework.boot.context.config.ResourceConfigDataLoader
org.springframework.boot.context.config.StandardConfigDataLoader
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\

@ -44,18 +44,20 @@ import static org.mockito.Mockito.mock;
*/
class ConfigDataEnvironmentContributorTests {
private static final ConfigDataLocation TEST_LOCATION = ConfigDataLocation.of("test");
private ConfigDataActivationContext activationContext = new ConfigDataActivationContext(CloudPlatform.KUBERNETES,
null);
@Test
void getKindReturnsKind() {
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport("test");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(TEST_LOCATION);
assertThat(contributor.getKind()).isEqualTo(Kind.INITIAL_IMPORT);
}
@Test
void isActiveWhenPropertiesIsNullReturnsTrue() {
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport("test");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(TEST_LOCATION);
assertThat(contributor.isActive(null)).isTrue();
}
@ -80,10 +82,10 @@ class ConfigDataEnvironmentContributorTests {
@Test
void getLocationReturnsLocation() {
ConfigData configData = new ConfigData(Collections.singleton(new MockPropertySource()));
ConfigDataLocation location = mock(ConfigDataLocation.class);
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofUnboundImport(location,
ConfigDataResource resource = mock(ConfigDataResource.class);
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofUnboundImport(resource,
configData, 0);
assertThat(contributor.getLocation()).isSameAs(location);
assertThat(contributor.getResource()).isSameAs(resource);
}
@Test
@ -117,7 +119,8 @@ class ConfigDataEnvironmentContributorTests {
propertySource.setProperty("spring.config.import", "spring,boot");
ConfigData configData = new ConfigData(Collections.singleton(propertySource));
ConfigDataEnvironmentContributor contributor = createBoundContributor(null, configData, 0);
assertThat(contributor.getImports()).containsExactly("spring", "boot");
assertThat(contributor.getImports()).containsExactly(ConfigDataLocation.of("spring"),
ConfigDataLocation.of("boot"));
}
@Test
@ -239,7 +242,7 @@ class ConfigDataEnvironmentContributorTests {
ConfigDataEnvironmentContributor two = createBoundContributor("two");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.of(Arrays.asList(one, two));
assertThat(contributor.getKind()).isEqualTo(Kind.ROOT);
assertThat(contributor.getLocation()).isNull();
assertThat(contributor.getResource()).isNull();
assertThat(contributor.getImports()).isEmpty();
assertThat(contributor.isActive(this.activationContext)).isTrue();
assertThat(contributor.getPropertySource()).isNull();
@ -249,10 +252,10 @@ class ConfigDataEnvironmentContributorTests {
@Test
void ofInitialImportCreatedInitialImportContributor() {
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport("test");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(TEST_LOCATION);
assertThat(contributor.getKind()).isEqualTo(Kind.INITIAL_IMPORT);
assertThat(contributor.getLocation()).isNull();
assertThat(contributor.getImports()).containsExactly("test");
assertThat(contributor.getResource()).isNull();
assertThat(contributor.getImports()).containsExactly(TEST_LOCATION);
assertThat(contributor.isActive(this.activationContext)).isTrue();
assertThat(contributor.getPropertySource()).isNull();
assertThat(contributor.getConfigurationPropertySource()).isNull();
@ -266,7 +269,7 @@ class ConfigDataEnvironmentContributorTests {
propertySource.setProperty("spring.config.activate.on-cloud-platform", "cloudfoundry");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofExisting(propertySource);
assertThat(contributor.getKind()).isEqualTo(Kind.EXISTING);
assertThat(contributor.getLocation()).isNull();
assertThat(contributor.getResource()).isNull();
assertThat(contributor.getImports()).isEmpty(); // Properties must not be bound
assertThat(contributor.isActive(this.activationContext)).isTrue();
assertThat(contributor.getPropertySource()).isEqualTo(propertySource);
@ -276,14 +279,14 @@ class ConfigDataEnvironmentContributorTests {
@Test
void ofUnboundImportCreatesImportedContributor() {
TestLocation location = new TestLocation("test");
TestResource location = new TestResource("test");
MockPropertySource propertySource = new MockPropertySource();
propertySource.setProperty("spring.config.import", "test");
ConfigData configData = new ConfigData(Collections.singleton(propertySource));
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofUnboundImport(location,
configData, 0);
assertThat(contributor.getKind()).isEqualTo(Kind.UNBOUND_IMPORT);
assertThat(contributor.getLocation()).isSameAs(location);
assertThat(contributor.getResource()).isSameAs(location);
assertThat(contributor.getImports()).isEmpty();
assertThat(contributor.isActive(this.activationContext)).isTrue();
assertThat(contributor.getPropertySource()).isEqualTo(propertySource);
@ -293,14 +296,14 @@ class ConfigDataEnvironmentContributorTests {
@Test
void bindCreatesImportedContributor() {
TestLocation location = new TestLocation("test");
TestResource location = new TestResource("test");
MockPropertySource propertySource = new MockPropertySource();
propertySource.setProperty("spring.config.import", "test");
ConfigData configData = new ConfigData(Collections.singleton(propertySource));
ConfigDataEnvironmentContributor contributor = createBoundContributor(location, configData, 0);
assertThat(contributor.getKind()).isEqualTo(Kind.BOUND_IMPORT);
assertThat(contributor.getLocation()).isSameAs(location);
assertThat(contributor.getImports()).containsExactly("test");
assertThat(contributor.getResource()).isSameAs(location);
assertThat(contributor.getImports()).containsExactly(TEST_LOCATION);
assertThat(contributor.isActive(this.activationContext)).isTrue();
assertThat(contributor.getPropertySource()).isEqualTo(propertySource);
assertThat(contributor.getConfigurationPropertySource()).isNotNull();
@ -309,13 +312,13 @@ class ConfigDataEnvironmentContributorTests {
@Test
void bindWhenConfigDataHasIgnoreImportsOptionsCreatesImportedContributorWithoutImports() {
TestLocation location = new TestLocation("test");
TestResource location = new TestResource("test");
MockPropertySource propertySource = new MockPropertySource();
propertySource.setProperty("spring.config.import", "test");
ConfigData configData = new ConfigData(Collections.singleton(propertySource), ConfigData.Option.IGNORE_IMPORTS);
ConfigDataEnvironmentContributor contributor = createBoundContributor(location, configData, 0);
assertThat(contributor.getKind()).isEqualTo(Kind.BOUND_IMPORT);
assertThat(contributor.getLocation()).isSameAs(location);
assertThat(contributor.getResource()).isSameAs(location);
assertThat(contributor.getImports()).isEmpty();
assertThat(contributor.isActive(this.activationContext)).isTrue();
assertThat(contributor.getPropertySource()).isEqualTo(propertySource);
@ -332,13 +335,13 @@ class ConfigDataEnvironmentContributorTests {
}
private ConfigDataEnvironmentContributor createBoundContributor(String location) {
return createBoundContributor(new TestLocation(location),
return createBoundContributor(new TestResource(location),
new ConfigData(Collections.singleton(new MockPropertySource())), 0);
}
private ConfigDataEnvironmentContributor createBoundContributor(ConfigDataLocation location, ConfigData configData,
private ConfigDataEnvironmentContributor createBoundContributor(ConfigDataResource resource, ConfigData configData,
int propertySourceIndex) {
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofUnboundImport(location,
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofUnboundImport(resource,
configData, propertySourceIndex);
Binder binder = new Binder(contributor.getConfigurationPropertySource());
return contributor.withBoundProperties(binder);
@ -351,14 +354,14 @@ class ConfigDataEnvironmentContributorTests {
}
private String getLocationName(ConfigDataEnvironmentContributor contributor) {
return contributor.getLocation().toString();
return contributor.getResource().toString();
}
static class TestLocation extends ConfigDataLocation {
static class TestResource extends ConfigDataResource {
private final String location;
TestLocation(String location) {
TestResource(String location) {
this.location = location;
}

@ -57,6 +57,10 @@ import static org.mockito.Mockito.verify;
@ExtendWith(MockitoExtension.class)
class ConfigDataEnvironmentContributorsTests {
private static final ConfigDataLocation LOCATION_1 = ConfigDataLocation.of("location1");
private static final ConfigDataLocation LOCATION_2 = ConfigDataLocation.of("location2");
private DeferredLogFactory logFactory = Supplier::get;
private DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
@ -80,16 +84,15 @@ class ConfigDataEnvironmentContributorsTests {
this.environment = new MockEnvironment();
this.binder = Binder.get(this.environment);
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, this.binder, null);
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL);
this.importer = new ConfigDataImporter(resolvers, loaders);
this.binder, null);
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, this.bootstrapContext);
this.importer = new ConfigDataImporter(this.logFactory, ConfigDataNotFoundAction.FAIL, resolvers, loaders);
this.activationContext = new ConfigDataActivationContext(CloudPlatform.KUBERNETES, null);
}
@Test
void createCreatesWithInitialContributors() {
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport("test");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(LOCATION_1);
ConfigDataEnvironmentContributors contributors = new ConfigDataEnvironmentContributors(this.logFactory,
this.bootstrapContext, Arrays.asList(contributor));
Iterator<ConfigDataEnvironmentContributor> iterator = contributors.iterator();
@ -111,13 +114,13 @@ class ConfigDataEnvironmentContributorsTests {
@Test
void withProcessedImportsResolvesAndLoads() {
this.importer = mock(ConfigDataImporter.class);
List<String> locations = Arrays.asList("testimport");
List<ConfigDataLocation> locations = Arrays.asList(LOCATION_1);
MockPropertySource propertySource = new MockPropertySource();
Map<ConfigDataLocation, ConfigData> imported = new LinkedHashMap<>();
imported.put(new TestConfigDataLocation("a"), new ConfigData(Arrays.asList(propertySource)));
Map<ConfigDataResource, ConfigData> imported = new LinkedHashMap<>();
imported.put(new TestConfigDataResource("a"), new ConfigData(Arrays.asList(propertySource)));
given(this.importer.resolveAndLoad(eq(this.activationContext), any(), any(), eq(locations)))
.willReturn(imported);
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport("testimport");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(LOCATION_1);
ConfigDataEnvironmentContributors contributors = new ConfigDataEnvironmentContributors(this.logFactory,
this.bootstrapContext, Arrays.asList(contributor));
ConfigDataEnvironmentContributors withProcessedImports = contributors.withProcessedImports(this.importer,
@ -132,21 +135,20 @@ class ConfigDataEnvironmentContributorsTests {
@Test
void withProcessedImportsResolvesAndLoadsChainedImports() {
this.importer = mock(ConfigDataImporter.class);
List<String> initialLocations = Arrays.asList("initialimport");
List<ConfigDataLocation> initialLocations = Arrays.asList(LOCATION_1);
MockPropertySource initialPropertySource = new MockPropertySource();
initialPropertySource.setProperty("spring.config.import", "secondimport");
Map<ConfigDataLocation, ConfigData> initialImported = new LinkedHashMap<>();
initialImported.put(new TestConfigDataLocation("a"), new ConfigData(Arrays.asList(initialPropertySource)));
initialPropertySource.setProperty("spring.config.import", "location2");
Map<ConfigDataResource, ConfigData> initialImported = new LinkedHashMap<>();
initialImported.put(new TestConfigDataResource("a"), new ConfigData(Arrays.asList(initialPropertySource)));
given(this.importer.resolveAndLoad(eq(this.activationContext), any(), any(), eq(initialLocations)))
.willReturn(initialImported);
List<String> secondLocations = Arrays.asList("secondimport");
List<ConfigDataLocation> secondLocations = Arrays.asList(LOCATION_2);
MockPropertySource secondPropertySource = new MockPropertySource();
Map<ConfigDataLocation, ConfigData> secondImported = new LinkedHashMap<>();
secondImported.put(new TestConfigDataLocation("b"), new ConfigData(Arrays.asList(secondPropertySource)));
Map<ConfigDataResource, ConfigData> secondImported = new LinkedHashMap<>();
secondImported.put(new TestConfigDataResource("b"), new ConfigData(Arrays.asList(secondPropertySource)));
given(this.importer.resolveAndLoad(eq(this.activationContext), any(), any(), eq(secondLocations)))
.willReturn(secondImported);
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor
.ofInitialImport("initialimport");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(LOCATION_1);
ConfigDataEnvironmentContributors contributors = new ConfigDataEnvironmentContributors(this.logFactory,
this.bootstrapContext, Arrays.asList(contributor));
ConfigDataEnvironmentContributors withProcessedImports = contributors.withProcessedImports(this.importer,
@ -166,13 +168,13 @@ class ConfigDataEnvironmentContributorsTests {
ConfigDataEnvironmentContributor existingContributor = ConfigDataEnvironmentContributor
.ofExisting(existingPropertySource);
this.importer = mock(ConfigDataImporter.class);
List<String> locations = Arrays.asList("testimport");
List<ConfigDataLocation> locations = Arrays.asList(LOCATION_1);
MockPropertySource propertySource = new MockPropertySource();
Map<ConfigDataLocation, ConfigData> imported = new LinkedHashMap<>();
imported.put(new TestConfigDataLocation("a'"), new ConfigData(Arrays.asList(propertySource)));
Map<ConfigDataResource, ConfigData> imported = new LinkedHashMap<>();
imported.put(new TestConfigDataResource("a'"), new ConfigData(Arrays.asList(propertySource)));
given(this.importer.resolveAndLoad(eq(this.activationContext), any(), any(), eq(locations)))
.willReturn(imported);
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport("testimport");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(LOCATION_1);
ConfigDataEnvironmentContributors contributors = new ConfigDataEnvironmentContributors(this.logFactory,
this.bootstrapContext, Arrays.asList(existingContributor, contributor));
contributors.withProcessedImports(this.importer, this.activationContext);
@ -184,21 +186,20 @@ class ConfigDataEnvironmentContributorsTests {
@Test
void withProcessedImportsProvidesLocationResolverContextWithAccessToParent() {
this.importer = mock(ConfigDataImporter.class);
List<String> initialLocations = Arrays.asList("initialimport");
List<ConfigDataLocation> initialLocations = Arrays.asList(LOCATION_1);
MockPropertySource initialPropertySource = new MockPropertySource();
initialPropertySource.setProperty("spring.config.import", "secondimport");
Map<ConfigDataLocation, ConfigData> initialImported = new LinkedHashMap<>();
initialImported.put(new TestConfigDataLocation("a"), new ConfigData(Arrays.asList(initialPropertySource)));
initialPropertySource.setProperty("spring.config.import", "location2");
Map<ConfigDataResource, ConfigData> initialImported = new LinkedHashMap<>();
initialImported.put(new TestConfigDataResource("a"), new ConfigData(Arrays.asList(initialPropertySource)));
given(this.importer.resolveAndLoad(eq(this.activationContext), any(), any(), eq(initialLocations)))
.willReturn(initialImported);
List<String> secondLocations = Arrays.asList("secondimport");
List<ConfigDataLocation> secondLocations = Arrays.asList(LOCATION_2);
MockPropertySource secondPropertySource = new MockPropertySource();
Map<ConfigDataLocation, ConfigData> secondImported = new LinkedHashMap<>();
secondImported.put(new TestConfigDataLocation("b"), new ConfigData(Arrays.asList(secondPropertySource)));
Map<ConfigDataResource, ConfigData> secondImported = new LinkedHashMap<>();
secondImported.put(new TestConfigDataResource("b"), new ConfigData(Arrays.asList(secondPropertySource)));
given(this.importer.resolveAndLoad(eq(this.activationContext), any(), any(), eq(secondLocations)))
.willReturn(secondImported);
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor
.ofInitialImport("initialimport");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(LOCATION_1);
ConfigDataEnvironmentContributors contributors = new ConfigDataEnvironmentContributors(this.logFactory,
this.bootstrapContext, Arrays.asList(contributor));
contributors.withProcessedImports(this.importer, this.activationContext);
@ -214,13 +215,13 @@ class ConfigDataEnvironmentContributorsTests {
ConfigDataEnvironmentContributor existingContributor = ConfigDataEnvironmentContributor
.ofExisting(existingPropertySource);
this.importer = mock(ConfigDataImporter.class);
List<String> locations = Arrays.asList("testimport");
List<ConfigDataLocation> locations = Arrays.asList(LOCATION_1);
MockPropertySource propertySource = new MockPropertySource();
Map<ConfigDataLocation, ConfigData> imported = new LinkedHashMap<>();
imported.put(new TestConfigDataLocation("a'"), new ConfigData(Arrays.asList(propertySource)));
Map<ConfigDataResource, ConfigData> imported = new LinkedHashMap<>();
imported.put(new TestConfigDataResource("a'"), new ConfigData(Arrays.asList(propertySource)));
given(this.importer.resolveAndLoad(eq(this.activationContext), any(), any(), eq(locations)))
.willReturn(imported);
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport("testimport");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(LOCATION_1);
ConfigDataEnvironmentContributors contributors = new ConfigDataEnvironmentContributors(this.logFactory,
this.bootstrapContext, Arrays.asList(existingContributor, contributor));
contributors.withProcessedImports(this.importer, this.activationContext);
@ -236,13 +237,13 @@ class ConfigDataEnvironmentContributorsTests {
ConfigDataEnvironmentContributor existingContributor = ConfigDataEnvironmentContributor
.ofExisting(existingPropertySource);
this.importer = mock(ConfigDataImporter.class);
List<String> locations = Arrays.asList("testimport");
List<ConfigDataLocation> locations = Arrays.asList(LOCATION_1);
MockPropertySource propertySource = new MockPropertySource();
Map<ConfigDataLocation, ConfigData> imported = new LinkedHashMap<>();
imported.put(new TestConfigDataLocation("a'"), new ConfigData(Arrays.asList(propertySource)));
Map<ConfigDataResource, ConfigData> imported = new LinkedHashMap<>();
imported.put(new TestConfigDataResource("a'"), new ConfigData(Arrays.asList(propertySource)));
given(this.importer.resolveAndLoad(eq(this.activationContext), any(), any(), eq(locations)))
.willReturn(imported);
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport("testimport");
ConfigDataEnvironmentContributor contributor = ConfigDataEnvironmentContributor.ofInitialImport(LOCATION_1);
ConfigDataEnvironmentContributors contributors = new ConfigDataEnvironmentContributors(this.logFactory,
this.bootstrapContext, Arrays.asList(existingContributor, contributor));
contributors.withProcessedImports(this.importer, this.activationContext);
@ -382,11 +383,11 @@ class ConfigDataEnvironmentContributorsTests {
return contributor.withBoundProperties(binder);
}
private static class TestConfigDataLocation extends ConfigDataLocation {
private static class TestConfigDataResource extends ConfigDataResource {
private final String value;
TestConfigDataLocation(String value) {
TestConfigDataResource(String value) {
this.value = value;
}

@ -51,7 +51,8 @@ class ConfigDataEnvironmentPostProcessorBootstrapContextIntegrationTests {
LoaderHelper bean = context.getBean(TestConfigDataBootstrap.LoaderHelper.class);
assertThat(bean).isNotNull();
assertThat(bean.getBound()).isEqualTo("igotbound");
assertThat(bean.getLocation().getResolverHelper().getLocation()).isEqualTo("testbootstrap:test");
assertThat(bean.getLocation().getResolverHelper().getLocation())
.isEqualTo(ConfigDataLocation.of("testbootstrap:test"));
}
}

@ -378,8 +378,8 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
List<String> names = StreamSupport.stream(context.getEnvironment().getPropertySources().spliterator(), false)
.map(org.springframework.core.env.PropertySource::getName).collect(Collectors.toList());
assertThat(names).contains(
"Resource config 'classpath:configdata/profiles/testsetprofiles.yml' imported via location \"classpath:configdata/profiles/\" (document #0)",
"Resource config 'classpath:configdata/profiles/testsetprofiles.yml' imported via location \"classpath:configdata/profiles/\" (document #1)");
"Config resource 'classpath:configdata/profiles/testsetprofiles.yml' via location 'classpath:configdata/profiles/' (document #0)",
"Config resource 'classpath:configdata/profiles/testsetprofiles.yml' via location 'classpath:configdata/profiles/' (document #1)");
}
@Test
@ -406,8 +406,8 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
String location = "file:src/test/resources/specificlocation.properties";
ConfigurableApplicationContext context = this.application.run("--spring.config.location=" + location);
assertThat(context.getEnvironment()).has(matchingPropertySource(
"Resource config 'file:src/test/resources/specificlocation.properties' imported via location \""
+ location + "\""));
"Config resource 'file:src/test/resources/specificlocation.properties' via location '" + location
+ "'"));
}
@Test
@ -415,8 +415,7 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
String location = "src/test/resources/specificlocation.properties";
ConfigurableApplicationContext context = this.application.run("--spring.config.location=" + location);
assertThat(context.getEnvironment()).has(matchingPropertySource(
"Resource config 'src/test/resources/specificlocation.properties' imported via location \"" + location
+ "\""));
"Config resource 'src/test/resources/specificlocation.properties' via location '" + location + "'"));
}
@Test
@ -519,7 +518,7 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
@Test
void runWhenConfigLocationHasNonOptionalMissingDirectoryThrowsException() {
String location = "classpath:application.unknown/";
assertThatExceptionOfType(ConfigDataLocationNotFoundException.class)
assertThatExceptionOfType(ConfigDataResourceNotFoundException.class)
.isThrownBy(() -> this.application.run("--spring.config.location=" + location));
}
@ -553,13 +552,13 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
@Test
void runWhenHasNonOptionalImportThrowsException() {
assertThatExceptionOfType(ConfigDataLocationNotFoundException.class).isThrownBy(
assertThatExceptionOfType(ConfigDataResourceNotFoundException.class).isThrownBy(
() -> this.application.run("--spring.config.location=classpath:missing-appplication.properties"));
}
@Test
void runWhenHasNonOptionalImportAndIgnoreNotFoundPropertyDoesNotThrowException() {
this.application.run("--spring.config.on-location-not-found=ignore",
this.application.run("--spring.config.on-not-found=ignore",
"--spring.config.location=classpath:missing-appplication.properties");
}
@ -613,6 +612,7 @@ class ConfigDataEnvironmentPostProcessorIntegrationTests {
@Override
public boolean matches(ConfigurableEnvironment value) {
value.getPropertySources().forEach((ps) -> System.out.println(ps.getName()));
return value.getPropertySources().contains(sourceName);
}

@ -214,11 +214,9 @@ class ConfigDataEnvironmentTests {
@Override
protected ConfigDataLocationResolvers createConfigDataLocationResolvers(DeferredLogFactory logFactory,
ConfigurableBootstrapContext bootstrapContext, ConfigDataLocationNotFoundAction locationNotFoundAction,
Binder binder, ResourceLoader resourceLoader) {
ConfigurableBootstrapContext bootstrapContext, Binder binder, ResourceLoader resourceLoader) {
this.configDataLocationResolversBinder = binder;
return super.createConfigDataLocationResolvers(logFactory, bootstrapContext, locationNotFoundAction, binder,
resourceLoader);
return super.createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
}
Binder getConfigDataLocationResolversBinder() {

@ -20,6 +20,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -28,6 +29,7 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.logging.DeferredLogFactory;
import org.springframework.mock.env.MockPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
@ -42,6 +44,8 @@ import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class ConfigDataImporterTests {
private DeferredLogFactory logFactory = Supplier::get;
@Mock
private ConfigDataLocationResolvers resolvers;
@ -70,43 +74,49 @@ class ConfigDataImporterTests {
@Test
void loadImportsResolvesAndLoadsLocations() throws Exception {
List<String> locations = Arrays.asList("test1", "test2");
TestLocation resolvedLocation1 = new TestLocation();
TestLocation resolvedLocation2 = new TestLocation();
List<ConfigDataLocation> resolvedLocations = Arrays.asList(resolvedLocation1, resolvedLocation2);
ConfigDataLocation location1 = ConfigDataLocation.of("test1");
ConfigDataLocation location2 = ConfigDataLocation.of("test2");
TestResource resource1 = new TestResource("r1");
TestResource resource2 = new TestResource("r2");
ConfigData configData1 = new ConfigData(Collections.singleton(new MockPropertySource()));
ConfigData configData2 = new ConfigData(Collections.singleton(new MockPropertySource()));
given(this.resolvers.resolveAll(this.locationResolverContext, locations, this.profiles))
.willReturn(resolvedLocations);
given(this.loaders.load(this.loaderContext, resolvedLocation1)).willReturn(configData1);
given(this.loaders.load(this.loaderContext, resolvedLocation2)).willReturn(configData2);
ConfigDataImporter importer = new ConfigDataImporter(this.resolvers, this.loaders);
Collection<ConfigData> loaded = importer
.resolveAndLoad(this.activationContext, this.locationResolverContext, this.loaderContext, locations)
.values();
given(this.resolvers.resolve(this.locationResolverContext, location1, this.profiles))
.willReturn(Collections.singletonList(new ConfigDataResolutionResult(location1, resource1)));
given(this.resolvers.resolve(this.locationResolverContext, location2, this.profiles))
.willReturn(Collections.singletonList(new ConfigDataResolutionResult(location2, resource2)));
given(this.loaders.load(this.loaderContext, resource1)).willReturn(configData1);
given(this.loaders.load(this.loaderContext, resource2)).willReturn(configData2);
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, ConfigDataNotFoundAction.FAIL,
this.resolvers, this.loaders);
Collection<ConfigData> loaded = importer.resolveAndLoad(this.activationContext, this.locationResolverContext,
this.loaderContext, Arrays.asList(location1, location2)).values();
assertThat(loaded).containsExactly(configData2, configData1);
}
@Test
void loadImportsWhenAlreadyImportedLocationSkipsLoad() throws Exception {
List<String> locations1and2 = Arrays.asList("test1", "test2");
List<String> locations2and3 = Arrays.asList("test2", "test3");
TestLocation resolvedLocation1 = new TestLocation();
TestLocation resolvedLocation2 = new TestLocation();
TestLocation resolvedLocation3 = new TestLocation();
List<ConfigDataLocation> resolvedLocations1and2 = Arrays.asList(resolvedLocation1, resolvedLocation2);
List<ConfigDataLocation> resolvedLocations2and3 = Arrays.asList(resolvedLocation2, resolvedLocation3);
ConfigDataLocation location1 = ConfigDataLocation.of("test1");
ConfigDataLocation location2 = ConfigDataLocation.of("test2");
ConfigDataLocation location3 = ConfigDataLocation.of("test3");
List<ConfigDataLocation> locations1and2 = Arrays.asList(location1, location2);
List<ConfigDataLocation> locations2and3 = Arrays.asList(location2, location3);
TestResource resource1 = new TestResource("r1");
TestResource resource2 = new TestResource("r2");
TestResource resource3 = new TestResource("r3");
ConfigData configData1 = new ConfigData(Collections.singleton(new MockPropertySource()));
ConfigData configData2 = new ConfigData(Collections.singleton(new MockPropertySource()));
ConfigData configData3 = new ConfigData(Collections.singleton(new MockPropertySource()));
given(this.resolvers.resolveAll(this.locationResolverContext, locations1and2, this.profiles))
.willReturn(resolvedLocations1and2);
given(this.resolvers.resolveAll(this.locationResolverContext, locations2and3, this.profiles))
.willReturn(resolvedLocations2and3);
given(this.loaders.load(this.loaderContext, resolvedLocation1)).willReturn(configData1);
given(this.loaders.load(this.loaderContext, resolvedLocation2)).willReturn(configData2);
given(this.loaders.load(this.loaderContext, resolvedLocation3)).willReturn(configData3);
ConfigDataImporter importer = new ConfigDataImporter(this.resolvers, this.loaders);
given(this.resolvers.resolve(this.locationResolverContext, location1, this.profiles))
.willReturn(Collections.singletonList(new ConfigDataResolutionResult(location1, resource1)));
given(this.resolvers.resolve(this.locationResolverContext, location2, this.profiles))
.willReturn(Collections.singletonList(new ConfigDataResolutionResult(location2, resource2)));
given(this.resolvers.resolve(this.locationResolverContext, location3, this.profiles))
.willReturn(Collections.singletonList(new ConfigDataResolutionResult(location3, resource3)));
given(this.loaders.load(this.loaderContext, resource1)).willReturn(configData1);
given(this.loaders.load(this.loaderContext, resource2)).willReturn(configData2);
given(this.loaders.load(this.loaderContext, resource3)).willReturn(configData3);
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, ConfigDataNotFoundAction.FAIL,
this.resolvers, this.loaders);
Collection<ConfigData> loaded1and2 = importer.resolveAndLoad(this.activationContext,
this.locationResolverContext, this.loaderContext, locations1and2).values();
Collection<ConfigData> loaded2and3 = importer.resolveAndLoad(this.activationContext,
@ -115,7 +125,18 @@ class ConfigDataImporterTests {
assertThat(loaded2and3).containsExactly(configData3);
}
static class TestLocation extends ConfigDataLocation {
static class TestResource extends ConfigDataResource {
private final String name;
TestResource(String name) {
this.name = name;
}
@Override
public String toString() {
return this.name;
}
}

@ -37,19 +37,19 @@ class ConfigDataLoaderTests {
@Test
void isLoadableAlwaysReturnsTrue() {
assertThat(this.loader.isLoadable(this.context, new TestConfigDataLocation())).isTrue();
assertThat(this.loader.isLoadable(this.context, new TestConfigDataResource())).isTrue();
}
static class TestConfigDataLoader implements ConfigDataLoader<TestConfigDataLocation> {
static class TestConfigDataLoader implements ConfigDataLoader<TestConfigDataResource> {
@Override
public ConfigData load(ConfigDataLoaderContext context, TestConfigDataLocation location) throws IOException {
public ConfigData load(ConfigDataLoaderContext context, TestConfigDataResource resource) throws IOException {
return null;
}
}
static class TestConfigDataLocation extends ConfigDataLocation {
static class TestConfigDataResource extends ConfigDataResource {
}

@ -53,50 +53,48 @@ class ConfigDataLoadersTests {
@Test
void createWhenLoaderHasLogParameterInjectsLog() {
new ConfigDataLoaders(this.logFactory, this.bootstrapContext, ConfigDataLocationNotFoundAction.FAIL,
new ConfigDataLoaders(this.logFactory, this.bootstrapContext,
Arrays.asList(LoggingConfigDataLoader.class.getName()));
}
@Test
void createWhenLoaderHasBootstrapParametersInjectsBootstrapContext() {
new ConfigDataLoaders(this.logFactory, this.bootstrapContext, ConfigDataLocationNotFoundAction.FAIL,
new ConfigDataLoaders(this.logFactory, this.bootstrapContext,
Arrays.asList(BootstrappingConfigDataLoader.class.getName()));
assertThat(this.bootstrapContext.get(String.class)).isEqualTo("boot");
}
@Test
void loadWhenSingleLoaderSupportsLocationReturnsLoadedConfigData() throws Exception {
TestConfigDataLocation location = new TestConfigDataLocation("test");
TestConfigDataResource location = new TestConfigDataResource("test");
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, Arrays.asList(TestConfigDataLoader.class.getName()));
Arrays.asList(TestConfigDataLoader.class.getName()));
ConfigData loaded = loaders.load(this.context, location);
assertThat(getLoader(loaded)).isInstanceOf(TestConfigDataLoader.class);
}
@Test
void loadWhenMultipleLoadersSupportLocationThrowsException() throws Exception {
TestConfigDataLocation location = new TestConfigDataLocation("test");
TestConfigDataResource location = new TestConfigDataResource("test");
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL,
Arrays.asList(LoggingConfigDataLoader.class.getName(), TestConfigDataLoader.class.getName()));
assertThatIllegalStateException().isThrownBy(() -> loaders.load(this.context, location))
.withMessageContaining("Multiple loaders found for location test");
.withMessageContaining("Multiple loaders found for resource 'test'");
}
@Test
void loadWhenNoLoaderSupportsLocationThrowsException() {
TestConfigDataLocation location = new TestConfigDataLocation("test");
TestConfigDataResource location = new TestConfigDataResource("test");
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, Arrays.asList(NonLoadableConfigDataLoader.class.getName()));
Arrays.asList(NonLoadableConfigDataLoader.class.getName()));
assertThatIllegalStateException().isThrownBy(() -> loaders.load(this.context, location))
.withMessage("No loader found for location 'test'");
.withMessage("No loader found for resource 'test'");
}
@Test
void loadWhenGenericTypeDoesNotMatchSkipsLoader() throws Exception {
TestConfigDataLocation location = new TestConfigDataLocation("test");
TestConfigDataResource location = new TestConfigDataResource("test");
ConfigDataLoaders loaders = new ConfigDataLoaders(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL,
Arrays.asList(OtherConfigDataLoader.class.getName(), SpecificConfigDataLoader.class.getName()));
ConfigData loaded = loaders.load(this.context, location);
assertThat(getLoader(loaded)).isInstanceOf(SpecificConfigDataLoader.class);
@ -106,20 +104,19 @@ class ConfigDataLoadersTests {
return (ConfigDataLoader<?>) loaded.getPropertySources().get(0).getProperty("loader");
}
private static ConfigData createConfigData(ConfigDataLoader<?> loader, ConfigDataLocation location) {
private static ConfigData createConfigData(ConfigDataLoader<?> loader, ConfigDataResource resource) {
MockPropertySource propertySource = new MockPropertySource();
propertySource.setProperty("loader", loader);
propertySource.setProperty("location", location);
propertySource.setProperty("resource", resource);
List<PropertySource<?>> propertySources = Arrays.asList(propertySource);
return new ConfigData(propertySources);
}
static class TestConfigDataLocation extends ConfigDataLocation {
static class TestConfigDataResource extends ConfigDataResource {
private final String value;
TestConfigDataLocation(String value) {
TestConfigDataResource(String value) {
this.value = value;
}
@ -130,24 +127,24 @@ class ConfigDataLoadersTests {
}
static class OtherConfigDataLocation extends ConfigDataLocation {
static class OtherConfigDataResource extends ConfigDataResource {
}
static class LoggingConfigDataLoader implements ConfigDataLoader<ConfigDataLocation> {
static class LoggingConfigDataLoader implements ConfigDataLoader<ConfigDataResource> {
LoggingConfigDataLoader(Log log) {
assertThat(log).isNotNull();
}
@Override
public ConfigData load(ConfigDataLoaderContext context, ConfigDataLocation location) throws IOException {
public ConfigData load(ConfigDataLoaderContext context, ConfigDataResource resource) throws IOException {
throw new AssertionError("Unexpected call");
}
}
static class BootstrappingConfigDataLoader implements ConfigDataLoader<ConfigDataLocation> {
static class BootstrappingConfigDataLoader implements ConfigDataLoader<ConfigDataResource> {
BootstrappingConfigDataLoader(ConfigurableBootstrapContext configurableBootstrapContext,
BootstrapRegistry bootstrapRegistry, BootstrapContext bootstrapContext) {
@ -159,17 +156,17 @@ class ConfigDataLoadersTests {
}
@Override
public ConfigData load(ConfigDataLoaderContext context, ConfigDataLocation location) throws IOException {
public ConfigData load(ConfigDataLoaderContext context, ConfigDataResource resource) throws IOException {
throw new AssertionError("Unexpected call");
}
}
static class TestConfigDataLoader implements ConfigDataLoader<ConfigDataLocation> {
static class TestConfigDataLoader implements ConfigDataLoader<ConfigDataResource> {
@Override
public ConfigData load(ConfigDataLoaderContext context, ConfigDataLocation location) throws IOException {
return createConfigData(this, location);
public ConfigData load(ConfigDataLoaderContext context, ConfigDataResource resource) throws IOException {
return createConfigData(this, resource);
}
}
@ -177,25 +174,25 @@ class ConfigDataLoadersTests {
static class NonLoadableConfigDataLoader extends TestConfigDataLoader {
@Override
public boolean isLoadable(ConfigDataLoaderContext context, ConfigDataLocation location) {
public boolean isLoadable(ConfigDataLoaderContext context, ConfigDataResource resource) {
return false;
}
}
static class SpecificConfigDataLoader implements ConfigDataLoader<TestConfigDataLocation> {
static class SpecificConfigDataLoader implements ConfigDataLoader<TestConfigDataResource> {
@Override
public ConfigData load(ConfigDataLoaderContext context, TestConfigDataLocation location) throws IOException {
public ConfigData load(ConfigDataLoaderContext context, TestConfigDataResource location) throws IOException {
return createConfigData(this, location);
}
}
static class OtherConfigDataLoader implements ConfigDataLoader<OtherConfigDataLocation> {
static class OtherConfigDataLoader implements ConfigDataLoader<OtherConfigDataResource> {
@Override
public ConfigData load(ConfigDataLoaderContext context, OtherConfigDataLocation location) throws IOException {
public ConfigData load(ConfigDataLoaderContext context, OtherConfigDataResource location) throws IOException {
return createConfigData(this, location);
}

@ -0,0 +1,121 @@
/*
* 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.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ConfigDataLocationBindHandler}.
*
* @author Phillip Webb
*/
class ConfigDataLocationBindHandlerTests {
private static final Bindable<ConfigDataLocation[]> ARRAY = Bindable.of(ConfigDataLocation[].class);
private static final Bindable<ValueObject> VALUE_OBJECT = Bindable.of(ValueObject.class);
private final ConfigDataLocationBindHandler handler = new ConfigDataLocationBindHandler();
@Test
void bindToArrayFromCommaStringPropertySetsOrigin() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource();
source.put("locations", "a,b,c");
Binder binder = new Binder(source);
ConfigDataLocation[] bound = binder.bind("locations", ARRAY, this.handler).get();
String expectedLocation = "\"locations\" from property source \"source\"";
assertThat(bound[0]).hasToString("a");
assertThat(bound[0].getOrigin()).hasToString(expectedLocation);
assertThat(bound[1]).hasToString("b");
assertThat(bound[1].getOrigin()).hasToString(expectedLocation);
assertThat(bound[2]).hasToString("c");
assertThat(bound[2].getOrigin()).hasToString(expectedLocation);
}
@Test
void bindToArrayFromIndexedPropertiesSetsOrigin() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource();
source.put("locations[0]", "a");
source.put("locations[1]", "b");
source.put("locations[2]", "c");
Binder binder = new Binder(source);
ConfigDataLocation[] bound = binder.bind("locations", ARRAY, this.handler).get();
assertThat(bound[0]).hasToString("a");
assertThat(bound[0].getOrigin()).hasToString("\"locations[0]\" from property source \"source\"");
assertThat(bound[1]).hasToString("b");
assertThat(bound[1].getOrigin()).hasToString("\"locations[1]\" from property source \"source\"");
assertThat(bound[2]).hasToString("c");
assertThat(bound[2].getOrigin()).hasToString("\"locations[2]\" from property source \"source\"");
}
@Test
void bindToValueObjectFromCommaStringPropertySetsOrigin() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource();
source.put("test.locations", "a,b,c");
Binder binder = new Binder(source);
ValueObject bound = binder.bind("test", VALUE_OBJECT, this.handler).get();
String expectedLocation = "\"test.locations\" from property source \"source\"";
assertThat(bound.getLocation(0)).hasToString("a");
assertThat(bound.getLocation(0).getOrigin()).hasToString(expectedLocation);
assertThat(bound.getLocation(1)).hasToString("b");
assertThat(bound.getLocation(1).getOrigin()).hasToString(expectedLocation);
assertThat(bound.getLocation(2)).hasToString("c");
assertThat(bound.getLocation(2).getOrigin()).hasToString(expectedLocation);
}
@Test
void bindToValueObjectFromIndexedPropertiesSetsOrigin() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource();
source.put("test.locations[0]", "a");
source.put("test.locations[1]", "b");
source.put("test.locations[2]", "c");
Binder binder = new Binder(source);
ValueObject bound = binder.bind("test", VALUE_OBJECT, this.handler).get();
assertThat(bound.getLocation(0)).hasToString("a");
assertThat(bound.getLocation(0).getOrigin())
.hasToString("\"test.locations[0]\" from property source \"source\"");
assertThat(bound.getLocation(1)).hasToString("b");
assertThat(bound.getLocation(1).getOrigin())
.hasToString("\"test.locations[1]\" from property source \"source\"");
assertThat(bound.getLocation(2)).hasToString("c");
assertThat(bound.getLocation(2).getOrigin())
.hasToString("\"test.locations[2]\" from property source \"source\"");
}
static class ValueObject {
private final List<ConfigDataLocation> locations;
ValueObject(List<ConfigDataLocation> locations) {
this.locations = locations;
}
ConfigDataLocation getLocation(int index) {
return this.locations.get(index);
}
}
}

@ -16,19 +16,12 @@
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 org.springframework.boot.origin.Origin;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
@ -38,98 +31,37 @@ import static org.mockito.Mockito.mock;
*/
class ConfigDataLocationNotFoundExceptionTests {
private ConfigDataLocation location = mock(ConfigDataLocation.class);
private Throwable cause = new RuntimeException();
private String message = "message";
private File exists;
private Origin origin = mock(Origin.class);
private File missing;
private final ConfigDataLocation location = ConfigDataLocation.of("optional:test").withOrigin(this.origin);
@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());
}
}
private final ConfigDataLocationNotFoundException exception = new ConfigDataLocationNotFoundException(
this.location);
@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);
void createWhenLocationIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigDataLocationNotFoundException(null))
.withMessage("Location must not be null");
}
@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);
assertThat(this.exception.getLocation()).isSameAs(this.location);
}
@Test
void throwIfDoesNotExistWhenFileDoesNotExistThrowsException() {
assertThatExceptionOfType(ConfigDataLocationNotFoundException.class)
.isThrownBy(() -> ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location, this.missing));
void getOriginReturnsLocationOrigin() {
assertThat(this.exception.getOrigin()).isSameAs(this.origin);
}
@Test
void throwIfDoesNotExistWhenResourceExistsDoesNothing() {
ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location, new FileSystemResource(this.exists));
void getReferenceDescriptionReturnsLocationString() {
assertThat(this.exception.getReferenceDescription()).isEqualTo("location 'optional:test'");
}
@Test
void throwIfDoesNotExistWhenResourceDoesNotExistThrowsException() {
assertThatExceptionOfType(ConfigDataLocationNotFoundException.class)
.isThrownBy(() -> ConfigDataLocationNotFoundException.throwIfDoesNotExist(this.location,
new FileSystemResource(this.missing)));
void getMessageReturnsMessage() {
assertThat(this.exception).hasMessage("Config data location 'optional:test' cannot be found");
}
}

@ -37,19 +37,19 @@ class ConfigDataLocationResolverTests {
@Test
void resolveProfileSpecificReturnsEmptyList() {
assertThat(this.resolver.resolveProfileSpecific(this.context, null, true, null)).isEmpty();
assertThat(this.resolver.resolveProfileSpecific(this.context, null, null)).isEmpty();
}
static class TestConfigDataLocationResolver implements ConfigDataLocationResolver<ConfigDataLocation> {
static class TestConfigDataLocationResolver implements ConfigDataLocationResolver<ConfigDataResource> {
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
return true;
}
@Override
public List<ConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) {
public List<ConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location) {
return null;
}

@ -71,8 +71,7 @@ class ConfigDataLocationResolversTests {
@Test
void createWhenInjectingBinderCreatesResolver() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, this.binder, this.resourceLoader,
Collections.singletonList(TestBoundResolver.class.getName()));
this.binder, this.resourceLoader, Collections.singletonList(TestBoundResolver.class.getName()));
assertThat(resolvers.getResolvers()).hasSize(1);
assertThat(resolvers.getResolvers().get(0)).isExactlyInstanceOf(TestBoundResolver.class);
assertThat(((TestBoundResolver) resolvers.getResolvers().get(0)).getBinder()).isSameAs(this.binder);
@ -81,25 +80,23 @@ class ConfigDataLocationResolversTests {
@Test
void createWhenNotInjectingBinderCreatesResolver() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, this.binder, this.resourceLoader,
Collections.singletonList(TestResolver.class.getName()));
this.binder, this.resourceLoader, Collections.singletonList(TestResolver.class.getName()));
assertThat(resolvers.getResolvers()).hasSize(1);
assertThat(resolvers.getResolvers().get(0)).isExactlyInstanceOf(TestResolver.class);
}
@Test
void createWhenResolverHasBootstrapParametersInjectsBootstrapContext() {
new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext, ConfigDataLocationNotFoundAction.FAIL,
this.binder, this.resourceLoader, Collections.singletonList(TestBootstrappingResolver.class.getName()));
new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext, this.binder, this.resourceLoader,
Collections.singletonList(TestBootstrappingResolver.class.getName()));
assertThat(this.bootstrapContext.get(String.class)).isEqualTo("boot");
}
@Test
void createWhenNameIsNotConfigDataLocationResolverThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, this.binder, this.resourceLoader,
Collections.singletonList(InputStream.class.getName())))
.isThrownBy(() -> new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext, this.binder,
this.resourceLoader, Collections.singletonList(InputStream.class.getName())))
.withMessageContaining("Unable to instantiate").havingCause().withMessageContaining("not assignable");
}
@ -110,73 +107,75 @@ class ConfigDataLocationResolversTests {
names.add(LowestTestResolver.class.getName());
names.add(HighestTestResolver.class.getName());
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, this.binder, this.resourceLoader, names);
this.binder, this.resourceLoader, names);
assertThat(resolvers.getResolvers().get(0)).isExactlyInstanceOf(HighestTestResolver.class);
assertThat(resolvers.getResolvers().get(1)).isExactlyInstanceOf(TestResolver.class);
assertThat(resolvers.getResolvers().get(2)).isExactlyInstanceOf(LowestTestResolver.class);
}
@Test
void resolveAllResolvesUsingFirstSupportedResolver() {
void resolveResolvesUsingFirstSupportedResolver() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, this.binder, this.resourceLoader,
this.binder, this.resourceLoader,
Arrays.asList(LowestTestResolver.class.getName(), HighestTestResolver.class.getName()));
List<ConfigDataLocation> resolved = resolvers.resolveAll(this.context,
Collections.singletonList("LowestTestResolver:test"), null);
ConfigDataLocation location = ConfigDataLocation.of("LowestTestResolver:test");
List<ConfigDataResolutionResult> resolved = resolvers.resolve(this.context, location, null);
assertThat(resolved).hasSize(1);
TestConfigDataLocation location = (TestConfigDataLocation) resolved.get(0);
assertThat(location.getResolver()).isInstanceOf(LowestTestResolver.class);
assertThat(location.getLocation()).isEqualTo("LowestTestResolver:test");
assertThat(location.isProfileSpecific()).isFalse();
TestConfigDataResource resource = (TestConfigDataResource) resolved.get(0).getResource();
assertThat(resource.getResolver()).isInstanceOf(LowestTestResolver.class);
assertThat(resource.getLocation()).isEqualTo(location);
assertThat(resource.isProfileSpecific()).isFalse();
}
@Test
void resolveAllWhenProfileMergesResolvedLocations() {
void resolveWhenProfileMergesResolvedLocations() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, this.binder, this.resourceLoader,
this.binder, this.resourceLoader,
Arrays.asList(LowestTestResolver.class.getName(), HighestTestResolver.class.getName()));
List<ConfigDataLocation> resolved = resolvers.resolveAll(this.context,
Collections.singletonList("LowestTestResolver:test"), this.profiles);
ConfigDataLocation location = ConfigDataLocation.of("LowestTestResolver:test");
List<ConfigDataResolutionResult> resolved = resolvers.resolve(this.context, location, this.profiles);
assertThat(resolved).hasSize(2);
TestConfigDataLocation location = (TestConfigDataLocation) resolved.get(0);
assertThat(location.getResolver()).isInstanceOf(LowestTestResolver.class);
assertThat(location.getLocation()).isEqualTo("LowestTestResolver:test");
assertThat(location.isProfileSpecific()).isFalse();
TestConfigDataLocation profileLocation = (TestConfigDataLocation) resolved.get(1);
assertThat(profileLocation.getResolver()).isInstanceOf(LowestTestResolver.class);
assertThat(profileLocation.getLocation()).isEqualTo("LowestTestResolver:test");
assertThat(profileLocation.isProfileSpecific()).isTrue();
TestConfigDataResource resource = (TestConfigDataResource) resolved.get(0).getResource();
assertThat(resource.getResolver()).isInstanceOf(LowestTestResolver.class);
assertThat(resource.getLocation()).isEqualTo(location);
assertThat(resource.isProfileSpecific()).isFalse();
TestConfigDataResource profileResource = (TestConfigDataResource) resolved.get(1).getResource();
assertThat(profileResource.getResolver()).isInstanceOf(LowestTestResolver.class);
assertThat(profileResource.getLocation()).isEqualTo(location);
assertThat(profileResource.isProfileSpecific()).isTrue();
}
@Test
void resolveWhenNoResolverThrowsException() {
ConfigDataLocationResolvers resolvers = new ConfigDataLocationResolvers(this.logFactory, this.bootstrapContext,
ConfigDataLocationNotFoundAction.FAIL, this.binder, this.resourceLoader,
this.binder, this.resourceLoader,
Arrays.asList(LowestTestResolver.class.getName(), HighestTestResolver.class.getName()));
ConfigDataLocation location = ConfigDataLocation.of("Missing:test");
assertThatExceptionOfType(UnsupportedConfigDataLocationException.class)
.isThrownBy(() -> resolvers.resolveAll(this.context, Collections.singletonList("Missing:test"), null))
.satisfies((ex) -> assertThat(ex.getLocation()).isEqualTo("Missing:test"));
.isThrownBy(() -> resolvers.resolve(this.context, location, null))
.satisfies((ex) -> assertThat(ex.getLocation()).isEqualTo(location));
}
static class TestResolver implements ConfigDataLocationResolver<TestConfigDataLocation> {
static class TestResolver implements ConfigDataLocationResolver<TestConfigDataResource> {
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
String name = getClass().getName();
name = name.substring(name.lastIndexOf("$") + 1);
return location.startsWith(name + ":");
return location.hasPrefix(name + ":");
}
@Override
public List<TestConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) {
return Collections.singletonList(new TestConfigDataLocation(this, location, false));
public List<TestConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location)
throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException {
return Collections.singletonList(new TestConfigDataResource(this, location, false));
}
@Override
public List<TestConfigDataLocation> resolveProfileSpecific(ConfigDataLocationResolverContext context,
String location, boolean optional, Profiles profiles) {
return Collections.singletonList(new TestConfigDataLocation(this, location, true));
public List<TestConfigDataResource> resolveProfileSpecific(ConfigDataLocationResolverContext context,
ConfigDataLocation location, Profiles profiles) throws ConfigDataLocationNotFoundException {
return Collections.singletonList(new TestConfigDataResource(this, location, true));
}
}
@ -218,15 +217,15 @@ class ConfigDataLocationResolversTests {
}
static class TestConfigDataLocation extends ConfigDataLocation {
static class TestConfigDataResource extends ConfigDataResource {
private final TestResolver resolver;
private final String location;
private final ConfigDataLocation location;
private final boolean profileSpecific;
TestConfigDataLocation(TestResolver resolver, String location, boolean profileSpecific) {
TestConfigDataResource(TestResolver resolver, ConfigDataLocation location, boolean profileSpecific) {
this.resolver = resolver;
this.location = location;
this.profileSpecific = profileSpecific;
@ -236,7 +235,7 @@ class ConfigDataLocationResolversTests {
return this.resolver;
}
String getLocation() {
ConfigDataLocation getLocation() {
return this.location;
}

@ -0,0 +1,137 @@
/*
* 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 org.junit.jupiter.api.Test;
import org.springframework.boot.origin.Origin;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ConfigDataLocation}.
*
* @author Phillip Webb
*/
class ConfigDataLocationTests {
@Test
void isOptionalWhenNotPrefixedWithOptionalReturnsFalse() {
ConfigDataLocation location = ConfigDataLocation.of("test");
assertThat(location.isOptional()).isFalse();
}
@Test
void isOptionalWhenPrefixedWithOptionalReturnsTrue() {
ConfigDataLocation location = ConfigDataLocation.of("optional:test");
assertThat(location.isOptional()).isTrue();
}
@Test
void getValueWhenNotPrefixedWithOptionalReturnsValue() {
ConfigDataLocation location = ConfigDataLocation.of("test");
assertThat(location.getValue()).isEqualTo("test");
}
@Test
void getValueWhenPrefixedWithOptionalReturnsValueWithoutPrefix() {
ConfigDataLocation location = ConfigDataLocation.of("optional:test");
assertThat(location.getValue()).isEqualTo("test");
}
@Test
void hasPrefixWhenPrefixedReturnsTrue() {
ConfigDataLocation location = ConfigDataLocation.of("optional:test:path");
assertThat(location.hasPrefix("test:")).isTrue();
}
@Test
void hasPrefixWhenNotPrefixedReturnsFalse() {
ConfigDataLocation location = ConfigDataLocation.of("optional:file:path");
assertThat(location.hasPrefix("test:")).isFalse();
}
@Test
void getNonPrefixedValueWhenPrefixedReturnsNonPrefixed() {
ConfigDataLocation location = ConfigDataLocation.of("optional:test:path");
assertThat(location.getNonPrefixedValue("test:")).isEqualTo("path");
}
@Test
void getNonPrefixedValueWhenNotPrefixedReturnsOriginalValue() {
ConfigDataLocation location = ConfigDataLocation.of("optional:file:path");
assertThat(location.getNonPrefixedValue("test:")).isEqualTo("file:path");
}
@Test
void getOriginWhenNoOriginReturnsNull() {
ConfigDataLocation location = ConfigDataLocation.of("test");
assertThat(location.getOrigin()).isNull();
}
@Test
void getOriginWhenWithOriginReturnsOrigin() {
Origin origin = mock(Origin.class);
ConfigDataLocation location = ConfigDataLocation.of("test").withOrigin(origin);
assertThat(location.getOrigin()).isSameAs(origin);
}
@Test
void equalsAndHashCode() {
ConfigDataLocation l1 = ConfigDataLocation.of("a");
ConfigDataLocation l2 = ConfigDataLocation.of("a");
ConfigDataLocation l3 = ConfigDataLocation.of("optional:a");
ConfigDataLocation l4 = ConfigDataLocation.of("b");
assertThat(l1.hashCode()).isEqualTo(l2.hashCode()).isEqualTo(l3.hashCode());
assertThat(l1).isEqualTo(l2).isEqualTo(l3).isNotEqualTo(l4);
}
@Test
void toStringReturnsOriginalString() {
ConfigDataLocation location = ConfigDataLocation.of("optional:test");
assertThat(location).hasToString("optional:test");
}
@Test
void withOriginSetsOrigin() {
Origin origin = mock(Origin.class);
ConfigDataLocation location = ConfigDataLocation.of("test").withOrigin(origin);
assertThat(location.getOrigin()).isSameAs(origin);
}
@Test
void ofWhenNullValueReturnsNull() {
assertThat(ConfigDataLocation.of(null)).isNull();
}
@Test
void ofWhenEmptyValueReturnsNull() {
assertThat(ConfigDataLocation.of("")).isNull();
}
@Test
void ofWhenEmptyOptionalValueReturnsNull() {
assertThat(ConfigDataLocation.of("optional:")).isNull();
}
@Test
void ofReturnsLocation() {
assertThat(ConfigDataLocation.of("test")).hasToString("test");
}
}

@ -43,13 +43,16 @@ class ConfigDataPropertiesTests {
private static final Profiles NULL_PROFILES = null;
private static final List<String> NO_IMPORTS = Collections.emptyList();
private static final List<ConfigDataLocation> NO_IMPORTS = Collections.emptyList();
@Test
void getImportsReturnsImports() {
List<String> imports = Arrays.asList("one", "two", "three");
ConfigDataLocation l1 = ConfigDataLocation.of("one");
ConfigDataLocation l2 = ConfigDataLocation.of("two");
ConfigDataLocation l3 = ConfigDataLocation.of("three");
List<ConfigDataLocation> imports = Arrays.asList(l1, l2, l3);
ConfigDataProperties properties = new ConfigDataProperties(imports, null);
assertThat(properties.getImports()).containsExactly("one", "two", "three");
assertThat(properties.getImports()).containsExactly(l1, l2, l3);
}
@Test
@ -164,7 +167,8 @@ class ConfigDataPropertiesTests {
ConfigDataProperties properties = ConfigDataProperties.get(binder);
ConfigDataActivationContext context = new ConfigDataActivationContext(CloudPlatform.KUBERNETES,
createTestProfiles());
assertThat(properties.getImports()).containsExactly("one", "two", "three");
assertThat(properties.getImports()).containsExactly(ConfigDataLocation.of("one"), ConfigDataLocation.of("two"),
ConfigDataLocation.of("three"));
assertThat(properties.isActive(context)).isTrue();
}
@ -195,7 +199,7 @@ class ConfigDataPropertiesTests {
source.put("spring.config.import", "one,two,three");
Binder binder = new Binder(source);
ConfigDataProperties properties = ConfigDataProperties.get(binder);
assertThat(properties.getImportOrigin("two"))
assertThat(properties.getImports().get(1).getOrigin())
.hasToString("\"spring.config.import\" from property source \"source\"");
}
@ -207,7 +211,7 @@ class ConfigDataPropertiesTests {
source.put("spring.config.import[2]", "three");
Binder binder = new Binder(source);
ConfigDataProperties properties = ConfigDataProperties.get(binder);
assertThat(properties.getImportOrigin("two"))
assertThat(properties.getImports().get(1).getOrigin())
.hasToString("\"spring.config.import[1]\" from property source \"source\"");
}

@ -0,0 +1,167 @@
/*
* 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.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ConfigDataResourceNotFoundException}.
*
* @author Phillip Webb
*/
class ConfigDataResourceNotFoundExceptionTests {
private ConfigDataResource resource = new TestConfigDataResource();
private ConfigDataLocation location = ConfigDataLocation.of("optional:test");
private Throwable cause = new RuntimeException();
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 createWhenResourceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigDataResourceNotFoundException(null))
.withMessage("Resource must not be null");
}
@Test
void createWithResourceCreatesInstance() {
ConfigDataResourceNotFoundException exception = new ConfigDataResourceNotFoundException(this.resource);
assertThat(exception.getResource()).isSameAs(this.resource);
}
@Test
void createWithResourceAndCauseCreatesInstance() {
ConfigDataResourceNotFoundException exception = new ConfigDataResourceNotFoundException(this.resource,
this.cause);
assertThat(exception.getResource()).isSameAs(this.resource);
assertThat(exception.getCause()).isSameAs(this.cause);
}
@Test
void getResourceReturnsResource() {
ConfigDataResourceNotFoundException exception = new ConfigDataResourceNotFoundException(this.resource);
assertThat(exception.getResource()).isSameAs(this.resource);
}
@Test
void getLocationWhenHasNoLocationReturnsNull() {
ConfigDataResourceNotFoundException exception = new ConfigDataResourceNotFoundException(this.resource);
assertThat(exception.getLocation()).isNull();
}
@Test
void getLocationWhenHasLocationReturnsLocation() {
ConfigDataResourceNotFoundException exception = new ConfigDataResourceNotFoundException(this.resource)
.withLocation(this.location);
assertThat(exception.getLocation()).isSameAs(this.location);
}
@Test
void getReferenceDescriptionWhenHasNoLocationReturnsDescription() {
ConfigDataResourceNotFoundException exception = new ConfigDataResourceNotFoundException(this.resource);
assertThat(exception.getReferenceDescription()).isEqualTo("resource 'mytestresource'");
}
@Test
void getReferenceDescriptionWhenHasLocationReturnsDescription() {
ConfigDataResourceNotFoundException exception = new ConfigDataResourceNotFoundException(this.resource)
.withLocation(this.location);
assertThat(exception.getReferenceDescription())
.isEqualTo("resource 'mytestresource' via location 'optional:test'");
}
@Test
void withLocationReturnsNewInstanceWithLocation() {
ConfigDataResourceNotFoundException exception = new ConfigDataResourceNotFoundException(this.resource)
.withLocation(this.location);
assertThat(exception.getLocation()).isSameAs(this.location);
}
@Test
void throwIfDoesNotExistWhenPathExistsDoesNothing() {
ConfigDataResourceNotFoundException.throwIfDoesNotExist(this.resource, this.exists.toPath());
}
@Test
void throwIfDoesNotExistWhenPathDoesNotExistThrowsException() {
assertThatExceptionOfType(ConfigDataResourceNotFoundException.class).isThrownBy(
() -> ConfigDataResourceNotFoundException.throwIfDoesNotExist(this.resource, this.missing.toPath()));
}
@Test
void throwIfDoesNotExistWhenFileExistsDoesNothing() {
ConfigDataResourceNotFoundException.throwIfDoesNotExist(this.resource, this.exists);
}
@Test
void throwIfDoesNotExistWhenFileDoesNotExistThrowsException() {
assertThatExceptionOfType(ConfigDataResourceNotFoundException.class)
.isThrownBy(() -> ConfigDataResourceNotFoundException.throwIfDoesNotExist(this.resource, this.missing));
}
@Test
void throwIfDoesNotExistWhenResourceExistsDoesNothing() {
ConfigDataResourceNotFoundException.throwIfDoesNotExist(this.resource, new FileSystemResource(this.exists));
}
@Test
void throwIfDoesNotExistWhenResourceDoesNotExistThrowsException() {
assertThatExceptionOfType(ConfigDataResourceNotFoundException.class)
.isThrownBy(() -> ConfigDataResourceNotFoundException.throwIfDoesNotExist(this.resource,
new FileSystemResource(this.missing)));
}
static class TestConfigDataResource extends ConfigDataResource {
@Override
public String toString() {
return "mytestresource";
}
}
}

@ -51,7 +51,7 @@ public class ConfigTreeConfigDataLoaderTests {
File file = this.directory.resolve("hello").toFile();
file.getParentFile().mkdirs();
FileCopyUtils.copy("world".getBytes(StandardCharsets.UTF_8), file);
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation(this.directory.toString());
ConfigTreeConfigDataResource location = new ConfigTreeConfigDataResource(this.directory.toString());
ConfigData configData = this.loader.load(this.loaderContext, location);
assertThat(configData.getPropertySources().size()).isEqualTo(1);
PropertySource<?> source = configData.getPropertySources().get(0);
@ -62,8 +62,8 @@ public class ConfigTreeConfigDataLoaderTests {
@Test
void loadWhenPathDoesNotExistThrowsException() {
File missing = this.directory.resolve("missing").toFile();
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation(missing.toString());
assertThatExceptionOfType(ConfigDataLocationNotFoundException.class)
ConfigTreeConfigDataResource location = new ConfigTreeConfigDataResource(missing.toString());
assertThatExceptionOfType(ConfigDataResourceNotFoundException.class)
.isThrownBy(() -> this.loader.load(this.loaderContext, location));
}

@ -38,19 +38,19 @@ class ConfigTreeConfigDataLocationResolverTests {
@Test
void isResolvableWhenPrefixMatchesReturnsTrue() {
assertThat(this.resolver.isResolvable(this.context, "configtree:/etc/config")).isTrue();
assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("configtree:/etc/config"))).isTrue();
}
@Test
void isResolvableWhenPrefixDoesNotMatchReturnsFalse() {
assertThat(this.resolver.isResolvable(this.context, "http://etc/config")).isFalse();
assertThat(this.resolver.isResolvable(this.context, "/etc/config")).isFalse();
assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("http://etc/config"))).isFalse();
assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("/etc/config"))).isFalse();
}
@Test
void resolveReturnsConfigVolumeMountLocation() {
List<ConfigTreeConfigDataLocation> locations = this.resolver.resolve(this.context, "configtree:/etc/config",
false);
List<ConfigTreeConfigDataResource> locations = this.resolver.resolve(this.context,
ConfigDataLocation.of("configtree:/etc/config"));
assertThat(locations.size()).isEqualTo(1);
assertThat(locations).extracting(Object::toString)
.containsExactly("config tree [" + new File("/etc/config").getAbsolutePath() + "]");

@ -24,36 +24,36 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link ConfigTreeConfigDataLocation}.
* Tests for {@link ConfigTreeConfigDataResource}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
public class ConfigTreeConfigDataLocationTests {
public class ConfigTreeConfigDataResourceTests {
@Test
void constructorWhenPathIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataLocation(null))
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataResource(null))
.withMessage("Path must not be null");
}
@Test
void equalsWhenPathIsTheSameReturnsTrue() {
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation("/etc/config");
ConfigTreeConfigDataLocation other = new ConfigTreeConfigDataLocation("/etc/config");
ConfigTreeConfigDataResource location = new ConfigTreeConfigDataResource("/etc/config");
ConfigTreeConfigDataResource other = new ConfigTreeConfigDataResource("/etc/config");
assertThat(location).isEqualTo(other);
}
@Test
void equalsWhenPathIsDifferentReturnsFalse() {
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation("/etc/config");
ConfigTreeConfigDataLocation other = new ConfigTreeConfigDataLocation("other-location");
ConfigTreeConfigDataResource location = new ConfigTreeConfigDataResource("/etc/config");
ConfigTreeConfigDataResource other = new ConfigTreeConfigDataResource("other-location");
assertThat(location).isNotEqualTo(other);
}
@Test
void toStringReturnsDescriptiveString() {
ConfigTreeConfigDataLocation location = new ConfigTreeConfigDataLocation("/etc/config");
ConfigTreeConfigDataResource location = new ConfigTreeConfigDataResource("/etc/config");
assertThat(location.toString()).isEqualTo("config tree [" + new File("/etc/config").getAbsolutePath() + "]");
}

@ -40,7 +40,7 @@ class InactiveConfigDataAccessExceptionTests {
private MockPropertySource propertySource = new MockPropertySource();
private ConfigDataLocation location = new TestConfigDataLocation();
private ConfigDataResource resource = new TestConfigDataResource();
private String propertyName = "spring";
@ -49,7 +49,7 @@ class InactiveConfigDataAccessExceptionTests {
@Test
void createHasCorrectMessage() {
InactiveConfigDataAccessException exception = new InactiveConfigDataAccessException(this.propertySource,
this.location, this.propertyName, this.origin);
this.resource, this.propertyName, this.origin);
assertThat(exception).hasMessage("Inactive property source 'mockProperties' imported from location 'test' "
+ "cannot contain property 'spring' [origin: \"spring\" from property source \"mockProperties\"]");
}
@ -65,7 +65,7 @@ class InactiveConfigDataAccessExceptionTests {
@Test
void createWhenNoOriginHasCorrectMessage() {
InactiveConfigDataAccessException exception = new InactiveConfigDataAccessException(this.propertySource,
this.location, this.propertyName, null);
this.resource, this.propertyName, null);
assertThat(exception).hasMessage("Inactive property source 'mockProperties' imported from location 'test' "
+ "cannot contain property 'spring'");
}
@ -73,28 +73,28 @@ class InactiveConfigDataAccessExceptionTests {
@Test
void getPropertySourceReturnsPropertySource() {
InactiveConfigDataAccessException exception = new InactiveConfigDataAccessException(this.propertySource,
this.location, this.propertyName, this.origin);
this.resource, this.propertyName, this.origin);
assertThat(exception.getPropertySource()).isSameAs(this.propertySource);
}
@Test
void getLocationReturnsLocation() {
InactiveConfigDataAccessException exception = new InactiveConfigDataAccessException(this.propertySource,
this.location, this.propertyName, this.origin);
assertThat(exception.getLocation()).isSameAs(this.location);
this.resource, this.propertyName, this.origin);
assertThat(exception.getLocation()).isSameAs(this.resource);
}
@Test
void getPropertyNameReturnsPropertyName() {
InactiveConfigDataAccessException exception = new InactiveConfigDataAccessException(this.propertySource,
this.location, this.propertyName, this.origin);
this.resource, this.propertyName, this.origin);
assertThat(exception.getPropertyName()).isSameAs(this.propertyName);
}
@Test
void getOriginReturnsOrigin() {
InactiveConfigDataAccessException exception = new InactiveConfigDataAccessException(this.propertySource,
this.location, this.propertyName, this.origin);
this.resource, this.propertyName, this.origin);
assertThat(exception.getOrigin()).isSameAs(this.origin);
}
@ -121,7 +121,7 @@ class InactiveConfigDataAccessExceptionTests {
ConfigurationPropertySource configurationPropertySource = ConfigurationPropertySource.from(this.propertySource);
given(contributor.getConfigurationPropertySource()).willReturn(configurationPropertySource);
given(contributor.getPropertySource()).willReturn((PropertySource) this.propertySource);
given(contributor.getLocation()).willReturn(this.location);
given(contributor.getResource()).willReturn(this.resource);
assertThatExceptionOfType(InactiveConfigDataAccessException.class)
.isThrownBy(() -> InactiveConfigDataAccessException.throwIfPropertyFound(contributor,
ConfigurationPropertyName.of("spring")))
@ -129,7 +129,7 @@ class InactiveConfigDataAccessExceptionTests {
+ "cannot contain property 'spring' [origin: \"spring\" from property source \"mockProperties\"]");
}
private static class TestConfigDataLocation extends ConfigDataLocation {
private static class TestConfigDataResource extends ConfigDataResource {
@Override
public String toString() {

@ -38,7 +38,7 @@ import static org.mockito.Mockito.verify;
*/
class InvalidConfigDataPropertyExceptionTests {
private ConfigDataLocation location = new TestConfigDataLocation();
private ConfigDataResource resource = new TestConfigDataResource();
private ConfigurationPropertyName replacement = ConfigurationPropertyName.of("replacement");
@ -50,7 +50,7 @@ class InvalidConfigDataPropertyExceptionTests {
@Test
void createHasCorrectMessage() {
assertThat(new InvalidConfigDataPropertyException(this.property, this.replacement, this.location)).hasMessage(
assertThat(new InvalidConfigDataPropertyException(this.property, this.replacement, this.resource)).hasMessage(
"Property 'invalid' imported from location 'test' is invalid and should be replaced with 'replacement' [origin: origin]");
}
@ -62,35 +62,35 @@ class InvalidConfigDataPropertyExceptionTests {
@Test
void createWhenNoReplacementHasCorrectMessage() {
assertThat(new InvalidConfigDataPropertyException(this.property, null, this.location))
assertThat(new InvalidConfigDataPropertyException(this.property, null, this.resource))
.hasMessage("Property 'invalid' imported from location 'test' is invalid [origin: origin]");
}
@Test
void createWhenNoOriginHasCorrectMessage() {
ConfigurationProperty property = new ConfigurationProperty(this.invalid, "bad", null);
assertThat(new InvalidConfigDataPropertyException(property, this.replacement, this.location)).hasMessage(
assertThat(new InvalidConfigDataPropertyException(property, this.replacement, this.resource)).hasMessage(
"Property 'invalid' imported from location 'test' is invalid and should be replaced with 'replacement'");
}
@Test
void getPropertyReturnsProperty() {
InvalidConfigDataPropertyException exception = new InvalidConfigDataPropertyException(this.property,
this.replacement, this.location);
this.replacement, this.resource);
assertThat(exception.getProperty()).isEqualTo(this.property);
}
@Test
void getLocationReturnsLocation() {
InvalidConfigDataPropertyException exception = new InvalidConfigDataPropertyException(this.property,
this.replacement, this.location);
assertThat(exception.getLocation()).isEqualTo(this.location);
this.replacement, this.resource);
assertThat(exception.getLocation()).isEqualTo(this.resource);
}
@Test
void getReplacementReturnsReplacement() {
InvalidConfigDataPropertyException exception = new InvalidConfigDataPropertyException(this.property,
this.replacement, this.location);
this.replacement, this.resource);
assertThat(exception.getReplacement()).isEqualTo(this.replacement);
}
@ -123,7 +123,7 @@ class InvalidConfigDataPropertyExceptionTests {
+ "'spring.config.activate.on-profile' [origin: \"spring.profiles\" from property source \"mockProperties\"]");
}
private static class TestConfigDataLocation extends ConfigDataLocation {
private static class TestConfigDataResource extends ConfigDataResource {
@Override
public String toString() {

@ -1,83 +0,0 @@
/*
* 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"), null, 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);
}
}

@ -29,36 +29,43 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ResourceConfigDataLoader}.
* Tests for {@link StandardConfigDataLoader}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
public class ResourceConfigDataLoaderTests {
public class StandardConfigDataLoaderTests {
private ResourceConfigDataLoader loader = new ResourceConfigDataLoader();
private StandardConfigDataLoader loader = new StandardConfigDataLoader();
private ConfigDataLoaderContext loaderContext = mock(ConfigDataLoaderContext.class);
@Test
void loadWhenLocationResultsInMultiplePropertySourcesAddsAllToConfigData() throws IOException {
ResourceConfigDataLocation location = new ResourceConfigDataLocation("application.yml",
new ClassPathResource("configdata/yaml/application.yml"), null, new YamlPropertySourceLoader());
ClassPathResource resource = new ClassPathResource("configdata/yaml/application.yml");
StandardConfigDataReference reference = new StandardConfigDataReference(
ConfigDataLocation.of("classpath:configdata/yaml/application.yml"), null,
"classpath:configdata/yaml/application", null, "yml", new YamlPropertySourceLoader());
StandardConfigDataResource location = new StandardConfigDataResource(reference, resource);
ConfigData configData = this.loader.load(this.loaderContext, location);
assertThat(configData.getPropertySources().size()).isEqualTo(2);
PropertySource<?> source1 = configData.getPropertySources().get(0);
PropertySource<?> source2 = configData.getPropertySources().get(1);
assertThat(source1.getName()).isEqualTo("application.yml (document #0)");
assertThat(source1.getName()).isEqualTo("Config resource 'classpath:configdata/yaml/application.yml' "
+ "via location 'classpath:configdata/yaml/application.yml' (document #0)");
assertThat(source1.getProperty("foo")).isEqualTo("bar");
assertThat(source2.getName()).isEqualTo("application.yml (document #1)");
assertThat(source2.getName()).isEqualTo("Config resource 'classpath:configdata/yaml/application.yml' "
+ "via location 'classpath:configdata/yaml/application.yml' (document #1)");
assertThat(source2.getProperty("hello")).isEqualTo("world");
}
@Test
void loadWhenPropertySourceIsEmptyAddsNothingToConfigData() throws IOException {
ResourceConfigDataLocation location = new ResourceConfigDataLocation("testproperties.properties",
new ClassPathResource("config/0-empty/testproperties.properties"), null,
new PropertiesPropertySourceLoader());
ClassPathResource resource = new ClassPathResource("config/0-empty/testproperties.properties");
StandardConfigDataReference reference = new StandardConfigDataReference(
ConfigDataLocation.of("classpath:config/0-empty/testproperties.properties"), null,
"config/0-empty/testproperties", null, "properties", new PropertiesPropertySourceLoader());
StandardConfigDataResource location = new StandardConfigDataResource(reference, resource);
ConfigData configData = this.loader.load(this.loaderContext, location);
assertThat(configData.getPropertySources().size()).isEqualTo(0);
}

@ -38,14 +38,14 @@ import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ResourceConfigDataLocationResolver}.
* Tests for {@link StandardConfigDataLocationResolver}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
public class ResourceConfigDataLocationResolverTests {
public class StandardConfigDataLocationResolverTests {
private ResourceConfigDataLocationResolver resolver;
private StandardConfigDataLocationResolver resolver;
private ConfigDataLocationResolverContext context = mock(ConfigDataLocationResolverContext.class);
@ -59,19 +59,19 @@ public class ResourceConfigDataLocationResolverTests {
void setup() {
this.environment = new MockEnvironment();
this.environmentBinder = Binder.get(this.environment);
this.resolver = new ResourceConfigDataLocationResolver(new DeferredLog(), this.environmentBinder,
this.resolver = new StandardConfigDataLocationResolver(new DeferredLog(), this.environmentBinder,
this.resourceLoader);
}
@Test
void isResolvableAlwaysReturnsTrue() {
assertThat(this.resolver.isResolvable(this.context, "test")).isTrue();
assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("test"))).isTrue();
}
@Test
void resolveWhenLocationIsDirectoryResolvesAllMatchingFilesInDirectory() {
String location = "classpath:/configdata/properties/";
List<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location, true);
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/");
List<StandardConfigDataResource> locations = this.resolver.resolve(this.context, location);
assertThat(locations.size()).isEqualTo(1);
assertThat(locations).extracting(Object::toString)
.containsExactly("class path resource [configdata/properties/application.properties]");
@ -79,8 +79,9 @@ 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, true);
ConfigDataLocation location = ConfigDataLocation
.of("file:src/test/resources/configdata/properties/application.properties");
List<StandardConfigDataResource> locations = this.resolver.resolve(this.context, location);
assertThat(locations.size()).isEqualTo(1);
assertThat(locations).extracting(Object::toString).containsExactly(
filePath("src", "test", "resources", "configdata", "properties", "application.properties"));
@ -88,23 +89,24 @@ public class ResourceConfigDataLocationResolverTests {
@Test
void resolveWhenLocationIsFileAndNoMatchingLoaderThrowsException() {
String location = "file:src/test/resources/configdata/properties/application.unknown";
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true))
ConfigDataLocation location = ConfigDataLocation
.of("file:src/test/resources/configdata/properties/application.unknown");
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
.withMessageStartingWith("Unable to load config data from")
.satisfies((ex) -> assertThat(ex.getCause()).hasMessageStartingWith("File extension is not known"));
}
@Test
void resolveWhenLocationWildcardIsSpecifiedForClasspathLocationThrowsException() {
String location = "classpath*:application.properties";
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true))
ConfigDataLocation location = ConfigDataLocation.of("classpath*:application.properties");
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
.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, true))
ConfigDataLocation location = ConfigDataLocation.of("file:src/test/resources/*/config/");
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
.withMessageStartingWith("Search location '").withMessageEndingWith("' must end with '*/'");
}
@ -113,24 +115,24 @@ public class ResourceConfigDataLocationResolverTests {
this.environment.setProperty("spring.config.name", "*/application");
assertThatIllegalStateException()
.isThrownBy(
() -> new ResourceConfigDataLocationResolver(null, this.environmentBinder, this.resourceLoader))
() -> new StandardConfigDataLocationResolver(null, this.environmentBinder, this.resourceLoader))
.withMessageStartingWith("Config name '").withMessageEndingWith("' cannot contain '*'");
}
@Test
void resolveWhenLocationHasMultipleWildcardsThrowsException() {
String location = "file:src/test/resources/config/**/";
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true))
ConfigDataLocation location = ConfigDataLocation.of("file:src/test/resources/config/**/");
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
.withMessageStartingWith("Search location '")
.withMessageEndingWith("' cannot contain multiple wildcards");
}
@Test
void resolveWhenLocationIsWildcardDirectoriesRestrictsToOneLevelDeep() {
String location = "file:src/test/resources/config/*/";
ConfigDataLocation location = ConfigDataLocation.of("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, true);
this.resolver = new StandardConfigDataLocationResolver(null, this.environmentBinder, this.resourceLoader);
List<StandardConfigDataResource> locations = this.resolver.resolve(this.context, location);
assertThat(locations.size()).isEqualTo(3);
assertThat(locations).extracting(Object::toString)
.contains(filePath("src", "test", "resources", "config", "1-first", "testproperties.properties"))
@ -140,10 +142,10 @@ public class ResourceConfigDataLocationResolverTests {
@Test
void resolveWhenLocationIsWildcardDirectoriesSortsAlphabeticallyBasedOnAbsolutePath() {
String location = "file:src/test/resources/config/*/";
ConfigDataLocation location = ConfigDataLocation.of("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, true);
this.resolver = new StandardConfigDataLocationResolver(null, this.environmentBinder, this.resourceLoader);
List<StandardConfigDataResource> locations = this.resolver.resolve(this.context, location);
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,8 +154,9 @@ public class ResourceConfigDataLocationResolverTests {
@Test
void resolveWhenLocationIsWildcardFilesLoadsAllFilesThatMatch() {
String location = "file:src/test/resources/config/*/testproperties.properties";
List<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location, true);
ConfigDataLocation location = ConfigDataLocation
.of("file:src/test/resources/config/*/testproperties.properties");
List<StandardConfigDataResource> locations = this.resolver.resolve(this.context, location);
assertThat(locations.size()).isEqualTo(3);
assertThat(locations).extracting(Object::toString)
.contains(filePath("src", "test", "resources", "config", "1-first", "testproperties.properties"))
@ -165,15 +168,17 @@ public class ResourceConfigDataLocationResolverTests {
@Test
void resolveWhenLocationIsRelativeAndFileResolves() {
this.environment.setProperty("spring.config.name", "other");
String location = "other.properties";
this.resolver = new ResourceConfigDataLocationResolver(new DeferredLog(), this.environmentBinder,
ConfigDataLocation location = ConfigDataLocation.of("other.properties");
this.resolver = new StandardConfigDataLocationResolver(new DeferredLog(), this.environmentBinder,
this.resourceLoader);
ClassPathResource parentResource = new ClassPathResource("configdata/properties/application.properties");
ResourceConfigDataLocation parent = new ResourceConfigDataLocation(
"classpath:/configdata/properties/application.properties", parentResource, null,
StandardConfigDataReference parentReference = new StandardConfigDataReference(
ConfigDataLocation.of("classpath:configdata/properties/application.properties"), null,
"classpath:configdata/properties/application", null, "properties",
new PropertiesPropertySourceLoader());
ClassPathResource parentResource = new ClassPathResource("configdata/properties/application.properties");
StandardConfigDataResource parent = new StandardConfigDataResource(parentReference, parentResource);
given(this.context.getParent()).willReturn(parent);
List<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location, true);
List<StandardConfigDataResource> locations = this.resolver.resolve(this.context, location);
assertThat(locations.size()).isEqualTo(1);
assertThat(locations).extracting(Object::toString)
.contains("class path resource [configdata/properties/other.properties]");
@ -182,14 +187,16 @@ public class ResourceConfigDataLocationResolverTests {
@Test
void resolveWhenLocationIsRelativeAndDirectoryResolves() {
this.environment.setProperty("spring.config.name", "testproperties");
String location = "nested/3-third/";
this.resolver = new ResourceConfigDataLocationResolver(new DeferredLog(), this.environmentBinder,
ConfigDataLocation location = ConfigDataLocation.of("nested/3-third/");
this.resolver = new StandardConfigDataLocationResolver(new DeferredLog(), this.environmentBinder,
this.resourceLoader);
StandardConfigDataReference parentReference = new StandardConfigDataReference(
ConfigDataLocation.of("optional:classpath:configdata/"), null, "classpath:config/specific", null,
"properties", new PropertiesPropertySourceLoader());
ClassPathResource parentResource = new ClassPathResource("config/specific.properties");
ResourceConfigDataLocation parent = new ResourceConfigDataLocation("classpath:/config/specific.properties",
parentResource, null, new PropertiesPropertySourceLoader());
StandardConfigDataResource parent = new StandardConfigDataResource(parentReference, parentResource);
given(this.context.getParent()).willReturn(parent);
List<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location, true);
List<StandardConfigDataResource> locations = this.resolver.resolve(this.context, location);
assertThat(locations.size()).isEqualTo(1);
assertThat(locations).extracting(Object::toString)
.contains("class path resource [config/nested/3-third/testproperties.properties]");
@ -197,34 +204,36 @@ public class ResourceConfigDataLocationResolverTests {
@Test
void resolveWhenLocationIsRelativeAndNoMatchingLoaderThrowsException() {
String location = "application.other";
ClassPathResource parentResource = new ClassPathResource("configdata/application.properties");
ResourceConfigDataLocation parent = new ResourceConfigDataLocation(
"classpath:/configdata/application.properties", parentResource, null,
new PropertiesPropertySourceLoader());
ConfigDataLocation location = ConfigDataLocation.of("application.other");
StandardConfigDataReference parentReference = new StandardConfigDataReference(
ConfigDataLocation.of("classpath:configdata/properties/application.properties"), null,
"configdata/properties/application", null, "properties", new PropertiesPropertySourceLoader());
ClassPathResource parentResource = new ClassPathResource("configdata/properties/application.properties");
StandardConfigDataResource parent = new StandardConfigDataResource(parentReference, parentResource);
given(this.context.getParent()).willReturn(parent);
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location, true))
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
.withMessageStartingWith("Unable to load config data from 'application.other'")
.satisfies((ex) -> assertThat(ex.getCause()).hasMessageStartingWith("File extension is not known"));
}
@Test
void resolveWhenLocationUsesOptionalExtensionSyntaxResolves() throws Exception {
String location = "classpath:/application-props-no-extension[.properties]";
List<ResourceConfigDataLocation> locations = this.resolver.resolve(this.context, location, true);
ConfigDataLocation location = ConfigDataLocation.of("classpath:/application-props-no-extension[.properties]");
List<StandardConfigDataResource> locations = this.resolver.resolve(this.context, location);
assertThat(locations.size()).isEqualTo(1);
ResourceConfigDataLocation resolved = locations.get(0);
StandardConfigDataResource resolved = locations.get(0);
assertThat(resolved.getResource().getFilename()).endsWith("application-props-no-extension");
PropertySource<?> propertySource = resolved.load().get(0);
ConfigData loaded = new StandardConfigDataLoader().load(null, resolved);
PropertySource<?> propertySource = loaded.getPropertySources().get(0);
assertThat(propertySource.getProperty("withnotext")).isEqualTo("test");
}
@Test
void resolveProfileSpecificReturnsProfileSpecificFiles() {
String location = "classpath:/configdata/properties/";
ConfigDataLocation location = ConfigDataLocation.of("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, true,
List<StandardConfigDataResource> locations = this.resolver.resolveProfileSpecific(this.context, location,
profiles);
assertThat(locations.size()).isEqualTo(1);
assertThat(locations).extracting(Object::toString)
@ -233,11 +242,11 @@ public class ResourceConfigDataLocationResolverTests {
@Test
void resolveProfileSpecificWhenLocationIsFileReturnsEmptyList() {
String location = "classpath:/configdata/properties/application.properties";
ConfigDataLocation location = ConfigDataLocation.of("classpath:/configdata/properties/application.properties");
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, true,
List<StandardConfigDataResource> locations = this.resolver.resolveProfileSpecific(this.context, location,
profiles);
assertThat(locations).isEmpty();
}

@ -18,7 +18,6 @@ package org.springframework.boot.context.config;
import org.junit.jupiter.api.Test;
import org.springframework.boot.env.PropertySourceLoader;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
@ -27,47 +26,34 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ResourceConfigDataLocation}.
* Tests for {@link StandardConfigDataResource}.
*
* @author Madhura Bhave
* @author Phillip Webb
*/
public class ResourceConfigDataLocationTests {
public class StandardConfigDataResourceTests {
private final String location = "location";
StandardConfigDataReference reference = mock(StandardConfigDataReference.class);
private final Resource resource = mock(Resource.class);
private final PropertySourceLoader propertySourceLoader = mock(PropertySourceLoader.class);
@Test
void constructorWhenNameIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceConfigDataLocation(null, this.resource, null, this.propertySourceLoader))
.withMessage("Name must not be null");
void createWhenReferenceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new StandardConfigDataResource(null, this.resource))
.withMessage("Reference must not be null");
}
@Test
void constructorWhenResourceIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceConfigDataLocation(this.location, null, null, this.propertySourceLoader))
void createWhenResourceIsNullThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new StandardConfigDataResource(this.reference, null))
.withMessage("Resource must not be null");
}
@Test
void constructorWhenLoaderIsNullThrowsException() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new ResourceConfigDataLocation(this.location, this.resource, null, null))
.withMessage("PropertySourceLoader must not be null");
}
@Test
void equalsWhenResourceIsTheSameReturnsTrue() {
Resource resource = new ClassPathResource("config/");
ResourceConfigDataLocation location = new ResourceConfigDataLocation("my-location", resource, null,
this.propertySourceLoader);
ResourceConfigDataLocation other = new ResourceConfigDataLocation("other-location", resource, null,
this.propertySourceLoader);
StandardConfigDataResource location = new StandardConfigDataResource(this.reference, resource);
StandardConfigDataResource other = new StandardConfigDataResource(this.reference, resource);
assertThat(location).isEqualTo(other);
}
@ -75,10 +61,8 @@ public class ResourceConfigDataLocationTests {
void equalsWhenResourceIsDifferentReturnsFalse() {
Resource resource1 = new ClassPathResource("config/");
Resource resource2 = new ClassPathResource("configdata/");
ResourceConfigDataLocation location = new ResourceConfigDataLocation("my-location", resource1, null,
this.propertySourceLoader);
ResourceConfigDataLocation other = new ResourceConfigDataLocation("other-location", resource2, null,
this.propertySourceLoader);
StandardConfigDataResource location = new StandardConfigDataResource(this.reference, resource1);
StandardConfigDataResource other = new StandardConfigDataResource(this.reference, resource2);
assertThat(location).isNotEqualTo(other);
}

@ -37,27 +37,27 @@ import org.springframework.core.env.MapPropertySource;
*/
class TestConfigDataBootstrap {
static class LocationResolver implements ConfigDataLocationResolver<Location> {
static class LocationResolver implements ConfigDataLocationResolver<Resource> {
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
return location.startsWith("testbootstrap:");
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
return location.hasPrefix("testbootstrap:");
}
@Override
public List<Location> resolve(ConfigDataLocationResolverContext context, String location, boolean optional) {
public List<Resource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
context.getBootstrapContext().registerIfAbsent(ResolverHelper.class,
InstanceSupplier.from(() -> new ResolverHelper(location)));
ResolverHelper helper = context.getBootstrapContext().get(ResolverHelper.class);
return Collections.singletonList(new Location(helper));
return Collections.singletonList(new Resource(helper));
}
}
static class Loader implements ConfigDataLoader<Location> {
static class Loader implements ConfigDataLoader<Resource> {
@Override
public ConfigData load(ConfigDataLoaderContext context, Location location) throws IOException {
public ConfigData load(ConfigDataLoaderContext context, Resource location) throws IOException {
context.getBootstrapContext().registerIfAbsent(LoaderHelper.class,
(bootstrapContext) -> new LoaderHelper(location, () -> bootstrapContext.get(Binder.class)));
LoaderHelper helper = context.getBootstrapContext().get(LoaderHelper.class);
@ -68,11 +68,11 @@ class TestConfigDataBootstrap {
}
static class Location extends ConfigDataLocation {
static class Resource extends ConfigDataResource {
private final ResolverHelper resolverHelper;
Location(ResolverHelper resolverHelper) {
Resource(ResolverHelper resolverHelper) {
this.resolverHelper = resolverHelper;
}
@ -89,13 +89,13 @@ class TestConfigDataBootstrap {
static class ResolverHelper {
private final String location;
private final ConfigDataLocation location;
ResolverHelper(String location) {
ResolverHelper(ConfigDataLocation location) {
this.location = location;
}
String getLocation() {
ConfigDataLocation getLocation() {
return this.location;
}
@ -103,16 +103,16 @@ class TestConfigDataBootstrap {
static class LoaderHelper implements ApplicationListener<BootstrapContextClosedEvent> {
private final Location location;
private final Resource location;
private final Supplier<Binder> binder;
LoaderHelper(Location location, Supplier<Binder> binder) {
LoaderHelper(Resource location, Supplier<Binder> binder) {
this.location = location;
this.binder = binder;
}
Location getLocation() {
Resource getLocation() {
return this.location;
}

@ -30,14 +30,16 @@ class UnsupportedConfigDataLocationExceptionTests {
@Test
void createSetsMessage() {
UnsupportedConfigDataLocationException exception = new UnsupportedConfigDataLocationException("test");
UnsupportedConfigDataLocationException exception = new UnsupportedConfigDataLocationException(
ConfigDataLocation.of("test"));
assertThat(exception).hasMessage("Unsupported config data location 'test'");
}
@Test
void getLocationReturnsLocation() {
UnsupportedConfigDataLocationException exception = new UnsupportedConfigDataLocationException("test");
assertThat(exception.getLocation()).isEqualTo("test");
ConfigDataLocation location = ConfigDataLocation.of("test");
UnsupportedConfigDataLocationException exception = new UnsupportedConfigDataLocationException(location);
assertThat(exception.getLocation()).isEqualTo(location);
}
}

@ -36,7 +36,7 @@ import org.springframework.core.env.PropertySource;
*
* @author Phillip Webb
*/
class SubversionConfigDataLoader implements ConfigDataLoader<SubversionConfigDataLocation> {
class SubversionConfigDataLoader implements ConfigDataLoader<SubversionConfigDataResource> {
private static final ApplicationListener<BootstrapContextClosedEvent> closeListener = SubversionConfigDataLoader::onBootstrapContextClosed;
@ -50,12 +50,12 @@ class SubversionConfigDataLoader implements ConfigDataLoader<SubversionConfigDat
}
@Override
public ConfigData load(ConfigDataLoaderContext context, SubversionConfigDataLocation location)
public ConfigData load(ConfigDataLoaderContext context, SubversionConfigDataResource resource)
throws IOException, ConfigDataLocationNotFoundException {
context.getBootstrapContext().registerIfAbsent(SubversionServerCertificate.class,
InstanceSupplier.of(location.getServerCertificate()));
InstanceSupplier.of(resource.getServerCertificate()));
SubversionClient client = context.getBootstrapContext().get(SubversionClient.class);
String loaded = client.load(location.getLocation());
String loaded = client.load(resource.getLocation());
PropertySource<?> propertySource = new MapPropertySource("svn", Collections.singletonMap("svn", loaded));
return new ConfigData(Collections.singleton(propertySource));
}

@ -19,30 +19,33 @@ package smoketest.bootstrapregistry.external.svn;
import java.util.Collections;
import java.util.List;
import org.springframework.boot.context.config.ConfigDataLocation;
import org.springframework.boot.context.config.ConfigDataLocationNotFoundException;
import org.springframework.boot.context.config.ConfigDataLocationResolver;
import org.springframework.boot.context.config.ConfigDataLocationResolverContext;
import org.springframework.boot.context.config.ConfigDataResourceNotFoundException;
/**
* {@link ConfigDataLocationResolver} for subversion.
*
* @author Phillip Webb
*/
class SubversionConfigDataLocationResolver implements ConfigDataLocationResolver<SubversionConfigDataLocation> {
class SubversionConfigDataLocationResolver implements ConfigDataLocationResolver<SubversionConfigDataResource> {
private static final String PREFIX = "svn:";
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, String location) {
return location.startsWith(PREFIX);
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
return location.hasPrefix(PREFIX);
}
@Override
public List<SubversionConfigDataLocation> resolve(ConfigDataLocationResolverContext context, String location,
boolean optional) throws ConfigDataLocationNotFoundException {
public List<SubversionConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location)
throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException {
String serverCertificate = context.getBinder().bind("spring.svn.server.certificate", String.class).orElse(null);
return Collections.singletonList(
new SubversionConfigDataLocation(location.substring(PREFIX.length()), serverCertificate));
new SubversionConfigDataResource(location.getNonPrefixedValue(PREFIX), serverCertificate));
}
}

@ -16,20 +16,20 @@
package smoketest.bootstrapregistry.external.svn;
import org.springframework.boot.context.config.ConfigDataLocation;
import org.springframework.boot.context.config.ConfigDataResource;
/**
* A subversion {@link ConfigDataLocation}.
* A subversion {@link ConfigDataResource}.
*
* @author Phillip Webb
*/
class SubversionConfigDataLocation extends ConfigDataLocation {
class SubversionConfigDataResource extends ConfigDataResource {
private final String location;
private final SubversionServerCertificate serverCertificate;
SubversionConfigDataLocation(String location, String serverCertificate) {
SubversionConfigDataResource(String location, String serverCertificate) {
this.location = location;
this.serverCertificate = SubversionServerCertificate.of(serverCertificate);
}
@ -50,7 +50,7 @@ class SubversionConfigDataLocation extends ConfigDataLocation {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
SubversionConfigDataLocation other = (SubversionConfigDataLocation) obj;
SubversionConfigDataResource other = (SubversionConfigDataResource) obj;
return this.location.equals(other.location);
}
Loading…
Cancel
Save