From c91c42aabaa30d38c88a8a4ddd7b5ba2a99e40eb Mon Sep 17 00:00:00 2001
From: gongdewei <kylixs@qq.com>
Date: Wed, 21 Jul 2021 16:53:05 +0800
Subject: [PATCH] truncate string value, polish object renderer

---
 .../arthas/core/command/model/ObjectVO.java   |  4 +-
 .../core/util/object/ObjectInspector.java     | 39 +++++---
 .../core/util/object/ObjectRenderer.java      | 98 ++++++++++---------
 .../arthas/core/model/ObjectRenderTest.java   | 72 +++++++++++++-
 4 files changed, 152 insertions(+), 61 deletions(-)

diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/ObjectVO.java b/core/src/main/java/com/taobao/arthas/core/command/model/ObjectVO.java
index 58ab88dd0..383758d6f 100644
--- a/core/src/main/java/com/taobao/arthas/core/command/model/ObjectVO.java
+++ b/core/src/main/java/com/taobao/arthas/core/command/model/ObjectVO.java
@@ -22,13 +22,13 @@ public class ObjectVO {
     //map entry key
     protected ObjectVO key;
 
-    // string, collection/array
+    //collection/array size
     private Integer size;
 
     //complex object's fields
     protected List<ObjectVO> fields;
 
-    //exceed number limit
+    // whether exceed number limit
     private Boolean exceedLimit;
 
     private Integer objectNumberLimit;
diff --git a/core/src/main/java/com/taobao/arthas/core/util/object/ObjectInspector.java b/core/src/main/java/com/taobao/arthas/core/util/object/ObjectInspector.java
index fa457fbcb..04be9efc6 100644
--- a/core/src/main/java/com/taobao/arthas/core/util/object/ObjectInspector.java
+++ b/core/src/main/java/com/taobao/arthas/core/util/object/ObjectInspector.java
@@ -22,23 +22,20 @@ public class ObjectInspector {
 
     public static final int DEFAULT_OBJECT_NUMBER_LIMIT = 500;
     public static final int DEFAULT_ARRAY_LEN_LIMIT = 100;
+    public static final int DEFAULT_STRING_LEN_LIMIT = 4096;
 
-    private int objectNumberLimit;
-    private int arrayLenLimit;
+
+    private int objectNumberLimit = DEFAULT_OBJECT_NUMBER_LIMIT;
+    private int arrayLenLimit = DEFAULT_ARRAY_LEN_LIMIT;
+    private int stringLenLimit = DEFAULT_STRING_LEN_LIMIT;
     //子对象数量
     private int objectCount;
 
     public ObjectInspector() {
-        this(DEFAULT_OBJECT_NUMBER_LIMIT, DEFAULT_ARRAY_LEN_LIMIT);
     }
 
     public ObjectInspector(int objectNumberLimit) {
-        this(objectNumberLimit, DEFAULT_ARRAY_LEN_LIMIT);
-    }
-
-    public ObjectInspector(int objectNumberLimit, int arrayLenLimit) {
-        this.setObjectNumberLimit(objectNumberLimit);
-        this.setArrayLenLimit(arrayLenLimit);
+        this.objectNumberLimit = objectNumberLimit;
     }
 
     public ObjectVO inspect(Object object, int expand) {
@@ -51,11 +48,24 @@ public class ObjectInspector {
             return objectVO;
         } catch (ObjectTooLargeException e) {
             // unreachable statement
-            return new ObjectVO(object != null ? object.getClass().getSimpleName() : "", "...");
+            return new ObjectVO(object != null ? object.getClass().getSimpleName() : "", e.getMessage());
         }
     }
 
     private ObjectVO inspectObject(Object obj, int deep, int expand) throws ObjectTooLargeException {
+        ObjectVO objectVO = this.inspectObject0(obj, deep, expand);
+        if (objectVO != null && objectVO.getValue() != null && objectVO.getValue() instanceof String) {
+            String stringValue = (String) objectVO.getValue();
+            if (stringValue.length() > stringLenLimit) {
+                // truncate string value
+                objectVO.setValue(stringValue.substring(0, stringLenLimit) + "...(truncated " +
+                        (stringValue.length() - stringLenLimit) + " chars)");
+            }
+        }
+        return objectVO;
+    }
+
+    private ObjectVO inspectObject0(Object obj, int deep, int expand) throws ObjectTooLargeException {
         checkObjectAmount();
 
         if (null == obj) {
@@ -588,8 +598,15 @@ public class ObjectInspector {
         this.arrayLenLimit = arrayLenLimit < 10 ? 10 :arrayLenLimit;
     }
 
+    public int getStringLenLimit() {
+        return stringLenLimit;
+    }
+
+    public void setStringLenLimit(int stringLenLimit) {
+        this.stringLenLimit = stringLenLimit < 100 ? 100 : stringLenLimit;
+    }
 
-    // --------------- static methods --------------------//
+// --------------- static methods --------------------//
 
     private static Object[] toArray(int[] arrays, int limit) {
         limit = Math.min(arrays.length, limit);
diff --git a/core/src/main/java/com/taobao/arthas/core/util/object/ObjectRenderer.java b/core/src/main/java/com/taobao/arthas/core/util/object/ObjectRenderer.java
index 51a3079dc..04fc12839 100644
--- a/core/src/main/java/com/taobao/arthas/core/util/object/ObjectRenderer.java
+++ b/core/src/main/java/com/taobao/arthas/core/util/object/ObjectRenderer.java
@@ -1,6 +1,7 @@
 package com.taobao.arthas.core.util.object;
 
 import com.taobao.arthas.core.command.model.ObjectVO;
+import com.taobao.arthas.core.util.StringUtils;
 
 import java.util.Collection;
 
@@ -29,30 +30,25 @@ public abstract class ObjectRenderer {
             return;
         }
         if (vo.getKey() != null) {
-            //kv entry
-            render(vo.getKey(), deep, sb);
-            sb.append(" : ");
-            render((ObjectVO) vo.getValue(), deep, sb);
+            renderKeyValueEntry(vo, deep, sb);
             return;
         }
 
         if (vo.getName() != null) {
+            // object field name
             sb.append(vo.getName()).append('=');
         }
+
         if (vo.getType() != null) {
-            //object
+            //object type
             sb.append('@').append(vo.getType()).append('[');
         }
 
+        // object value start
         int nextDeep = deep+1;
         if (vo.getFields() != null) {
-            //fields
-            sb.append('\n');
-            for (ObjectVO field : vo.getFields()) {
-                renderTab(sb, nextDeep);
-                render(field, nextDeep, sb);
-                sb.append(",\n");
-            }
+            // complex object: fields is not null
+            renderComplexObject(vo, nextDeep, sb);
         } else {
             //value
             if (vo.getSize() != null) {
@@ -68,6 +64,8 @@ public abstract class ObjectRenderer {
                 renderValue(vo, deep, sb);
             }
         }
+
+        // object value end
         if (vo.getType() != null) {
             if (sb.charAt(sb.length() - 1) == '\n') {
                 renderTab(sb, deep);
@@ -76,6 +74,25 @@ public abstract class ObjectRenderer {
         }
     }
 
+    private static void renderComplexObject(ObjectVO vo, int deep, StringBuffer sb) {
+        sb.append('\n');
+        for (ObjectVO field : vo.getFields()) {
+            if (StringUtils.isEmpty(field.getName())) {
+                throw new IllegalArgumentException("Complex object's field name is empty: " + sb + "... ");
+            }
+            renderTab(sb, deep);
+            render(field, deep, sb);
+            sb.append(",\n");
+        }
+    }
+
+    private static void renderKeyValueEntry(ObjectVO vo, int deep, StringBuffer sb) {
+        //kv entry
+        render(vo.getKey(), deep, sb);
+        sb.append(" : ");
+        render((ObjectVO) vo.getValue(), deep, sb);
+    }
+
     private static StringBuffer renderTab(StringBuffer sb, int deep) {
         for (int i = 0; i < deep; i++) {
             sb.append(TAB);
@@ -91,47 +108,40 @@ public abstract class ObjectRenderer {
             sb.append('\n');
             Collection collection = (Collection) value;
             for (Object e : collection) {
-                if (e instanceof ObjectVO) {
-                    ObjectVO objectVO = (ObjectVO) e;
-                    renderTab(sb, nextDeep);
-                    render(objectVO, nextDeep, sb);
-                    sb.append(",\n");
-                } else {
-                    renderTab(sb, nextDeep);
-                    sb.append(e).append(",\n");
-                }
-            }
-            //如果没有完全显示所有元素,则添加省略提示
-            int count = collection.size();
-            if (vo.getSize() > count) {
-                String msg = count + " out of " + vo.getSize() + " displayed, " + (vo.getSize() - count) + " remaining.\n";
-                renderTab(sb, nextDeep);
-                sb.append(msg);
+                renderElement(e, nextDeep, sb);
             }
+            renderSize(vo, collection.size(), nextDeep, sb);
         } else if (value instanceof Object[]) {
             sb.append('\n');
             Object[] objs = (Object[]) value;
             for (int i = 0; i < objs.length; i++) {
-                Object obj = objs[i];
-                if (obj instanceof ObjectVO) {
-                    ObjectVO objectVO = (ObjectVO) obj;
-                    renderTab(sb, nextDeep);
-                    render(objectVO, nextDeep, sb);
-                    sb.append(",\n");
-                } else {
-                    renderTab(sb, nextDeep);
-                    sb.append(obj).append(",\n");
-                }
-            }
-            //如果没有完全显示所有元素,则添加省略提示
-            int count = objs.length;
-            if (vo.getSize() > count) {
-                String msg = count + " out of " + vo.getSize() + " displayed, " + (vo.getSize() - count) + " remaining.\n";
-                renderTab(sb, nextDeep).append(msg);
+                renderElement(objs[i], nextDeep, sb);
             }
+            renderSize(vo, objs.length, nextDeep, sb);
         } else {
             sb.append(value);
         }
     }
 
+    private static void renderElement(Object obj, int deep, StringBuffer sb) {
+        if (obj instanceof ObjectVO) {
+            ObjectVO objectVO = (ObjectVO) obj;
+            renderTab(sb, deep);
+            render(objectVO, deep, sb);
+            sb.append(",\n");
+        } else {
+            renderTab(sb, deep);
+            sb.append(obj).append(",\n");
+        }
+    }
+
+    private static void renderSize(ObjectVO vo, int elemCount, int deep, StringBuffer sb) {
+        //如果没有完全显示所有元素,则添加省略提示
+        if (vo.getSize() > elemCount) {
+            String msg = elemCount + " out of " + vo.getSize() + " displayed, " + (vo.getSize() - elemCount) + " remaining.\n";
+            renderTab(sb, deep);
+            sb.append(msg);
+        }
+    }
+
 }
diff --git a/core/src/test/java/com/taobao/arthas/core/model/ObjectRenderTest.java b/core/src/test/java/com/taobao/arthas/core/model/ObjectRenderTest.java
index ee96d8a0d..317b097c6 100644
--- a/core/src/test/java/com/taobao/arthas/core/model/ObjectRenderTest.java
+++ b/core/src/test/java/com/taobao/arthas/core/model/ObjectRenderTest.java
@@ -14,6 +14,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Date;
+import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -26,6 +27,7 @@ public class ObjectRenderTest {
 
     static int arrayLenLimit = 15;
     static int objectNumLimit = 30;
+    static int stringLenLimit = 100;
 
     @Test
     public void testSimpleObjects() {
@@ -357,7 +359,8 @@ public class ObjectRenderTest {
     @Test
     public void testObjectTooLargeException() {
         NestedClass nestedClass = new NestedClass(100);
-        ObjectInspector objectInspector = new ObjectInspector(10, arrayLenLimit);
+        ObjectInspector objectInspector = new ObjectInspector(10);
+        objectInspector.setArrayLenLimit(arrayLenLimit);
         ObjectVO objectVO = objectInspector.inspect(nestedClass, 4);
         printObject(objectVO);
 
@@ -380,6 +383,64 @@ public class ObjectRenderTest {
                         "] Number of objects exceeds limit: 10"));
     }
 
+    @Test
+    public void testTruncateStringValue() {
+
+        String str = "This is a very long string.";
+        StringBuilder sb = new StringBuilder(str.length()*10);
+        for (int i = 0; i < 10; i++) {
+            sb.append(str);
+        }
+        String longString = sb.toString();
+
+        // string
+        ObjectVO objectVO = inspectObject(longString, 1);
+        printObject(objectVO);
+        Assert.assertTrue(ObjectRenderer.render(objectVO).contains("(truncated 170 chars)"));
+
+        // array
+        Object[] objects = new Object[]{longString};
+        objectVO = inspectObject(objects, 2);
+        printObject(objectVO);
+        Assert.assertTrue(ObjectRenderer.render(objectVO).contains("(truncated 170 chars)"));
+
+        // collection
+        List list = new ArrayList();
+        list.add(longString);
+        objectVO = inspectObject(objects, 2);
+        printObject(objectVO);
+        Assert.assertTrue(ObjectRenderer.render(objectVO).contains("(truncated 170 chars)"));
+
+        // map
+        Map map = new HashMap();
+        map.put("longString", longString);
+        objectVO = inspectObject(map, 2);
+        printObject(objectVO);
+        Assert.assertTrue(ObjectRenderer.render(objectVO).contains("(truncated 170 chars)"));
+
+        // complex object
+        SonBean sonBean = new SonBean();
+        sonBean.setJ(longString);
+        objectVO = inspectObject(sonBean, 2);
+        printObject(objectVO);
+        Assert.assertTrue(ObjectRenderer.render(objectVO).contains("(truncated 170 chars)"));
+
+        // nest map
+        Map nestMap = new HashMap();
+        nestMap.put("sonBean", sonBean);
+        objectVO = inspectObject(nestMap, 3);
+        printObject(objectVO);
+        Assert.assertTrue(ObjectRenderer.render(objectVO).contains("(truncated 170 chars)"));
+
+        // nest list
+        List nestList = new ArrayList();
+        nestList.add(nestMap);
+        objectVO = inspectObject(nestList, 3);
+        printObject(objectVO);
+        Assert.assertTrue(ObjectRenderer.render(objectVO).contains("(truncated 170 chars)"));
+
+    }
+
     /**
      * 显示基类属性值
      */
@@ -419,12 +480,15 @@ public class ObjectRenderTest {
         }
     }
 
-    private ObjectInspector newInspector() {
-        return new ObjectInspector(objectNumLimit, arrayLenLimit);
+    private ObjectInspector newInspector(int objectNumLimit, int arrayLenLimit, int stringLenLimit) {
+        ObjectInspector objectInspector = new ObjectInspector(objectNumLimit);
+        objectInspector.setArrayLenLimit(arrayLenLimit);
+        objectInspector.setStringLenLimit(stringLenLimit);
+        return objectInspector;
     }
 
     private ObjectVO inspectObject(Object object, int expand) {
-        return new ObjectInspector(objectNumLimit, arrayLenLimit).inspect(object, expand);
+        return newInspector(objectNumLimit, arrayLenLimit, stringLenLimit).inspect(object, expand);
     }
 
     private void testSimpleArray(Object array, String testJson, String testString) {