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..69fd6628e37 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,49 @@ public class ContextPairs { LinkedHashMap result = new LinkedHashMap<>(); this.addedPairs.forEach((addedPair) -> { addedPair.accept(item, joining((name, value) -> { - List nameParts = List.of(name.split("\\.")); Map destination = result; - for (int i = 0; i < nameParts.size() - 1; i++) { - Object existing = destination.computeIfAbsent(nameParts.get(i), (key) -> new LinkedHashMap<>()); + + int end = trimTrailingDelimiters(name); + if (end == 0) { + return; + } + + int start = 0; + while (true) { + int dot = name.indexOf('.', start); + if (dot == -1 || dot >= end) { + break; + } + + String part = name.substring(start, dot); + + Object existing = destination.computeIfAbsent(part, (key) -> new LinkedHashMap<>()); if (!(existing instanceof Map)) { - String common = String.join(".", nameParts.subList(0, i + 1)); + String common = name.substring(0, dot); throw new IllegalStateException( "Duplicate nested pairs added under '%s'".formatted(common)); } + destination = (Map) existing; + start = dot + 1; } - Object previous = destination.put(nameParts.get(nameParts.size() - 1), value); + + String leaf = name.substring(start, end); + Object previous = destination.put(leaf, value); Assert.state(previous == null, () -> "Duplicate nested pairs added under '%s'".formatted(name)); })); }); result.forEach(pairs); } + private int trimTrailingDelimiters(String name) { + int end = name.length(); + while (end > 0 && name.charAt(end - 1) == '.') { + end--; + } + return end; + } + 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);