Skip to content

Commit

Permalink
Defer creation of maps in MapBinder
Browse files Browse the repository at this point in the history
Closes gh-39375
  • Loading branch information
mhalbritter committed Apr 29, 2024
1 parent 8a3b0cd commit be50390
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,22 @@ protected boolean isAllowRecursiveBinding(ConfigurationPropertySource source) {
@Override
protected Object bindAggregate(ConfigurationPropertyName name, Bindable<?> target,
AggregateElementBinder elementBinder) {
Map<Object, Object> map = CollectionFactory
.createMap((target.getValue() != null) ? Map.class : target.getType().resolve(Object.class), 0);
Bindable<?> resolvedTarget = resolveTarget(target);
boolean hasDescendants = hasDescendants(name);
for (ConfigurationPropertySource source : getContext().getSources()) {
if (!ConfigurationPropertyName.EMPTY.equals(name)) {
if (!hasDescendants && !ConfigurationPropertyName.EMPTY.equals(name)) {
for (ConfigurationPropertySource source : getContext().getSources()) {
ConfigurationProperty property = source.getConfigurationProperty(name);
if (property != null && !hasDescendants) {
if (property != null) {
getContext().setConfigurationProperty(property);
Object result = getContext().getPlaceholdersResolver().resolvePlaceholders(property.getValue());
return getContext().getConverter().convert(result, target);
}
}
}
Map<Object, Object> map = CollectionFactory
.createMap((target.getValue() != null) ? Map.class : target.getType().resolve(Object.class), 0);
for (ConfigurationPropertySource source : getContext().getSources()) {
if (!ConfigurationPropertyName.EMPTY.equals(name)) {
source = source.filter(name::isAncestorOf);
}
new EntryBinder(name, resolvedTarget, elementBinder).bindEntries(source, map);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
package org.springframework.boot.context.properties.bind;

import java.net.InetAddress;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import org.junit.jupiter.api.Test;
Expand All @@ -33,6 +35,7 @@

import org.springframework.boot.context.properties.bind.BinderTests.ExampleEnum;
import org.springframework.boot.context.properties.bind.BinderTests.JavaBean;
import org.springframework.boot.context.properties.bind.MapBinderTests.CustomMapWithoutDefaultCtor.CustomMap;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
Expand Down Expand Up @@ -78,7 +81,7 @@ class MapBinderTests {

private final List<ConfigurationPropertySource> sources = new ArrayList<>();

private Binder binder = new Binder(this.sources);
private final Binder binder = new Binder(this.sources);

@Test
void bindToMapShouldReturnPopulatedMap() {
Expand Down Expand Up @@ -315,15 +318,13 @@ void bindToMapWithPlaceholdersShouldBeGreedyForScalars() {
TestPropertySourceUtils.addInlinedPropertiesToEnvironment(environment, "foo=boo");
MockConfigurationPropertySource source = new MockConfigurationPropertySource("foo.aaa.bbb.ccc", "baz-${foo}");
this.sources.add(source);
this.binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(environment));
Map<String, ExampleEnum> result = this.binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class))
.get();
Binder binder = new Binder(this.sources, new PropertySourcesPlaceholdersResolver(environment));
Map<String, ExampleEnum> result = binder.bind("foo", Bindable.mapOf(String.class, ExampleEnum.class)).get();
assertThat(result).containsEntry("aaa.bbb.ccc", ExampleEnum.BAZ_BOO);
}

@Test
void bindToMapWithNoPropertiesShouldReturnUnbound() {
this.binder = new Binder(this.sources);
BindResult<Map<String, ExampleEnum>> result = this.binder.bind("foo",
Bindable.mapOf(String.class, ExampleEnum.class));
assertThat(result.isBound()).isFalse();
Expand Down Expand Up @@ -624,6 +625,18 @@ void bindToMapWithPlaceholdersShouldResolve() {
assertThat(map).containsKey("bcd");
}

@Test
void bindToCustomMapWithoutCtorAndConverterShouldResolve() {
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(new CustomMapConverter());
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.custom-map", "value");
this.sources.add(source);
Binder binder = new Binder(this.sources, null, conversionService, null);
CustomMapWithoutDefaultCtor result = binder.bind("foo", Bindable.of(CustomMapWithoutDefaultCtor.class)).get();
assertThat(result.getCustomMap().getSource()).isEqualTo("value");
}

private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric, ResolvableType valueType) {
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
return Bindable.of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType));
Expand Down Expand Up @@ -761,6 +774,48 @@ void setAddresses(Map<String, ? extends List<? extends InetAddress>> addresses)

}

static class CustomMapWithoutDefaultCtor {

private final CustomMap customMap;

CustomMapWithoutDefaultCtor(CustomMap customMap) {
this.customMap = customMap;
}

CustomMap getCustomMap() {
return this.customMap;
}

static final class CustomMap extends AbstractMap<String, Object> {

private final String source;

CustomMap(String source) {
this.source = source;
}

@Override
public Set<Entry<String, Object>> entrySet() {
return Collections.emptySet();
}

String getSource() {
return this.source;
}

}

}

private static final class CustomMapConverter implements Converter<String, CustomMap> {

@Override
public CustomMap convert(String source) {
return new CustomMap(source);
}

}

private static final class InvocationArgument<T> implements Answer<T> {

private final int index;
Expand Down

0 comments on commit be50390

Please sign in to comment.