Skip to content

Commit 5294c34

Browse files
committed
Merge branch '2.2.x' into 2.3.x
Closes gh-23260
2 parents f5ae58e + 326a56d commit 5294c34

File tree

6 files changed

+169
-18
lines changed

6 files changed

+169
-18
lines changed

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/DataObjectPropertyName.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,29 @@ private DataObjectPropertyName() {
3535
* @return the dashed from
3636
*/
3737
public static String toDashedForm(String name) {
38-
StringBuilder result = new StringBuilder();
39-
String replaced = name.replace('_', '-');
40-
for (int i = 0; i < replaced.length(); i++) {
41-
char ch = replaced.charAt(i);
42-
if (Character.isUpperCase(ch) && result.length() > 0 && result.charAt(result.length() - 1) != '-') {
43-
result.append('-');
38+
StringBuilder result = new StringBuilder(name.length());
39+
boolean inIndex = false;
40+
for (int i = 0; i < name.length(); i++) {
41+
char ch = name.charAt(i);
42+
if (inIndex) {
43+
result.append(ch);
44+
if (ch == ']') {
45+
inIndex = false;
46+
}
47+
}
48+
else {
49+
if (ch == '[') {
50+
inIndex = true;
51+
result.append(ch);
52+
}
53+
else {
54+
ch = (ch != '_') ? ch : '-';
55+
if (Character.isUpperCase(ch) && result.length() > 0 && result.charAt(result.length() - 1) != '-') {
56+
result.append('-');
57+
}
58+
result.append(Character.toLowerCase(ch));
59+
}
4460
}
45-
result.append(Character.toLowerCase(ch));
4661
}
4762
return result.toString();
4863
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandler.java

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
import org.springframework.boot.context.properties.bind.DataObjectPropertyName;
3030
import org.springframework.boot.context.properties.source.ConfigurationProperty;
3131
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
32+
import org.springframework.boot.context.properties.source.ConfigurationPropertyName.Form;
3233
import org.springframework.core.ResolvableType;
34+
import org.springframework.util.ObjectUtils;
3335
import org.springframework.validation.AbstractBindingResult;
3436
import org.springframework.validation.Validator;
3537

@@ -165,28 +167,53 @@ public Object getTarget() {
165167

166168
@Override
167169
public Class<?> getFieldType(String field) {
168-
try {
169-
ResolvableType type = ValidationBindHandler.this.boundTypes.get(getName(field));
170-
Class<?> resolved = (type != null) ? type.resolve() : null;
171-
if (resolved != null) {
172-
return resolved;
173-
}
174-
}
175-
catch (Exception ex) {
170+
ResolvableType type = getBoundField(ValidationBindHandler.this.boundTypes, field);
171+
Class<?> resolved = (type != null) ? type.resolve() : null;
172+
if (resolved != null) {
173+
return resolved;
176174
}
177175
return super.getFieldType(field);
178176
}
179177

180178
@Override
181179
protected Object getActualFieldValue(String field) {
180+
return getBoundField(ValidationBindHandler.this.boundResults, field);
181+
}
182+
183+
private <T> T getBoundField(Map<ConfigurationPropertyName, T> boundFields, String field) {
182184
try {
183-
return ValidationBindHandler.this.boundResults.get(getName(field));
185+
ConfigurationPropertyName name = getName(field);
186+
T bound = boundFields.get(name);
187+
if (bound != null) {
188+
return bound;
189+
}
190+
if (name.hasIndexedElement()) {
191+
for (Map.Entry<ConfigurationPropertyName, T> entry : boundFields.entrySet()) {
192+
if (isFieldNameMatch(entry.getKey(), name)) {
193+
return entry.getValue();
194+
}
195+
}
196+
}
184197
}
185198
catch (Exception ex) {
186199
}
187200
return null;
188201
}
189202

203+
private boolean isFieldNameMatch(ConfigurationPropertyName name, ConfigurationPropertyName fieldName) {
204+
if (name.getNumberOfElements() != fieldName.getNumberOfElements()) {
205+
return false;
206+
}
207+
for (int i = 0; i < name.getNumberOfElements(); i++) {
208+
String element = name.getElement(i, Form.ORIGINAL);
209+
String fieldElement = fieldName.getElement(i, Form.ORIGINAL);
210+
if (!ObjectUtils.nullSafeEquals(element, fieldElement)) {
211+
return false;
212+
}
213+
}
214+
return true;
215+
}
216+
190217
private ConfigurationPropertyName getName(String field) {
191218
return this.name.append(DataObjectPropertyName.toDashedForm(field));
192219
}

spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,20 @@ public boolean isLastElementIndexed() {
8888
return (size > 0 && isIndexed(size - 1));
8989
}
9090

91+
/**
92+
* Return {@code true} if any element in the name is indexed.
93+
* @return if the element has one or more indexed elements
94+
* @since 2.2.10
95+
*/
96+
public boolean hasIndexedElement() {
97+
for (int i = 0; i < getNumberOfElements(); i++) {
98+
if (isIndexed(i)) {
99+
return true;
100+
}
101+
}
102+
return false;
103+
}
104+
91105
/**
92106
* Return if the element in the name is indexed.
93107
* @param elementIndex the index of the element

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/DataObjectPropertyNameTests.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2019 the original author or authors.
2+
* Copyright 2012-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@
2929
class DataObjectPropertyNameTests {
3030

3131
@Test
32-
void toDashedCaseShouldConvertValue() {
32+
void toDashedCaseConvertsValue() {
3333
assertThat(DataObjectPropertyName.toDashedForm("Foo")).isEqualTo("foo");
3434
assertThat(DataObjectPropertyName.toDashedForm("foo")).isEqualTo("foo");
3535
assertThat(DataObjectPropertyName.toDashedForm("fooBar")).isEqualTo("foo-bar");
@@ -38,4 +38,10 @@ void toDashedCaseShouldConvertValue() {
3838
assertThat(DataObjectPropertyName.toDashedForm("foo_Bar")).isEqualTo("foo-bar");
3939
}
4040

41+
@Test
42+
void toDashedFormWhenContainsIndexedAddsNoDashToIndex() throws Exception {
43+
assertThat(DataObjectPropertyName.toDashedForm("test[fooBar]")).isEqualTo("test[fooBar]");
44+
assertThat(DataObjectPropertyName.toDashedForm("testAgain[fooBar]")).isEqualTo("test-again[fooBar]");
45+
}
46+
4147
}

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/bind/validation/ValidationBindHandlerTests.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package org.springframework.boot.context.properties.bind.validation;
1818

1919
import java.util.ArrayList;
20+
import java.util.LinkedHashMap;
2021
import java.util.List;
22+
import java.util.Map;
2123
import java.util.Set;
2224

2325
import javax.validation.Valid;
@@ -35,11 +37,16 @@
3537
import org.springframework.boot.context.properties.source.ConfigurationProperty;
3638
import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
3739
import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
40+
import org.springframework.boot.context.properties.source.ConfigurationPropertySources;
3841
import org.springframework.boot.context.properties.source.MockConfigurationPropertySource;
3942
import org.springframework.boot.origin.Origin;
4043
import org.springframework.core.convert.ConverterNotFoundException;
44+
import org.springframework.core.env.MapPropertySource;
45+
import org.springframework.validation.Errors;
4146
import org.springframework.validation.FieldError;
4247
import org.springframework.validation.ObjectError;
48+
import org.springframework.validation.ValidationUtils;
49+
import org.springframework.validation.Validator;
4350
import org.springframework.validation.annotation.Validated;
4451
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
4552

@@ -210,6 +217,54 @@ void validationShouldBeSkippedIfPreviousValidationErrorPresent() {
210217
assertThat(fieldError.getField()).isEqualTo("personAge");
211218
}
212219

220+
@Test
221+
void validateMapValues() throws Exception {
222+
MockConfigurationPropertySource source = new MockConfigurationPropertySource();
223+
source.put("test.items.[itemOne].number", "one");
224+
source.put("test.items.[ITEM2].number", "two");
225+
this.sources.add(source);
226+
Validator validator = getMapValidator();
227+
this.handler = new ValidationBindHandler(validator);
228+
this.binder.bind(ConfigurationPropertyName.of("test"), Bindable.of(ExampleWithMap.class), this.handler);
229+
}
230+
231+
@Test
232+
void validateMapValuesWithNonUniformSource() throws Exception {
233+
Map<String, Object> map = new LinkedHashMap<>();
234+
map.put("test.items.itemOne.number", "one");
235+
map.put("test.items.ITEM2.number", "two");
236+
this.sources.add(ConfigurationPropertySources.from(new MapPropertySource("test", map)).iterator().next());
237+
Validator validator = getMapValidator();
238+
this.handler = new ValidationBindHandler(validator);
239+
this.binder.bind(ConfigurationPropertyName.of("test"), Bindable.of(ExampleWithMap.class), this.handler);
240+
}
241+
242+
private Validator getMapValidator() {
243+
return new Validator() {
244+
245+
@Override
246+
public boolean supports(Class<?> clazz) {
247+
return ExampleWithMap.class == clazz;
248+
249+
}
250+
251+
@Override
252+
public void validate(Object target, Errors errors) {
253+
ExampleWithMap value = (ExampleWithMap) target;
254+
value.getItems().forEach((k, v) -> {
255+
try {
256+
errors.pushNestedPath("items[" + k + "]");
257+
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "number", "NUMBER_ERR");
258+
}
259+
finally {
260+
errors.popNestedPath();
261+
}
262+
});
263+
}
264+
265+
};
266+
}
267+
213268
private BindValidationException bindAndExpectValidationError(Runnable action) {
214269
try {
215270
action.run();
@@ -358,6 +413,30 @@ int getAge() {
358413

359414
}
360415

416+
static class ExampleWithMap {
417+
418+
private Map<String, ExampleMapValue> items = new LinkedHashMap<>();
419+
420+
Map<String, ExampleMapValue> getItems() {
421+
return this.items;
422+
}
423+
424+
}
425+
426+
static class ExampleMapValue {
427+
428+
private String number;
429+
430+
String getNumber() {
431+
return this.number;
432+
}
433+
434+
void setNumber(String number) {
435+
this.number = number;
436+
}
437+
438+
}
439+
361440
static class TestHandler extends AbstractBindHandler {
362441

363442
private Object result;

spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,4 +669,14 @@ void hashCodeIsStored() {
669669
assertThat(ReflectionTestUtils.getField(name, "hashCode")).isEqualTo(hashCode);
670670
}
671671

672+
@Test
673+
void hasIndexedElementWhenHasIndexedElementReturnsTrue() throws Exception {
674+
assertThat(ConfigurationPropertyName.of("foo[bar]").hasIndexedElement()).isTrue();
675+
}
676+
677+
@Test
678+
void hasIndexedElementWhenHasNoIndexedElementReturnsFalse() throws Exception {
679+
assertThat(ConfigurationPropertyName.of("foo.bar").hasIndexedElement()).isFalse();
680+
}
681+
672682
}

0 commit comments

Comments
 (0)