Support profile specific ConfigData imports
Update the `ConfigData` import support to allow individual property sources to be imported with a higher precedence than profile specific imports. Prior to this commit, imported sources would always have a higher precedence than the file that imported them, but a lower precedence than any profile-specific variant of the same file. For example, given an `application.properties` that imports `myconfig`, the contributor tree would be as follows: ROOT +- `application.properties` | +- myconfig +- `application-<profile>.properties` The precedence would be: 1) `application-<profile>.properties` 2) myconfig 3) `application.properties` This works well for most situations, but can be confusing if import is for a profile-specific property source. For example: ROOT +- `application.properties` | +- myconfig | +- myconfig-<profile> +- `application-<profile>.properties` Results in the order precedence of: 1) `application-<profile>.properties` 2) myconfig-<profile> 3) myconfig 4) `application.properties` This means that whilst `myconfig` overrides `application.properties`, `myconfig-profile` does not override `application-<profile>.properties`. For this specific situation, the preferable order would be: 1) myconfig-<profile> 2) `application-<profile>.properties` 3) myconfig 4) `application.properties` To support this alternative ordering a new `PROFILE_SPECIFIC` config data option has been added. Additionally, options may now be specified on a per-source basis by using the `PropertySourceOptions` interface. Fixes gh-25766
This commit is contained in:
parent
f289f922fd
commit
5774ea3f0c
|
@ -43,7 +43,7 @@ public final class ConfigData {
|
|||
|
||||
private final List<PropertySource<?>> propertySources;
|
||||
|
||||
private final Set<Option> options;
|
||||
private final PropertySourceOptions propertySourceOptions;
|
||||
|
||||
/**
|
||||
* A {@link ConfigData} instance that contains no data.
|
||||
|
@ -51,17 +51,30 @@ public final class ConfigData {
|
|||
public static final ConfigData EMPTY = new ConfigData(Collections.emptySet());
|
||||
|
||||
/**
|
||||
* Create a new {@link ConfigData} instance.
|
||||
* Create a new {@link ConfigData} instance with the same options applied to each
|
||||
* source.
|
||||
* @param propertySources the config data property sources in ascending priority
|
||||
* order.
|
||||
* @param options the config data options
|
||||
* @param options the config data options applied to each source
|
||||
* @see #ConfigData(Collection, PropertySourceOptions)
|
||||
*/
|
||||
public ConfigData(Collection<? extends PropertySource<?>> propertySources, Option... options) {
|
||||
this(propertySources, PropertySourceOptions.always(Options.of(options)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ConfigData} instance with specific property source options.
|
||||
* @param propertySources the config data property sources in ascending priority
|
||||
* order.
|
||||
* @param propertySourceOptions the property source options
|
||||
* @since 2.4.5
|
||||
*/
|
||||
public ConfigData(Collection<? extends PropertySource<?>> propertySources,
|
||||
PropertySourceOptions propertySourceOptions) {
|
||||
Assert.notNull(propertySources, "PropertySources must not be null");
|
||||
Assert.notNull(options, "Options must not be null");
|
||||
Assert.notNull(propertySourceOptions, "PropertySourceOptions must not be null");
|
||||
this.propertySources = Collections.unmodifiableList(new ArrayList<>(propertySources));
|
||||
this.options = Collections.unmodifiableSet(
|
||||
(options.length != 0) ? EnumSet.copyOf(Arrays.asList(options)) : EnumSet.noneOf(Option.class));
|
||||
this.propertySourceOptions = propertySourceOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -77,18 +90,167 @@ public final class ConfigData {
|
|||
/**
|
||||
* Return a set of {@link Option config data options} for this source.
|
||||
* @return the config data options
|
||||
* @deprecated since 2.4.5 in favor of {@link #getOptions(PropertySource)}
|
||||
*/
|
||||
@Deprecated
|
||||
public Set<Option> getOptions() {
|
||||
return this.options;
|
||||
Assert.state(this.propertySourceOptions instanceof AlwaysPropertySourceOptions, "No global options defined");
|
||||
return this.propertySourceOptions.get(null).asSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Option flags that can be applied config data.
|
||||
* Return the {@link Options config data options} that apply to the given source.
|
||||
* @param propertySource the property source to check
|
||||
* @return the options that apply
|
||||
* @since 2.4.5
|
||||
*/
|
||||
public Options getOptions(PropertySource<?> propertySource) {
|
||||
Options options = this.propertySourceOptions.get(propertySource);
|
||||
return (options != null) ? options : Options.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy interface used to supply {@link Options} for a given
|
||||
* {@link PropertySource}.
|
||||
*
|
||||
* @since 2.4.5
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface PropertySourceOptions {
|
||||
|
||||
/**
|
||||
* Return the options that should apply for the given property source.
|
||||
* @param propertySource the property source
|
||||
* @return the options to apply
|
||||
*/
|
||||
Options get(PropertySource<?> propertySource);
|
||||
|
||||
/**
|
||||
* Create a new {@link PropertySourceOptions} instance that always returns the
|
||||
* same options regardless of the property source.
|
||||
* @param options the options to return
|
||||
* @return a new {@link PropertySourceOptions} instance
|
||||
*/
|
||||
static PropertySourceOptions always(Option... options) {
|
||||
return always(Options.of(options));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link PropertySourceOptions} instance that always returns the
|
||||
* same options regardless of the property source.
|
||||
* @param options the options to return
|
||||
* @return a new {@link PropertySourceOptions} instance
|
||||
*/
|
||||
static PropertySourceOptions always(Options options) {
|
||||
return new AlwaysPropertySourceOptions(options);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link PropertySourceOptions} that always returns the same result.
|
||||
*/
|
||||
private static class AlwaysPropertySourceOptions implements PropertySourceOptions {
|
||||
|
||||
private final Options options;
|
||||
|
||||
AlwaysPropertySourceOptions(Options options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Options get(PropertySource<?> propertySource) {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A set of {@link Option} flags.
|
||||
*
|
||||
* @since 2.4.5
|
||||
*/
|
||||
public static final class Options {
|
||||
|
||||
/**
|
||||
* No options.
|
||||
*/
|
||||
public static final Options NONE = Options.of();
|
||||
|
||||
private final Set<Option> options;
|
||||
|
||||
private Options(Set<Option> options) {
|
||||
this.options = Collections.unmodifiableSet(options);
|
||||
}
|
||||
|
||||
Set<Option> asSet() {
|
||||
return this.options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the given option is contained in this set.
|
||||
* @param option the option to check
|
||||
* @return {@code true} of the option is present
|
||||
*/
|
||||
public boolean contains(Option option) {
|
||||
return this.options.contains(option);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Options other = (Options) obj;
|
||||
return this.options.equals(other.options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.options.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.options.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Options} instance that contains the options in this set
|
||||
* excluding the given option.
|
||||
* @param option the option to exclude
|
||||
* @return a new {@link Options} instance
|
||||
*/
|
||||
Options without(Option option) {
|
||||
EnumSet<Option> options = EnumSet.noneOf(Option.class);
|
||||
options.addAll(this.options);
|
||||
options.remove(option);
|
||||
return new Options(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new instance with the given {@link Option} values.
|
||||
* @param options the options to include
|
||||
* @return a new {@link Options} instance
|
||||
*/
|
||||
public static Options of(Option... options) {
|
||||
Assert.notNull(options, "Options must not be null");
|
||||
return new Options(
|
||||
(options.length != 0) ? EnumSet.copyOf(Arrays.asList(options)) : EnumSet.noneOf(Option.class));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Option flags that can be applied.
|
||||
*/
|
||||
public enum Option {
|
||||
|
||||
/**
|
||||
* Ignore all imports properties from the sources.
|
||||
* Ignore all imports properties from the source.
|
||||
*/
|
||||
IGNORE_IMPORTS,
|
||||
|
||||
|
@ -96,7 +258,14 @@ public final class ConfigData {
|
|||
* Ignore all profile activation and include properties.
|
||||
* @since 2.4.3
|
||||
*/
|
||||
IGNORE_PROFILES;
|
||||
IGNORE_PROFILES,
|
||||
|
||||
/**
|
||||
* Indicates that the source is "profile specific" and should be included after
|
||||
* profile specific sibling imports.
|
||||
* @since 2.4.5
|
||||
*/
|
||||
PROFILE_SPECIFIC;
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -269,7 +269,8 @@ class ConfigDataEnvironment {
|
|||
ConfigDataActivationContext activationContext) {
|
||||
this.logger.trace("Deducing profiles from current config data environment contributors");
|
||||
Binder binder = contributors.getBinder(activationContext,
|
||||
ConfigDataEnvironmentContributor::isNotIgnoringProfiles, BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE);
|
||||
(contributor) -> !contributor.hasConfigDataOption(ConfigData.Option.IGNORE_PROFILES),
|
||||
BinderOption.FAIL_ON_BIND_TO_INACTIVE_SOURCE);
|
||||
try {
|
||||
Set<String> additionalProfiles = new LinkedHashSet<>(this.additionalProfiles);
|
||||
additionalProfiles.addAll(getIncludedProfiles(contributors, activationContext));
|
||||
|
@ -291,7 +292,7 @@ class ConfigDataEnvironment {
|
|||
Set<String> result = new LinkedHashSet<>();
|
||||
for (ConfigDataEnvironmentContributor contributor : contributors) {
|
||||
ConfigurationPropertySource source = contributor.getConfigurationPropertySource();
|
||||
if (source != null && contributor.isNotIgnoringProfiles()) {
|
||||
if (source != null && !contributor.hasConfigDataOption(ConfigData.Option.IGNORE_PROFILES)) {
|
||||
Binder binder = new Binder(Collections.singleton(source), placeholdersResolver);
|
||||
binder.bind(Profiles.INCLUDE_PROFILES, STRING_LIST).ifBound((includes) -> {
|
||||
if (!contributor.isActive(activationContext)) {
|
||||
|
|
|
@ -23,7 +23,6 @@ import java.util.LinkedHashMap;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.stream.StreamSupport;
|
||||
|
||||
|
@ -31,6 +30,7 @@ import org.springframework.boot.context.properties.bind.Binder;
|
|||
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* A single element that may directly or indirectly contribute configuration data to the
|
||||
|
@ -52,14 +52,14 @@ import org.springframework.core.env.PropertySource;
|
|||
*/
|
||||
class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironmentContributor> {
|
||||
|
||||
private static final Set<ConfigData.Option> EMPTY_LOCATION_OPTIONS = Collections
|
||||
.unmodifiableSet(Collections.singleton(ConfigData.Option.IGNORE_IMPORTS));
|
||||
private static final ConfigData.Options EMPTY_LOCATION_OPTIONS = ConfigData.Options
|
||||
.of(ConfigData.Option.IGNORE_IMPORTS);
|
||||
|
||||
private final ConfigDataLocation location;
|
||||
|
||||
private final ConfigDataResource resource;
|
||||
|
||||
private final boolean profileSpecific;
|
||||
private final boolean fromProfileSpecificImport;
|
||||
|
||||
private final PropertySource<?> propertySource;
|
||||
|
||||
|
@ -67,7 +67,7 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
|
||||
private final ConfigDataProperties properties;
|
||||
|
||||
private final Set<ConfigData.Option> configDataOptions;
|
||||
private final ConfigData.Options configDataOptions;
|
||||
|
||||
private final Map<ImportPhase, List<ConfigDataEnvironmentContributor>> children;
|
||||
|
||||
|
@ -78,7 +78,8 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
* @param kind the contributor kind
|
||||
* @param location the location of this contributor
|
||||
* @param resource the resource that contributed the data or {@code null}
|
||||
* @param profileSpecific if the contributor is from a profile specific import
|
||||
* @param fromProfileSpecificImport if the contributor is from a profile specific
|
||||
* import
|
||||
* @param propertySource the property source for the data or {@code null}
|
||||
* @param configurationPropertySource the configuration property source for the data
|
||||
* or {@code null}
|
||||
|
@ -87,18 +88,17 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
* @param children the children of this contributor at each {@link ImportPhase}
|
||||
*/
|
||||
ConfigDataEnvironmentContributor(Kind kind, ConfigDataLocation location, ConfigDataResource resource,
|
||||
boolean profileSpecific, PropertySource<?> propertySource,
|
||||
boolean fromProfileSpecificImport, PropertySource<?> propertySource,
|
||||
ConfigurationPropertySource configurationPropertySource, ConfigDataProperties properties,
|
||||
Set<ConfigData.Option> configDataOptions,
|
||||
Map<ImportPhase, List<ConfigDataEnvironmentContributor>> children) {
|
||||
ConfigData.Options configDataOptions, Map<ImportPhase, List<ConfigDataEnvironmentContributor>> children) {
|
||||
this.kind = kind;
|
||||
this.location = location;
|
||||
this.resource = resource;
|
||||
this.profileSpecific = profileSpecific;
|
||||
this.fromProfileSpecificImport = fromProfileSpecificImport;
|
||||
this.properties = properties;
|
||||
this.propertySource = propertySource;
|
||||
this.configurationPropertySource = configurationPropertySource;
|
||||
this.configDataOptions = (configDataOptions != null) ? configDataOptions : Collections.emptySet();
|
||||
this.configDataOptions = (configDataOptions != null) ? configDataOptions : ConfigData.Options.NONE;
|
||||
this.children = (children != null) ? children : Collections.emptyMap();
|
||||
}
|
||||
|
||||
|
@ -135,8 +135,8 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
* Return if the contributor is from a profile specific import.
|
||||
* @return if the contributor is profile specific
|
||||
*/
|
||||
boolean isProfileSpecific() {
|
||||
return this.profileSpecific;
|
||||
boolean isFromProfileSpecificImport() {
|
||||
return this.fromProfileSpecificImport;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -156,12 +156,18 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this contributor is not ignoring profile properties.
|
||||
* @return if the contributor is not ignoring profiles
|
||||
* @see ConfigData.Option#IGNORE_PROFILES
|
||||
* Return if the contributor has a specific config data option.
|
||||
* @param option the option to check
|
||||
* @return {@code true} if the option is present
|
||||
*/
|
||||
boolean isNotIgnoringProfiles() {
|
||||
return !this.configDataOptions.contains(ConfigData.Option.IGNORE_PROFILES);
|
||||
boolean hasConfigDataOption(ConfigData.Option option) {
|
||||
return this.configDataOptions.contains(option);
|
||||
}
|
||||
|
||||
ConfigDataEnvironmentContributor withoutConfigDataOption(ConfigData.Option option) {
|
||||
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.resource,
|
||||
this.fromProfileSpecificImport, this.propertySource, this.configurationPropertySource, this.properties,
|
||||
this.configDataOptions.without(option), this.children);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -227,7 +233,7 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
properties = properties.withoutImports();
|
||||
}
|
||||
return new ConfigDataEnvironmentContributor(Kind.BOUND_IMPORT, this.location, this.resource,
|
||||
this.profileSpecific, this.propertySource, this.configurationPropertySource, properties,
|
||||
this.fromProfileSpecificImport, this.propertySource, this.configurationPropertySource, properties,
|
||||
this.configDataOptions, null);
|
||||
}
|
||||
|
||||
|
@ -242,9 +248,60 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
List<ConfigDataEnvironmentContributor> children) {
|
||||
Map<ImportPhase, List<ConfigDataEnvironmentContributor>> updatedChildren = new LinkedHashMap<>(this.children);
|
||||
updatedChildren.put(importPhase, children);
|
||||
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.resource, this.profileSpecific,
|
||||
this.propertySource, this.configurationPropertySource, this.properties, this.configDataOptions,
|
||||
updatedChildren);
|
||||
if (importPhase == ImportPhase.AFTER_PROFILE_ACTIVATION) {
|
||||
moveProfileSpecific(updatedChildren);
|
||||
}
|
||||
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.resource,
|
||||
this.fromProfileSpecificImport, this.propertySource, this.configurationPropertySource, this.properties,
|
||||
this.configDataOptions, updatedChildren);
|
||||
}
|
||||
|
||||
private void moveProfileSpecific(Map<ImportPhase, List<ConfigDataEnvironmentContributor>> children) {
|
||||
List<ConfigDataEnvironmentContributor> before = children.get(ImportPhase.BEFORE_PROFILE_ACTIVATION);
|
||||
if (!hasAnyProfileSpecificChildren(before)) {
|
||||
return;
|
||||
}
|
||||
List<ConfigDataEnvironmentContributor> updatedBefore = new ArrayList<>(before.size());
|
||||
List<ConfigDataEnvironmentContributor> updatedAfter = new ArrayList<>();
|
||||
for (ConfigDataEnvironmentContributor contributor : before) {
|
||||
updatedBefore.add(moveProfileSpecificChildren(contributor, updatedAfter));
|
||||
}
|
||||
updatedAfter.addAll(children.getOrDefault(ImportPhase.AFTER_PROFILE_ACTIVATION, Collections.emptyList()));
|
||||
children.put(ImportPhase.BEFORE_PROFILE_ACTIVATION, updatedBefore);
|
||||
children.put(ImportPhase.AFTER_PROFILE_ACTIVATION, updatedAfter);
|
||||
}
|
||||
|
||||
private ConfigDataEnvironmentContributor moveProfileSpecificChildren(ConfigDataEnvironmentContributor contributor,
|
||||
List<ConfigDataEnvironmentContributor> removed) {
|
||||
for (ImportPhase importPhase : ImportPhase.values()) {
|
||||
List<ConfigDataEnvironmentContributor> children = contributor.getChildren(importPhase);
|
||||
List<ConfigDataEnvironmentContributor> updatedChildren = new ArrayList<>(children.size());
|
||||
for (ConfigDataEnvironmentContributor child : children) {
|
||||
if (child.hasConfigDataOption(ConfigData.Option.PROFILE_SPECIFIC)) {
|
||||
removed.add(child.withoutConfigDataOption(ConfigData.Option.PROFILE_SPECIFIC));
|
||||
}
|
||||
else {
|
||||
updatedChildren.add(child);
|
||||
}
|
||||
}
|
||||
contributor = contributor.withChildren(importPhase, updatedChildren);
|
||||
}
|
||||
return contributor;
|
||||
}
|
||||
|
||||
private boolean hasAnyProfileSpecificChildren(List<ConfigDataEnvironmentContributor> contributors) {
|
||||
if (CollectionUtils.isEmpty(contributors)) {
|
||||
return false;
|
||||
}
|
||||
for (ConfigDataEnvironmentContributor contributor : contributors) {
|
||||
for (ImportPhase importPhase : ImportPhase.values()) {
|
||||
if (contributor.getChildren(importPhase).stream()
|
||||
.anyMatch((child) -> child.hasConfigDataOption(ConfigData.Option.PROFILE_SPECIFIC))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -268,9 +325,36 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
}
|
||||
updatedChildren.put(importPhase, Collections.unmodifiableList(updatedContributors));
|
||||
});
|
||||
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.resource, this.profileSpecific,
|
||||
this.propertySource, this.configurationPropertySource, this.properties, this.configDataOptions,
|
||||
updatedChildren);
|
||||
return new ConfigDataEnvironmentContributor(this.kind, this.location, this.resource,
|
||||
this.fromProfileSpecificImport, this.propertySource, this.configurationPropertySource, this.properties,
|
||||
this.configDataOptions, updatedChildren);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
buildToString("", builder);
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private void buildToString(String prefix, StringBuilder builder) {
|
||||
builder.append(prefix);
|
||||
builder.append(this.kind);
|
||||
builder.append(" ");
|
||||
builder.append(this.location);
|
||||
builder.append(" ");
|
||||
builder.append(this.resource);
|
||||
builder.append(" ");
|
||||
builder.append(this.configDataOptions);
|
||||
builder.append("\n");
|
||||
for (ConfigDataEnvironmentContributor child : this.children.getOrDefault(ImportPhase.BEFORE_PROFILE_ACTIVATION,
|
||||
Collections.emptyList())) {
|
||||
child.buildToString(prefix + " ", builder);
|
||||
}
|
||||
for (ConfigDataEnvironmentContributor child : this.children.getOrDefault(ImportPhase.AFTER_PROFILE_ACTIVATION,
|
||||
Collections.emptyList())) {
|
||||
child.buildToString(prefix + " ", builder);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -324,9 +408,10 @@ class ConfigDataEnvironmentContributor implements Iterable<ConfigDataEnvironment
|
|||
static ConfigDataEnvironmentContributor ofUnboundImport(ConfigDataLocation location, ConfigDataResource resource,
|
||||
boolean profileSpecific, ConfigData configData, int propertySourceIndex) {
|
||||
PropertySource<?> propertySource = configData.getPropertySources().get(propertySourceIndex);
|
||||
ConfigData.Options options = configData.getOptions(propertySource);
|
||||
ConfigurationPropertySource configurationPropertySource = ConfigurationPropertySource.from(propertySource);
|
||||
return new ConfigDataEnvironmentContributor(Kind.UNBOUND_IMPORT, location, resource, profileSpecific,
|
||||
propertySource, configurationPropertySource, null, configData.getOptions(), null);
|
||||
propertySource, configurationPropertySource, null, options, null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -117,7 +117,8 @@ public class InvalidConfigDataPropertyException extends ConfigDataException {
|
|||
logger.warn(getMessage(property, false, replacement, contributor.getResource()));
|
||||
}
|
||||
});
|
||||
if (contributor.isProfileSpecific() && contributor.isNotIgnoringProfiles()) {
|
||||
if (contributor.isFromProfileSpecificImport()
|
||||
&& !contributor.hasConfigDataOption(ConfigData.Option.IGNORE_PROFILES)) {
|
||||
PROFILE_SPECIFIC_ERRORS.forEach((name) -> {
|
||||
ConfigurationProperty property = propertySource.getConfigurationProperty(name);
|
||||
if (property != null) {
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test;
|
|||
|
||||
import org.springframework.boot.cloud.CloudPlatform;
|
||||
import org.springframework.boot.context.config.ConfigData.Option;
|
||||
import org.springframework.boot.context.config.ConfigData.PropertySourceOptions;
|
||||
import org.springframework.boot.context.config.ConfigDataEnvironmentContributor.ImportPhase;
|
||||
import org.springframework.boot.context.config.ConfigDataEnvironmentContributor.Kind;
|
||||
import org.springframework.boot.context.properties.bind.Binder;
|
||||
|
@ -224,6 +225,21 @@ class ConfigDataEnvironmentContributorTests {
|
|||
assertThat(withChildren.getChildren(ImportPhase.BEFORE_PROFILE_ACTIVATION)).containsExactly(child);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withChildrenAfterProfileActivationMovesProfileSpecificChildren() {
|
||||
ConfigDataEnvironmentContributor root = createBoundContributor("root");
|
||||
ConfigDataEnvironmentContributor child1 = createBoundContributor("child1");
|
||||
ConfigDataEnvironmentContributor grandchild = createBoundContributor(new TestResource("grandchild"),
|
||||
new ConfigData(Collections.singleton(new MockPropertySource()),
|
||||
PropertySourceOptions.always(Option.PROFILE_SPECIFIC)),
|
||||
0);
|
||||
child1 = child1.withChildren(ImportPhase.BEFORE_PROFILE_ACTIVATION, Collections.singletonList(grandchild));
|
||||
root = root.withChildren(ImportPhase.BEFORE_PROFILE_ACTIVATION, Collections.singletonList(child1));
|
||||
ConfigDataEnvironmentContributor child2 = createBoundContributor("child2");
|
||||
root = root.withChildren(ImportPhase.AFTER_PROFILE_ACTIVATION, Collections.singletonList(child2));
|
||||
assertThat(asLocationsList(root.iterator())).containsExactly("grandchild", "child2", "child1", "root");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withReplacementReplacesChild() {
|
||||
ConfigDataEnvironmentContributor root = createBoundContributor("root");
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.context.config.ConfigData.Option;
|
||||
import org.springframework.boot.context.config.ConfigData.Options;
|
||||
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessorIntegrationTests.Config;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link ConfigDataEnvironmentPostProcessor} config data imports
|
||||
* that are combined with profile-specific files.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ConfigDataEnvironmentPostProcessorImportCombinedWithProfileSpecificIntegrationTests {
|
||||
|
||||
private SpringApplication application;
|
||||
|
||||
@TempDir
|
||||
public File temp;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.application = new SpringApplication(Config.class);
|
||||
this.application.setWebApplicationType(WebApplicationType.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithoutProfile() {
|
||||
ConfigurableApplicationContext context = this.application
|
||||
.run("--spring.config.name=configimportwithprofilespecific");
|
||||
String value = context.getEnvironment().getProperty("prop");
|
||||
assertThat(value).isEqualTo("fromicwps1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithProfile() {
|
||||
ConfigurableApplicationContext context = this.application
|
||||
.run("--spring.config.name=configimportwithprofilespecific", "--spring.profiles.active=prod");
|
||||
String value = context.getEnvironment().getProperty("prop");
|
||||
assertThat(value).isEqualTo("fromicwps2");
|
||||
}
|
||||
|
||||
static class LocationResolver implements ConfigDataLocationResolver<Resource> {
|
||||
|
||||
@Override
|
||||
public boolean isResolvable(ConfigDataLocationResolverContext context, ConfigDataLocation location) {
|
||||
return location.hasPrefix("icwps:");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Resource> resolve(ConfigDataLocationResolverContext context, ConfigDataLocation location)
|
||||
throws ConfigDataLocationNotFoundException, ConfigDataResourceNotFoundException {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Resource> resolveProfileSpecific(ConfigDataLocationResolverContext context,
|
||||
ConfigDataLocation location, Profiles profiles) throws ConfigDataLocationNotFoundException {
|
||||
return Collections.singletonList(new Resource(profiles));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class Loader implements ConfigDataLoader<Resource> {
|
||||
|
||||
@Override
|
||||
public ConfigData load(ConfigDataLoaderContext context, Resource resource)
|
||||
throws IOException, ConfigDataResourceNotFoundException {
|
||||
List<PropertySource<?>> propertySources = new ArrayList<>();
|
||||
Map<PropertySource<?>, Options> propertySourceOptions = new HashMap<>();
|
||||
propertySources.add(new MapPropertySource("icwps1", Collections.singletonMap("prop", "fromicwps1")));
|
||||
if (resource.profiles.isAccepted("prod")) {
|
||||
MapPropertySource profileSpecificPropertySource = new MapPropertySource("icwps2",
|
||||
Collections.singletonMap("prop", "fromicwps2"));
|
||||
propertySources.add(profileSpecificPropertySource);
|
||||
propertySourceOptions.put(profileSpecificPropertySource, Options.of(Option.PROFILE_SPECIFIC));
|
||||
}
|
||||
return new ConfigData(propertySources, propertySourceOptions::get);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class Resource extends ConfigDataResource {
|
||||
|
||||
private final Profiles profiles;
|
||||
|
||||
Resource(Profiles profiles) {
|
||||
this.profiles = profiles;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "icwps:";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
* 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.
|
||||
|
@ -17,16 +17,22 @@
|
|||
package org.springframework.boot.context.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.context.config.ConfigData.Option;
|
||||
import org.springframework.boot.context.config.ConfigData.Options;
|
||||
import org.springframework.boot.context.config.ConfigData.PropertySourceOptions;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.PropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ConfigData}.
|
||||
|
@ -58,7 +64,8 @@ class ConfigDataTests {
|
|||
}
|
||||
|
||||
@Test
|
||||
void getOptionsReturnsCopyOfOptions() {
|
||||
@Deprecated
|
||||
void getDeprecatedOptionsReturnsCopyOfOptions() {
|
||||
MapPropertySource source = new MapPropertySource("test", Collections.emptyMap());
|
||||
Option[] options = { Option.IGNORE_IMPORTS };
|
||||
ConfigData configData = new ConfigData(Collections.singleton(source), options);
|
||||
|
@ -66,4 +73,67 @@ class ConfigDataTests {
|
|||
assertThat(configData.getOptions()).containsExactly(Option.IGNORE_IMPORTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Deprecated
|
||||
void getDeprecatedOptionsWhenUsingPropertySourceOptionsThrowsException() {
|
||||
MapPropertySource source = new MapPropertySource("test", Collections.emptyMap());
|
||||
PropertySourceOptions propertySourceOptions = (propertySource) -> Options.NONE;
|
||||
ConfigData configData = new ConfigData(Collections.singleton(source), propertySourceOptions);
|
||||
assertThatIllegalStateException().isThrownBy(() -> configData.getOptions())
|
||||
.withMessage("No global options defined");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOptionsWhenOptionsSetAtConstructionAlwaysReturnsSameOptions() {
|
||||
MapPropertySource source = new MapPropertySource("test", Collections.emptyMap());
|
||||
ConfigData configData = new ConfigData(Collections.singleton(source), Option.IGNORE_IMPORTS);
|
||||
assertThat(configData.getOptions(source).asSet()).containsExactly(Option.IGNORE_IMPORTS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOptionsReturnsOptionsFromPropertySourceOptions() {
|
||||
MapPropertySource source1 = new MapPropertySource("test", Collections.emptyMap());
|
||||
MapPropertySource source2 = new MapPropertySource("test", Collections.emptyMap());
|
||||
Options options1 = Options.of(Option.IGNORE_IMPORTS);
|
||||
Options options2 = Options.of(Option.IGNORE_PROFILES);
|
||||
PropertySourceOptions propertySourceOptions = (source) -> source == source1 ? options1 : options2;
|
||||
ConfigData configData = new ConfigData(Arrays.asList(source1, source2), propertySourceOptions);
|
||||
assertThat(configData.getOptions(source1)).isEqualTo(options1);
|
||||
assertThat(configData.getOptions(source2)).isEqualTo(options2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOptionsWhenPropertySourceOptionsReturnsNullReturnsNone() {
|
||||
MapPropertySource source = new MapPropertySource("test", Collections.emptyMap());
|
||||
PropertySourceOptions propertySourceOptions = (propertySource) -> null;
|
||||
ConfigData configData = new ConfigData(Collections.singleton(source), propertySourceOptions);
|
||||
assertThat(configData.getOptions(source)).isEqualTo(Options.NONE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionsOfCreatesOptions() {
|
||||
Options options = Options.of(Option.IGNORE_IMPORTS, Option.IGNORE_PROFILES);
|
||||
assertThat(options.asSet()).containsExactly(Option.IGNORE_IMPORTS, Option.IGNORE_PROFILES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionsOfUsesCopyOfOptions() {
|
||||
Option[] array = { Option.IGNORE_IMPORTS, Option.IGNORE_PROFILES };
|
||||
Options options = Options.of(array);
|
||||
array[0] = Option.PROFILE_SPECIFIC;
|
||||
assertThat(options.asSet()).containsExactly(Option.IGNORE_IMPORTS, Option.IGNORE_PROFILES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void optionsNoneReturnsEmptyOptions() {
|
||||
assertThat(Options.NONE.asSet()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void propertySourceOptionsAlwaysReturnsSameOptionsEachTime() {
|
||||
PropertySourceOptions options = PropertySourceOptions.always(Option.IGNORE_IMPORTS, Option.IGNORE_PROFILES);
|
||||
assertThat(options.get(mock(PropertySource.class)).asSet()).containsExactly(Option.IGNORE_IMPORTS,
|
||||
Option.IGNORE_PROFILES);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,9 +16,6 @@
|
|||
|
||||
package org.springframework.boot.context.config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -148,7 +145,7 @@ class InvalidConfigDataPropertyExceptionTests {
|
|||
propertySource.setProperty(name, "a");
|
||||
ConfigDataEnvironmentContributor contributor = new ConfigDataEnvironmentContributor(Kind.BOUND_IMPORT, null,
|
||||
null, true, propertySource, ConfigurationPropertySource.from(propertySource), null,
|
||||
new HashSet<>(Arrays.asList(configDataOptions)), null);
|
||||
ConfigData.Options.of(configDataOptions), null);
|
||||
return contributor;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ org.springframework.boot.context.config.TestPropertySourceLoader1,\
|
|||
org.springframework.boot.context.config.TestPropertySourceLoader2
|
||||
|
||||
org.springframework.boot.context.config.ConfigDataLocationResolver=\
|
||||
org.springframework.boot.context.config.TestConfigDataBootstrap.LocationResolver
|
||||
org.springframework.boot.context.config.TestConfigDataBootstrap.LocationResolver,\
|
||||
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessorImportCombinedWithProfileSpecificIntegrationTests.LocationResolver
|
||||
|
||||
org.springframework.boot.context.config.ConfigDataLoader=\
|
||||
org.springframework.boot.context.config.TestConfigDataBootstrap.Loader
|
||||
org.springframework.boot.context.config.TestConfigDataBootstrap.Loader,\
|
||||
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessorImportCombinedWithProfileSpecificIntegrationTests.Loader
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
prop=fromprofilefile
|
|
@ -0,0 +1,2 @@
|
|||
spring.config.import=icwps:
|
||||
prop=fromfile
|
Loading…
Reference in New Issue