Skip to content

Commit b1f4320

Browse files
committed
Added new logic to RelaxedDataBinder to deal with nested stuff
* Leverages existing behaviour of BeanWrapperImpl where possible to autogrow collections and lists * Logic for scanning and converting bean paths encapsulated in BeanPath inner class [Fixes #53947797] [bs-249] @ConfigurationProperties cannot bind to Map<String,List<Thing>>
1 parent 2d1f758 commit b1f4320

File tree

4 files changed

+462
-52
lines changed

4 files changed

+462
-52
lines changed

Diff for: spring-bootstrap/src/main/java/org/springframework/bootstrap/bind/RelaxedDataBinder.java

+210-46
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.bootstrap.bind;
1818

1919
import java.net.InetAddress;
20+
import java.util.ArrayList;
2021
import java.util.LinkedHashMap;
2122
import java.util.List;
2223
import java.util.Map;
@@ -26,6 +27,7 @@
2627
import org.springframework.beans.InvalidPropertyException;
2728
import org.springframework.beans.MutablePropertyValues;
2829
import org.springframework.beans.PropertyValue;
30+
import org.springframework.core.convert.TypeDescriptor;
2931
import org.springframework.util.StringUtils;
3032
import org.springframework.validation.DataBinder;
3133

@@ -129,66 +131,228 @@ private MutablePropertyValues getProperyValuesForNamePrefix(
129131

130132
private void modifyProperty(MutablePropertyValues propertyValues, BeanWrapper target,
131133
PropertyValue propertyValue, int index) {
134+
String oldName = propertyValue.getName();
135+
String name = normalizePath(target, oldName);
136+
if (!name.equals(oldName)) {
137+
propertyValues.setPropertyValueAt(
138+
new PropertyValue(name, propertyValue.getValue()), index);
139+
}
140+
}
141+
142+
protected String normalizePath(BeanWrapper wrapper, String path) {
143+
return initializePath(wrapper, new BeanPath(path), 0);
144+
}
145+
146+
private static class BeanPath {
132147

133-
String name = propertyValue.getName();
134-
StringBuilder builder = new StringBuilder();
135-
Class<?> type = target.getWrappedClass();
148+
private List<PathNode> nodes;
136149

137-
for (String key : StringUtils.delimitedListToStringArray(name, ".")) {
138-
if (builder.length() != 0) {
139-
builder.append(".");
150+
public BeanPath(String path) {
151+
this.nodes = splitPath(path);
152+
}
153+
154+
public void mapIndex(int index) {
155+
PathNode node = this.nodes.get(index);
156+
if (node instanceof PropertyNode) {
157+
node = ((PropertyNode) node).mapIndex();
140158
}
141-
String oldKey = key;
142-
key = getActualPropertyName(target, builder.toString(), oldKey);
143-
builder.append(key);
144-
String base = builder.toString();
145-
if (!oldKey.equals(key)) {
146-
propertyValues.setPropertyValueAt(
147-
new PropertyValue(base, propertyValue.getValue()), index);
159+
this.nodes.set(index, node);
160+
}
161+
162+
public String prefix(int index) {
163+
return range(0, index);
164+
}
165+
166+
public void rename(int index, String name) {
167+
this.nodes.get(index).name = name;
168+
}
169+
170+
public String name(int index) {
171+
if (index < this.nodes.size()) {
172+
return this.nodes.get(index).name;
148173
}
149-
type = target.getPropertyType(base);
150-
151-
// Any nested properties that are maps, are assumed to be simple nested
152-
// maps of maps...
153-
if (type != null && Map.class.isAssignableFrom(type)) {
154-
Map<String, Object> nested = new LinkedHashMap<String, Object>();
155-
if (target.getPropertyValue(base) != null) {
156-
@SuppressWarnings("unchecked")
157-
Map<String, Object> existing = (Map<String, Object>) target
158-
.getPropertyValue(base);
159-
nested = existing;
160-
}
161-
else {
162-
target.setPropertyValue(base, nested);
174+
return null;
175+
}
176+
177+
public int length() {
178+
return this.nodes.size();
179+
}
180+
181+
private String range(int start, int end) {
182+
StringBuilder builder = new StringBuilder();
183+
for (int i = start; i < end; i++) {
184+
PathNode node = this.nodes.get(i);
185+
builder.append(node);
186+
}
187+
if (builder.toString().startsWith(("."))) {
188+
builder.replace(0, 1, "");
189+
}
190+
return builder.toString();
191+
}
192+
193+
public boolean isArrayIndex(int index) {
194+
return this.nodes.get(index) instanceof ArrayIndexNode;
195+
}
196+
197+
public boolean isProperty(int index) {
198+
return this.nodes.get(index) instanceof PropertyNode;
199+
}
200+
201+
@Override
202+
public String toString() {
203+
return prefix(this.nodes.size());
204+
}
205+
206+
private static class PathNode {
207+
208+
protected String name;
209+
210+
public PathNode(String name) {
211+
this.name = name;
212+
}
213+
214+
}
215+
216+
private static class ArrayIndexNode extends PathNode {
217+
218+
public ArrayIndexNode(String name) {
219+
super(name);
220+
}
221+
222+
@Override
223+
public String toString() {
224+
return "[" + this.name + "]";
225+
}
226+
227+
}
228+
229+
private static class MapIndexNode extends PathNode {
230+
231+
public MapIndexNode(String name) {
232+
super(name);
233+
}
234+
235+
@Override
236+
public String toString() {
237+
return "[" + this.name + "]";
238+
}
239+
}
240+
241+
private static class PropertyNode extends PathNode {
242+
243+
public PropertyNode(String name) {
244+
super(name);
245+
}
246+
247+
public MapIndexNode mapIndex() {
248+
return new MapIndexNode(this.name);
249+
}
250+
251+
@Override
252+
public String toString() {
253+
return "." + this.name;
254+
}
255+
}
256+
257+
private List<PathNode> splitPath(String path) {
258+
List<PathNode> nodes = new ArrayList<PathNode>();
259+
for (String name : StringUtils.delimitedListToStringArray(path, ".")) {
260+
for (String sub : StringUtils.delimitedListToStringArray(name, "[")) {
261+
if (StringUtils.hasText(sub)) {
262+
if (sub.endsWith("]")) {
263+
sub = sub.substring(0, sub.length() - 1);
264+
if (sub.matches("[0-9]+")) {
265+
nodes.add(new ArrayIndexNode(sub));
266+
}
267+
else {
268+
nodes.add(new MapIndexNode(sub));
269+
}
270+
}
271+
else {
272+
nodes.add(new PropertyNode(sub));
273+
}
274+
}
163275
}
164-
modifyPopertiesForMap(nested, propertyValues, index, base);
165-
break;
166276
}
277+
return nodes;
167278
}
279+
168280
}
169281

170-
private void modifyPopertiesForMap(Map<String, Object> target,
171-
MutablePropertyValues propertyValues, int index, String base) {
172-
PropertyValue propertyValue = propertyValues.getPropertyValueList().get(index);
173-
String name = propertyValue.getName();
174-
String suffix = name.substring(base.length());
175-
Map<String, Object> value = new LinkedHashMap<String, Object>();
176-
String[] tree = StringUtils.delimitedListToStringArray(
177-
suffix.startsWith(".") ? suffix.substring(1) : suffix, ".");
178-
for (int j = 0; j < tree.length - 1; j++) {
179-
if (!target.containsKey(tree[j])) {
180-
target.put(tree[j], value);
282+
private String initializePath(BeanWrapper wrapper, BeanPath path, int index) {
283+
String prefix = path.prefix(index);
284+
String key = path.name(index);
285+
if (key == null) {
286+
return path.toString();
287+
}
288+
if (path.isProperty(index)) {
289+
key = getActualPropertyName(wrapper, prefix, key);
290+
path.rename(index, key);
291+
}
292+
if (index >= path.length() - 1) {
293+
return path.toString();
294+
}
295+
String name = path.prefix(++index);
296+
TypeDescriptor descriptor = wrapper.getPropertyTypeDescriptor(name);
297+
if (descriptor == null || descriptor.isMap()) {
298+
if (descriptor != null) {
299+
wrapper.getPropertyValue(name + "[foo]");
300+
}
301+
path.mapIndex(index);
302+
extendMapIfNecessary(wrapper, path, index);
303+
}
304+
else if (descriptor.isCollection()) {
305+
// TODO: test collection extension
306+
extendCollectionIfNecessary(wrapper, path, index);
307+
}
308+
else if (descriptor.getType().equals(Object.class)) {
309+
path.mapIndex(index);
310+
name = path.prefix(index + 1);
311+
if (wrapper.getPropertyValue(name) == null) {
312+
wrapper.setPropertyValue(name, new LinkedHashMap<String, Object>());
181313
}
182-
target = value;
183-
value = new LinkedHashMap<String, Object>();
184314
}
185-
String refName = base + suffix.replaceAll("\\.([a-zA-Z0-9]*)", "[$1]");
186-
propertyValues.setPropertyValueAt(
187-
new PropertyValue(refName, propertyValue.getValue()), index);
315+
if (index < path.length()) {
316+
return initializePath(wrapper, path, index);
317+
}
318+
return path.toString();
319+
}
188320

321+
private void extendCollectionIfNecessary(BeanWrapper wrapper, BeanPath path, int index) {
322+
String name = path.prefix(index);
323+
TypeDescriptor elementDescriptor = wrapper.getPropertyTypeDescriptor(name)
324+
.getElementTypeDescriptor();
325+
if (!elementDescriptor.isMap() && !elementDescriptor.isCollection()
326+
&& !elementDescriptor.getType().equals(Object.class)) {
327+
return;
328+
}
329+
Object extend = new LinkedHashMap<String, Object>();
330+
if (!elementDescriptor.isMap() && path.isArrayIndex(index + 1)) {
331+
extend = new ArrayList<Object>();
332+
}
333+
wrapper.setPropertyValue(path.prefix(index + 1), extend);
334+
}
335+
336+
private void extendMapIfNecessary(BeanWrapper wrapper, BeanPath path, int index) {
337+
String name = path.prefix(index);
338+
TypeDescriptor parent = wrapper.getPropertyTypeDescriptor(name);
339+
if (parent == null) {
340+
return;
341+
}
342+
TypeDescriptor descriptor = parent.getMapValueTypeDescriptor();
343+
if (!descriptor.isMap() && !descriptor.isCollection()
344+
&& !descriptor.getType().equals(Object.class)) {
345+
return;
346+
}
347+
Object extend = new LinkedHashMap<String, Object>();
348+
if (descriptor.isCollection()) {
349+
extend = new ArrayList<Object>();
350+
}
351+
wrapper.setPropertyValue(path.prefix(index + 1), extend);
189352
}
190353

191354
private String getActualPropertyName(BeanWrapper target, String prefix, String name) {
355+
prefix = StringUtils.hasText(prefix) ? prefix + "." : "";
192356
for (Variation variation : Variation.values()) {
193357
for (Manipulation manipulation : Manipulation.values()) {
194358
// Apply all manipulations before attempting variations
@@ -199,7 +363,7 @@ private String getActualPropertyName(BeanWrapper target, String prefix, String n
199363
}
200364
}
201365
catch (InvalidPropertyException ex) {
202-
// swallow and contrinue
366+
// swallow and continue
203367
}
204368
}
205369
}

0 commit comments

Comments
 (0)