Skip to content

Commit 1cf9fc1

Browse files
committed
Improve ConfigData processing code
Refactor `ConfigData` processing code to make it less awkward to follow. Prior to this commit the `ConfigDataLocationResolver` would take a String location and return a `ConfigDataLocation` instance. This was a little confusing since sometimes we would refer to `location` as the String value, and sometimes it would be the typed instance. We also had nowhere sensible to put the `optional:` prefix logic and we needed to pass a `boolean` parameter to a number of methods. The recently introduced `Orgin` support also didn't have a good home. To solve this, `ConfigDataLocation` has been renamed to `ConfigDataResource`. This frees up `ConfigDataLocation` to be used as a richer `location` type that holds the String value, the `Orgin` and provides a home for the `optional:` logic. This commit also cleans up a few other areas of the code, including renaming `ResourceConfigData...` to `StandardConfigData...`. It also introduces a new exception hierarchy for `ConfigDataNotFoundExceptions`. Closes gh-23711
1 parent f89b99b commit 1cf9fc1

File tree

65 files changed

+2078
-1486
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+2078
-1486
lines changed

Diff for: spring-boot-project/spring-boot-docs/src/docs/asciidoc/spring-boot-features.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,7 @@ You can use this prefix with the `spring.config.location` and `spring.config.add
663663

664664
For example, a `spring.config.import` value of `optional:file:./myconfig.properties` allows your application to start, even if the `myconfig.properties` file is missing.
665665

666-
If you want to ignore all `ConfigDataLocationNotFoundExceptions` and always continue to start your application, you can use the `spring.config.on-location-not-found` property.
666+
If you want to ignore all `ConfigDataLocationNotFoundExceptions` and always continue to start your application, you can use the `spring.config.on-not-found` property.
667667
Set the value to `ignore` using `SpringApplication.setDefaultProperties(...)` or with a system/environment variable.
668668

669669

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigData.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@
2929
import org.springframework.util.Assert;
3030

3131
/**
32-
* Configuration data that has been loaded from an external {@link ConfigDataLocation
33-
* location} and may ultimately contribute {@link PropertySource property sources} to
34-
* Spring's {@link Environment}.
32+
* Configuration data that has been loaded from a {@link ConfigDataResource} and may
33+
* ultimately contribute {@link PropertySource property sources} to Spring's
34+
* {@link Environment}.
3535
*
3636
* @author Phillip Webb
3737
* @author Madhura Bhave

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironment.java

+40-27
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,10 @@
4848
* Wrapper around a {@link ConfigurableEnvironment} that can be used to import and apply
4949
* {@link ConfigData}. Configures the initial set of
5050
* {@link ConfigDataEnvironmentContributors} by wrapping property sources from the Spring
51-
* {@link Environment} and adding the initial set of imports.
51+
* {@link Environment} and adding the initial set of locations.
5252
* <p>
53-
* The initial imports can be influenced via the {@link #LOCATION_PROPERTY},
54-
* {@value #ADDITIONAL_LOCATION_PROPERTY} and {@value #IMPORT_PROPERTY} properties. If not
53+
* The initial locations can be influenced via the {@link #LOCATION_PROPERTY},
54+
* {@value #ADDITIONAL_LOCATION_PROPERTY} and {@value #IMPORT_PROPERTY} properties. If no
5555
* explicit properties are set, the {@link #DEFAULT_SEARCH_LOCATIONS} will be used.
5656
*
5757
* @author Phillip Webb
@@ -76,28 +76,41 @@ class ConfigDataEnvironment {
7676

7777
/**
7878
* Property used to determine what action to take when a
79-
* {@code ConfigDataLocationNotFoundException} is thrown.
80-
* @see ConfigDataLocationNotFoundAction
79+
* {@code ConfigDataNotFoundAction} is thrown.
80+
* @see ConfigDataNotFoundAction
8181
*/
82-
static final String ON_LOCATION_NOT_FOUND_PROPERTY = "spring.config.on-location-not-found";
82+
static final String ON_NOT_FOUND_PROPERTY = "spring.config.on-not-found";
8383

8484
/**
8585
* Default search locations used if not {@link #LOCATION_PROPERTY} is found.
8686
*/
87-
static final String[] DEFAULT_SEARCH_LOCATIONS = { "optional:classpath:/", "optional:classpath:/config/",
88-
"optional:file:./", "optional:file:./config/*/", "optional:file:./config/" };
89-
90-
private static final String[] EMPTY_LOCATIONS = new String[0];
87+
static final ConfigDataLocation[] DEFAULT_SEARCH_LOCATIONS;
88+
static {
89+
List<ConfigDataLocation> locations = new ArrayList<>();
90+
locations.add(ConfigDataLocation.of("optional:classpath:/"));
91+
locations.add(ConfigDataLocation.of("optional:classpath:/config/"));
92+
locations.add(ConfigDataLocation.of("optional:file:./"));
93+
locations.add(ConfigDataLocation.of("optional:file:./config/*/"));
94+
locations.add(ConfigDataLocation.of("optional:file:./config/"));
95+
DEFAULT_SEARCH_LOCATIONS = locations.toArray(new ConfigDataLocation[0]);
96+
};
97+
98+
private static final ConfigDataLocation[] EMPTY_LOCATIONS = new ConfigDataLocation[0];
9199

92100
private static final ConfigurationPropertyName INCLUDE_PROFILES = ConfigurationPropertyName
93101
.of(Profiles.INCLUDE_PROFILES_PROPERTY_NAME);
94102

103+
private static final Bindable<ConfigDataLocation[]> CONFIG_DATA_LOCATION_ARRAY = Bindable
104+
.of(ConfigDataLocation[].class);
105+
95106
private static final Bindable<List<String>> STRING_LIST = Bindable.listOf(String.class);
96107

97108
private final DeferredLogFactory logFactory;
98109

99110
private final Log logger;
100111

112+
private final ConfigDataNotFoundAction notFoundAction;
113+
101114
private final ConfigurableBootstrapContext bootstrapContext;
102115

103116
private final ConfigurableEnvironment environment;
@@ -122,25 +135,21 @@ class ConfigDataEnvironment {
122135
ConfigurableEnvironment environment, ResourceLoader resourceLoader, Collection<String> additionalProfiles) {
123136
Binder binder = Binder.get(environment);
124137
UseLegacyConfigProcessingException.throwIfRequested(binder);
125-
ConfigDataLocationNotFoundAction locationNotFoundAction = binder
126-
.bind(ON_LOCATION_NOT_FOUND_PROPERTY, ConfigDataLocationNotFoundAction.class)
127-
.orElse(ConfigDataLocationNotFoundAction.FAIL);
128138
this.logFactory = logFactory;
129139
this.logger = logFactory.getLog(getClass());
140+
this.notFoundAction = binder.bind(ON_NOT_FOUND_PROPERTY, ConfigDataNotFoundAction.class)
141+
.orElse(ConfigDataNotFoundAction.FAIL);
130142
this.bootstrapContext = bootstrapContext;
131143
this.environment = environment;
132-
this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, locationNotFoundAction, binder,
133-
resourceLoader);
144+
this.resolvers = createConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
134145
this.additionalProfiles = additionalProfiles;
135-
this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext, locationNotFoundAction);
146+
this.loaders = new ConfigDataLoaders(logFactory, bootstrapContext);
136147
this.contributors = createContributors(binder);
137148
}
138149

139150
protected ConfigDataLocationResolvers createConfigDataLocationResolvers(DeferredLogFactory logFactory,
140-
ConfigurableBootstrapContext bootstrapContext, ConfigDataLocationNotFoundAction locationNotFoundAction,
141-
Binder binder, ResourceLoader resourceLoader) {
142-
return new ConfigDataLocationResolvers(logFactory, bootstrapContext, locationNotFoundAction, binder,
143-
resourceLoader);
151+
ConfigurableBootstrapContext bootstrapContext, Binder binder, ResourceLoader resourceLoader) {
152+
return new ConfigDataLocationResolvers(logFactory, bootstrapContext, binder, resourceLoader);
144153
}
145154

146155
private ConfigDataEnvironmentContributors createContributors(Binder binder) {
@@ -172,23 +181,26 @@ ConfigDataEnvironmentContributors getContributors() {
172181

173182
private List<ConfigDataEnvironmentContributor> getInitialImportContributors(Binder binder) {
174183
List<ConfigDataEnvironmentContributor> initialContributors = new ArrayList<>();
184+
addInitialImportContributors(initialContributors, bindLocations(binder, IMPORT_PROPERTY, EMPTY_LOCATIONS));
175185
addInitialImportContributors(initialContributors,
176-
binder.bind(IMPORT_PROPERTY, String[].class).orElse(EMPTY_LOCATIONS));
186+
bindLocations(binder, ADDITIONAL_LOCATION_PROPERTY, EMPTY_LOCATIONS));
177187
addInitialImportContributors(initialContributors,
178-
binder.bind(ADDITIONAL_LOCATION_PROPERTY, String[].class).orElse(EMPTY_LOCATIONS));
179-
addInitialImportContributors(initialContributors,
180-
binder.bind(LOCATION_PROPERTY, String[].class).orElse(DEFAULT_SEARCH_LOCATIONS));
188+
bindLocations(binder, LOCATION_PROPERTY, DEFAULT_SEARCH_LOCATIONS));
181189
return initialContributors;
182190
}
183191

192+
private ConfigDataLocation[] bindLocations(Binder binder, String propertyName, ConfigDataLocation[] other) {
193+
return binder.bind(propertyName, CONFIG_DATA_LOCATION_ARRAY).orElse(other);
194+
}
195+
184196
private void addInitialImportContributors(List<ConfigDataEnvironmentContributor> initialContributors,
185-
String[] locations) {
197+
ConfigDataLocation[] locations) {
186198
for (int i = locations.length - 1; i >= 0; i--) {
187199
initialContributors.add(createInitialImportContributor(locations[i]));
188200
}
189201
}
190202

191-
private ConfigDataEnvironmentContributor createInitialImportContributor(String location) {
203+
private ConfigDataEnvironmentContributor createInitialImportContributor(ConfigDataLocation location) {
192204
this.logger.trace(LogMessage.format("Adding initial config data import from location '%s'", location));
193205
return ConfigDataEnvironmentContributor.ofInitialImport(location);
194206
}
@@ -198,7 +210,8 @@ private ConfigDataEnvironmentContributor createInitialImportContributor(String l
198210
* {@link Environment}.
199211
*/
200212
void processAndApply() {
201-
ConfigDataImporter importer = new ConfigDataImporter(this.resolvers, this.loaders);
213+
ConfigDataImporter importer = new ConfigDataImporter(this.logFactory, this.notFoundAction, this.resolvers,
214+
this.loaders);
202215
this.bootstrapContext.register(Binder.class, InstanceSupplier
203216
.from(() -> this.contributors.getBinder(null, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE)));
204217
ConfigDataEnvironmentContributors contributors = processInitial(this.contributors, importer);

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributor.java

+24-23
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828

2929
import org.springframework.boot.context.properties.bind.Binder;
3030
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
31-
import org.springframework.boot.origin.Origin;
3231
import org.springframework.core.env.Environment;
3332
import org.springframework.core.env.PropertySource;
3433

@@ -52,7 +51,7 @@
5251
*/
5352
class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironmentContributor> {
5453

55-
private final ConfigDataLocation location;
54+
private final ConfigDataResource resource;
5655

5756
private final PropertySource<?> propertySource;
5857

@@ -69,19 +68,19 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
6968
/**
7069
* Create a new {@link ConfigDataEnvironmentContributor} instance.
7170
* @param kind the contributor kind
72-
* @param location the location that contributed the data or {@code null}
71+
* @param resource the resource that contributed the data or {@code null}
7372
* @param propertySource the property source for the data or {@code null}
7473
* @param configurationPropertySource the configuration property source for the data
7574
* or {@code null}
7675
* @param properties the config data properties or {@code null}
7776
* @param ignoreImports if import properties should be ignored
7877
* @param children the children of this contributor at each {@link ImportPhase}
7978
*/
80-
ConfigDataEnvironmentContributor(Kind kind, ConfigDataLocation location, PropertySource<?> propertySource,
79+
ConfigDataEnvironmentContributor(Kind kind, ConfigDataResource resource, PropertySource<?> propertySource,
8180
ConfigurationPropertySource configurationPropertySource, ConfigDataProperties properties,
8281
boolean ignoreImports, Map<ImportPhase, List<ConfigDataEnvironmentContributor>> children) {
8382
this.kind = kind;
84-
this.location = location;
83+
this.resource = resource;
8584
this.properties = properties;
8685
this.propertySource = propertySource;
8786
this.configurationPropertySource = configurationPropertySource;
@@ -107,11 +106,11 @@ boolean isActive(ConfigDataActivationContext activationContext) {
107106
}
108107

109108
/**
110-
* Return the location that contributed this instance.
111-
* @return the location or {@code null}
109+
* Return the resource that contributed this instance.
110+
* @return the resource or {@code null}
112111
*/
113-
ConfigDataLocation getLocation() {
114-
return this.location;
112+
ConfigDataResource getResource() {
113+
return this.resource;
115114
}
116115

117116
/**
@@ -134,14 +133,10 @@ ConfigurationPropertySource getConfigurationPropertySource() {
134133
* Return any imports requested by this contributor.
135134
* @return the imports
136135
*/
137-
List<String> getImports() {
136+
List<ConfigDataLocation> getImports() {
138137
return (this.properties != null) ? this.properties.getImports() : Collections.emptyList();
139138
}
140139

141-
Origin getImportOrigin(String importLocation) {
142-
return (this.properties != null) ? this.properties.getImportOrigin(importLocation) : null;
143-
}
144-
145140
/**
146141
* Return true if this contributor has imports that have not yet been processed in the
147142
* given phase.
@@ -184,13 +179,19 @@ public Iterator<ConfigDataEnvironmentContributor> iterator() {
184179
return new ContributorIterator();
185180
}
186181

182+
/**
183+
* Create an new {@link ConfigDataEnvironmentContributor} with bound
184+
* {@link ConfigDataProperties}.
185+
* @param binder the binder to use
186+
* @return a new contributor instance
187+
*/
187188
ConfigDataEnvironmentContributor withBoundProperties(Binder binder) {
188189
UseLegacyConfigProcessingException.throwIfRequested(binder);
189190
ConfigDataProperties properties = ConfigDataProperties.get(binder);
190191
if (this.ignoreImports) {
191192
properties = properties.withoutImports();
192193
}
193-
return new ConfigDataEnvironmentContributor(Kind.BOUND_IMPORT, this.location, this.propertySource,
194+
return new ConfigDataEnvironmentContributor(Kind.BOUND_IMPORT, this.resource, this.propertySource,
194195
this.configurationPropertySource, properties, this.ignoreImports, null);
195196
}
196197

@@ -205,7 +206,7 @@ ConfigDataEnvironmentContributor withChildren(ImportPhase importPhase,
205206
List<ConfigDataEnvironmentContributor> children) {
206207
Map<ImportPhase, List<ConfigDataEnvironmentContributor>> updatedChildren = new LinkedHashMap<>(this.children);
207208
updatedChildren.put(importPhase, children);
208-
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.propertySource,
209+
return new ConfigDataEnvironmentContributor(this.kind, this.resource, this.propertySource,
209210
this.configurationPropertySource, this.properties, this.ignoreImports, updatedChildren);
210211
}
211212

@@ -230,7 +231,7 @@ ConfigDataEnvironmentContributor withReplacement(ConfigDataEnvironmentContributo
230231
}
231232
updatedChildren.put(importPhase, Collections.unmodifiableList(updatedContributors));
232233
});
233-
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.propertySource,
234+
return new ConfigDataEnvironmentContributor(this.kind, this.resource, this.propertySource,
234235
this.configurationPropertySource, this.properties, this.ignoreImports, updatedChildren);
235236
}
236237

@@ -249,11 +250,11 @@ static ConfigDataEnvironmentContributor of(List<ConfigDataEnvironmentContributor
249250
* Factory method to create a {@link Kind#INITIAL_IMPORT initial import} contributor.
250251
* This contributor is used to trigger initial imports of additional contributors. It
251252
* does not contribute any properties itself.
252-
* @param importLocation the initial import location (with placeholder resolved)
253+
* @param initialImport the initial import location (with placeholders resolved)
253254
* @return a new {@link ConfigDataEnvironmentContributor} instance
254255
*/
255-
static ConfigDataEnvironmentContributor ofInitialImport(String importLocation) {
256-
List<String> imports = Collections.singletonList(importLocation);
256+
static ConfigDataEnvironmentContributor ofInitialImport(ConfigDataLocation initialImport) {
257+
List<ConfigDataLocation> imports = Collections.singletonList(initialImport);
257258
ConfigDataProperties properties = new ConfigDataProperties(imports, null);
258259
return new ConfigDataEnvironmentContributor(Kind.INITIAL_IMPORT, null, null, null, properties, false, null);
259260
}
@@ -274,17 +275,17 @@ static ConfigDataEnvironmentContributor ofExisting(PropertySource<?> propertySou
274275
* Factory method to create an {@link Kind#UNBOUND_IMPORT unbound import} contributor.
275276
* This contributor has been actively imported from another contributor and may itself
276277
* import further contributors later.
277-
* @param location the location of imported config data
278+
* @param resource the condig data resource
278279
* @param configData the config data
279280
* @param propertySourceIndex the index of the property source that should be used
280281
* @return a new {@link ConfigDataEnvironmentContributor} instance
281282
*/
282-
static ConfigDataEnvironmentContributor ofUnboundImport(ConfigDataLocation location, ConfigData configData,
283+
static ConfigDataEnvironmentContributor ofUnboundImport(ConfigDataResource resource, ConfigData configData,
283284
int propertySourceIndex) {
284285
PropertySource<?> propertySource = configData.getPropertySources().get(propertySourceIndex);
285286
ConfigurationPropertySource configurationPropertySource = ConfigurationPropertySource.from(propertySource);
286287
boolean ignoreImports = configData.getOptions().contains(ConfigData.Option.IGNORE_IMPORTS);
287-
return new ConfigDataEnvironmentContributor(Kind.UNBOUND_IMPORT, location, propertySource,
288+
return new ConfigDataEnvironmentContributor(Kind.UNBOUND_IMPORT, resource, propertySource,
288289
configurationPropertySource, null, ignoreImports, null);
289290
}
290291

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributorPlaceholdersResolver.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ private String resolvePlaceholder(String placeholder) {
6464
Object value = (propertySource != null) ? propertySource.getProperty(placeholder) : null;
6565
if (value != null && !contributor.isActive(this.activationContext)) {
6666
if (this.failOnResolveFromInactiveContributor) {
67+
ConfigDataResource resource = contributor.getResource();
6768
Origin origin = OriginLookup.getOrigin(propertySource, placeholder);
68-
throw new InactiveConfigDataAccessException(propertySource, contributor.getLocation(), placeholder,
69-
origin);
69+
throw new InactiveConfigDataAccessException(propertySource, resource, placeholder, origin);
7070
}
7171
value = null;
7272
}

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentContributors.java

+6-12
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
4040
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
4141
import org.springframework.boot.logging.DeferredLogFactory;
42-
import org.springframework.boot.origin.Origin;
4342
import org.springframework.core.log.LogMessage;
4443
import org.springframework.util.ObjectUtils;
4544

@@ -114,12 +113,12 @@ ConfigDataEnvironmentContributors withProcessedImports(ConfigDataImporter import
114113
ConfigDataLocationResolverContext locationResolverContext = new ContributorConfigDataLocationResolverContext(
115114
result, contributor, activationContext);
116115
ConfigDataLoaderContext loaderContext = new ContributorDataLoaderContext(this);
117-
List<String> imports = contributor.getImports();
116+
List<ConfigDataLocation> imports = contributor.getImports();
118117
this.logger.trace(LogMessage.format("Processing imports %s", imports));
119-
Map<ConfigDataLocation, ConfigData> imported = importer.resolveAndLoad(activationContext,
118+
Map<ConfigDataResource, ConfigData> imported = importer.resolveAndLoad(activationContext,
120119
locationResolverContext, loaderContext, imports);
121120
this.logger.trace(LogMessage.of(() -> imported.isEmpty() ? "Nothing imported" : "Imported "
122-
+ imported.size() + " location " + ((imported.size() != 1) ? "s" : "") + imported.keySet()));
121+
+ imported.size() + " resource " + ((imported.size() != 1) ? "s" : "") + imported.keySet()));
123122
ConfigDataEnvironmentContributor contributorAndChildren = contributor.withChildren(importPhase,
124123
asContributors(imported));
125124
result = new ConfigDataEnvironmentContributors(this.logger, this.bootstrapContext,
@@ -148,7 +147,7 @@ private boolean isActiveWithUnprocessedImports(ConfigDataActivationContext activ
148147
return contributor.isActive(activationContext) && contributor.hasUnprocessedImports(importPhase);
149148
}
150149

151-
private List<ConfigDataEnvironmentContributor> asContributors(Map<ConfigDataLocation, ConfigData> imported) {
150+
private List<ConfigDataEnvironmentContributor> asContributors(Map<ConfigDataResource, ConfigData> imported) {
152151
List<ConfigDataEnvironmentContributor> contributors = new ArrayList<>(imported.size() * 5);
153152
imported.forEach((location, data) -> {
154153
for (int i = data.getPropertySources().size() - 1; i >= 0; i--) {
@@ -259,20 +258,15 @@ public Binder getBinder() {
259258
}
260259

261260
@Override
262-
public ConfigDataLocation getParent() {
263-
return this.contributor.getLocation();
261+
public ConfigDataResource getParent() {
262+
return this.contributor.getResource();
264263
}
265264

266265
@Override
267266
public ConfigurableBootstrapContext getBootstrapContext() {
268267
return this.contributors.getBootstrapContext();
269268
}
270269

271-
@Override
272-
public Origin getLocationOrigin(String location) {
273-
return this.contributor.getImportOrigin(location);
274-
}
275-
276270
}
277271

278272
private class InactiveSourceChecker implements BindHandler {

Diff for: spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/config/ConfigDataEnvironmentPostProcessor.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ public class ConfigDataEnvironmentPostProcessor implements EnvironmentPostProces
5353
/**
5454
* Property used to determine what action to take when a
5555
* {@code ConfigDataLocationNotFoundException} is thrown.
56-
* @see ConfigDataLocationNotFoundAction
56+
* @see ConfigDataNotFoundAction
5757
*/
58-
public static final String ON_LOCATION_NOT_FOUND_PROPERTY = ConfigDataEnvironment.ON_LOCATION_NOT_FOUND_PROPERTY;
58+
public static final String ON_LOCATION_NOT_FOUND_PROPERTY = ConfigDataEnvironment.ON_NOT_FOUND_PROPERTY;
5959

6060
private final DeferredLogFactory logFactory;
6161

0 commit comments

Comments
 (0)