From a0862f91464f99782ddbe4106b9fa698397feb37 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 27 Oct 2020 10:27:14 -0700 Subject: [PATCH] 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 --- .../docs/asciidoc/spring-boot-features.adoc | 35 +++- .../ConfigTreeConfigDataLocationResolver.java | 36 +++- .../config/ConfigTreeConfigDataResource.java | 5 + .../config/LocationResourceLoader.java | 167 ++++++++++++++++++ .../StandardConfigDataLocationResolver.java | 83 +-------- .../config/StandardConfigDataReference.java | 4 - ...igTreeConfigDataLocationResolverTests.java | 28 ++- .../ConfigTreeConfigDataResourceTests.java | 9 +- .../config/LocationResourceLoaderTests.java | 150 ++++++++++++++++ ...andardConfigDataLocationResolverTests.java | 7 +- 10 files changed, 434 insertions(+), 90 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/LocationResourceLoader.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/LocationResourceLoaderTests.java diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc index 026d64c0cfd..76867dcf993 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc @@ -796,13 +796,46 @@ To import these properties, you can add the following to your `application.prope ---- spring: 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. 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]] diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolver.java index 9810491c9ca..3c2b6a5bf6c 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolver.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolver.java @@ -16,9 +16,16 @@ package org.springframework.boot.context.config; +import java.io.IOException; +import java.util.ArrayList; import java.util.Collections; 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. * @@ -30,6 +37,12 @@ public class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationR private static final String PREFIX = "configtree:"; + private final LocationResourceLoader resourceLoader; + + public ConfigTreeConfigDataLocationResolver(ResourceLoader resourceLoader) { + this.resourceLoader = new LocationResourceLoader(resourceLoader); + } + @Override public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) { return location.hasPrefix(PREFIX); @@ -38,8 +51,27 @@ public class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationR @Override public List resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location) { - ConfigTreeConfigDataResource resolved = new ConfigTreeConfigDataResource(location.getNonPrefixedValue(PREFIX)); - return Collections.singletonList(resolved); + try { + return resolve(context, location.getNonPrefixedValue(PREFIX)); + } + catch (IOException ex) { + throw new ConfigDataLocationNotFoundException(location, ex); + } + } + + private List 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 resolved = new ArrayList<>(resources.length); + for (Resource resource : resources) { + resolved.add(new ConfigTreeConfigDataResource(resource.getFile().toPath())); + } + return resolved; } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataResource.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataResource.java index 4cb6eeb9b53..b0977f9c085 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataResource.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigTreeConfigDataResource.java @@ -40,6 +40,11 @@ public class ConfigTreeConfigDataResource extends ConfigDataResource { this.path = Paths.get(path).toAbsolutePath(); } + ConfigTreeConfigDataResource(Path path) { + Assert.notNull(path, "Path must not be null"); + this.path = path.toAbsolutePath(); + } + Path getPath() { return this.path; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/LocationResourceLoader.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/LocationResourceLoader.java new file mode 100644 index 00000000000..fbc9e33d573 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/LocationResourceLoader.java @@ -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_PATH_COMPARATOR = Comparator.comparing(File::getAbsolutePath); + + private static final Comparator 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 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 + + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java index 6cf710f42c5..c4226a6f0f2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataLocationResolver.java @@ -16,12 +16,8 @@ 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; @@ -30,18 +26,16 @@ import java.util.regex.Pattern; 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.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; /** @@ -61,10 +55,6 @@ public class StandardConfigDataLocationResolver private static final String[] DEFAULT_CONFIG_NAMES = { "application" }; - private static final Resource[] EMPTY_RESOURCES = {}; - - private static final Comparator 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+)\\](?!\\[)$"); @@ -77,7 +67,7 @@ public class StandardConfigDataLocationResolver private final String[] configNames; - private final ResourceLoader resourceLoader; + private final LocationResourceLoader resourceLoader; /** * Create a new {@link StandardConfigDataLocationResolver} instance. @@ -90,7 +80,7 @@ public class StandardConfigDataLocationResolver this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, getClass().getClassLoader()); this.configNames = getConfigNames(binder); - this.resourceLoader = resourceLoader; + this.resourceLoader = new LocationResourceLoader(resourceLoader); } private String[] getConfigNames(Binder binder) { @@ -243,20 +233,20 @@ public class StandardConfigDataLocationResolver } private void assertDirectoryExists(StandardConfigDataReference reference) { - Resource resource = loadResource(reference.getDirectory()); + Resource resource = this.resourceLoader.getResource(reference.getDirectory()); StandardConfigDataResource configDataResource = new StandardConfigDataResource(reference, resource); ConfigDataResourceNotFoundException.throwIfDoesNotExist(configDataResource, resource); } private List resolve(StandardConfigDataReference reference) { - if (!reference.isPatternLocation()) { + if (!this.resourceLoader.isPattern(reference.getResourceLocation())) { return resolveNonPattern(reference); } return resolvePattern(reference); } private List resolveNonPattern(StandardConfigDataReference reference) { - Resource resource = loadResource(reference.getResourceLocation()); + Resource resource = this.resourceLoader.getResource(reference.getResourceLocation()); if (!resource.exists() && reference.isSkippable()) { logSkippingResource(reference); return Collections.emptyList(); @@ -265,9 +255,8 @@ public class StandardConfigDataLocationResolver } private List resolvePattern(StandardConfigDataReference reference) { - validatePatternLocation(reference.getResourceLocation()); List resolved = new ArrayList<>(); - for (Resource resource : getResourcesFromResourceLocationPattern(reference.getResourceLocation())) { + for (Resource resource : this.resourceLoader.getResources(reference.getResourceLocation(), ResourceType.FILE)) { if (!resource.exists() && reference.isSkippable()) { logSkippingResource(reference); } @@ -287,62 +276,4 @@ public class StandardConfigDataLocationResolver 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 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("."); - } - } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java index 55878557bf7..0150bab2b27 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/StandardConfigDataReference.java @@ -78,10 +78,6 @@ class StandardConfigDataReference { return this.configDataLocation.isOptional() || this.directory != null || this.profile != null; } - boolean isPatternLocation() { - return this.resourceLocation.contains("*"); - } - PropertySourceLoader getPropertySourceLoader() { return this.propertySourceLoader; } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolverTests.java index b3eaacf4574..33dc8171145 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolverTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataLocationResolverTests.java @@ -20,6 +20,10 @@ import java.io.File; import java.util.List; 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.mockito.Mockito.mock; @@ -32,10 +36,14 @@ import static org.mockito.Mockito.mock; */ class ConfigTreeConfigDataLocationResolverTests { - private ConfigTreeConfigDataLocationResolver resolver = new ConfigTreeConfigDataLocationResolver(); + private ConfigTreeConfigDataLocationResolver resolver = new ConfigTreeConfigDataLocationResolver( + new DefaultResourceLoader()); private ConfigDataLocationResolverContext context = mock(ConfigDataLocationResolverContext.class); + @TempDir + File temp; + @Test void isResolvableWhenPrefixMatchesReturnsTrue() { assertThat(this.resolver.isResolvable(this.context, ConfigDataLocation.of("configtree:/etc/config"))).isTrue(); @@ -50,10 +58,26 @@ class ConfigTreeConfigDataLocationResolverTests { @Test void resolveReturnsConfigVolumeMountLocation() { List locations = this.resolver.resolve(this.context, - ConfigDataLocation.of("configtree:/etc/config")); + ConfigDataLocation.of("configtree:/etc/config/")); assertThat(locations.size()).isEqualTo(1); assertThat(locations).extracting(Object::toString) .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 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() + "]"); + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataResourceTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataResourceTests.java index 39ac16b99b8..4bc1cb4a7cc 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataResourceTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigTreeConfigDataResourceTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.context.config; import java.io.File; +import java.nio.file.Path; import org.junit.jupiter.api.Test; @@ -31,9 +32,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException */ public class ConfigTreeConfigDataResourceTests { + @Test + void constructorWhenPathStringIsNullThrowsException() { + assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataResource((String) null)) + .withMessage("Path must not be null"); + } + @Test void constructorWhenPathIsNullThrowsException() { - assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataResource(null)) + assertThatIllegalArgumentException().isThrownBy(() -> new ConfigTreeConfigDataResource((Path) null)) .withMessage("Path must not be null"); } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/LocationResourceLoaderTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/LocationResourceLoaderTests.java new file mode 100644 index 00000000000..66eadf01ce8 --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/LocationResourceLoaderTests.java @@ -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")); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java index f60ee7a9c9e..d41f3552167 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/StandardConfigDataLocationResolverTests.java @@ -100,14 +100,14 @@ public class StandardConfigDataLocationResolverTests { void resolveWhenLocationWildcardIsSpecifiedForClasspathLocationThrowsException() { 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"); + .withMessageContaining("Location 'classpath*:application.properties' cannot use classpath wildcards"); } @Test void resolveWhenLocationWildcardIsNotBeforeLastSlashThrowsException() { ConfigDataLocation location = ConfigDataLocation.of("file:src/test/resources/*/config/"); assertThatIllegalStateException().isThrownBy(() -> this.resolver.resolve(this.context, location)) - .withMessageStartingWith("Search location '").withMessageEndingWith("' must end with '*/'"); + .withMessageStartingWith("Location '").withMessageEndingWith("' must end with '*/'"); } @Test @@ -123,8 +123,7 @@ public class StandardConfigDataLocationResolverTests { void resolveWhenLocationHasMultipleWildcardsThrowsException() { 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"); + .withMessageStartingWith("Location '").withMessageEndingWith("' cannot contain multiple wildcards"); } @Test