Commit a0862f91 authored by Phillip Webb's avatar Phillip Webb

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
parent 8b6b0505
...@@ -796,13 +796,46 @@ To import these properties, you can add the following to your `application.prope ...@@ -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 @@ ...@@ -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 ...@@ -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 ...@@ -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 { ...@@ -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;
} }
......
/*
* 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 @@ ...@@ -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; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 { ...@@ -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; ...@@ -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; ...@@ -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 { ...@@ -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 @@ ...@@ -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 ...@@ -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");
} }
......
/*
* 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 { ...@@ -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 { ...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment