Commit 8ec16bd0 authored by Madhura Bhave's avatar Madhura Bhave

Restrict wildcard pattern support for configuration files

This commit restricts how wildcards can be used in search
locations for property files. If a search location contains
a pattern, there must be only one '*' and the location should
end with a '*/'. For search locations that specify the file
name, the pattern should end with '*/<filename>'.

The list of files read from wildcard locations are now sorted
alphabetically according to the absolute path of the file.

Closes gh-21217
parent 080123eb
......@@ -499,7 +499,8 @@ For example, if you have some Redis configuration and some MySQL configuration,
This might result in two separate `application.properties` files mounted at different locations such as `/config/redis/application.properties` and `/config/mysql/application.properties`.
In such a case, having a wildcard location of `config/*/`, will result in both files being processed.
NOTE: Locations with wildcards are not processed in a deterministic order and files that match the wildcard cannot be used to override keys in the other.
NOTE: A wildcard location must contain only one `*` and end with `*/` for search locations that are directories or `*/<filename>` for search locations that are files.
Locations with wildcards are sorted alphabetically based on the absolute path of the file names.
[[boot-features-external-config-application-json]]
......
......@@ -16,10 +16,12 @@
package org.springframework.boot.context.config;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
......@@ -28,9 +30,12 @@ import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
......@@ -60,9 +65,9 @@ import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.Profiles;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.util.Assert;
......@@ -163,6 +168,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private static final Resource[] EMPTY_RESOURCES = {};
private static final Comparator<File> FILE_COMPARATOR = Comparator.comparing(File::getAbsolutePath);
private String searchLocations;
private String names;
......@@ -304,8 +311,6 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private final ResourceLoader resourceLoader;
private final PathMatchingResourcePatternResolver patternResolver;
private final List<PropertySourceLoader> propertySourceLoaders;
private Deque<Profile> profiles;
......@@ -325,7 +330,6 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
: new DefaultResourceLoader(getClass().getClassLoader());
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
this.patternResolver = new PathMatchingResourcePatternResolver(this.resourceLoader);
}
void load() {
......@@ -555,9 +559,22 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
private Resource[] getResources(String location) {
try {
return this.patternResolver.getResources(location);
if (location.contains("*")) {
String directoryPath = location.substring(0, location.indexOf("*/"));
String fileName = location.substring(location.lastIndexOf("/") + 1);
Resource resource = this.resourceLoader.getResource(directoryPath);
File[] files = resource.getFile().listFiles(File::isDirectory);
if (files != null) {
Arrays.sort(files, FILE_COMPARATOR);
return Arrays.stream(files).map((file) -> file.listFiles((dir, name) -> name.equals(fileName)))
.filter(Objects::nonNull).flatMap((Function<File[], Stream<File>>) Arrays::stream)
.map(FileSystemResource::new).toArray(Resource[]::new);
}
return EMPTY_RESOURCES;
}
return new Resource[] { this.resourceLoader.getResource(location) };
}
catch (IOException ex) {
catch (Exception ex) {
return EMPTY_RESOURCES;
}
}
......@@ -658,7 +675,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
if (!path.contains("$")) {
path = StringUtils.cleanPath(path);
Assert.state(!path.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX),
"Classpath wildard patterns cannot be used as a search location");
"Classpath wildcard patterns cannot be used as a search location");
validateWildcardLocation(path);
if (!ResourceUtils.isUrl(path)) {
path = ResourceUtils.FILE_URL_PREFIX + path;
}
......@@ -669,6 +687,15 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
return locations;
}
private void validateWildcardLocation(String path) {
if (path.contains("*")) {
Assert.state(StringUtils.countOccurrencesOf(path, "*") == 1,
"Wildard pattern with multiple '*'s cannot be used as search location");
String directoryPath = path.substring(0, path.lastIndexOf("/") + 1);
Assert.state(directoryPath.endsWith("*/"), "Wildcard patterns must end with '*/'");
}
}
private Set<String> getSearchNames() {
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);
......
......@@ -716,7 +716,7 @@ class ConfigFileApplicationListenerTests {
"spring.config.location=classpath*:override.properties");
assertThatIllegalStateException()
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
.withMessage("Classpath wildard patterns cannot be used as a search location");
.withMessage("Classpath wildcard patterns cannot be used as a search location");
}
@Test
......@@ -1032,6 +1032,36 @@ class ConfigFileApplicationListenerTests {
this.initializer.postProcessEnvironment(this.environment, this.application);
}
@Test
void directoryLocationsWithWildcardShouldHaveWildcardAsLastCharacterBeforeSlash() {
String location = "file:src/test/resources/*/config/";
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.config.location=" + location);
assertThatIllegalStateException()
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
.withMessage("Wildcard patterns must end with '*/'");
}
@Test
void directoryLocationsWithMultipleWildcardsShouldThrowException() {
String location = "file:src/test/resources/config/**/";
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.config.location=" + location);
assertThatIllegalStateException()
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
.withMessage("Wildard pattern with multiple '*'s cannot be used as search location");
}
@Test
void locationsWithWildcardDirectoriesShouldRestrictToOneLevelDeep() {
String location = "file:src/test/resources/config/*/";
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.config.location=" + location);
this.initializer.setSearchNames("testproperties");
this.initializer.postProcessEnvironment(this.environment, this.application);
assertThat(this.environment.getProperty("third.property")).isNull();
}
@Test
void locationsWithWildcardDirectoriesShouldLoadAllFilesThatMatch() {
String location = "file:src/test/resources/config/*/";
......@@ -1045,6 +1075,22 @@ class ConfigFileApplicationListenerTests {
assertThat(second).isEqualTo("ball");
}
@Test
void locationsWithWildcardDirectoriesShouldSortAlphabeticallyBasedOnAbsolutePath() {
String location = "file:src/test/resources/config/*/";
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
"spring.config.location=" + location);
this.initializer.setSearchNames("testproperties");
this.initializer.postProcessEnvironment(this.environment, this.application);
List<String> sources = this.environment.getPropertySources().stream()
.filter((source) -> source.getName().contains("applicationConfig")).map((source) -> {
String name = source.getName();
return name.substring(name.indexOf("src/test/resources"));
}).collect(Collectors.toList());
assertThat(sources).containsExactly("src/test/resources/config/1-first/testproperties.properties]]",
"src/test/resources/config/2-second/testproperties.properties]]");
}
@Test
void locationsWithWildcardFilesShouldLoadAllFilesThatMatch() {
String location = "file:src/test/resources/config/*/testproperties.properties";
......
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