Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -912,7 +912,7 @@ TIP: We recommend that, when possible, properties are stored in lower-case kebab
==== Binding Maps

When binding to `Map` properties you may need to use a special bracket notation so that the original `key` value is preserved.
If the key is not surrounded by `[]`, any characters that are not alpha-numeric, `-` or `.` are removed.
A binding failure occurs if the key contains any characters that are not alpha-numeric, `-` or `.` but not surrounded by `[]`.

For example, consider binding the following properties to a `Map<String,String>`:

Expand All @@ -927,8 +927,8 @@ my:

NOTE: For YAML files, the brackets need to be surrounded by quotes for the keys to be parsed properly.

The properties above will bind to a `Map` with `/key1`, `/key2` and `key3` as the keys in the map.
The slash has been removed from `key3` because it was not surrounded by square brackets.
The properties above is failed to bind to a `Map` with `/key1`, `/key2` and `/key3` as the keys in the map,
because `/key3` was not surrounded by square brackets.

When binding to scalar values, keys with `.` in them do not need to be surrounded by `[]`.
Scalar values include enums and all types in the `java.lang` package except for `Object`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,14 @@
import org.springframework.boot.context.properties.source.IterableConfigurationPropertySource;
import org.springframework.core.CollectionFactory;
import org.springframework.core.ResolvableType;
import org.springframework.core.env.PropertySource;

/**
* {@link AggregateBinder} for Maps.
*
* @author Phillip Webb
* @author Madhura Bhave
* @author Yanming Zhou
*/
class MapBinder extends AggregateBinder<Map<Object, Object>> {

Expand Down Expand Up @@ -166,6 +168,23 @@ private class EntryBinder {
void bindEntries(ConfigurationPropertySource source, Map<Object, Object> map) {
if (source instanceof IterableConfigurationPropertySource iterableSource) {
for (ConfigurationPropertyName name : iterableSource) {
if (name.isLastElementNonUniform()) {
String sourceName = name.getSource();
int index = sourceName.lastIndexOf('.');
String key = sourceName.substring(index + 1);
if (!key.equals(name.getLastElement(Form.ORIGINAL))
&& !key.equalsIgnoreCase(name.getLastElement(Form.UNIFORM))) {
boolean forYaml = false;
if (source.getUnderlyingSource() instanceof PropertySource<?> ps) {
String propertySourceName = ps.getName();
forYaml = propertySourceName.contains(".yaml") || propertySourceName.contains(".yml");
}
String validKey = (forYaml ? "\"[" : "[") + key + (forYaml ? "]\"" : "]");
throw new IllegalArgumentException(
"Please rewrite key '" + key + "' to '" + validKey + "'");

}
}
Bindable<?> valueBindable = getValueBindable(name);
ConfigurationPropertyName entryName = getEntryName(source, name);
Object key = getContext().getConverter().convert(getKeyName(entryName), this.keyType);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
*
* @author Phillip Webb
* @author Madhura Bhave
* @author Yanming Zhou
* @since 2.0.0
* @see #of(CharSequence)
* @see ConfigurationPropertySource
Expand Down Expand Up @@ -121,6 +122,23 @@ public boolean isNumericIndex(int elementIndex) {
return this.elements.getType(elementIndex) == ElementType.NUMERICALLY_INDEXED;
}

/**
* Return whether the last element is non-uniform.
* @return whether the last element is non-uniform
*/
public boolean isLastElementNonUniform() {
int size = getNumberOfElements();
return size != 0 && this.elements.getType(size - 1) == ElementType.NON_UNIFORM;
}

/**
* Return the source.
* @return the source
*/
public String getSource() {
return this.elements.source.toString();
}

/**
* Return the last element in the name in the given form.
* @param form the form to return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
*
* @author Phillip Webb
* @author Madhura Bhave
* @author Yanming Zhou
*/
class MapBinderTests {

Expand Down Expand Up @@ -651,6 +652,42 @@ void bindToEnumMapShouldBind() {
assertThat(result).hasSize(1).containsEntry(ExampleEnum.FOO_BAR, "value");
}

@Test
void rejectNonUniformKey() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource(
Collections.singletonMap("props./foobar", "value"));
this.sources.add(source);
Binder binder = new Binder(this.sources, null, null, null);
Bindable<Map<String, String>> bindable = Bindable
.of(ResolvableType.forClassWithGenerics(Map.class, String.class, String.class));
assertThatExceptionOfType(BindException.class).isThrownBy(() -> binder.bind("props", bindable))
.withCauseInstanceOf(IllegalArgumentException.class);
}

@Test
void doNotRejectUnderscoreNonUniformKey() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource(
Collections.singletonMap("props.foo_bar", "value"));
this.sources.add(source);
Binder binder = new Binder(this.sources, null, null, null);
Bindable<Map<String, String>> bindable = Bindable
.of(ResolvableType.forClassWithGenerics(Map.class, String.class, String.class));
Map<String, String> result = binder.bind("props", bindable).get();
assertThat(result).hasSize(1).containsEntry("foo_bar", "value");
}

@Test
void doNotRejectCaseInsensitiveNonUniformKey() {
MapConfigurationPropertySource source = new MapConfigurationPropertySource(
Collections.singletonMap("props.FOOBAR", "value"));
this.sources.add(source);
Binder binder = new Binder(this.sources, null, null, null);
Bindable<Map<String, String>> bindable = Bindable
.of(ResolvableType.forClassWithGenerics(Map.class, String.class, String.class));
Map<String, String> result = binder.bind("props", bindable).get();
assertThat(result).hasSize(1).containsEntry("FOOBAR", "value");
}

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));
Expand Down
Loading