You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by rg...@apache.org on 2020/02/15 19:28:04 UTC

[logging-log4j2] branch master updated: LOG4J2-2703 Complex data type support for the default MapMessage JSON formatter. (#339)

This is an automated email from the ASF dual-hosted git repository.

rgoers pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/master by this push:
     new 79acdd0  LOG4J2-2703 Complex data type support for the default MapMessage JSON formatter. (#339)
79acdd0 is described below

commit 79acdd0a17e009e0e69a19c4c31d0af3ccfe4e6d
Author: Volkan Yazıcı <vo...@gmail.com>
AuthorDate: Sat Feb 15 20:27:56 2020 +0100

    LOG4J2-2703 Complex data type support for the default MapMessage JSON formatter. (#339)
---
 .../apache/logging/log4j/message/MapMessage.java   |  17 +-
 .../log4j/message/MapMessageJsonFormatter.java     | 403 +++++++++++++++++++++
 .../logging/log4j/message/MapMessageTest.java      |  98 ++++-
 3 files changed, 497 insertions(+), 21 deletions(-)

diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
index 1302ef6..2c4dc10 100644
--- a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessage.java
@@ -424,22 +424,7 @@ public class MapMessage<M extends MapMessage<M, V>, V> implements MultiFormatStr
     }
 
     protected void asJson(final StringBuilder sb) {
-        sb.append('{');
-        for (int i = 0; i < data.size(); i++) {
-            if (i > 0) {
-                sb.append(", ");
-            }
-            sb.append(Chars.DQUOTE);
-            int start = sb.length();
-            sb.append(data.getKeyAt(i));
-            StringBuilders.escapeJson(sb, start);
-            sb.append(Chars.DQUOTE).append(':').append(Chars.DQUOTE);
-            start = sb.length();
-            ParameterFormatter.recursiveDeepToString(data.getValueAt(i), sb, null);
-            StringBuilders.escapeJson(sb, start);
-            sb.append(Chars.DQUOTE);
-        }
-        sb.append('}');
+        MapMessageJsonFormatter.format(sb, data);
     }
 
     protected void asJavaUnquoted(final StringBuilder sb) {
diff --git a/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessageJsonFormatter.java b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessageJsonFormatter.java
new file mode 100644
index 0000000..7c40233
--- /dev/null
+++ b/log4j-api/src/main/java/org/apache/logging/log4j/message/MapMessageJsonFormatter.java
@@ -0,0 +1,403 @@
+package org.apache.logging.log4j.message;
+
+import org.apache.logging.log4j.util.IndexedStringMap;
+import org.apache.logging.log4j.util.PropertiesUtil;
+import org.apache.logging.log4j.util.StringBuilderFormattable;
+import org.apache.logging.log4j.util.StringBuilders;
+
+import java.math.BigDecimal;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The default JSON formatter for {@link MapMessage}s.
+ * <p>
+ * The following types have specific handlers:
+ * <p>
+ * <ul>
+ *     <li>{@link Map}
+ *     <li>{@link Collection} ({@link List}, {@link Set}, etc.)
+ *     <li>{@link Number} ({@link BigDecimal}, {@link Double}, {@link Long}, {@link Byte}, etc.)
+ *     <li>{@link Boolean}
+ *     <li>{@link StringBuilderFormattable}
+ *     <li><tt>char/boolean/byte/short/int/long/float/double/Object</tt> arrays
+ *     <li>{@link String}
+ * </ul>
+ * <p>
+ * It supports nesting up to a maximum depth of 8, which is set by
+ * <tt>log4j2.mapMessage.jsonFormatter.maxDepth</tt> property.
+ */
+enum MapMessageJsonFormatter {;
+
+    public static final int MAX_DEPTH = readMaxDepth();
+
+    private static final char DQUOTE = '"';
+
+    private static final char RBRACE = ']';
+
+    private static final char LBRACE = '[';
+
+    private static final char COMMA = ',';
+
+    private static final char RCURLY = '}';
+
+    private static final char LCURLY = '{';
+
+    private static final char COLON = ':';
+
+    private static int readMaxDepth() {
+        final int maxDepth = PropertiesUtil
+                .getProperties()
+                .getIntegerProperty("log4j2.mapMessage.jsonFormatter.maxDepth", 8);
+        if (maxDepth < 0) {
+            throw new IllegalArgumentException(
+                    "was expecting a positive maxDepth, found: " + maxDepth);
+        }
+        return maxDepth;
+    }
+
+    static void format(final StringBuilder sb, final Object object) {
+        format(sb, object, 0);
+    }
+
+    private static void format(
+            final StringBuilder sb,
+            final Object object,
+            final int depth) {
+
+        if (depth >= MAX_DEPTH) {
+            throw new IllegalArgumentException("maxDepth has been exceeded");
+        }
+
+        // null
+        if (object == null) {
+            sb.append("null");
+        }
+
+        // map
+        else if (object instanceof IndexedStringMap) {
+            final IndexedStringMap map = (IndexedStringMap) object;
+            formatIndexedStringMap(sb, map, depth);
+        } else if (object instanceof Map) {
+            @SuppressWarnings("unchecked")
+            final Map<Object, Object> map = (Map<Object, Object>) object;
+            formatMap(sb, map, depth);
+        }
+
+        // list & collection
+        else if (object instanceof List) {
+            @SuppressWarnings("unchecked")
+            final List<Object> list = (List<Object>) object;
+            formatList(sb, list, depth);
+        } else if (object instanceof Collection) {
+            @SuppressWarnings("unchecked")
+            final Collection<Object> collection = (Collection<Object>) object;
+            formatCollection(sb, collection, depth);
+        }
+
+        // number & boolean
+        else if (object instanceof Number) {
+            final Number number = (Number) object;
+            formatNumber(sb, number);
+        } else if (object instanceof Boolean) {
+            final boolean booleanValue = (boolean) object;
+            formatBoolean(sb, booleanValue);
+        }
+
+        // formattable
+        else if (object instanceof StringBuilderFormattable) {
+            final StringBuilderFormattable formattable = (StringBuilderFormattable) object;
+            formatFormattable(sb, formattable);
+        }
+
+        // arrays
+        else if (object instanceof char[]) {
+            final char[] charValues = (char[]) object;
+            formatCharArray(sb, charValues);
+        } else if (object instanceof boolean[]) {
+            final boolean[] booleanValues = (boolean[]) object;
+            formatBooleanArray(sb, booleanValues);
+        } else if (object instanceof byte[]) {
+            final byte[] byteValues = (byte[]) object;
+            formatByteArray(sb, byteValues);
+        } else if (object instanceof short[]) {
+            final short[] shortValues = (short[]) object;
+            formatShortArray(sb, shortValues);
+        } else if (object instanceof int[]) {
+            final int[] intValues = (int[]) object;
+            formatIntArray(sb, intValues);
+        } else if (object instanceof long[]) {
+            final long[] longValues = (long[]) object;
+            formatLongArray(sb, longValues);
+        } else if (object instanceof float[]) {
+            final float[] floatValues = (float[]) object;
+            formatFloatArray(sb, floatValues);
+        } else if (object instanceof double[]) {
+            final double[] doubleValues = (double[]) object;
+            formatDoubleArray(sb, doubleValues);
+        } else if (object instanceof Object[]) {
+            final Object[] objectValues = (Object[]) object;
+            formatObjectArray(sb, objectValues, depth);
+        }
+
+        // string
+        else {
+            formatString(sb, object);
+        }
+
+    }
+
+    private static void formatIndexedStringMap(
+            final StringBuilder sb,
+            final IndexedStringMap map,
+            final int depth) {
+        sb.append(LCURLY);
+        final int nextDepth = depth + 1;
+        for (int entryIndex = 0; entryIndex < map.size(); entryIndex++) {
+            final String key = map.getKeyAt(entryIndex);
+            final Object value = map.getValueAt(entryIndex);
+            if (entryIndex > 0) {
+                sb.append(COMMA);
+            }
+            sb.append(DQUOTE);
+            final int keyStartIndex = sb.length();
+            sb.append(key);
+            StringBuilders.escapeJson(sb, keyStartIndex);
+            sb.append(DQUOTE).append(COLON);
+            format(sb, value, nextDepth);
+        }
+        sb.append(RCURLY);
+    }
+
+    private static void formatMap(
+            final StringBuilder sb,
+            final Map<Object, Object> map,
+            final int depth) {
+        sb.append(LCURLY);
+        final int nextDepth = depth + 1;
+        final boolean[] firstEntry = {true};
+        map.forEach((final Object key, final Object value) -> {
+            if (key == null) {
+                throw new IllegalArgumentException("null keys are not allowed");
+            }
+            if (firstEntry[0]) {
+                firstEntry[0] = false;
+            } else {
+                sb.append(COMMA);
+            }
+            sb.append(DQUOTE);
+            final String keyString = String.valueOf(key);
+            final int keyStartIndex = sb.length();
+            sb.append(keyString);
+            StringBuilders.escapeJson(sb, keyStartIndex);
+            sb.append(DQUOTE).append(COLON);
+            format(sb, value, nextDepth);
+        });
+        sb.append(RCURLY);
+    }
+
+    private static void formatList(
+            final StringBuilder sb,
+            final List<Object> items,
+            final int depth) {
+        sb.append(LBRACE);
+        final int nextDepth = depth + 1;
+        for (int itemIndex = 0; itemIndex < items.size(); itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final Object item = items.get(itemIndex);
+            format(sb, item, nextDepth);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatCollection(
+            final StringBuilder sb,
+            final Collection<Object> items,
+            final int depth) {
+        sb.append(LBRACE);
+        final int nextDepth = depth + 1;
+        final boolean[] firstItem = {true};
+        items.forEach((final Object item) -> {
+            if (firstItem[0]) {
+                firstItem[0] = false;
+            } else {
+                sb.append(COMMA);
+            }
+            format(sb, item, nextDepth);
+        });
+        sb.append(RBRACE);
+    }
+
+    private static void formatNumber(final StringBuilder sb, final Number number) {
+        if (number instanceof BigDecimal) {
+            final BigDecimal decimalNumber = (BigDecimal) number;
+            sb.append(decimalNumber.toString());
+        } else if (number instanceof Double) {
+            final double doubleNumber = (Double) number;
+            sb.append(doubleNumber);
+        } else if (number instanceof Float) {
+            final float floatNumber = (float) number;
+            sb.append(floatNumber);
+        } else if (number instanceof Byte ||
+                number instanceof Short ||
+                number instanceof Integer ||
+                number instanceof Long) {
+            final long longNumber = number.longValue();
+            sb.append(longNumber);
+        } else {
+            final long longNumber = number.longValue();
+            final double doubleValue = number.doubleValue();
+            if (Double.compare(longNumber, doubleValue) == 0) {
+                sb.append(longNumber);
+            } else {
+                sb.append(doubleValue);
+            }
+        }
+    }
+
+    private static void formatBoolean(final StringBuilder sb, final boolean booleanValue) {
+        sb.append(booleanValue);
+    }
+
+    private static void formatFormattable(
+            final StringBuilder sb,
+            final StringBuilderFormattable formattable) {
+        sb.append(DQUOTE);
+        final int startIndex = sb.length();
+        formattable.formatTo(sb);
+        StringBuilders.escapeJson(sb, startIndex);
+        sb.append(DQUOTE);
+    }
+
+    private static void formatCharArray(final StringBuilder sb, final char[] items) {
+        sb.append(LBRACE);
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final char item = items[itemIndex];
+            sb.append(DQUOTE);
+            final int startIndex = sb.length();
+            sb.append(item);
+            StringBuilders.escapeJson(sb, startIndex);
+            sb.append(DQUOTE);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatBooleanArray(final StringBuilder sb, final boolean[] items) {
+        sb.append(LBRACE);
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final boolean item = items[itemIndex];
+            sb.append(item);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatByteArray(final StringBuilder sb, final byte[] items) {
+        sb.append(LBRACE);
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final byte item = items[itemIndex];
+            sb.append(item);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatShortArray(final StringBuilder sb, final short[] items) {
+        sb.append(LBRACE);
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final short item = items[itemIndex];
+            sb.append(item);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatIntArray(final StringBuilder sb, final int[] items) {
+        sb.append(LBRACE);
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final int item = items[itemIndex];
+            sb.append(item);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatLongArray(final StringBuilder sb, final long[] items) {
+        sb.append(LBRACE);
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final long item = items[itemIndex];
+            sb.append(item);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatFloatArray(final StringBuilder sb, final float[] items) {
+        sb.append(LBRACE);
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final float item = items[itemIndex];
+            sb.append(item);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatDoubleArray(
+            final StringBuilder sb,
+            final double[] items) {
+        sb.append(LBRACE);
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final double item = items[itemIndex];
+            sb.append(item);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatObjectArray(
+            final StringBuilder sb,
+            final Object[] items,
+            final int depth) {
+        sb.append(LBRACE);
+        final int nextDepth = depth + 1;
+        for (int itemIndex = 0; itemIndex < items.length; itemIndex++) {
+            if (itemIndex > 0) {
+                sb.append(COMMA);
+            }
+            final Object item = items[itemIndex];
+            format(sb, item, nextDepth);
+        }
+        sb.append(RBRACE);
+    }
+
+    private static void formatString(final StringBuilder sb, final Object value) {
+        sb.append(DQUOTE);
+        final int startIndex = sb.length();
+        final String valueString = String.valueOf(value);
+        sb.append(valueString);
+        StringBuilders.escapeJson(sb, startIndex);
+        sb.append(DQUOTE);
+    }
+
+}
diff --git a/log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java b/log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
index 44a15aa..c70afa0 100644
--- a/log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
+++ b/log4j-api/src/test/java/org/apache/logging/log4j/message/MapMessageTest.java
@@ -16,10 +16,18 @@
  */
 package org.apache.logging.log4j.message;
 
+import com.google.common.base.Strings;
 import org.apache.logging.log4j.util.StringBuilderFormattable;
 import org.junit.Test;
 
-import static org.junit.Assert.*;
+import java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
 
 /**
  *
@@ -79,7 +87,7 @@ public class MapMessageTest {
         msg.put("message", testMsg);
         msg.put("project", "Log4j");
         final String result = msg.getFormattedMessage(new String[]{"JSON"});
-        final String expected = "{\"message\":\"Test message {}\", \"project\":\"Log4j\"}";
+        final String expected = "{'message':'Test message {}','project':'Log4j'}".replace('\'', '"');
         assertEquals(expected, result);
     }
 
@@ -105,6 +113,87 @@ public class MapMessageTest {
     }
 
     @Test
+    public void testJsonFormatterNestedObjectSupport() {
+        final String actualJson = new ObjectMapMessage()
+                .with("key1", "val1")
+                .with("key2", Collections.singletonMap("key2.1", "val2.1"))
+                .with("key3", Arrays.asList(
+                        3,
+                        (byte) 127,
+                        4.5D,
+                        4.6F,
+                        Arrays.asList(true, false),
+                        new BigDecimal(30),
+                        Collections.singletonMap("key3.3", "val3.3")))
+                .with("key4", new LinkedHashMap<String, Object>() {{
+                    put("chars", new char[]{'a', 'b', 'c'});
+                    put("booleans", new boolean[]{true, false});
+                    put("bytes", new byte[]{1, 2});
+                    put("shorts", new short[]{3, 4});
+                    put("ints", new int[]{256, 257});
+                    put("longs", new long[]{2147483648L, 2147483649L});
+                    put("floats", new float[]{1.0F, 1.1F});
+                    put("doubles", new double[]{2.0D, 2.1D});
+                    put("objects", new Object[]{"foo", "bar"});
+                }})
+                .getFormattedMessage(new String[]{"JSON"});
+        final String expectedJson = ("{" +
+                "'key1':'val1'," +
+                "'key2':{'key2.1':'val2.1'}," +
+                "'key3':[3,127,4.5,4.6,[true,false],30,{'key3.3':'val3.3'}]," +
+                "'key4':{" +
+                "'chars':['a','b','c']," +
+                "'booleans':[true,false]," +
+                "'bytes':[1,2]," +
+                "'shorts':[3,4]," +
+                "'ints':[256,257]," +
+                "'longs':[2147483648,2147483649]," +
+                "'floats':[1.0,1.1]," +
+                "'doubles':[2.0,2.1]," +
+                "'objects':['foo','bar']" +
+                "}}").replace('\'', '"');
+        assertEquals(expectedJson, actualJson);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testJsonFormatterInfiniteRecursionPrevention() {
+        final List<Object> recursiveValue = Arrays.asList(1, null);
+        // noinspection CollectionAddedToSelf
+        recursiveValue.set(1, recursiveValue);
+        new ObjectMapMessage()
+                .with("key", recursiveValue)
+                .getFormattedMessage(new String[]{"JSON"});
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testJsonFormatterMaxDepthViolation() {
+        testJsonFormatterMaxDepth(MapMessageJsonFormatter.MAX_DEPTH - 1);
+    }
+
+    @Test
+    public void testJsonFormatterMaxDepthConformance() {
+        int depth = MapMessageJsonFormatter.MAX_DEPTH - 2;
+        String expectedJson = String
+                .format("{'key':%s1%s}",
+                        Strings.repeat("[", depth),
+                        Strings.repeat("]", depth))
+                .replace('\'', '"');
+        String actualJson = testJsonFormatterMaxDepth(depth);
+        assertEquals(expectedJson, actualJson);
+    }
+
+    public static String testJsonFormatterMaxDepth(int depth) {
+        List<Object> list = new LinkedList<>();
+        list.add(1);
+        while (--depth > 0) {
+            list = new LinkedList<>(Collections.singletonList(list));
+        }
+        return new ObjectMapMessage()
+                .with("key", list)
+                .getFormattedMessage(new String[]{"JSON"});
+    }
+
+    @Test
     public void testJava() {
         final String testMsg = "Test message {}";
         final StringMapMessage msg = new StringMapMessage();
@@ -151,10 +240,9 @@ public class MapMessageTest {
 
     @Test
     public void testJSONFormatNonStringValue() {
-        final ObjectMapMessage msg = new ObjectMapMessage()
-                .with("key", 1L);
+        final ObjectMapMessage msg = new ObjectMapMessage().with("key", 1L);
         final String result = msg.getFormattedMessage(new String[]{"JSON"});
-        final String expected = "{\"key\":\"1\"}";
+        final String expected = "{'key':1}".replace('\'', '"');
         assertEquals(expected, result);
     }