Browse Source

Bind maps with dots in key names and list values without []

Closes gh-9255
pull/4914/merge
Madhura Bhave 9 years ago
parent
commit
84a38c5606
  1. 17
      spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java
  2. 21
      spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java
  3. 104
      spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java

17
spring-boot/src/main/java/org/springframework/boot/context/properties/bind/MapBinder.java

@ -16,6 +16,7 @@ @@ -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>> { @@ -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>> { @@ -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));
}

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

@ -97,12 +97,21 @@ public final class ConfigurationPropertyName @@ -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 @@ -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.

104
spring-boot/src/test/java/org/springframework/boot/context/properties/bind/MapBinderTests.java

@ -22,6 +22,7 @@ import java.util.HashMap; @@ -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 { @@ -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;

Loading…
Cancel
Save