Add 'contains descendant of' support

Update `ConfigurationPropertySource` with a `containsDescendantOf`
method that can be used to tell if a source contains any descendants
of the given name.

The result is Optional so that sources that cannot determine a result
may return `empty()`.

The existing `IterableConfigurationPropertiesSource` has a default
implementation that works by iterating the contained values. Most other
sources return `empty()` with the exception of the adapted
`RandomProperySource` with will return true for `random.*` names.

See gh-9023
This commit is contained in:
Phillip Webb
2017-05-01 22:08:39 -07:00
parent 35a51e45d8
commit c22a21e386
14 changed files with 257 additions and 10 deletions

View File

@@ -16,6 +16,8 @@
package org.springframework.boot.context.properties.source;
import java.util.Optional;
import org.springframework.util.Assert;
/**
@@ -50,6 +52,17 @@ class AliasedConfigurationPropertySource implements ConfigurationPropertySource
return result;
}
@Override
public Optional<Boolean> containsDescendantOf(ConfigurationPropertyName name) {
Assert.notNull(name, "Name must not be null");
Optional<Boolean> result = this.source.containsDescendantOf(name);
for (ConfigurationPropertyName alias : getAliases().getAliases(name)) {
Optional<Boolean> aliasResult = this.source.containsDescendantOf(alias);
result = result.flatMap((r) -> aliasResult.flatMap(a -> Optional.of(r || a)));
}
return result;
}
protected ConfigurationPropertySource getSource() {
return this.source;
}

View File

@@ -16,6 +16,7 @@
package org.springframework.boot.context.properties.source;
import java.util.Optional;
import java.util.function.Predicate;
import org.springframework.boot.origin.OriginTrackedValue;
@@ -40,6 +41,27 @@ public interface ConfigurationPropertySource {
*/
ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name);
/**
* Optionally returns if the source contains any descendants of the specified name.
* <ul>
* <li>A result of {@code true} means that there is at least on property in the source
* with a name that's an
* {@link ConfigurationPropertyName#isAncestorOf(ConfigurationPropertyName) ancestor}
* of {@code name}.</li>
* <li>A result of {@code false} means that that there are no properties in the source
* with a name that's an
* {@link ConfigurationPropertyName#isAncestorOf(ConfigurationPropertyName) ancestor}
* of {@code name}.</li>
* <li>A result of {@code empty} means it is not possible to determine up determine if
* there's a property in the source with a name that's an
* {@link ConfigurationPropertyName#isAncestorOf(ConfigurationPropertyName) ancestor}
* of {@code name}.
* </ul>
* @param name the name to check
* @return an optional boolean determining if a descendant is contained in the source
*/
Optional<Boolean> containsDescendantOf(ConfigurationPropertyName name);
/**
* Return a filtered variant of this source, containing only names that match the
* given {@link Predicate}.

View File

@@ -18,15 +18,19 @@ package org.springframework.boot.context.properties.source;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
import java.util.WeakHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import org.springframework.boot.env.RandomValuePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySource.StubPropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.core.env.PropertySourcesPropertyResolver;
import org.springframework.core.env.SystemEnvironmentPropertySource;
@@ -45,6 +49,9 @@ import org.springframework.util.Assert;
public class ConfigurationPropertySources
implements Iterable<ConfigurationPropertySource> {
private static final ConfigurationPropertyName RANDOM = ConfigurationPropertyName
.of("random");
/**
* The name of the {@link PropertySource} {@link #adapt adapter}.
*/
@@ -71,7 +78,8 @@ public class ConfigurationPropertySources
}
private Stream<PropertySource<?>> streamPropertySources(PropertySources sources) {
return StreamSupport.stream(sources.spliterator(), false).flatMap(this::flatten);
return StreamSupport.stream(sources.spliterator(), false).flatMap(this::flatten)
.filter(this::notStubSource);
}
private Stream<PropertySource<?>> flatten(PropertySource<?> source) {
@@ -82,6 +90,10 @@ public class ConfigurationPropertySources
return Stream.of(source);
}
private boolean notStubSource(PropertySource<?> source) {
return !(source instanceof StubPropertySource);
}
private ConfigurationPropertySource adapt(PropertySource<?> source) {
return this.adapters.computeIfAbsent(source, this::createAdapter);
}
@@ -92,7 +104,8 @@ public class ConfigurationPropertySources
return new PropertySourceIterableConfigurationPropertySource(
(EnumerablePropertySource<?>) source, mapper);
}
return new PropertySourceConfigurationPropertySource(source, mapper);
return new PropertySourceConfigurationPropertySource(source, mapper,
getContainsDescendantOfMethod(source));
}
private PropertyMapper getPropertyMapper(PropertySource<?> source) {
@@ -124,6 +137,15 @@ public class ConfigurationPropertySources
return source;
}
private Function<ConfigurationPropertyName, Optional<Boolean>> getContainsDescendantOfMethod(
PropertySource<?> source) {
if (source instanceof RandomValuePropertySource) {
return (name) -> Optional
.of(name.isAncestorOf(RANDOM) || name.equals(RANDOM));
}
return null;
}
/**
* Attach a {@link ConfigurationPropertySources} instance to the specified
* {@link ConfigurableEnvironment} so that classic

View File

@@ -16,6 +16,7 @@
package org.springframework.boot.context.properties.source;
import java.util.Optional;
import java.util.function.Predicate;
import org.springframework.util.Assert;
@@ -47,6 +48,13 @@ class FilteredConfigurationPropertiesSource implements ConfigurationPropertySour
return (filtered ? getSource().getConfigurationProperty(name) : null);
}
@Override
public Optional<Boolean> containsDescendantOf(ConfigurationPropertyName name) {
// We can't be sure a contained descendant won't be filtered
return this.source.containsDescendantOf(name)
.flatMap((result) -> result ? Optional.empty() : Optional.of(result));
}
protected ConfigurationPropertySource getSource() {
return this.source;
}

View File

@@ -16,6 +16,7 @@
package org.springframework.boot.context.properties.source;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -46,4 +47,9 @@ class FilteredIterableConfigurationPropertiesSource
return (IterableConfigurationPropertySource) super.getSource();
}
@Override
public Optional<Boolean> containsDescendantOf(ConfigurationPropertyName name) {
return Optional.of(stream().filter(name::isAncestorOf).findFirst().isPresent());
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.boot.context.properties.source;
import java.util.Iterator;
import java.util.Optional;
import java.util.function.Predicate;
import java.util.stream.Stream;
@@ -60,6 +61,11 @@ public interface IterableConfigurationPropertySource
*/
Stream<ConfigurationPropertyName> stream();
@Override
default Optional<Boolean> containsDescendantOf(ConfigurationPropertyName name) {
return Optional.of(stream().filter(name::isAncestorOf).findFirst().isPresent());
}
@Override
default IterableConfigurationPropertySource filter(
Predicate<ConfigurationPropertyName> filter) {

View File

@@ -19,6 +19,8 @@ package org.springframework.boot.context.properties.source;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.PropertySourceOrigin;
@@ -54,17 +56,24 @@ class PropertySourceConfigurationPropertySource implements ConfigurationProperty
private final PropertyMapper mapper;
private final Function<ConfigurationPropertyName, Optional<Boolean>> containsDescendantOfMethod;
/**
* Create a new {@link PropertySourceConfigurationPropertySource} implementation.
* @param propertySource the source property source
* @param mapper the property mapper
* @param containsDescendantOfMethod function used to implement
* {@link #containsDescendantOf(ConfigurationPropertyName)} (may be {@code null})
*/
PropertySourceConfigurationPropertySource(PropertySource<?> propertySource,
PropertyMapper mapper) {
PropertyMapper mapper,
Function<ConfigurationPropertyName, Optional<Boolean>> containsDescendantOfMethod) {
Assert.notNull(propertySource, "PropertySource must not be null");
Assert.notNull(mapper, "Mapper must not be null");
this.propertySource = propertySource;
this.mapper = new ExceptionSwallowingPropertyMapper(mapper);
this.containsDescendantOfMethod = (containsDescendantOfMethod != null ? containsDescendantOfMethod
: (n) -> Optional.empty());
}
@Override
@@ -74,6 +83,11 @@ class PropertySourceConfigurationPropertySource implements ConfigurationProperty
return find(mappings, name);
}
@Override
public Optional<Boolean> containsDescendantOf(ConfigurationPropertyName name) {
return this.containsDescendantOfMethod.apply(name);
}
protected final ConfigurationProperty find(List<PropertyMapping> mappings,
ConfigurationPropertyName name) {
return mappings.stream().filter((m) -> m.isApplicable(name)).map(this::find)
@@ -101,6 +115,11 @@ class PropertySourceConfigurationPropertySource implements ConfigurationProperty
return this.mapper;
}
@Override
public String toString() {
return this.propertySource.toString();
}
/**
* {@link PropertyMapper} that swallows exceptions when the mapping fails.
*/

View File

@@ -20,6 +20,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import org.springframework.core.env.EnumerablePropertySource;
@@ -45,7 +46,7 @@ class PropertySourceIterableConfigurationPropertySource
PropertySourceIterableConfigurationPropertySource(
EnumerablePropertySource<?> propertySource, PropertyMapper mapper) {
super(propertySource, mapper);
super(propertySource, mapper, null);
assertEnumerablePropertySource(propertySource);
}
@@ -87,6 +88,11 @@ class PropertySourceIterableConfigurationPropertySource
return getConfigurationPropertyNames().iterator();
}
@Override
public Optional<Boolean> containsDescendantOf(ConfigurationPropertyName name) {
return Optional.of(stream().filter(name::isAncestorOf).findFirst().isPresent());
}
private List<ConfigurationPropertyName> getConfigurationPropertyNames() {
Cache cache = getCache();
List<ConfigurationPropertyName> names = (cache != null ? cache.getNames() : null);

View File

@@ -16,9 +16,15 @@
package org.springframework.boot.context.properties.source;
import java.util.Optional;
import org.junit.Test;
import org.mockito.Answers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
/**
* Tests for {@link AliasedConfigurationPropertySource}.
@@ -50,6 +56,59 @@ public class AliasedConfigurationPropertySourceTests {
assertThat(getValue(aliased, "foo.baz")).isEqualTo("biff");
}
@Test
public void containsDescendantOfWhenSourceReturnsEmptyShouldReturnEmpty()
throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo");
ConfigurationPropertySource source = mock(ConfigurationPropertySource.class,
withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
given(source.containsDescendantOf(name)).willReturn(Optional.empty());
ConfigurationPropertySource aliased = source
.withAliases(new ConfigurationPropertyNameAliases("foo.bar", "foo.bar1"));
assertThat(aliased.containsDescendantOf(name)).isEmpty();
}
@Test
public void containsDescendantOfWhenAliasReturnsEmptyShouldReturnEmpty()
throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo");
ConfigurationPropertySource source = mock(ConfigurationPropertySource.class,
withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
given(source.containsDescendantOf(name)).willReturn(Optional.of(true));
given(source.containsDescendantOf(ConfigurationPropertyName.of("bar")))
.willReturn(Optional.empty());
ConfigurationPropertySource aliased = source
.withAliases(new ConfigurationPropertyNameAliases("foo", "bar"));
assertThat(aliased.containsDescendantOf(name)).isEmpty();
}
@Test
public void containsDescendantOfWhenAllAreFalseShouldReturnFalse() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo");
ConfigurationPropertySource source = mock(ConfigurationPropertySource.class,
withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
given(source.containsDescendantOf(name)).willReturn(Optional.of(false));
given(source.containsDescendantOf(ConfigurationPropertyName.of("bar")))
.willReturn(Optional.of(false));
ConfigurationPropertySource aliased = source
.withAliases(new ConfigurationPropertyNameAliases("foo", "bar"));
assertThat(aliased.containsDescendantOf(name)).contains(false);
}
@Test
public void containsDescendantOfWhenAnyIsTrueShouldReturnTrue() throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo");
ConfigurationPropertySource source = mock(ConfigurationPropertySource.class,
withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
given(source.containsDescendantOf(name)).willReturn(Optional.of(false));
given(source.containsDescendantOf(ConfigurationPropertyName.of("bar")))
.willReturn(Optional.of(true));
ConfigurationPropertySource aliased = source
.withAliases(new ConfigurationPropertyNameAliases("foo", "bar"));
assertThat(aliased.containsDescendantOf(name)).contains(true);
}
private Object getValue(ConfigurationPropertySource source, String name) {
ConfigurationProperty property = source
.getConfigurationProperty(ConfigurationPropertyName.of(name));

View File

@@ -17,12 +17,17 @@
package org.springframework.boot.context.properties.source;
import java.util.Objects;
import java.util.Optional;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Answers;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.withSettings;
/**
* Test for {@link FilteredIterableConfigurationPropertiesSource}.
@@ -61,7 +66,39 @@ public class FilteredConfigurationPropertiesSourceTests {
assertThat(source.getConfigurationProperty(bracketName).getValue())
.isEqualTo("2");
assertThat(filtered.getConfigurationProperty(bracketName)).isNull();
}
@Test
public void containsDescendantOfWhenSourceReturnsEmptyShouldReturnEmpty()
throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo");
ConfigurationPropertySource source = mock(ConfigurationPropertySource.class,
withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
given(source.containsDescendantOf(name)).willReturn(Optional.empty());
ConfigurationPropertySource filtered = source.filter((n) -> true);
assertThat(filtered.containsDescendantOf(name)).isEmpty();
}
@Test
public void containsDescendantOfWhenSourceReturnsFalseShouldReturnFalse()
throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo");
ConfigurationPropertySource source = mock(ConfigurationPropertySource.class,
withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
given(source.containsDescendantOf(name)).willReturn(Optional.of(false));
ConfigurationPropertySource filtered = source.filter((n) -> true);
assertThat(filtered.containsDescendantOf(name)).contains(false);
}
@Test
public void containsDescendantOfWhenSourceReturnsTrueShouldReturnEmpty()
throws Exception {
ConfigurationPropertyName name = ConfigurationPropertyName.of("foo");
ConfigurationPropertySource source = mock(ConfigurationPropertySource.class,
withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS));
given(source.containsDescendantOf(name)).willReturn(Optional.of(true));
ConfigurationPropertySource filtered = source.filter((n) -> true);
assertThat(filtered.containsDescendantOf(name)).isEmpty();
}
protected final ConfigurationPropertySource createTestSource() {

View File

@@ -43,6 +43,19 @@ public class FilteredIterableConfigurationPropertiesSourceTests
return source;
}
@Test
public void containsDescendantOfShouldUseContents() throws Exception {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.bar.baz", "1");
source.put("foo.bar[0]", "1");
source.put("faf.bar[0]", "1");
IterableConfigurationPropertySource filtered = source.filter(this::noBrackets);
assertThat(filtered.containsDescendantOf(ConfigurationPropertyName.of("foo")))
.contains(true);
assertThat(filtered.containsDescendantOf(ConfigurationPropertyName.of("faf")))
.contains(false);
}
private boolean noBrackets(ConfigurationPropertyName name) {
return name.toString().indexOf("[") == -1;
}

View File

@@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.source;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.springframework.boot.origin.MockOrigin;
@@ -97,6 +98,11 @@ public class MockConfigurationPropertySource
return MockConfigurationPropertySource.this.getConfigurationProperty(name);
}
@Override
public Optional<Boolean> containsDescendantOf(ConfigurationPropertyName name) {
return Optional.empty();
}
}
}

View File

@@ -46,14 +46,16 @@ public class PropertySourceConfigurationPropertySourceTests {
public void createWhenPropertySourceIsNullShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("PropertySource must not be null");
new PropertySourceConfigurationPropertySource(null, mock(PropertyMapper.class));
new PropertySourceConfigurationPropertySource(null, mock(PropertyMapper.class),
null);
}
@Test
public void createWhenMapperIsNullShouldThrowException() throws Exception {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("Mapper must not be null");
new PropertySourceConfigurationPropertySource(mock(PropertySource.class), null);
new PropertySourceConfigurationPropertySource(mock(PropertySource.class), null,
null);
}
@Test
@@ -67,7 +69,7 @@ public class PropertySourceConfigurationPropertySourceTests {
ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key");
mapper.addFromConfigurationProperty(name, "key2");
PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource(
propertySource, mapper);
propertySource, mapper, null);
assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("value2");
}
@@ -81,7 +83,7 @@ public class PropertySourceConfigurationPropertySourceTests {
mapper.addFromConfigurationProperty(name, "key",
(value) -> value.toString().replace("ue", "let"));
PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource(
propertySource, mapper);
propertySource, mapper, null);
assertThat(adapter.getConfigurationProperty(name).getValue()).isEqualTo("vallet");
}
@@ -94,7 +96,7 @@ public class PropertySourceConfigurationPropertySourceTests {
ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key");
mapper.addFromConfigurationProperty(name, "key");
PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource(
propertySource, mapper);
propertySource, mapper, null);
assertThat(adapter.getConfigurationProperty(name).getOrigin().toString())
.isEqualTo("\"key\" from property source \"test\"");
}
@@ -109,11 +111,22 @@ public class PropertySourceConfigurationPropertySourceTests {
ConfigurationPropertyName name = ConfigurationPropertyName.of("my.key");
mapper.addFromConfigurationProperty(name, "key");
PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource(
propertySource, mapper);
propertySource, mapper, null);
assertThat(adapter.getConfigurationProperty(name).getOrigin().toString())
.isEqualTo("TestOrigin key");
}
@Test
public void containsDescendantOfShouldReturnEmpty() throws Exception {
Map<String, Object> source = new LinkedHashMap<>();
source.put("foo.bar", "value");
PropertySource<?> propertySource = new MapPropertySource("test", source);
PropertySourceConfigurationPropertySource adapter = new PropertySourceConfigurationPropertySource(
propertySource, new DefaultPropertyMapper(), null);
assertThat(adapter.containsDescendantOf(ConfigurationPropertyName.of("foo")))
.isEmpty();
}
/**
* Test {@link PropertySource} that's also a {@link OriginLookup}.
*/

View File

@@ -156,6 +156,23 @@ public class PropertySourceIterableConfigurationPropertySourceTests {
.isEqualTo("TestOrigin key");
}
@Test
public void containsDescendantOfShouldCheckSourceNames() throws Exception {
Map<String, Object> source = new LinkedHashMap<>();
source.put("foo.bar", "value");
source.put("faf", "value");
EnumerablePropertySource<?> propertySource = new OriginCapablePropertySource<>(
new MapPropertySource("test", source));
PropertySourceIterableConfigurationPropertySource adapter = new PropertySourceIterableConfigurationPropertySource(
propertySource, new DefaultPropertyMapper());
assertThat(adapter.containsDescendantOf(ConfigurationPropertyName.of("foo")))
.contains(true);
assertThat(adapter.containsDescendantOf(ConfigurationPropertyName.of("faf")))
.contains(false);
assertThat(adapter.containsDescendantOf(ConfigurationPropertyName.of("fof")))
.contains(false);
}
/**
* Test {@link PropertySource} that's also a {@link OriginLookup}.
*/