diff --git a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java index 75c268d7201..d5aebb1c10f 100644 --- a/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java +++ b/core/spring-boot/src/main/java/org/springframework/boot/logging/structured/ContextPairs.java @@ -193,24 +193,37 @@ public class ContextPairs { LinkedHashMap result = new LinkedHashMap<>(); this.addedPairs.forEach((addedPair) -> { addedPair.accept(item, joining((name, value) -> { - List nameParts = List.of(name.split("\\.")); + StringBuilder part = new StringBuilder(name.length()); + int length = (!name.endsWith(".")) ? name.length() : name.length() - 1; Map destination = result; - for (int i = 0; i < nameParts.size() - 1; i++) { - Object existing = destination.computeIfAbsent(nameParts.get(i), (key) -> new LinkedHashMap<>()); - if (!(existing instanceof Map)) { - String common = String.join(".", nameParts.subList(0, i + 1)); - throw new IllegalStateException( - "Duplicate nested pairs added under '%s'".formatted(common)); + for (int i = 0; i < length; i++) { + char ch = name.charAt(i); + if (i == length - 1) { + part.append(ch); + Object previous = destination.put(part.toString(), value); + assertNotDuplicateNestedPairs(previous == null, name, length); + } + else if (ch == '.') { + Object current = destination.computeIfAbsent(part.toString(), + (key) -> new LinkedHashMap<>()); + assertNotDuplicateNestedPairs(current instanceof Map, name, i); + destination = (Map) current; + part.setLength(0); + } + else { + part.append(ch); } - destination = (Map) existing; } - Object previous = destination.put(nameParts.get(nameParts.size() - 1), value); - Assert.state(previous == null, () -> "Duplicate nested pairs added under '%s'".formatted(name)); })); }); result.forEach(pairs); } + private void assertNotDuplicateNestedPairs(boolean expression, String name, int index) { + Assert.state(expression, + () -> "Duplicate nested pairs added under '%s'".formatted(name.substring(0, index))); + } + private BiConsumer joining(BiConsumer pairs) { return (name, value) -> { name = this.joiner.join(ContextPairs.this.prefix, (name != null) ? name : ""); diff --git a/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java b/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java index e0013d0a868..0b9b7973045 100644 --- a/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java +++ b/core/spring-boot/src/test/java/org/springframework/boot/logging/structured/ContextPairsTests.java @@ -120,6 +120,19 @@ class ContextPairsTests { assertThat(actual).isEqualTo(expected); } + @Test + void nestedWhenNameEndsWithDelimiterDropsTrailingDelimiter() { + ContextPairs contextPairs = new ContextPairs(true, null); + Map map = new LinkedHashMap<>(); + map.put("a1.b1.", "A1B1"); + Map actual = apply(contextPairs.nested((pairs) -> pairs.addMapEntries((item) -> map))); + Map expected = new LinkedHashMap<>(); + Map a1 = new LinkedHashMap<>(); + expected.put("a1", a1); + a1.put("b1", "A1B1"); + assertThat(actual).isEqualTo(expected); + } + @Test void nestedWhenDuplicateInParentThrowsException() { ContextPairs contextPairs = new ContextPairs(true, null);