/
StandardConfigDataLocationResolver.java
332 lines (289 loc) · 13.1 KB
/
StandardConfigDataLocationResolver.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/*
* Copyright 2012-2021 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.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
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.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* {@link ConfigDataLocationResolver} for standard locations.
*
* @author Madhura Bhave
* @author Phillip Webb
* @author Scott Frederick
* @since 2.4.0
*/
public class StandardConfigDataLocationResolver
implements ConfigDataLocationResolver<StandardConfigDataResource>, Ordered {
private static final String PREFIX = "resource:";
static final String CONFIG_NAME_PROPERTY = "spring.config.name";
private static final String[] DEFAULT_CONFIG_NAMES = { "application" };
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 String NO_PROFILE = null;
private final Log logger;
private final List<PropertySourceLoader> propertySourceLoaders;
private final String[] configNames;
private final LocationResourceLoader resourceLoader;
/**
* Create a new {@link StandardConfigDataLocationResolver} instance.
* @param logger the logger to use
* @param binder a binder backed by the initial {@link Environment}
* @param resourceLoader a {@link ResourceLoader} used to load resources
*/
public StandardConfigDataLocationResolver(Log logger, Binder binder, ResourceLoader resourceLoader) {
this.logger = logger;
this.propertySourceLoaders = SpringFactoriesLoader.loadFactories(PropertySourceLoader.class,
getClass().getClassLoader());
this.configNames = getConfigNames(binder);
this.resourceLoader = new LocationResourceLoader(resourceLoader);
}
private String[] getConfigNames(Binder binder) {
String[] configNames = binder.bind(CONFIG_NAME_PROPERTY, String[].class).orElse(DEFAULT_CONFIG_NAMES);
for (String configName : configNames) {
validateConfigName(configName);
}
return configNames;
}
private void validateConfigName(String name) {
Assert.state(!name.contains("*"), () -> "Config name '" + name + "' cannot contain '*'");
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
@Override
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
return true;
}
@Override
public List<StandardConfigDataResource> resolve(ConfigDataLocationResolverContext context,
ConfigDataLocation location) throws ConfigDataNotFoundException {
return resolve(getReferences(context, location.split()));
}
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
ConfigDataLocation[] configDataLocations) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
for (ConfigDataLocation configDataLocation : configDataLocations) {
references.addAll(getReferences(context, configDataLocation));
}
return references;
}
private Set<StandardConfigDataReference> getReferences(ConfigDataLocationResolverContext context,
ConfigDataLocation configDataLocation) {
String resourceLocation = getResourceLocation(context, configDataLocation);
try {
if (isDirectory(resourceLocation)) {
return getReferencesForDirectory(configDataLocation, resourceLocation, NO_PROFILE);
}
return getReferencesForFile(configDataLocation, resourceLocation, NO_PROFILE);
}
catch (RuntimeException ex) {
throw new IllegalStateException("Unable to load config data from '" + configDataLocation + "'", ex);
}
}
@Override
public List<StandardConfigDataResource> resolveProfileSpecific(ConfigDataLocationResolverContext context,
ConfigDataLocation location, Profiles profiles) {
if (context.getParent() != null) {
return null;
}
return resolve(getProfileSpecificReferences(context, location.split(), profiles));
}
private Set<StandardConfigDataReference> getProfileSpecificReferences(ConfigDataLocationResolverContext context,
ConfigDataLocation[] configDataLocations, Profiles profiles) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
for (String profile : profiles) {
for (ConfigDataLocation configDataLocation : configDataLocations) {
String resourceLocation = getResourceLocation(context, configDataLocation);
references.addAll(getReferences(configDataLocation, resourceLocation, profile));
}
}
return references;
}
private String getResourceLocation(ConfigDataLocationResolverContext context,
ConfigDataLocation configDataLocation) {
String resourceLocation = configDataLocation.getNonPrefixedValue(PREFIX);
boolean isAbsolute = resourceLocation.startsWith("/") || URL_PREFIX.matcher(resourceLocation).matches();
if (isAbsolute) {
return resourceLocation;
}
ConfigDataResource parent = context.getParent();
if (parent instanceof StandardConfigDataResource) {
String parentResourceLocation = ((StandardConfigDataResource) parent).getReference().getResourceLocation();
String parentDirectory = parentResourceLocation.substring(0, parentResourceLocation.lastIndexOf("/") + 1);
return parentDirectory + resourceLocation;
}
return resourceLocation;
}
private Set<StandardConfigDataReference> getReferences(ConfigDataLocation configDataLocation,
String resourceLocation, String profile) {
if (isDirectory(resourceLocation)) {
return getReferencesForDirectory(configDataLocation, resourceLocation, profile);
}
return getReferencesForFile(configDataLocation, resourceLocation, profile);
}
private Set<StandardConfigDataReference> getReferencesForDirectory(ConfigDataLocation configDataLocation,
String directory, String profile) {
Set<StandardConfigDataReference> references = new LinkedHashSet<>();
for (String name : this.configNames) {
Deque<StandardConfigDataReference> referencesForName = getReferencesForConfigName(name, configDataLocation,
directory, profile);
references.addAll(referencesForName);
}
return references;
}
private Deque<StandardConfigDataReference> getReferencesForConfigName(String name,
ConfigDataLocation configDataLocation, String directory, String profile) {
Deque<StandardConfigDataReference> references = new ArrayDeque<>();
for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {
for (String extension : propertySourceLoader.getFileExtensions()) {
StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, directory,
directory + name, profile, extension, propertySourceLoader);
if (!references.contains(reference)) {
references.addFirst(reference);
}
}
}
return references;
}
private Set<StandardConfigDataReference> getReferencesForFile(ConfigDataLocation configDataLocation, String file,
String profile) {
Matcher extensionHintMatcher = EXTENSION_HINT_PATTERN.matcher(file);
boolean extensionHintLocation = extensionHintMatcher.matches();
if (extensionHintLocation) {
file = extensionHintMatcher.group(1) + extensionHintMatcher.group(2);
}
for (PropertySourceLoader propertySourceLoader : this.propertySourceLoaders) {
String extension = getLoadableFileExtension(propertySourceLoader, file);
if (extension != null) {
String root = file.substring(0, file.length() - extension.length() - 1);
StandardConfigDataReference reference = new StandardConfigDataReference(configDataLocation, null, root,
profile, (!extensionHintLocation) ? extension : null, propertySourceLoader);
return Collections.singleton(reference);
}
}
throw new IllegalStateException("File extension is not known to any PropertySourceLoader. "
+ "If the location is meant to reference a directory, it must end in '/' or File.separator");
}
private String getLoadableFileExtension(PropertySourceLoader loader, String file) {
for (String fileExtension : loader.getFileExtensions()) {
if (StringUtils.endsWithIgnoreCase(file, fileExtension)) {
return fileExtension;
}
}
return null;
}
private boolean isDirectory(String resourceLocation) {
return resourceLocation.endsWith("/") || resourceLocation.endsWith(File.separator);
}
private List<StandardConfigDataResource> resolve(Set<StandardConfigDataReference> references) {
List<StandardConfigDataResource> resolved = new ArrayList<>();
for (StandardConfigDataReference reference : references) {
resolved.addAll(resolve(reference));
}
if (resolved.isEmpty()) {
resolved.addAll(resolveEmptyDirectories(references));
}
return resolved;
}
private Collection<StandardConfigDataResource> resolveEmptyDirectories(
Set<StandardConfigDataReference> references) {
Set<StandardConfigDataResource> empty = new LinkedHashSet<>();
for (StandardConfigDataReference reference : references) {
empty.addAll(resolveEmptyDirectories(reference));
}
return empty;
}
private Set<StandardConfigDataResource> resolveEmptyDirectories(StandardConfigDataReference reference) {
if (!this.resourceLoader.isPattern(reference.getResourceLocation())) {
return resolveNonPatternEmptyDirectories(reference);
}
return resolvePatternEmptyDirectories(reference);
}
private Set<StandardConfigDataResource> resolveNonPatternEmptyDirectories(StandardConfigDataReference reference) {
Resource resource = this.resourceLoader.getResource(reference.getDirectory());
return (resource instanceof ClassPathResource || !resource.exists()) ? Collections.emptySet()
: Collections.singleton(new StandardConfigDataResource(reference, resource, true));
}
private Set<StandardConfigDataResource> resolvePatternEmptyDirectories(StandardConfigDataReference reference) {
Resource[] subdirectories = this.resourceLoader.getResources(reference.getDirectory(), ResourceType.DIRECTORY);
ConfigDataLocation location = reference.getConfigDataLocation();
if (!location.isOptional() && ObjectUtils.isEmpty(subdirectories)) {
String message = String.format("Config data location '%s' contains no subdirectories", location);
throw new ConfigDataLocationNotFoundException(location, message, null);
}
return Arrays.stream(subdirectories).filter(Resource::exists)
.map((resource) -> new StandardConfigDataResource(reference, resource, true))
.collect(Collectors.toCollection(LinkedHashSet::new));
}
private List<StandardConfigDataResource> resolve(StandardConfigDataReference reference) {
if (!this.resourceLoader.isPattern(reference.getResourceLocation())) {
return resolveNonPattern(reference);
}
return resolvePattern(reference);
}
private List<StandardConfigDataResource> resolveNonPattern(StandardConfigDataReference reference) {
Resource resource = this.resourceLoader.getResource(reference.getResourceLocation());
if (!resource.exists() && reference.isSkippable()) {
logSkippingResource(reference);
return Collections.emptyList();
}
return Collections.singletonList(createConfigResourceLocation(reference, resource));
}
private List<StandardConfigDataResource> resolvePattern(StandardConfigDataReference reference) {
List<StandardConfigDataResource> resolved = new ArrayList<>();
for (Resource resource : this.resourceLoader.getResources(reference.getResourceLocation(), ResourceType.FILE)) {
if (!resource.exists() && reference.isSkippable()) {
logSkippingResource(reference);
}
else {
resolved.add(createConfigResourceLocation(reference, resource));
}
}
return resolved;
}
private void logSkippingResource(StandardConfigDataReference reference) {
this.logger.trace(LogMessage.format("Skipping missing resource %s", reference));
}
private StandardConfigDataResource createConfigResourceLocation(StandardConfigDataReference reference,
Resource resource) {
return new StandardConfigDataResource(reference, resource);
}
}