Skip to content
Open
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 @@ -340,7 +340,19 @@ private void buildFlattenedMap(Map<String, Object> result, Map<String, Object> s
source.forEach((key, value) -> {
if (StringUtils.hasText(path)) {
if (key.startsWith("[")) {
key = path + key;
if (containsKeyPathSeparator(key)) {
// User-supplied YAML key whose literal brackets must be preserved
// when flattened to property paths (for example a YAML key such as
// "[domain.test:8080]" must round-trip as "[[domain.test:8080]]" so
// that consumers like the Spring Boot binder treat the original
// brackets as part of the key rather than as a map-key marker).
key = path + '[' + key + ']';
}
else {
// Bracket-wrapped key generated internally — collection index or
// the toString() of a non-CharSequence map key. No dot separator.
key = path + key;
}
}
else {
key = path + '.' + key;
Expand Down Expand Up @@ -372,6 +384,25 @@ else if (value instanceof Collection collection) {
});
}

/**
* Detect whether the given bracket-prefixed key contains characters that downstream
* consumers of the flattened properties would interpret as path separators. Keys
* generated internally by this class for collection indices and for the
* {@code toString()} of non-CharSequence map keys do not contain such characters,
* so the presence of a {@code '.'} or {@code ':'} indicates a user-supplied key
* whose brackets are part of the key itself and need additional wrapping to be
* preserved.
*/
private static boolean containsKeyPathSeparator(String key) {
for (int i = 0; i < key.length(); i++) {
char c = key.charAt(i);
if (c == '.' || c == ':') {
return true;
}
}
return false;
}


/**
* Callback interface used to process the YAML parsing results.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,40 @@ void loadArrayOfObject() {
assertThat(properties.get("foo")).isNull();
}

@Test // gh-27020
void loadNestedMapWithBracketedKeyContainingDotAndColon() {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new ByteArrayResource((
"root:\n" +
" webservices:\n" +
" \"[domain.test:8080]\":\n" +
" - username: me\n" +
" password: mypassword\n").getBytes()));
Properties properties = factory.getObject();
assertThat(properties.getProperty("root.webservices[[domain.test:8080]][0].username"))
.isEqualTo("me");
assertThat(properties.getProperty("root.webservices[[domain.test:8080]][0].password"))
.isEqualTo("mypassword");
}

@Test // gh-27020
void loadNestedMapWithBracketedKeyContainingDot() {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new ByteArrayResource(
"root:\n \"[my.dotted.key]\": value\n".getBytes()));
Properties properties = factory.getObject();
assertThat(properties.getProperty("root[[my.dotted.key]]")).isEqualTo("value");
}

@Test // gh-27020
void loadNestedMapWithBracketedKeyContainingColon() {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new ByteArrayResource(
"root:\n \"[host:8080]\": value\n".getBytes()));
Properties properties = factory.getObject();
assertThat(properties.getProperty("root[[host:8080]]")).isEqualTo("value");
}

@Test
@SuppressWarnings("unchecked")
void yaml() {
Expand Down