diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java index b0334a1059..d6a833d8a6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/context/properties/source/ConfigurationPropertyName.java @@ -29,7 +29,7 @@ import org.springframework.util.ObjectUtils; /** * A configuration property name composed of elements separated by dots. User created * names may contain the characters "{@code a-z}" "{@code 0-9}") and "{@code -}", they - * must be lower-case and must start with a letter. The "{@code -}" is used purely for + * must be lower-case and must start with an alpha-numeric character. The "{@code -}" is used purely for * formatting, i.e. "{@code foo-bar}" and "{@code foobar}" are considered equivalent. *

* The "{@code [}" and "{@code ]}" characters may be used to indicate an associative @@ -675,7 +675,7 @@ public final class ConfigurationPropertyName } public static boolean isValidChar(char ch, int index) { - return isAlpha(ch) || (index != 0 && (isNumeric(ch) || ch == '-')); + return isAlpha(ch) || isNumeric(ch) || (index != 0 && ch == '-'); } private static boolean isAlpha(char ch) { diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java index a2293d5749..6f0ba6e31d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/ConfigurationPropertiesBindingPostProcessorTests.java @@ -273,6 +273,20 @@ public class ConfigurationPropertiesBindingPostProcessorTests { this.context.refresh(); } + @Test + public void bindToMapWithNumericKey() { + this.context = new AnnotationConfigApplicationContext(); + MutablePropertySources sources = this.context.getEnvironment() + .getPropertySources(); + Map source = new LinkedHashMap<>(); + source.put("sample.foos.1.name", "One"); + sources.addFirst(new MapPropertySource("test-source", source)); + this.context.register(NumericKeyConfiguration.class); + this.context.refresh(); + NumericKeyConfiguration foo = this.context.getBean(NumericKeyConfiguration.class); + assertThat(foo.getFoos().get("1")).isNotNull(); + } + private void prepareConverterContext(Class... config) { this.context = new AnnotationConfigApplicationContext(); MutablePropertySources sources = this.context.getEnvironment() @@ -604,4 +618,29 @@ public class ConfigurationPropertiesBindingPostProcessorTests { } + @Configuration + @EnableConfigurationProperties + @ConfigurationProperties(prefix = "sample") + static class NumericKeyConfiguration { + + private Map foos = new LinkedHashMap<>(); + + public Map getFoos() { + return this.foos; + } + + static class Foo { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + } + } + } diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java index aa89e8ab53..e6ea34d050 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/context/properties/source/ConfigurationPropertyNameTests.java @@ -49,13 +49,6 @@ public class ConfigurationPropertyNameTests { ConfigurationPropertyName.of(null); } - @Test - public void ofNameShouldNotStartWithNumber() { - this.thrown.expect(InvalidConfigurationPropertyNameException.class); - this.thrown.expectMessage("is not valid"); - ConfigurationPropertyName.of("1foo"); - } - @Test public void ofNameShouldNotStartWithDash() { this.thrown.expect(InvalidConfigurationPropertyNameException.class); @@ -107,6 +100,15 @@ public class ConfigurationPropertyNameTests { assertThat(name.isIndexed(0)).isFalse(); } + @Test + public void ofNameWhenStartsWithNumber() { + ConfigurationPropertyName name = ConfigurationPropertyName.of("1foo"); + assertThat(name.toString()).isEqualTo("1foo"); + assertThat(name.getNumberOfElements()).isEqualTo(1); + assertThat(name.getElement(0, Form.ORIGINAL)).isEqualTo("1foo"); + assertThat(name.isIndexed(0)).isFalse(); + } + @Test public void ofNameWhenRunOnAssociative() { ConfigurationPropertyName name = ConfigurationPropertyName.of("foo[bar]"); @@ -420,8 +422,8 @@ public class ConfigurationPropertyNameTests { @Test public void appendWhenElementNameIsNotValidShouldThrowException() { this.thrown.expect(InvalidConfigurationPropertyNameException.class); - this.thrown.expectMessage("Configuration property name '1bar' is not valid"); - ConfigurationPropertyName.of("foo").append("1bar"); + this.thrown.expectMessage("Configuration property name '-bar' is not valid"); + ConfigurationPropertyName.of("foo").append("-bar"); } @Test @@ -589,7 +591,7 @@ public class ConfigurationPropertyNameTests { @Test public void isValidWhenNotValidShouldReturnFalse() { assertThat(ConfigurationPropertyName.isValid(null)).isFalse(); - assertThat(ConfigurationPropertyName.isValid("1foo")).isFalse(); + assertThat(ConfigurationPropertyName.isValid("-foo")).isFalse(); assertThat(ConfigurationPropertyName.isValid("FooBar")).isFalse(); assertThat(ConfigurationPropertyName.isValid("foo!bar")).isFalse(); }