Allow recursive binding in Maps
Update `Binder` so that Maps containing references to themselves may be bound. The existing stack-overflow protection (required when binding a bean to a non enumerable source) now only applies to bean properties. Fixes gh-9801
This commit is contained in:
parent
3ec3b64d45
commit
f337323819
|
@ -183,18 +183,18 @@ public class Binder {
|
||||||
Assert.notNull(target, "Target must not be null");
|
Assert.notNull(target, "Target must not be null");
|
||||||
handler = (handler != null ? handler : BindHandler.DEFAULT);
|
handler = (handler != null ? handler : BindHandler.DEFAULT);
|
||||||
Context context = new Context();
|
Context context = new Context();
|
||||||
T bound = bind(name, target, handler, context);
|
T bound = bind(name, target, handler, context, false);
|
||||||
return BindResult.of(bound);
|
return BindResult.of(bound);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
|
protected final <T> T bind(ConfigurationPropertyName name, Bindable<T> target,
|
||||||
BindHandler handler, Context context) {
|
BindHandler handler, Context context, boolean skipIfHasBoundBean) {
|
||||||
context.clearConfigurationProperty();
|
context.clearConfigurationProperty();
|
||||||
try {
|
try {
|
||||||
if (!handler.onStart(name, target, context)) {
|
if (!handler.onStart(name, target, context)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Object bound = bindObject(name, target, handler, context);
|
Object bound = bindObject(name, target, handler, context, skipIfHasBoundBean);
|
||||||
return handleBindResult(name, target, handler, context, bound);
|
return handleBindResult(name, target, handler, context, bound);
|
||||||
}
|
}
|
||||||
catch (Exception ex) {
|
catch (Exception ex) {
|
||||||
|
@ -234,7 +234,8 @@ public class Binder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target,
|
private <T> Object bindObject(ConfigurationPropertyName name, Bindable<T> target,
|
||||||
BindHandler handler, Context context) throws Exception {
|
BindHandler handler, Context context, boolean skipIfHasBoundBean)
|
||||||
|
throws Exception {
|
||||||
ConfigurationProperty property = findProperty(name, context);
|
ConfigurationProperty property = findProperty(name, context);
|
||||||
if (property == null && containsNoDescendantOf(context.streamSources(), name)) {
|
if (property == null && containsNoDescendantOf(context.streamSources(), name)) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -246,7 +247,7 @@ public class Binder {
|
||||||
if (property != null) {
|
if (property != null) {
|
||||||
return bindProperty(name, target, handler, context, property);
|
return bindProperty(name, target, handler, context, property);
|
||||||
}
|
}
|
||||||
return bindBean(name, target, handler, context);
|
return bindBean(name, target, handler, context, skipIfHasBoundBean);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AggregateBinder<?> getAggregateBinder(Bindable<?> target, Context context) {
|
private AggregateBinder<?> getAggregateBinder(Bindable<?> target, Context context) {
|
||||||
|
@ -266,7 +267,8 @@ public class Binder {
|
||||||
private <T> Object bindAggregate(ConfigurationPropertyName name, Bindable<T> target,
|
private <T> Object bindAggregate(ConfigurationPropertyName name, Bindable<T> target,
|
||||||
BindHandler handler, Context context, AggregateBinder<?> aggregateBinder) {
|
BindHandler handler, Context context, AggregateBinder<?> aggregateBinder) {
|
||||||
AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> {
|
AggregateElementBinder elementBinder = (itemName, itemTarget, source) -> {
|
||||||
Supplier<?> supplier = () -> bind(itemName, itemTarget, handler, context);
|
Supplier<?> supplier = () -> bind(itemName, itemTarget, handler, context,
|
||||||
|
false);
|
||||||
return context.withSource(source, supplier);
|
return context.withSource(source, supplier);
|
||||||
};
|
};
|
||||||
return context.withIncreasedDepth(
|
return context.withIncreasedDepth(
|
||||||
|
@ -290,15 +292,16 @@ public class Binder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object bindBean(ConfigurationPropertyName name, Bindable<?> target,
|
private Object bindBean(ConfigurationPropertyName name, Bindable<?> target,
|
||||||
BindHandler handler, Context context) {
|
BindHandler handler, Context context, boolean skipIfHasBoundBean) {
|
||||||
if (containsNoDescendantOf(context.streamSources(), name)
|
if (containsNoDescendantOf(context.streamSources(), name)
|
||||||
|| isUnbindableBean(name, target, context)) {
|
|| isUnbindableBean(name, target, context)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(
|
BeanPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(
|
||||||
name.append(propertyName), propertyTarget, handler, context);
|
name.append(propertyName), propertyTarget, handler, context, true);
|
||||||
Class<?> type = target.getType().resolve();
|
Class<?> type = target.getType().resolve();
|
||||||
if (context.hasBoundBean(type)) {
|
if (skipIfHasBoundBean && context.hasBoundBean(type)) {
|
||||||
|
System.err.println(type + " " + name);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return context.withBean(type, () -> {
|
return context.withBean(type, () -> {
|
||||||
|
|
|
@ -251,10 +251,12 @@ public class BinderTests {
|
||||||
@Test
|
@Test
|
||||||
public void bindToValidatedBeanWithResourceAndNonEnumerablePropertySource() {
|
public void bindToValidatedBeanWithResourceAndNonEnumerablePropertySource() {
|
||||||
ConfigurationPropertySources.from(new PropertySource<String>("test") {
|
ConfigurationPropertySources.from(new PropertySource<String>("test") {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getProperty(String name) {
|
public Object getProperty(String name) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}).forEach(this.sources::add);
|
}).forEach(this.sources::add);
|
||||||
Validator validator = new SpringValidatorAdapter(Validation.byDefaultProvider()
|
Validator validator = new SpringValidatorAdapter(Validation.byDefaultProvider()
|
||||||
.configure().buildValidatorFactory().getValidator());
|
.configure().buildValidatorFactory().getValidator());
|
||||||
|
@ -262,6 +264,14 @@ public class BinderTests {
|
||||||
new ValidationBindHandler(validator));
|
new ValidationBindHandler(validator));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void bindToBeanWithCycle() throws Exception {
|
||||||
|
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||||
|
this.sources.add(source.nonIterable());
|
||||||
|
Bindable<CycleBean1> target = Bindable.of(CycleBean1.class);
|
||||||
|
this.binder.bind("foo", target);
|
||||||
|
}
|
||||||
|
|
||||||
public static class JavaBean {
|
public static class JavaBean {
|
||||||
|
|
||||||
private String value;
|
private String value;
|
||||||
|
@ -303,4 +313,32 @@ public class BinderTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class CycleBean1 {
|
||||||
|
|
||||||
|
private CycleBean2 two;
|
||||||
|
|
||||||
|
public CycleBean2 getTwo() {
|
||||||
|
return this.two;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTwo(CycleBean2 two) {
|
||||||
|
this.two = two;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class CycleBean2 {
|
||||||
|
|
||||||
|
private CycleBean1 one;
|
||||||
|
|
||||||
|
public CycleBean1 getOne() {
|
||||||
|
return this.one;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOne(CycleBean1 one) {
|
||||||
|
this.one = one;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.springframework.boot.context.properties.bind;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
@ -515,6 +516,19 @@ public class MapBinderTests {
|
||||||
assertThat(map).containsEntry("x [B] y", "[ball]");
|
assertThat(map).containsEntry("x [B] y", "[ball]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void nestedMapsShouldNotBindToNull() throws Exception {
|
||||||
|
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
|
||||||
|
source.put("foo.value", "one");
|
||||||
|
source.put("foo.foos.foo1.value", "two");
|
||||||
|
source.put("foo.foos.foo2.value", "three");
|
||||||
|
this.sources.add(source);
|
||||||
|
BindResult<NestableFoo> foo = this.binder.bind("foo", NestableFoo.class);
|
||||||
|
assertThat(foo.get().getValue()).isNotNull();
|
||||||
|
assertThat(foo.get().getFoos().get("foo1").getValue()).isEqualTo("two");
|
||||||
|
assertThat(foo.get().getFoos().get("foo2").getValue()).isEqualTo("three");
|
||||||
|
}
|
||||||
|
|
||||||
private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric,
|
private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric,
|
||||||
ResolvableType valueType) {
|
ResolvableType valueType) {
|
||||||
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
|
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
|
||||||
|
@ -547,4 +561,24 @@ public class MapBinderTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class NestableFoo {
|
||||||
|
|
||||||
|
private Map<String, NestableFoo> foos = new LinkedHashMap<>();
|
||||||
|
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public Map<String, NestableFoo> getFoos() {
|
||||||
|
return this.foos;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return this.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue