Bind maps with dots in key names and list values without []
Closes gh-9255
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.boot.context.properties.bind;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
@@ -117,6 +118,11 @@ class MapBinder extends AggregateBinder<Map<Object, Object>> {
|
||||
|
||||
private ConfigurationPropertyName getEntryName(ConfigurationPropertySource source,
|
||||
ConfigurationPropertyName name) {
|
||||
Class<?> resolved = this.valueType.resolve();
|
||||
if (Collection.class.isAssignableFrom(resolved)
|
||||
|| this.valueType.isArray()) {
|
||||
return chopNameAtNumericIndex(name);
|
||||
}
|
||||
if (!this.root.isParentOf(name)
|
||||
&& (isValueTreatedAsNestedMap() || !isScalarValue(source, name))) {
|
||||
return name.chop(this.root.getNumberOfElements() + 1);
|
||||
@@ -124,6 +130,17 @@ class MapBinder extends AggregateBinder<Map<Object, Object>> {
|
||||
return name;
|
||||
}
|
||||
|
||||
private ConfigurationPropertyName chopNameAtNumericIndex(ConfigurationPropertyName name) {
|
||||
int start = this.root.getNumberOfElements() + 1;
|
||||
int size = name.getNumberOfElements();
|
||||
for (int i = start; i < size; i++) {
|
||||
if (name.IsNumericIndex(i)) {
|
||||
return name.chop(i);
|
||||
}
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
private boolean isValueTreatedAsNestedMap() {
|
||||
return Object.class.equals(this.valueType.resolve(Object.class));
|
||||
}
|
||||
|
||||
@@ -97,12 +97,21 @@ public final class ConfigurationPropertyName
|
||||
/**
|
||||
* Return if the an element in the name is indexed.
|
||||
* @param elementIndex the index of the element
|
||||
* @return {@code true} if the last element is indexed
|
||||
* @return {@code true} if the element is indexed
|
||||
*/
|
||||
boolean isIndexed(int elementIndex) {
|
||||
return isIndexed(this.elements[elementIndex]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the an element in the name is indexed and numeric.
|
||||
* @param elementIndex the index of the element
|
||||
* @return {@code true} if the element is indexed and numeric
|
||||
*/
|
||||
public boolean IsNumericIndex(int elementIndex) {
|
||||
return isIndexed(elementIndex) && isNumeric(getElement(elementIndex, Form.ORIGINAL));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the last element in the name in the given form.
|
||||
* @param form the form to return
|
||||
@@ -392,6 +401,16 @@ public final class ConfigurationPropertyName
|
||||
&& element.charAt(length - 1) == ']';
|
||||
}
|
||||
|
||||
private static boolean isNumeric(CharSequence element) {
|
||||
int length = element.length();
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (!Character.isDigit(element.charAt(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if the given name is valid. If this method returns {@code true} then the
|
||||
* name may be used with {@link #of(CharSequence)} without throwing an exception.
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -429,6 +430,109 @@ public class MapBinderTests {
|
||||
eq(target), any(), isA(Map.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToMapStringArrayWithDotKeysShouldPreserveDot() throws Exception {
|
||||
MockConfigurationPropertySource mockSource = new MockConfigurationPropertySource();
|
||||
mockSource.put("foo.bar.baz[0]", "a");
|
||||
mockSource.put("foo.bar.baz[1]", "b");
|
||||
mockSource.put("foo.bar.baz[2]", "c");
|
||||
this.sources
|
||||
.add(mockSource);
|
||||
Map<String, String[]> map = this.binder.bind("foo", STRING_ARRAY_MAP).get();
|
||||
assertThat(map.get("bar.baz")).containsExactly("a", "b", "c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToMapStringArrayWithDotKeysAndCommaSeparatedShouldPreserveDot() throws Exception {
|
||||
MockConfigurationPropertySource mockSource = new MockConfigurationPropertySource();
|
||||
mockSource.put("foo.bar.baz", "a,b,c");
|
||||
this.sources
|
||||
.add(mockSource);
|
||||
Map<String, String[]> map = this.binder.bind("foo", STRING_ARRAY_MAP).get();
|
||||
assertThat(map.get("bar.baz")).containsExactly("a", "b", "c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToMapStringCollectionWithDotKeysShouldPreserveDot() throws Exception {
|
||||
Bindable<List<String>> valueType = Bindable.listOf(String.class);
|
||||
Bindable<Map<String, List<String>>> target = getMapBindable(String.class, valueType.getType());
|
||||
MockConfigurationPropertySource mockSource = new MockConfigurationPropertySource();
|
||||
mockSource.put("foo.bar.baz[0]", "a");
|
||||
mockSource.put("foo.bar.baz[1]", "b");
|
||||
mockSource.put("foo.bar.baz[2]", "c");
|
||||
this.sources
|
||||
.add(mockSource);
|
||||
Map<String, List<String>> map = this.binder.bind("foo", target).get();
|
||||
List<String> values = map.get("bar.baz");
|
||||
assertThat(values).containsExactly("a", "b", "c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToMapNonScalarCollectionWithDotKeysShouldBind() throws Exception {
|
||||
Bindable<List<JavaBean>> valueType = Bindable.listOf(JavaBean.class);
|
||||
Bindable<Map<String, List<JavaBean>>> target = getMapBindable(String.class, valueType.getType());
|
||||
MockConfigurationPropertySource mockSource = new MockConfigurationPropertySource();
|
||||
mockSource.put("foo.bar.baz[0].value", "a");
|
||||
mockSource.put("foo.bar.baz[1].value", "b");
|
||||
mockSource.put("foo.bar.baz[2].value", "c");
|
||||
this.sources
|
||||
.add(mockSource);
|
||||
Map<String, List<JavaBean>> map = this.binder.bind("foo", target).get();
|
||||
List<JavaBean> values = map.get("bar.baz");
|
||||
assertThat(values.stream().map(JavaBean::getValue).collect(Collectors.toList())).containsExactly("a", "b", "c");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToListOfMaps() throws Exception {
|
||||
Bindable<List<Integer>> listBindable = Bindable.listOf(Integer.class);
|
||||
Bindable<Map<String, List<Integer>>> mapBindable = getMapBindable(String.class, listBindable.getType());
|
||||
Bindable<List<Map<String, List<Integer>>>> target = getListBindable(mapBindable.getType());
|
||||
MockConfigurationPropertySource mockSource = new MockConfigurationPropertySource();
|
||||
mockSource.put("foo[0].a", "1,2,3");
|
||||
mockSource.put("foo[1].b", "4,5,6");
|
||||
this.sources
|
||||
.add(mockSource);
|
||||
List<Map<String, List<Integer>>> list = this.binder.bind("foo", target).get();
|
||||
assertThat(list.get(0).get("a")).containsExactly(1, 2, 3);
|
||||
assertThat(list.get(1).get("b")).containsExactly(4, 5, 6);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToMapWithNumberKeyAndCommaSeparated() throws Exception {
|
||||
Bindable<List<String>> listBindable = Bindable.listOf(String.class);
|
||||
Bindable<Map<Integer, List<String>>> target = getMapBindable(Integer.class, listBindable.getType());
|
||||
MockConfigurationPropertySource mockSource = new MockConfigurationPropertySource();
|
||||
mockSource.put("foo[0]", "a,b,c");
|
||||
mockSource.put("foo[1]", "e,f,g");
|
||||
this.sources
|
||||
.add(mockSource);
|
||||
Map<Integer, List<String>> map = this.binder.bind("foo", target).get();
|
||||
assertThat(map.get(0)).containsExactly("a", "b", "c");
|
||||
assertThat(map.get(1)).containsExactly("e", "f", "g");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void bindToMapWithNumberKeyAndIndexed() throws Exception {
|
||||
Bindable<List<Integer>> listBindable = Bindable.listOf(Integer.class);
|
||||
Bindable<Map<Integer, List<Integer>>> target = getMapBindable(Integer.class, listBindable.getType());
|
||||
MockConfigurationPropertySource mockSource = new MockConfigurationPropertySource();
|
||||
mockSource.put("foo[0][0]", "8");
|
||||
mockSource.put("foo[0][1]", "9");
|
||||
this.sources
|
||||
.add(mockSource);
|
||||
Map<Integer, List<Integer>> map = this.binder.bind("foo", target).get();
|
||||
assertThat(map.get(0)).containsExactly(8, 9);
|
||||
}
|
||||
|
||||
private <K, V> Bindable<Map<K, V>> getMapBindable(Class<K> keyGeneric, ResolvableType valueType) {
|
||||
ResolvableType keyType = ResolvableType.forClass(keyGeneric);
|
||||
return Bindable.of(ResolvableType.forClassWithGenerics(Map.class, keyType, valueType));
|
||||
}
|
||||
|
||||
private <T> Bindable<List<T>> getListBindable(ResolvableType type) {
|
||||
return Bindable.of(ResolvableType.forClassWithGenerics(List.class, type));
|
||||
}
|
||||
|
||||
public static class Foo {
|
||||
private String pattern;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user