diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java index 7f2e38312e1e..9fffcf5b7003 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/IndexedElementsBinder.java @@ -40,6 +40,7 @@ * @param the type being bound * @author Phillip Webb * @author Madhura Bhave + * @author Tommy Karlsson */ abstract class IndexedElementsBinder extends AggregateBinder { @@ -129,10 +130,9 @@ private void bindIndexed(ConfigurationPropertySource source, ConfigurationProper private Set getKnownIndexedChildren(IterableConfigurationPropertySource source, ConfigurationPropertyName root) { Set knownIndexedChildren = new HashSet<>(); - for (ConfigurationPropertyName name : source.filter(root::isAncestorOf)) { - ConfigurationPropertyName choppedName = name.chop(root.getNumberOfElements() + 1); - if (choppedName.isLastElementIndexed()) { - knownIndexedChildren.add(choppedName.getLastElement(Form.UNIFORM)); + for (ConfigurationPropertyName child : source.getChildrenOf(root)) { + if (child.isLastElementIndexed()) { + knownIndexedChildren.add(child.getLastElement(Form.UNIFORM)); } } return knownIndexedChildren; diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java index ca22264f5e81..e75c65ab2abb 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/IterableConfigurationPropertySource.java @@ -17,6 +17,8 @@ package org.springframework.boot.context.properties.source; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; import java.util.function.Predicate; import java.util.stream.Stream; @@ -34,6 +36,7 @@ * * @author Phillip Webb * @author Madhura Bhave + * @author Tommy Karlsson * @since 2.0.0 * @see ConfigurationPropertyName * @see OriginTrackedValue @@ -66,6 +69,31 @@ default ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyNam return ConfigurationPropertyState.search(this, name::isAncestorOf); } + /** + * Return the names directly beneath the given name. For example, if this source + * contains {@code foo.bar} and {@code foo.baz[0]}, the children of {@code foo} are + * {@code foo.bar} and {@code foo.baz}. + *

+ * A returned name is not necessarily a name that this source has a value for. Given + * only {@code foo.bar.baz}, the children of {@code foo} are {@code foo.bar}, for + * which {@link #getConfigurationProperty(ConfigurationPropertyName)} returns + * {@code null}. + *

+ * Implementations that can answer without inspecting every name they contain should + * override this method, since callers may ask about many names in turn. + * @param name the name whose children should be returned + * @return the child names (never {@code null}) + * @since 4.2.0 + */ + default Set getChildrenOf(ConfigurationPropertyName name) { + Set children = new LinkedHashSet<>(); + int childElements = name.getNumberOfElements() + 1; + for (ConfigurationPropertyName candidate : this.filter(name::isAncestorOf)) { + children.add(candidate.chop(childElements)); + } + return children; + } + @Override default IterableConfigurationPropertySource filter(Predicate filter) { return new FilteredIterableConfigurationPropertiesSource(this, filter); diff --git a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java index 2a638d5e2aef..4696b2d59c1c 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySource.java @@ -23,6 +23,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; @@ -52,6 +53,7 @@ * * @author Phillip Webb * @author Madhura Bhave + * @author Tommy Karlsson * @see PropertyMapper */ class SpringIterableConfigurationPropertySource extends SpringConfigurationPropertySource @@ -136,6 +138,11 @@ public Iterator iterator() { return new ConfigurationPropertyNamesIterator(getConfigurationPropertyNames()); } + @Override + public Set getChildrenOf(ConfigurationPropertyName name) { + return getCache().getChildren(name); + } + @Override public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName name) { ConfigurationPropertyState result = super.containsDescendantOf(name); @@ -143,14 +150,8 @@ public ConfigurationPropertyState containsDescendantOf(ConfigurationPropertyName return result; } if (this.ancestorOfCheck == PropertyMapper.DEFAULT_ANCESTOR_OF_CHECK) { - Set descendants = getCache().getDescendants(); - if (descendants != null) { - if (name.isEmpty() && !descendants.isEmpty()) { - return ConfigurationPropertyState.PRESENT; - } - return !descendants.contains(name) ? ConfigurationPropertyState.ABSENT - : ConfigurationPropertyState.PRESENT; - } + return getCache().hasChildren(name) ? ConfigurationPropertyState.PRESENT + : ConfigurationPropertyState.ABSENT; } result = (this.containsDescendantOfCache != null) ? this.containsDescendantOfCache.get(name) : null; if (result == null) { @@ -191,8 +192,7 @@ private Cache getCache() { private Cache createCache() { boolean immutable = isImmutablePropertySource(); - boolean captureDescendants = this.ancestorOfCheck == PropertyMapper.DEFAULT_ANCESTOR_OF_CHECK; - return new Cache(getMappers(), immutable, captureDescendants, isSystemEnvironmentSource()); + return new Cache(getMappers(), immutable, isSystemEnvironmentSource()); } private Cache updateCache(Cache cache) { @@ -224,17 +224,13 @@ private static class Cache { private final boolean immutable; - private final boolean captureDescendants; - private final boolean systemEnvironmentSource; private volatile @Nullable Data data; - Cache(PropertyMapper[] mappers, boolean immutable, boolean captureDescendants, - boolean systemEnvironmentSource) { + Cache(PropertyMapper[] mappers, boolean immutable, boolean systemEnvironmentSource) { this.mappers = mappers; this.immutable = immutable; - this.captureDescendants = captureDescendants; this.systemEnvironmentSource = systemEnvironmentSource; } @@ -267,7 +263,6 @@ private void tryUpdate(EnumerablePropertySource propertySource) { (data != null) ? data.mappings() : null, size); Map reverseMappings = cloneOrCreate( (data != null) ? data.reverseMappings() : null, size); - Set descendants = (!this.captureDescendants) ? null : new HashSet<>(); Map systemEnvironmentCopy = (!this.systemEnvironmentSource) ? null : copySource(propertySource); for (PropertyMapper propertyMapper : this.mappers) { @@ -281,14 +276,15 @@ private void tryUpdate(EnumerablePropertySource propertySource) { } } } + Map> children = new HashMap<>(size); for (String propertyName : propertyNames) { - addParents(descendants, reverseMappings.get(propertyName)); + addChildren(children, reverseMappings.get(propertyName)); } ConfigurationPropertyName[] configurationPropertyNames = this.immutable ? reverseMappings.values().toArray(new ConfigurationPropertyName[0]) : null; lastUpdated = this.immutable ? null : propertyNames; - this.data = new Data(mappings, reverseMappings, descendants, configurationPropertyNames, - systemEnvironmentCopy, lastUpdated); + this.data = new Data(mappings, reverseMappings, children, configurationPropertyNames, systemEnvironmentCopy, + lastUpdated); } @SuppressWarnings("unchecked") @@ -300,16 +296,33 @@ private Map cloneOrCreate(@Nullable Map source, int size) { return (source != null) ? new LinkedHashMap<>(source) : new LinkedHashMap<>(size); } - private void addParents(@Nullable Set descendants, + /** + * Record the given name against its parent, and each of its ancestors against + * theirs. The walk stops as soon as a parent is already known, since whichever + * name first recorded it walked the rest of the way up. + * @param children the children to update + * @param name the name to record + */ + private void addChildren(Map> children, @Nullable ConfigurationPropertyName name) { - if (descendants == null || name == null || name.isEmpty()) { + if (name == null || name.isEmpty()) { return; } + ConfigurationPropertyName child = name; ConfigurationPropertyName parent = name.getParent(); - while (!parent.isEmpty()) { - if (!descendants.add(parent)) { + while (true) { + Set known = children.get(parent); + if (known != null) { + known.add(child); return; } + Set added = new LinkedHashSet<>(); + added.add(child); + children.put(parent, added); + if (parent.isEmpty()) { + return; + } + child = parent; parent = parent.getParent(); } } @@ -342,10 +355,10 @@ Set getMapped(ConfigurationPropertyName configurationPropertyName) { return names; } - @Nullable Set getDescendants() { + boolean hasChildren(ConfigurationPropertyName name) { Data data = this.data; Assert.state(data != null, "'data' must not be null"); - return data.descendants(); + return data.children().containsKey(name); } @Nullable Object getSystemEnvironmentProperty(String name) { @@ -356,9 +369,15 @@ Set getMapped(ConfigurationPropertyName configurationPropertyName) { return systemEnvironmentCopy.get(name); } + Set getChildren(ConfigurationPropertyName name) { + Data data = this.data; + Assert.state(data != null, "'data' must not be null"); + return data.children().getOrDefault(name, Collections.emptySet()); + } + private record Data(Map> mappings, Map reverseMappings, - @Nullable Set descendants, + Map> children, ConfigurationPropertyName @Nullable [] configurationPropertyNames, @Nullable Map systemEnvironmentCopy, String @Nullable [] lastUpdated) { diff --git a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySourceTests.java b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySourceTests.java index aeef71b022c9..b9a660bd07c6 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySourceTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/context/properties/source/SpringIterableConfigurationPropertySourceTests.java @@ -45,6 +45,7 @@ * @author Phillip Webb * @author Madhura Bhave * @author Fahim Farook + * @author Tommy Karlsson */ class SpringIterableConfigurationPropertySourceTests { @@ -143,6 +144,82 @@ void getValueWhenOriginCapableShouldIncludeSourceOrigin() { assertThat(configurationProperty.getOrigin()).hasToString("TestOrigin key"); } + @Test + void getChildrenOfShouldReturnDirectChildren() { + Map source = new LinkedHashMap<>(); + source.put("foo.bar", "value"); + source.put("foo.baz[0]", "value"); + source.put("foo.baz[1]", "value"); + source.put("foo.qux.deep", "value"); + source.put("faf", "value"); + SpringIterableConfigurationPropertySource adapter = new SpringIterableConfigurationPropertySource( + new OriginCapablePropertySource<>(new MapPropertySource("test", source)), false, + DefaultPropertyMapper.INSTANCE); + assertThat(adapter.getChildrenOf(ConfigurationPropertyName.of("foo"))).containsExactlyInAnyOrder( + ConfigurationPropertyName.of("foo.bar"), ConfigurationPropertyName.of("foo.baz"), + ConfigurationPropertyName.of("foo.qux")); + assertThat(adapter.getChildrenOf(ConfigurationPropertyName.of("foo.baz"))).containsExactlyInAnyOrder( + ConfigurationPropertyName.of("foo.baz[0]"), ConfigurationPropertyName.of("foo.baz[1]")); + assertThat(adapter.getChildrenOf(ConfigurationPropertyName.of("faf"))).isEmpty(); + assertThat(adapter.getChildrenOf(ConfigurationPropertyName.of("missing"))).isEmpty(); + } + + @Test + void getChildrenOfShouldMatchDefaultImplementation() { + Map source = new LinkedHashMap<>(); + source.put("foo.bar-baz[0].name", "value"); + source.put("foo.bar-baz[1].name", "value"); + source.put("foo.bazBar", "value"); + source.put("foo.other", "value"); + SpringIterableConfigurationPropertySource adapter = new SpringIterableConfigurationPropertySource( + new OriginCapablePropertySource<>(new MapPropertySource("test", source)), false, + DefaultPropertyMapper.INSTANCE); + for (String name : new String[] { "", "foo", "foo.bar-baz", "foo.barbaz", "foo.bar-baz[0]", "foo.bazbar" }) { + ConfigurationPropertyName propertyName = ConfigurationPropertyName.of(name); + assertThat(adapter.getChildrenOf(propertyName)).as("children of '%s'", name) + .isEqualTo(defaultGetChildrenOf(adapter, propertyName)); + } + } + + private Set defaultGetChildrenOf(IterableConfigurationPropertySource source, + ConfigurationPropertyName name) { + Set children = new LinkedHashSet<>(); + int childElements = name.getNumberOfElements() + 1; + for (ConfigurationPropertyName candidate : source.filter(name::isAncestorOf)) { + children.add(candidate.chop(childElements)); + } + return children; + } + + @Test + void getChildrenOfShouldCombineChildrenOfEquivalentNames() { + Map source = new LinkedHashMap<>(); + source.put("foo-bar.baz", "x"); + source.put("fooBar.zoo", "y"); + SpringIterableConfigurationPropertySource adapter = new SpringIterableConfigurationPropertySource( + new MapPropertySource("test", source), false, DefaultPropertyMapper.INSTANCE); + Set expected = Set.of(ConfigurationPropertyName.of("foo-bar.baz"), + ConfigurationPropertyName.of("foobar.zoo")); + assertThat(adapter.getChildrenOf(ConfigurationPropertyName.of("foo-bar"))).isEqualTo(expected); + assertThat(adapter.getChildrenOf(ConfigurationPropertyName.of("foobar"))).isEqualTo(expected); + } + + @Test + void getChildrenOfShouldMatchDefaultImplementationForSystemEnvironmentSource() { + Map source = new LinkedHashMap<>(); + source.put("FOO_BARBAZ_BONG", "bing"); + source.put("FOO_BAR_BAZ", "value"); + SystemEnvironmentPropertySource propertySource = new SystemEnvironmentPropertySource( + StandardEnvironment.SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, source); + SpringIterableConfigurationPropertySource adapter = new SpringIterableConfigurationPropertySource( + propertySource, true, SystemEnvironmentPropertyMapper.INSTANCE); + for (String name : new String[] { "", "foo", "foo.bar-baz", "foo.barbaz" }) { + ConfigurationPropertyName propertyName = ConfigurationPropertyName.of(name); + assertThat(adapter.getChildrenOf(propertyName)).as("children of '%s'", name) + .isEqualTo(defaultGetChildrenOf(adapter, propertyName)); + } + } + @Test void containsDescendantOfShouldCheckSourceNames() { Map source = new LinkedHashMap<>();