Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@
import java.util.function.Supplier;

import org.jspecify.annotations.Nullable;

import org.springframework.beans.PropertyEditorRegistry;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.properties.bind.Bindable.BindRestriction;
Expand Down Expand Up @@ -495,6 +494,15 @@ public <T> T bindOrCreate(ConfigurationPropertyName name, Bindable<T> target, @N
return result;
}

private boolean hasDescendantInAnySource(Context context, ConfigurationPropertyName name) {
for (ConfigurationPropertySource source : context.getSources()) {
if (source.containsDescendantOf(name) == ConfigurationPropertyState.PRESENT) {
return true;
}
}
return false;
}

private @Nullable Object bindDataObject(ConfigurationPropertyName name, Bindable<?> target, BindHandler handler,
Context context, boolean allowRecursiveBinding, boolean fallbackToDefaultValue) {
if (isUnbindableBean(name, target, context)) {
Expand All @@ -505,8 +513,13 @@ public <T> T bindOrCreate(ConfigurationPropertyName name, Bindable<T> target, @N
if (!allowRecursiveBinding && context.isBindingDataObject(type)) {
return null;
}
DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> bind(name.append(propertyName),
propertyTarget, handler, context, false, false);
DataObjectPropertyBinder propertyBinder = (propertyName, propertyTarget) -> {
ConfigurationPropertyName fullName = name.append(propertyName);
ConfigurationProperty property = findProperty(fullName, propertyTarget, context);
boolean fromSource = (property != null) || hasDescendantInAnySource(context, fullName);
Object value = bind(fullName, propertyTarget, handler, context, false, false);
return new DataObjectPropertyBinder.PropertyBinding(value, fromSource);
};
Supplier<@Nullable Object> supplier = () -> fromDataObjectBinders(bindMethod,
(dataObjectBinder) -> dataObjectBinder.bind(name, target, context, propertyBinder,
fallbackToDefaultValue));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,22 @@
*/
interface DataObjectPropertyBinder {

/**
* The result of binding a single property.
*
* @param value the bound value or {@code null}
* @param fromSource whether the value was bound from a configuration source
*/
record PropertyBinding(@Nullable Object value, boolean fromSource) {
}

/**
* Bind the given property.
* @param propertyName the property name (in lowercase dashed form, e.g.
* {@code first-name})
* @param target the target bindable
* @return the bound value or {@code null}
* @return the binding result (never {@code null})
*/
@Nullable Object bindProperty(String propertyName, Bindable<?> target);
PropertyBinding bindProperty(String propertyName, Bindable<?> target);

}
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,12 @@ private <T> boolean bind(BeanSupplier<T> beanSupplier, DataObjectPropertyBinder
ResolvableType type = property.getType();
Supplier<Object> value = property.getValue(beanSupplier);
Annotation[] annotations = property.getAnnotations();
Object bound = propertyBinder.bindProperty(propertyName,
DataObjectPropertyBinder.PropertyBinding binding = propertyBinder.bindProperty(propertyName,
Bindable.of(type).withSuppliedValue(value).withAnnotations(annotations));
if (!binding.fromSource()) {
return false;
}
Object bound = binding.value();
if (bound == null) {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,9 @@ class ValueObjectBinder implements DataObjectBinder {
List<@Nullable Object> args = new ArrayList<>(parameters.size());
boolean bound = false;
for (ConstructorParameter parameter : parameters) {
Object arg = parameter.bind(propertyBinder);
bound = bound || arg != null;
DataObjectPropertyBinder.PropertyBinding binding = parameter.bind(propertyBinder);
bound = bound || binding.fromSource();
Object arg = binding.value();
arg = (arg != null) ? arg : getDefaultValue(context, parameter);
args.add(arg);
}
Expand Down Expand Up @@ -384,7 +385,7 @@ private static class ConstructorParameter {
this.annotations = annotations;
}

@Nullable Object bind(DataObjectPropertyBinder propertyBinder) {
DataObjectPropertyBinder.PropertyBinding bind(DataObjectPropertyBinder propertyBinder) {
return propertyBinder.bindProperty(this.name, Bindable.of(this.type).withAnnotations(this.annotations));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

Expand All @@ -34,6 +35,7 @@
import org.springframework.boot.context.properties.bind.JavaBeanBinder.Bean;
import org.springframework.boot.context.properties.bind.JavaBeanBinder.BeanProperty;
import org.springframework.boot.context.properties.bind.handler.IgnoreErrorsBindHandler;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource;
Expand All @@ -44,6 +46,7 @@
import org.springframework.format.annotation.DateTimeFormat;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.entry;

Expand Down Expand Up @@ -1275,4 +1278,76 @@ public String toString() {

}

}
@Test
void bindToListWithOptionalFieldAndNonIterableSourceShouldNotLoopInfinitely() {
ConfigurationPropertySource source = new ConfigurationPropertySource() {
@Override
public @Nullable ConfigurationProperty getConfigurationProperty(ConfigurationPropertyName name) {
return null;
}
};
this.sources.add(source);
assertThatCode(() -> this.binder.bind("foo", Bindable.of(ListOfOptionalExample.class)))
.doesNotThrowAnyException();
}

@Test
void bindToBeanWithOptionalFieldAndActualConfigurationShouldBindValue() {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("foo.optional-value", "hello");
this.sources.add(source);
ExampleWithOptional bound = this.binder.bind("foo", Bindable.of(ExampleWithOptional.class)).get();
assertThat(bound.getOptionalValue()).contains("hello");
}

@Test
void bindToBeanWithOptionalFieldAndNoConfigurationShouldNotBind() {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
this.sources.add(source);
ExampleWithOptional bound = this.binder.bind("foo", Bindable.of(ExampleWithOptional.class)).orElse(null);
assertThat(bound).isNull();
}

static class ListOfOptionalExample {

private List<OptionalItem> items = new ArrayList<>();

List<OptionalItem> getItems() {
return this.items;
}

void setItems(List<OptionalItem> items) {
this.items = items;
}

}

static class OptionalItem {

private Optional<String> regex = Optional.empty();

Optional<String> getRegex() {
return this.regex;
}

void setRegex(Optional<String> regex) {
this.regex = regex;
}

}

static class ExampleWithOptional {

private Optional<String> optionalValue = Optional.empty();

Optional<String> getOptionalValue() {
return this.optionalValue;
}

void setOptionalValue(Optional<String> optionalValue) {
this.optionalValue = optionalValue;
}

}

}