Skip to content

Commit b3d8e32

Browse files
dsyerphilwebb
authored andcommitted
Fix overlapping property name binding to Maps
Update RelaxedDataBinder so that multiple overlapping nested property names can be bound to a Map. Prior to this commit, properties of the following form could not be bound to Maps: foo: baz foo.bar: spam This was due to BeanWrapperImpl throwing an InvalidPropertyException when binding `map[foo][bar]` because `foo` is already bound to `baz`. The updated code now detects such cases and instead uses the binding property `map[foo.bar]`. Fixes gh-2610
1 parent e3f203a commit b3d8e32

File tree

2 files changed

+138
-29
lines changed

2 files changed

+138
-29
lines changed

spring-boot/src/main/java/org/springframework/boot/bind/RelaxedDataBinder.java

Lines changed: 96 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Collection;
2222
import java.util.Collections;
2323
import java.util.LinkedHashMap;
24+
import java.util.LinkedList;
2425
import java.util.List;
2526
import java.util.Map;
2627

@@ -46,6 +47,8 @@
4647
*/
4748
public class RelaxedDataBinder extends DataBinder {
4849

50+
private static final Object BLANK = new Object();
51+
4952
private String namePrefix;
5053

5154
private boolean ignoreNestedProperties;
@@ -112,11 +115,10 @@ public void initBeanPropertyAccess() {
112115

113116
@Override
114117
protected void doBind(MutablePropertyValues propertyValues) {
115-
propertyValues = modifyProperties(propertyValues, getTarget());
116118
// Harmless additional property editor comes in very handy sometimes...
117119
getPropertyEditorRegistry().registerCustomEditor(InetAddress.class,
118120
new InetAddressEditor());
119-
super.doBind(propertyValues);
121+
super.doBind(modifyProperties(propertyValues, getTarget()));
120122
}
121123

122124
/**
@@ -129,22 +131,52 @@ protected void doBind(MutablePropertyValues propertyValues) {
129131
*/
130132
private MutablePropertyValues modifyProperties(MutablePropertyValues propertyValues,
131133
Object target) {
132-
133134
propertyValues = getPropertyValuesForNamePrefix(propertyValues);
134-
135135
if (target instanceof MapHolder) {
136136
propertyValues = addMapPrefix(propertyValues);
137137
}
138-
139138
BeanWrapper wrapper = new BeanWrapperImpl(target);
140139
wrapper.setConversionService(new RelaxedConversionService(getConversionService()));
141140
wrapper.setAutoGrowNestedPaths(true);
141+
List<PropertyValue> sortedValues = new ArrayList<PropertyValue>();
142+
List<String> sortedNames = getSortedPropertyNames(propertyValues);
143+
for (String name : sortedNames) {
144+
sortedValues.add(modifyProperty(wrapper,
145+
propertyValues.getPropertyValue(name)));
146+
}
147+
return new MutablePropertyValues(sortedValues);
148+
}
149+
150+
private List<String> getSortedPropertyNames(MutablePropertyValues propertyValues) {
151+
List<String> names = new LinkedList<String>();
152+
for (PropertyValue propertyValue : propertyValues.getPropertyValueList()) {
153+
names.add(propertyValue.getName());
154+
}
155+
sortPropertyNames(names);
156+
return names;
157+
}
142158

143-
List<PropertyValue> list = propertyValues.getPropertyValueList();
144-
for (int i = 0; i < list.size(); i++) {
145-
modifyProperty(propertyValues, wrapper, list.get(i), i);
159+
/**
160+
* Sort by name so that parent properties get processed first (e.g. 'foo.bar' before
161+
* 'foo.bar.spam'). Don't use Collections.sort() because the order might be
162+
* significant for other property names (it shouldn't be but who knows what people
163+
* might be relying on, e.g. HSQL has a JDBCXADataSource where "databaseName" is a
164+
* synonym for "url").
165+
* @param names the names to sort
166+
*/
167+
private void sortPropertyNames(List<String> names) {
168+
for (String name : new ArrayList<String>(names)) {
169+
int propertyIndex = names.indexOf(name);
170+
BeanPath path = new BeanPath(name);
171+
for (String prefix : path.prefixes()) {
172+
int prefixIndex = names.indexOf(prefix);
173+
if (prefixIndex >= propertyIndex) {
174+
// The child property has a parent in the list in the wrong order
175+
names.remove(name);
176+
names.add(prefixIndex, name);
177+
}
178+
}
146179
}
147-
return propertyValues;
148180
}
149181

150182
private MutablePropertyValues addMapPrefix(MutablePropertyValues propertyValues) {
@@ -175,14 +207,13 @@ private MutablePropertyValues getPropertyValuesForNamePrefix(
175207
return rtn;
176208
}
177209

178-
private void modifyProperty(MutablePropertyValues propertyValues, BeanWrapper target,
179-
PropertyValue propertyValue, int index) {
180-
String oldName = propertyValue.getName();
181-
String name = normalizePath(target, oldName);
182-
if (!name.equals(oldName)) {
183-
propertyValues.setPropertyValueAt(
184-
new PropertyValue(name, propertyValue.getValue()), index);
210+
private PropertyValue modifyProperty(BeanWrapper target, PropertyValue propertyValue) {
211+
String name = propertyValue.getName();
212+
String normalizedName = normalizePath(target, name);
213+
if (!normalizedName.equals(name)) {
214+
return new PropertyValue(normalizedName, propertyValue.getValue());
185215
}
216+
return propertyValue;
186217
}
187218

188219
/**
@@ -214,15 +245,9 @@ private String initializePath(BeanWrapper wrapper, BeanPath path, int index) {
214245
String name = path.prefix(index);
215246
TypeDescriptor descriptor = wrapper.getPropertyTypeDescriptor(name);
216247
if (descriptor == null || descriptor.isMap()) {
217-
if (descriptor != null) {
218-
TypeDescriptor valueDescriptor = descriptor.getMapValueTypeDescriptor();
219-
if (valueDescriptor != null) {
220-
Class<?> valueType = valueDescriptor.getObjectType();
221-
if (valueType != null
222-
&& CharSequence.class.isAssignableFrom(valueType)) {
223-
path.collapseKeys(index);
224-
}
225-
}
248+
if (isMapValueStringType(descriptor)
249+
|| isBlanked(wrapper, name, path.name(index))) {
250+
path.collapseKeys(index);
226251
}
227252
path.mapIndex(index);
228253
extendMapIfNecessary(wrapper, path, index);
@@ -231,16 +256,43 @@ else if (descriptor.isCollection()) {
231256
extendCollectionIfNecessary(wrapper, path, index);
232257
}
233258
else if (descriptor.getType().equals(Object.class)) {
259+
if (isBlanked(wrapper, name, path.name(index))) {
260+
path.collapseKeys(index);
261+
}
234262
path.mapIndex(index);
235-
String next = path.prefix(index + 1);
236-
if (wrapper.getPropertyValue(next) == null) {
237-
wrapper.setPropertyValue(next, new LinkedHashMap<String, Object>());
263+
if (path.isLastNode(index)) {
264+
wrapper.setPropertyValue(path.toString(), BLANK);
265+
}
266+
else {
267+
String next = path.prefix(index + 1);
268+
if (wrapper.getPropertyValue(next) == null) {
269+
wrapper.setPropertyValue(next, new LinkedHashMap<String, Object>());
270+
}
238271
}
239272
}
240-
241273
return initializePath(wrapper, path, index);
242274
}
243275

276+
private boolean isMapValueStringType(TypeDescriptor descriptor) {
277+
if (descriptor == null || descriptor.getMapValueTypeDescriptor() == null) {
278+
return false;
279+
}
280+
Class<?> valueType = descriptor.getMapValueTypeDescriptor().getObjectType();
281+
return (valueType != null && CharSequence.class.isAssignableFrom(valueType));
282+
}
283+
284+
@SuppressWarnings("rawtypes")
285+
private boolean isBlanked(BeanWrapper wrapper, String propertyName, String key) {
286+
Object value = (wrapper.isReadableProperty(propertyName) ? wrapper
287+
.getPropertyValue(propertyName) : null);
288+
if (value instanceof Map) {
289+
if (((Map) value).get(key) == BLANK) {
290+
return true;
291+
}
292+
}
293+
return false;
294+
}
295+
244296
private void extendCollectionIfNecessary(BeanWrapper wrapper, BeanPath path, int index) {
245297
String name = path.prefix(index);
246298
TypeDescriptor elementDescriptor = wrapper.getPropertyTypeDescriptor(name)
@@ -282,6 +334,9 @@ private void extendMapIfNecessary(BeanWrapper wrapper, BeanPath path, int index)
282334
if (descriptor.isCollection()) {
283335
extend = new ArrayList<Object>();
284336
}
337+
if (descriptor.getType().equals(Object.class) && path.isLastNode(index)) {
338+
extend = BLANK;
339+
}
285340
wrapper.setPropertyValue(extensionName, extend);
286341
}
287342

@@ -382,6 +437,18 @@ public BeanPath(String path) {
382437
this.nodes = splitPath(path);
383438
}
384439

440+
public List<String> prefixes() {
441+
List<String> prefixes = new ArrayList<String>();
442+
for (int index = 1; index < this.nodes.size(); index++) {
443+
prefixes.add(prefix(index));
444+
}
445+
return prefixes;
446+
}
447+
448+
public boolean isLastNode(int index) {
449+
return index >= this.nodes.size() - 1;
450+
}
451+
385452
private List<PathNode> splitPath(String path) {
386453
List<PathNode> nodes = new ArrayList<PathNode>();
387454
for (String name : StringUtils.delimitedListToStringArray(path, ".")) {

spring-boot/src/test/java/org/springframework/boot/bind/RelaxedDataBinderTests.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,48 @@ public void testBindMap() throws Exception {
421421
assertEquals("123", target.get("value"));
422422
}
423423

424+
@Test
425+
public void testBindMapWithClashInProperties() throws Exception {
426+
Map<String, Object> target = new LinkedHashMap<String, Object>();
427+
BindingResult result = bind(target, "vanilla.spam: bar\n"
428+
+ "vanilla.spam.value: 123", "vanilla");
429+
assertEquals(0, result.getErrorCount());
430+
assertEquals(2, target.size());
431+
assertEquals("bar", target.get("spam"));
432+
assertEquals("123", target.get("spam.value"));
433+
}
434+
435+
@Test
436+
public void testBindMapWithDeepClashInProperties() throws Exception {
437+
Map<String, Object> target = new LinkedHashMap<String, Object>();
438+
BindingResult result = bind(target, "vanilla.spam.foo: bar\n"
439+
+ "vanilla.spam.foo.value: 123", "vanilla");
440+
assertEquals(0, result.getErrorCount());
441+
@SuppressWarnings("unchecked")
442+
Map<String, Object> map = (Map<String, Object>) target.get("spam");
443+
assertEquals("123", map.get("foo.value"));
444+
}
445+
446+
@Test
447+
public void testBindMapWithDifferentDeepClashInProperties() throws Exception {
448+
Map<String, Object> target = new LinkedHashMap<String, Object>();
449+
BindingResult result = bind(target, "vanilla.spam.bar: bar\n"
450+
+ "vanilla.spam.bar.value: 123", "vanilla");
451+
assertEquals(0, result.getErrorCount());
452+
@SuppressWarnings("unchecked")
453+
Map<String, Object> map = (Map<String, Object>) target.get("spam");
454+
assertEquals("123", map.get("bar.value"));
455+
}
456+
457+
@Test
458+
public void testBindShallowMap() throws Exception {
459+
Map<String, Object> target = new LinkedHashMap<String, Object>();
460+
BindingResult result = bind(target, "vanilla.spam: bar\n" + "vanilla.value: 123",
461+
"vanilla");
462+
assertEquals(0, result.getErrorCount());
463+
assertEquals("123", target.get("value"));
464+
}
465+
424466
@Test
425467
public void testBindMapNestedMap() throws Exception {
426468
Map<String, Object> target = new LinkedHashMap<String, Object>();

0 commit comments

Comments
 (0)