Skip to content

Commit a0862f9

Browse files
committed
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
1 parent 8b6b050 commit a0862f9

File tree

10 files changed

+434
-90
lines changed

10 files changed

+434
-90
lines changed

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

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -796,13 +796,46 @@ To import these properties, you can add the following to your `application.prope
796796
----
797797
spring:
798798
config:
799-
import: "optional:configtree:/etc/config"
799+
import: "optional:configtree:/etc/config/"
800800
----
801801

802802
You can then access or inject `myapp.username` and `myapp.password` properties from the `Environment` in the usual way.
803803

804804
TIP: Configuration tree values can be bound to both string `String` and `byte[]` types depending on the contents expected.
805805

806+
If you have multiple config trees to import from the same parent folder you can use a wildcard shortcut.
807+
Any `configtree:` location that ends with `/*/` will import all immediate children as config trees.
808+
809+
For example, given the following volume:
810+
811+
[source,indent=0]
812+
----
813+
etc/
814+
config/
815+
dbconfig/
816+
db/
817+
username
818+
password
819+
mqconfig/
820+
mq/
821+
username
822+
password
823+
----
824+
825+
You can use `configtree:/etc/config/*/` as the import location:
826+
827+
[source,yaml,indent=0,configprops,configblocks]
828+
----
829+
spring:
830+
config:
831+
import: "optional:configtree:/etc/config/*/"
832+
----
833+
834+
This will add `db.username`, `db.password`, `mq.username` and `mq.password` properties.
835+
836+
NOTE: Directories loaded using a wildcard are sorted alphabetically.
837+
If you need a different order, then you should list each location as a separate import
838+
806839

807840

808841
[[boot-features-external-config-placeholders-in-properties]]

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

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@
1616

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

19+
import java.io.IOException;
20+
import java.util.ArrayList;
1921
import java.util.Collections;
2022
import java.util.List;
2123

24+
import org.springframework.boot.context.config.LocationResourceLoader.ResourceType;
25+
import org.springframework.core.io.Resource;
26+
import org.springframework.core.io.ResourceLoader;
27+
import org.springframework.util.Assert;
28+
2229
/**
2330
* {@link ConfigDataLocationResolver} for config tree locations.
2431
*
@@ -30,6 +37,12 @@ public class ConfigTreeConfigDataLocationResolver implements ConfigDataLocationR
3037

3138
private static final String PREFIX = "configtree:";
3239

40+
private final LocationResourceLoader resourceLoader;
41+
42+
public ConfigTreeConfigDataLocationResolver(ResourceLoader resourceLoader) {
43+
this.resourceLoader = new LocationResourceLoader(resourceLoader);
44+
}
45+
3346
@Override
3447
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
3548
return location.hasPrefix(PREFIX);
@@ -38,8 +51,27 @@ public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDat
3851
@Override
3952
public List<ConfigTreeConfigDataResource> resolve(ConfigDataLocationResolverContext context,
4053
ConfigDataLocation location) {
41-
ConfigTreeConfigDataResource resolved = new ConfigTreeConfigDataResource(location.getNonPrefixedValue(PREFIX));
42-
return Collections.singletonList(resolved);
54+
try {
55+
return resolve(context, location.getNonPrefixedValue(PREFIX));
56+
}
57+
catch (IOException ex) {
58+
throw new ConfigDataLocationNotFoundException(location, ex);
59+
}
60+
}
61+
62+
private List<ConfigTreeConfigDataResource> resolve(ConfigDataLocationResolverContext context, String location)
63+
throws IOException {
64+
Assert.isTrue(location.endsWith("/"),
65+
() -> String.format("Config tree location '%s' must end with '/'", location));
66+
if (!this.resourceLoader.isPattern(location)) {
67+
return Collections.singletonList(new ConfigTreeConfigDataResource(location));
68+
}
69+
Resource[] resources = this.resourceLoader.getResources(location, ResourceType.DIRECTORY);
70+
List<ConfigTreeConfigDataResource> resolved = new ArrayList<>(resources.length);
71+
for (Resource resource : resources) {
72+
resolved.add(new ConfigTreeConfigDataResource(resource.getFile().toPath()));
73+
}
74+
return resolved;
4375
}
4476

4577
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public class ConfigTreeConfigDataResource extends ConfigDataResource {
4040
this.path = Paths.get(path).toAbsolutePath();
4141
}
4242

43+
ConfigTreeConfigDataResource(Path path) {
44+
Assert.notNull(path, "Path must not be null");
45+
this.path = path.toAbsolutePath();
46+
}
47+
4348
Path getPath() {
4449
return this.path;
4550
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.context.config;
18+
19+
import java.io.File;
20+
import java.io.FilenameFilter;
21+
import java.util.ArrayList;
22+
import java.util.Arrays;
23+
import java.util.Comparator;
24+
import java.util.List;
25+
26+
import org.springframework.core.io.FileSystemResource;
27+
import org.springframework.core.io.Resource;
28+
import org.springframework.core.io.ResourceLoader;
29+
import org.springframework.core.io.support.ResourcePatternResolver;
30+
import org.springframework.util.Assert;
31+
import org.springframework.util.ResourceUtils;
32+
import org.springframework.util.StringUtils;
33+
34+
/**
35+
* Strategy interface for loading resources from a location. Supports single resource and
36+
* simple wildcard directory patterns.
37+
*
38+
* @author Phillip Webb
39+
* @author Madhura Bhave
40+
*/
41+
class LocationResourceLoader {
42+
43+
private static final Resource[] EMPTY_RESOURCES = {};
44+
45+
private static final Comparator<File> FILE_PATH_COMPARATOR = Comparator.comparing(File::getAbsolutePath);
46+
47+
private static final Comparator<File> FILE_NAME_COMPARATOR = Comparator.comparing(File::getName);
48+
49+
private final ResourceLoader resourceLoader;
50+
51+
/**
52+
* Create a new {@link LocationResourceLoader} instance.
53+
* @param resourceLoader the underlying resource loader
54+
*/
55+
LocationResourceLoader(ResourceLoader resourceLoader) {
56+
this.resourceLoader = resourceLoader;
57+
}
58+
59+
/**
60+
* Returns if the location contains a pattern.
61+
* @param location the location to check
62+
* @return if the location is a pattern
63+
*/
64+
boolean isPattern(String location) {
65+
return StringUtils.hasLength(location) && location.contains("*");
66+
}
67+
68+
/**
69+
* Get a single resource from a non-pattern location.
70+
* @param location the location
71+
* @return the resource
72+
* @see #isPattern(String)
73+
*/
74+
Resource getResource(String location) {
75+
validateNonPattern(location);
76+
location = StringUtils.cleanPath(location);
77+
if (!ResourceUtils.isUrl(location)) {
78+
location = ResourceUtils.FILE_URL_PREFIX + location;
79+
}
80+
return this.resourceLoader.getResource(location);
81+
}
82+
83+
private void validateNonPattern(String location) {
84+
Assert.state(!isPattern(location), () -> String.format("Location '%s' must not be a pattern", location));
85+
}
86+
87+
/**
88+
* Get a multiple resources from a location pattern.
89+
* @param location the location pattern
90+
* @param type the type of resource to return
91+
* @return the resources
92+
* @see #isPattern(String)
93+
*/
94+
Resource[] getResources(String location, ResourceType type) {
95+
validatePattern(location, type);
96+
String directoryPath = location.substring(0, location.indexOf("*/"));
97+
String fileName = location.substring(location.lastIndexOf("/") + 1);
98+
Resource directoryResource = getResource(directoryPath);
99+
if (!directoryResource.exists()) {
100+
return new Resource[] { directoryResource };
101+
}
102+
File directory = getDirectory(location, directoryResource);
103+
File[] subDirectories = directory.listFiles(this::isVisibleDirectory);
104+
if (subDirectories == null) {
105+
return EMPTY_RESOURCES;
106+
}
107+
Arrays.sort(subDirectories, FILE_PATH_COMPARATOR);
108+
if (type == ResourceType.DIRECTORY) {
109+
return Arrays.stream(subDirectories).map(FileSystemResource::new).toArray(Resource[]::new);
110+
}
111+
List<Resource> resources = new ArrayList<>();
112+
FilenameFilter filter = (dir, name) -> name.equals(fileName);
113+
for (File subDirectory : subDirectories) {
114+
File[] files = subDirectory.listFiles(filter);
115+
if (files != null) {
116+
Arrays.sort(files, FILE_NAME_COMPARATOR);
117+
Arrays.stream(files).map(FileSystemResource::new).forEach(resources::add);
118+
}
119+
}
120+
return resources.toArray(EMPTY_RESOURCES);
121+
}
122+
123+
private void validatePattern(String location, ResourceType type) {
124+
Assert.state(isPattern(location), () -> String.format("Location '%s' must be a pattern", location));
125+
Assert.state(!location.startsWith(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX),
126+
() -> String.format("Location '%s' cannot use classpath wildcards", location));
127+
Assert.state(StringUtils.countOccurrencesOf(location, "*") == 1,
128+
() -> String.format("Location '%s' cannot contain multiple wildcards", location));
129+
String directoryPath = (type != ResourceType.DIRECTORY) ? location.substring(0, location.lastIndexOf("/") + 1)
130+
: location;
131+
Assert.state(directoryPath.endsWith("*/"), () -> String.format("Location '%s' must end with '*/'", location));
132+
}
133+
134+
private File getDirectory(String patternLocation, Resource resource) {
135+
try {
136+
File directory = resource.getFile();
137+
Assert.state(directory.isDirectory(), () -> "'" + directory + "' is not a directory");
138+
return directory;
139+
}
140+
catch (Exception ex) {
141+
throw new IllegalStateException(
142+
"Unable to load config data resource from pattern '" + patternLocation + "'", ex);
143+
}
144+
}
145+
146+
private boolean isVisibleDirectory(File file) {
147+
return file.isDirectory() && !file.getName().startsWith(".");
148+
}
149+
150+
/**
151+
* Resource types that can be returned.
152+
*/
153+
enum ResourceType {
154+
155+
/**
156+
* Return file resources.
157+
*/
158+
FILE,
159+
160+
/**
161+
* Return directory resources.
162+
*/
163+
DIRECTORY
164+
165+
}
166+
167+
}

0 commit comments

Comments
 (0)