Support wildcard configtree imports
Update `ConfigTreeConfigDataResource` so that a wildcard suffix can be used to import multiple folders. The pattern logic from `StandardConfigDataLocationResolver` has been extracted into a new `LocationResourceLoader` class so that it can be reused. Closes gh-22958
This commit is contained in:
parent
8b6b0505fb
commit
a0862f9146
|
|
@ -796,13 +796,46 @@ To import these properties, you can add the following to your `application.prope
|
||||||
----
|
----
|
||||||
spring:
|
spring:
|
||||||
config:
|
config:
|
||||||
import: "optional:configtree:/etc/config"
|
import: "optional:configtree:/etc/config/"
|
||||||
----
|
----
|
||||||
|
|
||||||
You can then access or inject `myapp.username` and `myapp.password` properties from the `Environment` in the usual way.
|
You can then access or inject `myapp.username` and `myapp.password` properties from the `Environment` in the usual way.
|
||||||
|
|
||||||
TIP: Configuration tree values can be bound to both string `String` and `byte[]` types depending on the contents expected.
|
TIP: Configuration tree values can be bound to both string `String` and `byte[]` types depending on the contents expected.
|
||||||
|
|
||||||
|
If you have multiple config trees to import from the same parent folder you can use a wildcard shortcut.
|
||||||
|
Any `configtree:` location that ends with `/*/` will import all immediate children as config trees.
|
||||||
|
|
||||||
|
For example, given the following volume:
|
||||||
|
|
||||||
|
[source,indent=0]
|
||||||
|
----
|
||||||
|
etc/
|
||||||
|
config/
|
||||||
|
dbconfig/
|
||||||
|
db/
|
||||||
|
username
|
||||||
|
password
|
||||||
|
mqconfig/
|
||||||
|
mq/
|
||||||
|
username
|
||||||
|
password
|
||||||
|
----
|
||||||
|
|
||||||
|
You can use `configtree:/etc/config/*/` as the import location:
|
||||||
|
|
||||||
|
[source,yaml,indent=0,configprops,configblocks]
|
||||||
|
----
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
import: "optional:configtree:/etc/config/*/"
|
||||||
|
----
|
||||||
|
|
||||||
|
This will add `db.username`, `db.password`, `mq.username` and `mq.password` properties.
|
||||||
|
|
||||||
|
NOTE: Directories loaded using a wildcard are sorted alphabetically.
|
||||||
|
If you need a different order, then you should list each location as a separate import
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[[boot-features-external-config-placeholders-in-properties]]
|
[[boot-features-external-config-placeholders-in-properties]]
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,16 @@
|
||||||
|
|
||||||
package org.springframework.boot.context.config;
|
package org.springframework.boot.context.config;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.config.LocationResourceLoader.ResourceType;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.core.io.ResourceLoader;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link ConfigDataLocationResolver} for config tree locations.
|
* {@link ConfigDataLocationResolver} for config tree locations.
|
||||||
*
|
*
|
||||||
|
|
@ -30,6 +37,12 @@ public class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationR
|
||||||
|
|
||||||
private static final String PREFIX = "configtree:";
|
private static final String PREFIX = "configtree:";
|
||||||
|
|
||||||
|
private final LocationResourceLoader resourceLoader;
|
||||||
|
|
||||||
|
public ConfigTreeConfigDataLocationResolver(ResourceLoader resourceLoader) {
|
||||||
|
this.resourceLoader = new LocationResourceLoader(resourceLoader);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
|
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
|
||||||
return location.hasPrefix(PREFIX);
|
return location.hasPrefix(PREFIX);
|
||||||
|
|
@ -38,8 +51,27 @@ public class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationR
|
||||||
@Override
|
@Override
|
||||||
public List<ConfigTreeConfigDataResource> resolve(ConfigDataLocationResolverContext context,
|
public List<ConfigTreeConfigDataResource> resolve(ConfigDataLocationResolverContext context,
|
||||||
ConfigDataLocation location) {
|
ConfigDataLocation location) {
|
||||||
ConfigTreeConfigDataResource resolved = new ConfigTreeConfigDataResource(location.getNonPrefixedValue(PREFIX));
|
try {
|
||||||
return Collections.singletonList(resolved);
|
return resolve(context, location.getNonPrefixedValue(PREFIX));
|
||||||
|
}
|
||||||
|
catch (IOException ex) {
|
||||||
|
throw new ConfigDataLocationNotFoundException(location, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ConfigTreeConfigDataResource> resolve(ConfigDataLocationResolverContext context, String location)
|
||||||
|
throws IOException {
|
||||||
|
Assert.isTrue(location.endsWith("/"),
|
||||||
|
() -> String.format("Config tree location '%s' must end with '/'", location));
|
||||||
|
if (!this.resourceLoader.isPattern(location)) {
|
||||||
|
return Collections.singletonList(new ConfigTreeConfigDataResource(location));
|
||||||
|
}
|
||||||
|
Resource[] resources = this.resourceLoader.getResources(location, ResourceType.DIRECTORY);
|
||||||
|
List<ConfigTreeConfigDataResource> resolved = new ArrayList<>(resources.length);
|
||||||
|
for (Resource resource : resources) {
|
||||||
|
resolved.add(new ConfigTreeConfigDataResource(resource.getFile().toPath()));
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,11 @@ public class ConfigTreeConfigDataResource extends ConfigDataResource {
|
||||||
this.path = Paths.get(path).toAbsolutePath();
|
this.path = Paths.get(path).toAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfigTreeConfigDataResource(Path path) {
|
||||||
|
Assert.notNull(path, "Path must not be null");
|
||||||
|
this.path = path.toAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
Path getPath() {
|
Path getPath() {
|
||||||
return this.path;
|
return this.path;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.FilenameFilter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
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.util.Assert;
|
||||||
|
import org.springframework.util.ResourceUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strategy interface for loading resources from a location. Supports single resource and
|
||||||
|
* simple wildcard directory patterns.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
* @author Madhura Bhave
|
||||||
|
*/
|
||||||
|
class LocationResourceLoader {
|
||||||
|
|
||||||
|
private static final Resource[] EMPTY_RESOURCES = {};
|
||||||
|
|
||||||
|
private static final Comparator<File> FILE_PATH_COMPARATOR = Comparator.comparing(File::getAbsolutePath);
|
||||||
|
|
||||||
|
private static final Comparator<File> FILE_NAME_COMPARATOR = Comparator.comparing(File::getName);
|
||||||
|
|
||||||
|
private final ResourceLoader resourceLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new {@link LocationResourceLoader} instance.
|
||||||
|
* @param resourceLoader the underlying resource loader
|
||||||
|
*/
|
||||||
|
LocationResourceLoader(ResourceLoader resourceLoader) {
|
||||||
|
this.resourceLoader = resourceLoader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if the location contains a pattern.
|
||||||
|
* @param location the location to check
|
||||||
|
* @return if the location is a pattern
|
||||||
|
*/
|
||||||
|
boolean isPattern(String location) {
|
||||||
|
return StringUtils.hasLength(location) && location.contains("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single resource from a non-pattern location.
|
||||||
|
* @param location the location
|
||||||
|
* @return the resource
|
||||||
|
* @see #isPattern(String)
|
||||||
|
*/
|
||||||
|
Resource getResource(String location) {
|
||||||
|
validateNonPattern(location);
|
||||||
|
location = StringUtils.cleanPath(location);
|
||||||
|
if (!ResourceUtils.isUrl(location)) {
|
||||||
|
location = ResourceUtils.FILE_URL_PREFIX + location;
|
||||||
|
}
|
||||||
|
return this.resourceLoader.getResource(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateNonPattern(String location) {
|
||||||
|
Assert.state(!isPattern(location), () -> String.format("Location '%s' must not be a pattern", location));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a multiple resources from a location pattern.
|
||||||
|
* @param location the location pattern
|
||||||
|
* @param type the type of resource to return
|
||||||
|
* @return the resources
|
||||||
|
* @see #isPattern(String)
|
||||||
|
*/
|
||||||
|
Resource[] getResources(String location, ResourceType type) {
|
||||||
|
validatePattern(location, type);
|
||||||
|
String directoryPath = location.substring(0, location.indexOf("*/"));
|
||||||
|
String fileName = location.substring(location.lastIndexOf("/") + 1);
|
||||||
|
Resource directoryResource = getResource(directoryPath);
|
||||||
|
if (!directoryResource.exists()) {
|
||||||
|
return new Resource[] { directoryResource };
|
||||||
|
}
|
||||||
|
File directory = getDirectory(location, directoryResource);
|
||||||
|
File[] subDirectories = directory.listFiles(this::isVisibleDirectory);
|
||||||
|
if (subDirectories == null) {
|
||||||
|
return EMPTY_RESOURCES;
|
||||||
|
}
|
||||||
|
Arrays.sort(subDirectories, FILE_PATH_COMPARATOR);
|
||||||
|
if (type == ResourceType.DIRECTORY) {
|
||||||
|
return Arrays.stream(subDirectories).map(FileSystemResource::new).toArray(Resource[]::new);
|
||||||
|
}
|
||||||
|
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.sort(files, FILE_NAME_COMPARATOR);
|
||||||
|
Arrays.stream(files).map(FileSystemResource::new).forEach(resources::add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resources.toArray(EMPTY_RESOURCES);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePattern(String location, ResourceType type) {
|
||||||
|
Assert.state(isPattern(location), () -> String.format("Location '%s' must be a pattern", location));
|
||||||
|
Assert.state(!location.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX),
|
||||||
|
() -> String.format("Location '%s' cannot use classpath wildcards", location));
|
||||||
|
Assert.state(StringUtils.countOccurrencesOf(location, "*") == 1,
|
||||||
|
() -> String.format("Location '%s' cannot contain multiple wildcards", location));
|
||||||
|
String directoryPath = (type != ResourceType.DIRECTORY) ? location.substring(0, location.lastIndexOf("/") + 1)
|
||||||
|
: location;
|
||||||
|
Assert.state(directoryPath.endsWith("*/"), () -> String.format("Location '%s' must end with '*/'", 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isVisibleDirectory(File file) {
|
||||||
|
return file.isDirectory() && !file.getName().startsWith(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resource types that can be returned.
|
||||||
|
*/
|
||||||
|
enum ResourceType {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return file resources.
|
||||||
|
*/
|
||||||
|
FILE,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return directory resources.
|
||||||
|
*/
|
||||||
|
DIRECTORY
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,12 +16,8 @@
|
||||||
|
|
||||||
package org.springframework.boot.context.config;
|
package org.springframework.boot.context.config;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FilenameFilter;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
@ -30,18 +26,16 @@ import java.util.regex.Pattern;
|
||||||
|
|
||||||
import org.apache.commons.logging.Log;
|
import org.apache.commons.logging.Log;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.config.LocationResourceLoader.ResourceType;
|
||||||
import org.springframework.boot.context.properties.bind.Binder;
|
import org.springframework.boot.context.properties.bind.Binder;
|
||||||
import org.springframework.boot.env.PropertySourceLoader;
|
import org.springframework.boot.env.PropertySourceLoader;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.core.io.FileSystemResource;
|
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
import org.springframework.core.io.support.ResourcePatternResolver;
|
|
||||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||||
import org.springframework.core.log.LogMessage;
|
import org.springframework.core.log.LogMessage;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ResourceUtils;
|
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -61,10 +55,6 @@ public class StandardConfigDataLocationResolver
|
||||||
|
|
||||||
private static final String[] DEFAULT_CONFIG_NAMES = { "application" };
|
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 URL_PREFIX = Pattern.compile("^([a-zA-Z][a-zA-Z0-9*]*?:)(.*$)");
|
||||||
|
|
||||||
private static final Pattern EXTENSION_HINT_PATTERN = Pattern.compile("^(.*)\\[(\\.\\w+)\\](?!\\[)$");
|
private static final Pattern EXTENSION_HINT_PATTERN = Pattern.compile("^(.*)\\[(\\.\\w+)\\](?!\\[)$");
|
||||||
|
|
@ -77,7 +67,7 @@ public class StandardConfigDataLocationResolver
|
||||||
|
|
||||||
private final String[] configNames;
|
private final String[] configNames;
|
||||||
|
|
||||||
private final ResourceLoader resourceLoader;
|
private final LocationResourceLoader resourceLoader;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new {@link StandardConfigDataLocationResolver} instance.
|
* Create a new {@link StandardConfigDataLocationResolver} instance.
|
||||||
|
|
@ -90,7 +80,7 @@ public class StandardConfigDataLocationResolver
|
||||||
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
|
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
|
||||||
getClass().getClassLoader());
|
getClass().getClassLoader());
|
||||||
this.configNames = getConfigNames(binder);
|
this.configNames = getConfigNames(binder);
|
||||||
this.resourceLoader = resourceLoader;
|
this.resourceLoader = new LocationResourceLoader(resourceLoader);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] getConfigNames(Binder binder) {
|
private String[] getConfigNames(Binder binder) {
|
||||||
|
|
@ -243,20 +233,20 @@ public class StandardConfigDataLocationResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
private void assertDirectoryExists(StandardConfigDataReference reference) {
|
private void assertDirectoryExists(StandardConfigDataReference reference) {
|
||||||
Resource resource = loadResource(reference.getDirectory());
|
Resource resource = this.resourceLoader.getResource(reference.getDirectory());
|
||||||
StandardConfigDataResource configDataResource = new StandardConfigDataResource(reference, resource);
|
StandardConfigDataResource configDataResource = new StandardConfigDataResource(reference, resource);
|
||||||
ConfigDataResourceNotFoundException.throwIfDoesNotExist(configDataResource, resource);
|
ConfigDataResourceNotFoundException.throwIfDoesNotExist(configDataResource, resource);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<StandardConfigDataResource> resolve(StandardConfigDataReference reference) {
|
private List<StandardConfigDataResource> resolve(StandardConfigDataReference reference) {
|
||||||
if (!reference.isPatternLocation()) {
|
if (!this.resourceLoader.isPattern(reference.getResourceLocation())) {
|
||||||
return resolveNonPattern(reference);
|
return resolveNonPattern(reference);
|
||||||
}
|
}
|
||||||
return resolvePattern(reference);
|
return resolvePattern(reference);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<StandardConfigDataResource> resolveNonPattern(StandardConfigDataReference reference) {
|
private List<StandardConfigDataResource> resolveNonPattern(StandardConfigDataReference reference) {
|
||||||
Resource resource = loadResource(reference.getResourceLocation());
|
Resource resource = this.resourceLoader.getResource(reference.getResourceLocation());
|
||||||
if (!resource.exists() && reference.isSkippable()) {
|
if (!resource.exists() && reference.isSkippable()) {
|
||||||
logSkippingResource(reference);
|
logSkippingResource(reference);
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
|
|
@ -265,9 +255,8 @@ public class StandardConfigDataLocationResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<StandardConfigDataResource> resolvePattern(StandardConfigDataReference reference) {
|
private List<StandardConfigDataResource> resolvePattern(StandardConfigDataReference reference) {
|
||||||
validatePatternLocation(reference.getResourceLocation());
|
|
||||||
List<StandardConfigDataResource> resolved = new ArrayList<>();
|
List<StandardConfigDataResource> resolved = new ArrayList<>();
|
||||||
for (Resource resource : getResourcesFromResourceLocationPattern(reference.getResourceLocation())) {
|
for (Resource resource : this.resourceLoader.getResources(reference.getResourceLocation(), ResourceType.FILE)) {
|
||||||
if (!resource.exists() && reference.isSkippable()) {
|
if (!resource.exists() && reference.isSkippable()) {
|
||||||
logSkippingResource(reference);
|
logSkippingResource(reference);
|
||||||
}
|
}
|
||||||
|
|
@ -287,62 +276,4 @@ public class StandardConfigDataLocationResolver
|
||||||
return new StandardConfigDataResource(reference, 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(this::isVisibleDirectory);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isVisibleDirectory(File file) {
|
|
||||||
return file.isDirectory() && !file.getName().startsWith(".");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,10 +78,6 @@ class StandardConfigDataReference {
|
||||||
return this.configDataLocation.isOptional() || this.directory != null || this.profile != null;
|
return this.configDataLocation.isOptional() || this.directory != null || this.profile != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isPatternLocation() {
|
|
||||||
return this.resourceLocation.contains("*");
|
|
||||||
}
|
|
||||||
|
|
||||||
PropertySourceLoader getPropertySourceLoader() {
|
PropertySourceLoader getPropertySourceLoader() {
|
||||||
return this.propertySourceLoader;
|
return this.propertySourceLoader;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ import java.io.File;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.core.io.DefaultResourceLoader;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.Mockito.mock;
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
@ -32,10 +36,14 @@ import static org.mockito.Mockito.mock;
|
||||||
*/
|
*/
|
||||||
class ConfigTreeConfigDataLocationResolverTests {
|
class ConfigTreeConfigDataLocationResolverTests {
|
||||||
|
|
||||||
private ConfigTreeConfigDataLocationResolver resolver = new ConfigTreeConfigDataLocationResolver();
|
private ConfigTreeConfigDataLocationResolver resolver = new ConfigTreeConfigDataLocationResolver(
|
||||||
|
new DefaultResourceLoader());
|
||||||
|
|
||||||
private ConfigDataLocationResolverContext context = mock(ConfigDataLocationResolverContext.class);
|
private ConfigDataLocationResolverContext context = mock(ConfigDataLocationResolverContext.class);
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File temp;
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void isResolvableWhenPrefixMatchesReturnsTrue() {
|
void isResolvableWhenPrefixMatchesReturnsTrue() {
|
||||||
assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("configtree:/etc/config"))).isTrue();
|
assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("configtree:/etc/config"))).isTrue();
|
||||||
|
|
@ -50,10 +58,26 @@ class ConfigTreeConfigDataLocationResolverTests {
|
||||||
@Test
|
@Test
|
||||||
void resolveReturnsConfigVolumeMountLocation() {
|
void resolveReturnsConfigVolumeMountLocation() {
|
||||||
List<ConfigTreeConfigDataResource> locations = this.resolver.resolve(this.context,
|
List<ConfigTreeConfigDataResource> locations = this.resolver.resolve(this.context,
|
||||||
ConfigDataLocation.of("configtree:/etc/config"));
|
ConfigDataLocation.of("configtree:/etc/config/"));
|
||||||
assertThat(locations.size()).isEqualTo(1);
|
assertThat(locations.size()).isEqualTo(1);
|
||||||
assertThat(locations).extracting(Object::toString)
|
assertThat(locations).extracting(Object::toString)
|
||||||
.containsExactly("config tree [" + new File("/etc/config").getAbsolutePath() + "]");
|
.containsExactly("config tree [" + new File("/etc/config").getAbsolutePath() + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void resolveWilcardPattern() throws Exception {
|
||||||
|
File directoryA = new File(this.temp, "a");
|
||||||
|
File directoryB = new File(this.temp, "b");
|
||||||
|
directoryA.mkdirs();
|
||||||
|
directoryB.mkdirs();
|
||||||
|
FileCopyUtils.copy("test".getBytes(), new File(directoryA, "spring"));
|
||||||
|
FileCopyUtils.copy("test".getBytes(), new File(directoryB, "boot"));
|
||||||
|
List<ConfigTreeConfigDataResource> locations = this.resolver.resolve(this.context,
|
||||||
|
ConfigDataLocation.of("configtree:" + this.temp.getAbsolutePath() + "/*/"));
|
||||||
|
assertThat(locations.size()).isEqualTo(2);
|
||||||
|
assertThat(locations).extracting(Object::toString).containsExactly(
|
||||||
|
"config tree [" + directoryA.getAbsolutePath() + "]",
|
||||||
|
"config tree [" + directoryB.getAbsolutePath() + "]");
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
package org.springframework.boot.context.config;
|
package org.springframework.boot.context.config;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
|
@ -31,9 +32,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||||
*/
|
*/
|
||||||
public class ConfigTreeConfigDataResourceTests {
|
public class ConfigTreeConfigDataResourceTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void constructorWhenPathStringIsNullThrowsException() {
|
||||||
|
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataResource((String) null))
|
||||||
|
.withMessage("Path must not be null");
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void constructorWhenPathIsNullThrowsException() {
|
void constructorWhenPathIsNullThrowsException() {
|
||||||
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataResource(null))
|
assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataResource((Path) null))
|
||||||
.withMessage("Path must not be null");
|
.withMessage("Path must not be null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
/*
|
||||||
|
* 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.IOException;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.io.TempDir;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.config.LocationResourceLoader.ResourceType;
|
||||||
|
import org.springframework.core.io.DefaultResourceLoader;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.util.FileCopyUtils;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link LocationResourceLoader}.
|
||||||
|
*
|
||||||
|
* @author Phillip Webb
|
||||||
|
*/
|
||||||
|
class LocationResourceLoaderTests {
|
||||||
|
|
||||||
|
private LocationResourceLoader loader = new LocationResourceLoader(new DefaultResourceLoader());
|
||||||
|
|
||||||
|
@TempDir
|
||||||
|
File temp;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isPatternWhenHasAsteriskReturnsTrue() {
|
||||||
|
assertThat(this.loader.isPattern("spring/*/boot")).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isPatternWhenNoAsteriskReturnsFalse() {
|
||||||
|
assertThat(this.loader.isPattern("spring/boot")).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourceWhenPatternThrowsException() {
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> this.loader.getResource("spring/boot/*"))
|
||||||
|
.withMessage("Location 'spring/boot/*' must not be a pattern");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourceReturnsResource() throws Exception {
|
||||||
|
File file = new File(this.temp, "file");
|
||||||
|
FileCopyUtils.copy("test".getBytes(), file);
|
||||||
|
Resource resource = this.loader.getResource(file.toURI().toString());
|
||||||
|
assertThat(resource.getInputStream()).hasContent("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourceWhenNotUrlReturnsResource() throws Exception {
|
||||||
|
File file = new File(this.temp, "file");
|
||||||
|
FileCopyUtils.copy("test".getBytes(), file);
|
||||||
|
Resource resource = this.loader.getResource(file.getAbsolutePath());
|
||||||
|
assertThat(resource.getInputStream()).hasContent("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourceWhenNonCleanPathReturnsResource() throws Exception {
|
||||||
|
File file = new File(this.temp, "file");
|
||||||
|
FileCopyUtils.copy("test".getBytes(), file);
|
||||||
|
Resource resource = this.loader.getResource(this.temp.getAbsolutePath() + "/spring/../file");
|
||||||
|
assertThat(resource.getInputStream()).hasContent("test");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourcesWhenNotPatternThrowsException() {
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> this.loader.getResources("spring/boot", ResourceType.FILE))
|
||||||
|
.withMessage("Location 'spring/boot' must be a pattern");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourcesWhenLocationStartsWithClasspathWildcardThrowsException() {
|
||||||
|
assertThatIllegalStateException()
|
||||||
|
.isThrownBy(() -> this.loader.getResources("classpath*:spring/boot/*/", ResourceType.FILE))
|
||||||
|
.withMessage("Location 'classpath*:spring/boot/*/' cannot use classpath wildcards");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourcesWhenLocationContainsMultipleWildcardsThrowsException() {
|
||||||
|
assertThatIllegalStateException()
|
||||||
|
.isThrownBy(() -> this.loader.getResources("spring/*/boot/*/", ResourceType.FILE))
|
||||||
|
.withMessage("Location 'spring/*/boot/*/' cannot contain multiple wildcards");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourcesWhenPatternDoesNotEndWithAsteriskSlashThrowsException() {
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> this.loader.getResources("spring/boot/*", ResourceType.FILE))
|
||||||
|
.withMessage("Location 'spring/boot/*' must end with '*/'");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getFileResourceReturnsResources() throws Exception {
|
||||||
|
createTree();
|
||||||
|
Resource[] resources = this.loader.getResources(this.temp.getAbsolutePath() + "/*/file", ResourceType.FILE);
|
||||||
|
assertThat(resources).hasSize(2);
|
||||||
|
assertThat(resources[0].getInputStream()).hasContent("a");
|
||||||
|
assertThat(resources[1].getInputStream()).hasContent("b");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getDirectoryResourceReturnsResources() throws Exception {
|
||||||
|
createTree();
|
||||||
|
Resource[] resources = this.loader.getResources(this.temp.getAbsolutePath() + "/*/", ResourceType.DIRECTORY);
|
||||||
|
assertThat(resources).hasSize(2);
|
||||||
|
assertThat(resources[0].getFilename()).isEqualTo("a");
|
||||||
|
assertThat(resources[1].getFilename()).isEqualTo("b");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getResourcesWhenHasHiddenDirectoriesFiltersResults() throws IOException {
|
||||||
|
createTree();
|
||||||
|
File hiddenDirectory = new File(this.temp, ".a");
|
||||||
|
hiddenDirectory.mkdirs();
|
||||||
|
FileCopyUtils.copy("h".getBytes(), new File(hiddenDirectory, "file"));
|
||||||
|
Resource[] resources = this.loader.getResources(this.temp.getAbsolutePath() + "/*/file", ResourceType.FILE);
|
||||||
|
assertThat(resources).hasSize(2);
|
||||||
|
assertThat(resources[0].getInputStream()).hasContent("a");
|
||||||
|
assertThat(resources[1].getInputStream()).hasContent("b");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createTree() throws IOException {
|
||||||
|
File directoryA = new File(this.temp, "a");
|
||||||
|
File directoryB = new File(this.temp, "b");
|
||||||
|
directoryA.mkdirs();
|
||||||
|
directoryB.mkdirs();
|
||||||
|
FileCopyUtils.copy("a".getBytes(), new File(directoryA, "file"));
|
||||||
|
FileCopyUtils.copy("b".getBytes(), new File(directoryB, "file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -100,14 +100,14 @@ public class StandardConfigDataLocationResolverTests {
|
||||||
void resolveWhenLocationWildcardIsSpecifiedForClasspathLocationThrowsException() {
|
void resolveWhenLocationWildcardIsSpecifiedForClasspathLocationThrowsException() {
|
||||||
ConfigDataLocation location = ConfigDataLocation.of("classpath*:application.properties");
|
ConfigDataLocation location = ConfigDataLocation.of("classpath*:application.properties");
|
||||||
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
|
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
|
||||||
.withMessageContaining("Classpath wildcard patterns cannot be used as a search location");
|
.withMessageContaining("Location 'classpath*:application.properties' cannot use classpath wildcards");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void resolveWhenLocationWildcardIsNotBeforeLastSlashThrowsException() {
|
void resolveWhenLocationWildcardIsNotBeforeLastSlashThrowsException() {
|
||||||
ConfigDataLocation location = ConfigDataLocation.of("file:src/test/resources/*/config/");
|
ConfigDataLocation location = ConfigDataLocation.of("file:src/test/resources/*/config/");
|
||||||
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
|
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
|
||||||
.withMessageStartingWith("Search location '").withMessageEndingWith("' must end with '*/'");
|
.withMessageStartingWith("Location '").withMessageEndingWith("' must end with '*/'");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -123,8 +123,7 @@ public class StandardConfigDataLocationResolverTests {
|
||||||
void resolveWhenLocationHasMultipleWildcardsThrowsException() {
|
void resolveWhenLocationHasMultipleWildcardsThrowsException() {
|
||||||
ConfigDataLocation location = ConfigDataLocation.of("file:src/test/resources/config/**/");
|
ConfigDataLocation location = ConfigDataLocation.of("file:src/test/resources/config/**/");
|
||||||
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
|
assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location))
|
||||||
.withMessageStartingWith("Search location '")
|
.withMessageStartingWith("Location '").withMessageEndingWith("' cannot contain multiple wildcards");
|
||||||
.withMessageEndingWith("' cannot contain multiple wildcards");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue