Skip to content

Commit b3d8e32

Browse files
Dave Syerphilwebb
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)