Skip to content

Commit 8ec16bd

Browse files
committed
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
1 parent 080123e commit 8ec16bd

File tree

4 files changed

+84
-9
lines changed

4 files changed

+84
-9
lines changed

spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,8 @@ For example, if you have some Redis configuration and some MySQL configuration,
499499
This might result in two separate `application.properties` files mounted at different locations such as `/config/redis/application.properties` and `/config/mysql/application.properties`.
500500
In such a case, having a wildcard location of `config/*/`, will result in both files being processed.
501501

502-
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.
502+
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.
503+
Locations with wildcards are sorted alphabetically based on the absolute path of the file names.
503504

504505

505506
[[boot-features-external-config-application-json]]

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigFileApplicationListener.java

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
package org.springframework.boot.context.config;
1818

19+
import java.io.File;
1920
import java.io.IOException;
2021
import java.util.ArrayList;
2122
import java.util.Arrays;
2223
import java.util.Collections;
24+
import java.util.Comparator;
2325
import java.util.Deque;
2426
import java.util.HashMap;
2527
import java.util.HashSet;
@@ -28,9 +30,12 @@
2830
import java.util.LinkedList;
2931
import java.util.List;
3032
import java.util.Map;
33+
import java.util.Objects;
3134
import java.util.Set;
3235
import java.util.function.BiConsumer;
36+
import java.util.function.Function;
3337
import java.util.stream.Collectors;
38+
import java.util.stream.Stream;
3439

3540
import org.apache.commons.logging.Log;
3641

@@ -60,9 +65,9 @@
6065
import org.springframework.core.env.Profiles;
6166
import org.springframework.core.env.PropertySource;
6267
import org.springframework.core.io.DefaultResourceLoader;
68+
import org.springframework.core.io.FileSystemResource;
6369
import org.springframework.core.io.Resource;
6470
import org.springframework.core.io.ResourceLoader;
65-
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
6671
import org.springframework.core.io.support.ResourcePatternResolver;
6772
import org.springframework.core.io.support.SpringFactoriesLoader;
6873
import org.springframework.util.Assert;
@@ -163,6 +168,8 @@ public class ConfigFileApplicationListener implements EnvironmentPostProcessor,
163168

164169
private static final Resource[] EMPTY_RESOURCES = {};
165170

171+
private static final Comparator<File> FILE_COMPARATOR = Comparator.comparing(File::getAbsolutePath);
172+
166173
private String searchLocations;
167174

168175
private String names;
@@ -304,8 +311,6 @@ private class Loader {
304311

305312
private final ResourceLoader resourceLoader;
306313

307-
private final PathMatchingResourcePatternResolver patternResolver;
308-
309314
private final List<PropertySourceLoader> propertySourceLoaders;
310315

311316
private Deque<Profile> profiles;
@@ -325,7 +330,6 @@ private class Loader {
325330
: new DefaultResourceLoader(getClass().getClassLoader());
326331
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
327332
getClass().getClassLoader());
328-
this.patternResolver = new PathMatchingResourcePatternResolver(this.resourceLoader);
329333
}
330334

331335
void load() {
@@ -555,9 +559,22 @@ private void load(PropertySourceLoader loader, String location, Profile profile,
555559

556560
private Resource[] getResources(String location) {
557561
try {
558-
return this.patternResolver.getResources(location);
562+
if (location.contains("*")) {
563+
String directoryPath = location.substring(0, location.indexOf("*/"));
564+
String fileName = location.substring(location.lastIndexOf("/") + 1);
565+
Resource resource = this.resourceLoader.getResource(directoryPath);
566+
File[] files = resource.getFile().listFiles(File::isDirectory);
567+
if (files != null) {
568+
Arrays.sort(files, FILE_COMPARATOR);
569+
return Arrays.stream(files).map((file) -> file.listFiles((dir, name) -> name.equals(fileName)))
570+
.filter(Objects::nonNull).flatMap((Function<File[], Stream<File>>) Arrays::stream)
571+
.map(FileSystemResource::new).toArray(Resource[]::new);
572+
}
573+
return EMPTY_RESOURCES;
574+
}
575+
return new Resource[] { this.resourceLoader.getResource(location) };
559576
}
560-
catch (IOException ex) {
577+
catch (Exception ex) {
561578
return EMPTY_RESOURCES;
562579
}
563580
}
@@ -658,7 +675,8 @@ private Set<String> getSearchLocations(String propertyName) {
658675
if (!path.contains("$")) {
659676
path = StringUtils.cleanPath(path);
660677
Assert.state(!path.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX),
661-
"Classpath wildard patterns cannot be used as a search location");
678+
"Classpath wildcard patterns cannot be used as a search location");
679+
validateWildcardLocation(path);
662680
if (!ResourceUtils.isUrl(path)) {
663681
path = ResourceUtils.FILE_URL_PREFIX + path;
664682
}
@@ -669,6 +687,15 @@ private Set<String> getSearchLocations(String propertyName) {
669687
return locations;
670688
}
671689

690+
private void validateWildcardLocation(String path) {
691+
if (path.contains("*")) {
692+
Assert.state(StringUtils.countOccurrencesOf(path, "*") == 1,
693+
"Wildard pattern with multiple '*'s cannot be used as search location");
694+
String directoryPath = path.substring(0, path.lastIndexOf("/") + 1);
695+
Assert.state(directoryPath.endsWith("*/"), "Wildcard patterns must end with '*/'");
696+
}
697+
}
698+
672699
private Set<String> getSearchNames() {
673700
if (this.environment.containsProperty(CONFIG_NAME_PROPERTY)) {
674701
String property = this.environment.getProperty(CONFIG_NAME_PROPERTY);

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/config/ConfigFileApplicationListenerTests.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ void classpathWildcardResourceThrowsException() {
716716
"spring.config.location=classpath*:override.properties");
717717
assertThatIllegalStateException()
718718
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
719-
.withMessage("Classpath wildard patterns cannot be used as a search location");
719+
.withMessage("Classpath wildcard patterns cannot be used as a search location");
720720
}
721721

722722
@Test
@@ -1032,6 +1032,36 @@ void whenConfigLocationSpecifiesDirectoryConfigFileProcessingContinues() {
10321032
this.initializer.postProcessEnvironment(this.environment, this.application);
10331033
}
10341034

1035+
@Test
1036+
void directoryLocationsWithWildcardShouldHaveWildcardAsLastCharacterBeforeSlash() {
1037+
String location = "file:src/test/resources/*/config/";
1038+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
1039+
"spring.config.location=" + location);
1040+
assertThatIllegalStateException()
1041+
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
1042+
.withMessage("Wildcard patterns must end with '*/'");
1043+
}
1044+
1045+
@Test
1046+
void directoryLocationsWithMultipleWildcardsShouldThrowException() {
1047+
String location = "file:src/test/resources/config/**/";
1048+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
1049+
"spring.config.location=" + location);
1050+
assertThatIllegalStateException()
1051+
.isThrownBy(() -> this.initializer.postProcessEnvironment(this.environment, this.application))
1052+
.withMessage("Wildard pattern with multiple '*'s cannot be used as search location");
1053+
}
1054+
1055+
@Test
1056+
void locationsWithWildcardDirectoriesShouldRestrictToOneLevelDeep() {
1057+
String location = "file:src/test/resources/config/*/";
1058+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
1059+
"spring.config.location=" + location);
1060+
this.initializer.setSearchNames("testproperties");
1061+
this.initializer.postProcessEnvironment(this.environment, this.application);
1062+
assertThat(this.environment.getProperty("third.property")).isNull();
1063+
}
1064+
10351065
@Test
10361066
void locationsWithWildcardDirectoriesShouldLoadAllFilesThatMatch() {
10371067
String location = "file:src/test/resources/config/*/";
@@ -1045,6 +1075,22 @@ void locationsWithWildcardDirectoriesShouldLoadAllFilesThatMatch() {
10451075
assertThat(second).isEqualTo("ball");
10461076
}
10471077

1078+
@Test
1079+
void locationsWithWildcardDirectoriesShouldSortAlphabeticallyBasedOnAbsolutePath() {
1080+
String location = "file:src/test/resources/config/*/";
1081+
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment,
1082+
"spring.config.location=" + location);
1083+
this.initializer.setSearchNames("testproperties");
1084+
this.initializer.postProcessEnvironment(this.environment, this.application);
1085+
List<String> sources = this.environment.getPropertySources().stream()
1086+
.filter((source) -> source.getName().contains("applicationConfig")).map((source) -> {
1087+
String name = source.getName();
1088+
return name.substring(name.indexOf("src/test/resources"));
1089+
}).collect(Collectors.toList());
1090+
assertThat(sources).containsExactly("src/test/resources/config/1-first/testproperties.properties]]",
1091+
"src/test/resources/config/2-second/testproperties.properties]]");
1092+
}
1093+
10481094
@Test
10491095
void locationsWithWildcardFilesShouldLoadAllFilesThatMatch() {
10501096
String location = "file:src/test/resources/config/*/testproperties.properties";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
third.property=shouldnotbefound

0 commit comments

Comments
 (0)