diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 2506d0c96..b8f42198c 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -81,6 +81,7 @@ The type attribute can be add,update,fix,remove. Add ObjectUtils.toString(Object, Supplier<String>). Fixed Javadocs for setTestRecursive() #556. ToStringBuilder.reflectionToString - Wrong JSON format when object has a List of Enum. + [JSON string for maps] ToStringBuilder.reflectionToString doesnt render nested maps correctly. Make org.apache.commons.lang3.CharSequenceUtils.toCharArray(CharSequence) public. Add org.apache.commons.lang3.StringUtils.substringAfter(String, int). Add org.apache.commons.lang3.StringUtils.substringAfterLast(String, int). diff --git a/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java b/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java index 56fda5556..1c3391030 100644 --- a/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java +++ b/src/main/java/org/apache/commons/lang3/builder/ToStringStyle.java @@ -19,7 +19,10 @@ import java.io.Serializable; import java.lang.reflect.Array; import java.util.Collection; +import java.util.Iterator; import java.util.Map; +import java.util.Objects; +import java.util.Map.Entry; import java.util.WeakHashMap; import org.apache.commons.lang3.ClassUtils; @@ -2595,6 +2598,37 @@ protected void appendDetail(final StringBuffer buffer, final String fieldName, f appendDetail(buffer, fieldName, valueAsString); } + @Override + protected void appendDetail(final StringBuffer buffer, final String fieldName, final Map map) { + if (map != null && !map.isEmpty()) { + buffer.append(getContentStart()); + + boolean firstItem = true; + for (final Entry entry : map.entrySet()) { + final String keyStr = Objects.toString(entry.getKey(), null); + if (keyStr != null) { + if (firstItem) { + firstItem = false; + } else { + appendFieldEnd(buffer, keyStr); + } + appendFieldStart(buffer, keyStr); + final Object value = entry.getValue(); + if (value == null) { + appendNullText(buffer, keyStr); + } else { + appendInternal(buffer, keyStr, value, true); + } + } + } + + buffer.append(getContentEnd()); + return; + } + + buffer.append(map); + } + private boolean isJsonArray(final String valueAsString) { return valueAsString.startsWith(getArrayStart()) && valueAsString.endsWith(getArrayEnd()); diff --git a/src/test/java/org/apache/commons/lang3/builder/JsonToStringStyleTest.java b/src/test/java/org/apache/commons/lang3/builder/JsonToStringStyleTest.java index 23895118a..b8d9f6e70 100644 --- a/src/test/java/org/apache/commons/lang3/builder/JsonToStringStyleTest.java +++ b/src/test/java/org/apache/commons/lang3/builder/JsonToStringStyleTest.java @@ -26,6 +26,8 @@ import java.util.HashMap; import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; import org.apache.commons.lang3.builder.ToStringStyleTest.Person; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -532,6 +534,70 @@ public void testLANG1396() { assertEquals("{\"Let's \\\"quote\\\" this\":\"value\"}", new ToStringBuilder(base).append("Let's \"quote\" this", "value").toString()); } + @Test + public void testRootMap() { + final Map map = new LinkedHashMap<>(); + map.put("k1", "v1"); + map.put("k2", 2); + + assertEquals("{\"map\":{\"k1\":\"v1\",\"k2\":2}}", + new ToStringBuilder(base).append("map", map).toString()); + } + + @Test + public void testObjectWithInnerMap() { + final Map map = new LinkedHashMap<>(); + map.put("k1", "value1"); + map.put("k2", 2); + + final InnerMapObject object = new InnerMapObject(){ + @Override + public String toString() { + return new ToStringBuilder(this).append("pid", this.pid) + .append("map", this.map).toString(); + } + }; + object.pid = "dummy-text"; + object.map = map; + + assertEquals("{\"object\":{\"pid\":\"dummy-text\",\"map\":{\"k1\":\"value1\",\"k2\":2}}}", + new ToStringBuilder(base).append("object", object).toString()); + } + + @Test + public void testNestedMaps() { + final Map innerMap = new LinkedHashMap<>(); + innerMap.put("k2.1", "v2.1"); + innerMap.put("k2.2", "v2.2"); + final Map baseMap = new LinkedHashMap<>(); + baseMap.put("k1", "v1"); + baseMap.put("k2", innerMap); + + final InnerMapObject object = new InnerMapObject(){ + @Override + public String toString() { + return new ToStringBuilder(this).append("pid", this.pid) + .append("map", this.map).toString(); + } + }; + object.pid = "dummy-text"; + object.map = baseMap; + + assertEquals("{\"object\":{\"pid\":\"dummy-text\",\"map\":{\"k1\":\"v1\"," + + "\"k2\":{\"k2.1\":\"v2.1\",\"k2.2\":\"v2.2\"}}}}", + new ToStringBuilder(base).append("object", object).toString()); + } + + @Test + public void testMapSkipNullKey() { + final Map map = new LinkedHashMap<>(); + map.put("k1", "v1"); + map.put(null, "v2"); + + assertEquals("{\"map\":{\"k1\":\"v1\"}}", + new ToStringBuilder(base).append("map", map).toString()); + } + /** * An object with nested object structures used to test {@link ToStringStyle.JsonToStringStyle}. * @@ -616,4 +682,20 @@ public String toString() { return ToStringBuilder.reflectionToString(this); } } + + /** + * An object with a Map field used to test {@link ToStringStyle.JsonToStringStyle}. + * + */ + static class InnerMapObject { + /** + * Test String field. + */ + String pid; + + /** + * Test inner map field. + */ + Map map; + } }