Merge branch '2.2.x' into 2.3.x

Closes gh-23260
pull/23585/head
Phillip Webb 4 years ago
commit 5294c34807

@ -35,14 +35,29 @@ public abstract class DataObjectPropertyName {
* @return the dashed from
*/
public static String toDashedForm(String name) {
StringBuilder result = new StringBuilder();
String replaced = name.replace('_', '-');
for (int i = 0; i < replaced.length(); i++) {
char ch = replaced.charAt(i);
if (Character.isUpperCase(ch) && result.length() > 0 && result.charAt(result.length() - 1) != '-') {
result.append('-');
StringBuilder result = new StringBuilder(name.length());
boolean inIndex = false;
for (int i = 0; i < name.length(); i++) {
char ch = name.charAt(i);
if (inIndex) {
result.append(ch);
if (ch == ']') {
inIndex = false;
}
}
else {
if (ch == '[') {
inIndex = true;
result.append(ch);
}
else {
ch = (ch != '_') ? ch : '-';
if (Character.isUpperCase(ch) && result.length() > 0 && result.charAt(result.length() - 1) != '-') {
result.append('-');
}
result.append(Character.toLowerCase(ch));
}
}
result.append(Character.toLowerCase(ch));
}
return result.toString();
}

@ -29,7 +29,9 @@ import org.springframework.boot.context.properties.bind.Bindable;
import org.springframework.boot.context.properties.bind.DataObjectPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationProperty;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form;
import org.springframework.core.ResolvableType;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.AbstractBindingResult;
import org.springframework.validation.Validator;
@ -165,28 +167,53 @@ public class ValidationBindHandler extends AbstractBindHandler {
@Override
public Class<?> getFieldType(String field) {
try {
ResolvableType type = ValidationBindHandler.this.boundTypes.get(getName(field));
Class<?> resolved = (type != null) ? type.resolve() : null;
if (resolved != null) {
return resolved;
}
}
catch (Exception ex) {
ResolvableType type = getBoundField(ValidationBindHandler.this.boundTypes, field);
Class<?> resolved = (type != null) ? type.resolve() : null;
if (resolved != null) {
return resolved;
}
return super.getFieldType(field);
}
@Override
protected Object getActualFieldValue(String field) {
return getBoundField(ValidationBindHandler.this.boundResults, field);
}
private <T> T getBoundField(Map<ConfigurationPropertyName, T> boundFields, String field) {
try {
return ValidationBindHandler.this.boundResults.get(getName(field));
ConfigurationPropertyName name = getName(field);
T bound = boundFields.get(name);
if (bound != null) {
return bound;
}
if (name.hasIndexedElement()) {
for (Map.Entry<ConfigurationPropertyName, T> entry : boundFields.entrySet()) {
if (isFieldNameMatch(entry.getKey(), name)) {
return entry.getValue();
}
}
}
}
catch (Exception ex) {
}
return null;
}
private boolean isFieldNameMatch(ConfigurationPropertyName name, ConfigurationPropertyName fieldName) {
if (name.getNumberOfElements() != fieldName.getNumberOfElements()) {
return false;
}
for (int i = 0; i < name.getNumberOfElements(); i++) {
String element = name.getElement(i, Form.ORIGINAL);
String fieldElement = fieldName.getElement(i, Form.ORIGINAL);
if (!ObjectUtils.nullSafeEquals(element, fieldElement)) {
return false;
}
}
return true;
}
private ConfigurationPropertyName getName(String field) {
return this.name.append(DataObjectPropertyName.toDashedForm(field));
}

@ -88,6 +88,20 @@ public final class ConfigurationPropertyName implements Comparable<Configuration
return (size > 0 && isIndexed(size - 1));
}
/**
* Return {@code true} if any element in the name is indexed.
* @return if the element has one or more indexed elements
* @since 2.2.10
*/
public boolean hasIndexedElement() {
for (int i = 0; i < getNumberOfElements(); i++) {
if (isIndexed(i)) {
return true;
}
}
return false;
}
/**
* Return if the element in the name is indexed.
* @param elementIndex the index of the element

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class DataObjectPropertyNameTests {
@Test
void toDashedCaseShouldConvertValue() {
void toDashedCaseConvertsValue() {
assertThat(DataObjectPropertyName.toDashedForm("Foo")).isEqualTo("foo");
assertThat(DataObjectPropertyName.toDashedForm("foo")).isEqualTo("foo");
assertThat(DataObjectPropertyName.toDashedForm("fooBar")).isEqualTo("foo-bar");
@ -38,4 +38,10 @@ class DataObjectPropertyNameTests {
assertThat(DataObjectPropertyName.toDashedForm("foo_Bar")).isEqualTo("foo-bar");
}
@Test
void toDashedFormWhenContainsIndexedAddsNoDashToIndex() throws Exception {
assertThat(DataObjectPropertyName.toDashedForm("test[fooBar]")).isEqualTo("test[fooBar]");
assertThat(DataObjectPropertyName.toDashedForm("testAgain[fooBar]")).isEqualTo("test-again[fooBar]");
}
}

@ -17,7 +17,9 @@
package org.springframework.boot.context.properties.bind.validation;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.validation.Valid;
@ -35,11 +37,16 @@ import org.springframework.boot.context.properties.bind.Binder;
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.ConfigurationPropertySources;
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource;
import org.springframework.boot.origin.Origin;
import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.env.MapPropertySource;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@ -210,6 +217,54 @@ class ValidationBindHandlerTests {
assertThat(fieldError.getField()).isEqualTo("personAge");
}
@Test
void validateMapValues() throws Exception {
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
source.put("test.items.[itemOne].number", "one");
source.put("test.items.[ITEM2].number", "two");
this.sources.add(source);
Validator validator = getMapValidator();
this.handler = new ValidationBindHandler(validator);
this.binder.bind(ConfigurationPropertyName.of("test"), Bindable.of(ExampleWithMap.class), this.handler);
}
@Test
void validateMapValuesWithNonUniformSource() throws Exception {
Map<String, Object> map = new LinkedHashMap<>();
map.put("test.items.itemOne.number", "one");
map.put("test.items.ITEM2.number", "two");
this.sources.add(ConfigurationPropertySources.from(new MapPropertySource("test", map)).iterator().next());
Validator validator = getMapValidator();
this.handler = new ValidationBindHandler(validator);
this.binder.bind(ConfigurationPropertyName.of("test"), Bindable.of(ExampleWithMap.class), this.handler);
}
private Validator getMapValidator() {
return new Validator() {
@Override
public boolean supports(Class<?> clazz) {
return ExampleWithMap.class == clazz;
}
@Override
public void validate(Object target, Errors errors) {
ExampleWithMap value = (ExampleWithMap) target;
value.getItems().forEach((k, v) -> {
try {
errors.pushNestedPath("items[" + k + "]");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "number", "NUMBER_ERR");
}
finally {
errors.popNestedPath();
}
});
}
};
}
private BindValidationException bindAndExpectValidationError(Runnable action) {
try {
action.run();
@ -358,6 +413,30 @@ class ValidationBindHandlerTests {
}
static class ExampleWithMap {
private Map<String, ExampleMapValue> items = new LinkedHashMap<>();
Map<String, ExampleMapValue> getItems() {
return this.items;
}
}
static class ExampleMapValue {
private String number;
String getNumber() {
return this.number;
}
void setNumber(String number) {
this.number = number;
}
}
static class TestHandler extends AbstractBindHandler {
private Object result;

@ -669,4 +669,14 @@ class ConfigurationPropertyNameTests {
assertThat(ReflectionTestUtils.getField(name, "hashCode")).isEqualTo(hashCode);
}
@Test
void hasIndexedElementWhenHasIndexedElementReturnsTrue() throws Exception {
assertThat(ConfigurationPropertyName.of("foo[bar]").hasIndexedElement()).isTrue();
}
@Test
void hasIndexedElementWhenHasNoIndexedElementReturnsFalse() throws Exception {
assertThat(ConfigurationPropertyName.of("foo.bar").hasIndexedElement()).isFalse();
}
}

Loading…
Cancel
Save