Repository: ignite
Updated Branches:
  refs/heads/master 09ce06cc0 -> d67c5bf4c


IGNITE-602 Fixed possible StackOverflowError in GridToStringBuilder when 
circular references are present - Fixes #1558.

Signed-off-by: Alexey Goncharuk <[email protected]>


Project: http://git-wip-us.apache.org/repos/asf/ignite/repo
Commit: http://git-wip-us.apache.org/repos/asf/ignite/commit/d67c5bf4
Tree: http://git-wip-us.apache.org/repos/asf/ignite/tree/d67c5bf4
Diff: http://git-wip-us.apache.org/repos/asf/ignite/diff/d67c5bf4

Branch: refs/heads/master
Commit: d67c5bf4c22338695a116e0fbf0a58a4d581af5d
Parents: 09ce06c
Author: Dmitrii Ryabov <[email protected]>
Authored: Mon Jul 16 18:51:39 2018 +0300
Committer: Alexey Goncharuk <[email protected]>
Committed: Mon Jul 16 18:51:39 2018 +0300

----------------------------------------------------------------------
 .../util/tostring/GridToStringBuilder.java      | 706 +++++++++----------
 .../util/tostring/GridToStringThreadLocal.java  |  66 --
 .../internal/util/tostring/SBLimitedLength.java |  20 +
 .../tostring/GridToStringBuilderSelfTest.java   | 247 ++++++-
 4 files changed, 600 insertions(+), 439 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ignite/blob/d67c5bf4/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java
index 77c333d..86daf7c 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java
@@ -28,19 +28,16 @@ import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.EventListener;
-import java.util.LinkedList;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Queue;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.locks.Condition;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReadWriteLock;
 import org.apache.ignite.IgniteException;
 import org.apache.ignite.IgniteSystemProperties;
-import org.apache.ignite.internal.util.typedef.F;
 import org.apache.ignite.internal.util.typedef.internal.SB;
-import org.apache.ignite.internal.util.typedef.internal.U;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
@@ -50,6 +47,9 @@ import static 
org.apache.ignite.IgniteSystemProperties.IGNITE_TO_STRING_INCLUDE_
 /**
  * Provides auto-generation framework for {@code toString()} output.
  * <p>
+ * In case of recursion, repeatable objects will be shown as "ClassName@hash".
+ * But fields will be printed only for the first entry to prevent recursion.
+ * <p>
  * Default exclusion policy (can be overridden with {@link GridToStringInclude}
  * annotation):
  * <ul>
@@ -84,6 +84,9 @@ import static 
org.apache.ignite.IgniteSystemProperties.IGNITE_TO_STRING_INCLUDE_
  */
 public class GridToStringBuilder {
     /** */
+    private static final Object[] EMPTY_ARRAY = new Object[0];
+
+    /** */
     private static final Map<String, GridToStringClassDescriptor> classCache = 
new ConcurrentHashMap<>();
 
     /** {@link IgniteSystemProperties#IGNITE_TO_STRING_INCLUDE_SENSITIVE} */
@@ -94,25 +97,30 @@ public class GridToStringBuilder {
     private static final int COLLECTION_LIMIT =
         IgniteSystemProperties.getInteger(IGNITE_TO_STRING_COLLECTION_LIMIT, 
100);
 
-    /** */
-    private static ThreadLocal<Queue<GridToStringThreadLocal>> threadCache = 
new ThreadLocal<Queue<GridToStringThreadLocal>>() {
-        @Override protected Queue<GridToStringThreadLocal> initialValue() {
-            Queue<GridToStringThreadLocal> queue = new LinkedList<>();
+    /** Every thread has its own string builder. */
+    private static ThreadLocal<SBLimitedLength> threadLocSB = new 
ThreadLocal<SBLimitedLength>() {
+        @Override protected SBLimitedLength initialValue() {
+            SBLimitedLength sb = new SBLimitedLength(256);
 
-            queue.offer(new GridToStringThreadLocal());
+            sb.initLimit(new SBLengthLimit());
 
-            return queue;
+            return sb;
         }
     };
 
-    /** */
-    private static ThreadLocal<SBLengthLimit> threadCurLen = new 
ThreadLocal<SBLengthLimit>() {
-        @Override protected SBLengthLimit initialValue() {
-            return new SBLengthLimit();
+    /**
+     * Contains objects currently printing in the string builder.
+     * <p>
+     * Since {@code toString()} methods can be chain-called from the same 
thread we
+     * have to keep a map of this objects pointed to the position of previous 
occurrence
+     * and remove/add them in each {@code toString()} apply.
+     */
+    private static ThreadLocal<IdentityHashMap<Object, Integer>> savedObjects 
= new ThreadLocal<IdentityHashMap<Object, Integer>>() {
+        @Override protected IdentityHashMap<Object, Integer> initialValue() {
+            return new IdentityHashMap<>();
         }
     };
 
-
     /**
      * Produces auto-generated output of string presentation for given object 
and its declaration class.
      *
@@ -261,18 +269,9 @@ public class GridToStringBuilder {
         assert name3 != null;
         assert name4 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] addNames = tmp.getAdditionalNames();
-        Object[] addVals = tmp.getAdditionalValues();
-        boolean[] addSens = tmp.getAdditionalSensitives();
+        Object[] addNames = new Object[5];
+        Object[] addVals = new Object[5];
+        boolean[] addSens = new boolean[5];
 
         addNames[0] = name0;
         addVals[0] = val0;
@@ -290,20 +289,16 @@ public class GridToStringBuilder {
         addVals[4] = val4;
         addSens[4] = sens4;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(cls, tmp.getStringBuilder(lenLim), obj, 
addNames, addVals, addSens, 5);
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 5);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -349,18 +344,9 @@ public class GridToStringBuilder {
         assert name4 != null;
         assert name5 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] addNames = tmp.getAdditionalNames();
-        Object[] addVals = tmp.getAdditionalValues();
-        boolean[] addSens = tmp.getAdditionalSensitives();
+        Object[] addNames = new Object[6];
+        Object[] addVals = new Object[6];
+        boolean[] addSens = new boolean[6];
 
         addNames[0] = name0;
         addVals[0] = val0;
@@ -381,20 +367,16 @@ public class GridToStringBuilder {
         addVals[5] = val5;
         addSens[5] = sens5;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(cls, tmp.getStringBuilder(lenLim), obj, 
addNames, addVals, addSens, 6);
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 6);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -445,18 +427,9 @@ public class GridToStringBuilder {
         assert name5 != null;
         assert name6 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] addNames = tmp.getAdditionalNames();
-        Object[] addVals = tmp.getAdditionalValues();
-        boolean[] addSens = tmp.getAdditionalSensitives();
+        Object[] addNames = new Object[7];
+        Object[] addVals = new Object[7];
+        boolean[] addSens = new boolean[7];
 
         addNames[0] = name0;
         addVals[0] = val0;
@@ -480,20 +453,16 @@ public class GridToStringBuilder {
         addVals[6] = val6;
         addSens[6] = sens6;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(cls, tmp.getStringBuilder(lenLim), obj, 
addNames, addVals, addSens, 7);
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 7);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -557,18 +526,9 @@ public class GridToStringBuilder {
         assert name2 != null;
         assert name3 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] addNames = tmp.getAdditionalNames();
-        Object[] addVals = tmp.getAdditionalValues();
-        boolean[] addSens = tmp.getAdditionalSensitives();
+        Object[] addNames = new Object[4];
+        Object[] addVals = new Object[4];
+        boolean[] addSens = new boolean[4];
 
         addNames[0] = name0;
         addVals[0] = val0;
@@ -583,20 +543,16 @@ public class GridToStringBuilder {
         addVals[3] = val3;
         addSens[3] = sens3;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(cls, tmp.getStringBuilder(lenLim), obj, 
addNames, addVals, addSens, 4);
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 4);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -652,18 +608,9 @@ public class GridToStringBuilder {
         assert name1 != null;
         assert name2 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] addNames = tmp.getAdditionalNames();
-        Object[] addVals = tmp.getAdditionalValues();
-        boolean[] addSens = tmp.getAdditionalSensitives();
+        Object[] addNames = new Object[3];
+        Object[] addVals = new Object[3];
+        boolean[] addSens = new boolean[3];
 
         addNames[0] = name0;
         addVals[0] = val0;
@@ -675,20 +622,16 @@ public class GridToStringBuilder {
         addVals[2] = val2;
         addSens[2] = sens2;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(cls, tmp.getStringBuilder(lenLim), obj, 
addNames, addVals, addSens, 3);
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 3);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -732,18 +675,9 @@ public class GridToStringBuilder {
         assert name0 != null;
         assert name1 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] addNames = tmp.getAdditionalNames();
-        Object[] addVals = tmp.getAdditionalValues();
-        boolean[] addSens = tmp.getAdditionalSensitives();
+        Object[] addNames = new Object[2];
+        Object[] addVals = new Object[2];
+        boolean[] addSens = new boolean[2];
 
         addNames[0] = name0;
         addVals[0] = val0;
@@ -752,20 +686,16 @@ public class GridToStringBuilder {
         addVals[1] = val1;
         addSens[1] = sens1;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(cls, tmp.getStringBuilder(lenLim), obj, 
addNames, addVals, addSens, 2);
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 2);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -799,37 +729,24 @@ public class GridToStringBuilder {
         assert obj != null;
         assert name != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] addNames = tmp.getAdditionalNames();
-        Object[] addVals = tmp.getAdditionalValues();
-        boolean[] addSens = tmp.getAdditionalSensitives();
+        Object[] addNames = new Object[1];
+        Object[] addVals = new Object[1];
+        boolean[] addSens = new boolean[1];
 
         addNames[0] = name;
         addVals[0] = val;
         addSens[0] = sens;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(cls, tmp.getStringBuilder(lenLim), obj, 
addNames, addVals, addSens, 1);
+            return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 1);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -845,30 +762,16 @@ public class GridToStringBuilder {
         assert cls != null;
         assert obj != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
+        SBLimitedLength sb = threadLocSB.get();
 
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        SBLengthLimit lenLim = threadCurLen.get();
-
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(cls, tmp.getStringBuilder(lenLim), obj, 
tmp.getAdditionalNames(),
-                tmp.getAdditionalValues(), null, 0);
+            return toStringImpl(cls, sb, obj, EMPTY_ARRAY, EMPTY_ARRAY, null, 
0);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -886,68 +789,164 @@ public class GridToStringBuilder {
     }
 
     /**
-     * Print value with length limitation
+     * Print value with length limitation.
+     *
      * @param buf buffer to print to.
      * @param val value to print, can be {@code null}.
      */
     private static void toString(SBLimitedLength buf, Object val) {
-        if (val == null)
-            buf.a("null");
-        else
-            toString(buf, val.getClass(), val);
+        toString(buf, null, val);
     }
 
     /**
-     * Print value with length limitation
+     * Print value with length limitation.
+     *
      * @param buf buffer to print to.
-     * @param valClass value class.
-     * @param val value to print
+     * @param cls value class.
+     * @param val value to print.
      */
-    private static void toString(SBLimitedLength buf, Class<?> valClass, 
Object val) {
-        if (valClass.isArray())
-            buf.a(arrayToString(valClass, val));
-        else {
-            int overflow = 0;
-            char bracket = ' ';
+    @SuppressWarnings({"unchecked"})
+    private static void toString(SBLimitedLength buf, Class<?> cls, Object 
val) {
+        if (val == null) {
+            buf.a("null");
 
-            if (val instanceof Collection && ((Collection)val).size() > 
COLLECTION_LIMIT) {
-                overflow = ((Collection)val).size() - COLLECTION_LIMIT;
-                bracket = ']';
-                val = F.retain((Collection) val, true, COLLECTION_LIMIT);
-            }
-            else if (val instanceof Map && ((Map)val).size() > 
COLLECTION_LIMIT) {
-                Map<Object, Object> tmp = U.newHashMap(COLLECTION_LIMIT);
+            return;
+        }
 
-                overflow = ((Map)val).size() - COLLECTION_LIMIT;
+        if (cls == null)
+            cls = val.getClass();
 
-                bracket= '}';
+        if (cls.isPrimitive()) {
+            buf.a(val);
 
-                int cntr = 0;
+            return;
+        }
 
-                for (Object o : ((Map)val).entrySet()) {
-                    Map.Entry e = (Map.Entry)o;
+        IdentityHashMap<Object, Integer> svdObjs = savedObjects.get();
 
-                    tmp.put(e.getKey(), e.getValue());
+        if (handleRecursion(buf, val, svdObjs))
+            return;
 
-                    if (++cntr >= COLLECTION_LIMIT)
-                        break;
-                }
+        svdObjs.put(val, buf.length());
 
-                val = tmp;
-            }
+        try {
+            if (cls.isArray())
+                addArray(buf, cls, val);
+            else if (val instanceof Collection)
+                addCollection(buf, (Collection) val);
+            else if (val instanceof Map)
+                addMap(buf, (Map<?, ?>) val);
+            else
+                buf.a(val);
+        }
+        finally {
+            svdObjs.remove(val);
+        }
+    }
 
-            buf.a(val);
+    /**
+     * Writes array to buffer.
+     *
+     * @param buf String builder buffer.
+     * @param arrType Type of the array.
+     * @param obj Array object.
+     */
+    private static void addArray(SBLimitedLength buf, Class arrType, Object 
obj) {
+        if (arrType.getComponentType().isPrimitive()) {
+            buf.a(arrayToString(arrType, obj));
 
-            if (overflow > 0) {
-                buf.d(buf.length() - 1);
-                buf.a("... and ").a(overflow).a(" more").a(bracket);
-            }
+            return;
+        }
+
+        Object[] arr = (Object[]) obj;
+
+        buf.a(arrType.getSimpleName()).a(" [");
+
+        for (int i = 0; i < arr.length; i++) {
+            toString(buf, arr[i]);
+
+            if (i == COLLECTION_LIMIT - 1 || i == arr.length - 1)
+                break;
+
+            buf.a(", ");
+        }
+
+        handleOverflow(buf, arr.length);
+
+        buf.a(']');
+    }
+
+    /**
+     * Writes collection to buffer.
+     *
+     * @param buf String builder buffer.
+     * @param col Collection object.
+     */
+    private static void addCollection(SBLimitedLength buf, Collection col) {
+        buf.a(col.getClass().getSimpleName()).a(" [");
+
+        int cnt = 0;
+
+        for (Object obj : col) {
+            toString(buf, obj);
+
+            if (++cnt == COLLECTION_LIMIT || cnt == col.size())
+                break;
+
+            buf.a(", ");
+        }
+
+        handleOverflow(buf, col.size());
+
+        buf.a(']');
+    }
+
+    /**
+     * Writes map to buffer.
+     *
+     * @param buf String builder buffer.
+     * @param map Map object.
+     */
+    private static <K, V> void addMap(SBLimitedLength buf, Map<K, V> map) {
+        buf.a(map.getClass().getSimpleName()).a(" {");
+
+        int cnt = 0;
+
+        for (Map.Entry<K, V> e : map.entrySet()) {
+            toString(buf, e.getKey());
+
+            buf.a('=');
+
+            toString(buf, e.getValue());
+
+            if (++cnt == COLLECTION_LIMIT || cnt == map.size())
+                break;
+
+            buf.a(", ");
         }
+
+        handleOverflow(buf, map.size());
+
+        buf.a('}');
+    }
+
+    /**
+     * Writes overflow message to buffer if needed.
+     *
+     * @param buf String builder buffer.
+     * @param size Size to compare with limit.
+     */
+    private static void handleOverflow(SBLimitedLength buf, int size) {
+        int overflow = size - COLLECTION_LIMIT;
+
+        if (overflow > 0)
+            buf.a("... and ").a(overflow).a(" more");
     }
 
     /**
      * Creates an uniformed string presentation for the given object.
      *
+     * @param <T> Type of object.
      * @param cls Class of the object.
      * @param buf String builder buffer.
      * @param obj Object for which to get string presentation.
@@ -956,9 +955,7 @@ public class GridToStringBuilder {
      * @param addSens Sensitive flag of values or {@code null} if all values 
are not sensitive.
      * @param addLen How many additional values will be included.
      * @return String presentation of the given object.
-     * @param <T> Type of object.
      */
-    @SuppressWarnings({"unchecked"})
     private static <T> String toStringImpl(
         Class<T> cls,
         SBLimitedLength buf,
@@ -975,13 +972,55 @@ public class GridToStringBuilder {
         assert addNames.length == addVals.length;
         assert addLen <= addNames.length;
 
+        boolean newStr = buf.length() == 0;
+
+        IdentityHashMap<Object, Integer> svdObjs = savedObjects.get();
+
+        if (newStr)
+            svdObjs.put(obj, buf.length());
+
+        try {
+            String s = toStringImpl0(cls, buf, obj, addNames, addVals, 
addSens, addLen);
+
+            if (newStr)
+                return s;
+
+            return "";
+        }
+        finally {
+            if (newStr)
+                svdObjs.remove(obj);
+        }
+    }
+
+    /**
+     * Creates an uniformed string presentation for the given object.
+     *
+     * @param cls Class of the object.
+     * @param buf String builder buffer.
+     * @param obj Object for which to get string presentation.
+     * @param addNames Names of additional values to be included.
+     * @param addVals Additional values to be included.
+     * @param addSens Sensitive flag of values or {@code null} if all values 
are not sensitive.
+     * @param addLen How many additional values will be included.
+     * @return String presentation of the given object.
+     * @param <T> Type of object.
+     */
+    @SuppressWarnings({"unchecked"})
+    private static <T> String toStringImpl0(
+        Class<T> cls,
+        SBLimitedLength buf,
+        T obj,
+        Object[] addNames,
+        Object[] addVals,
+        @Nullable boolean[] addSens,
+        int addLen
+    ) {
         try {
             GridToStringClassDescriptor cd = getClassDescriptor(cls);
 
             assert cd != null;
 
-            buf.setLength(0);
-
             buf.a(cd.getSimpleClassName()).a(" [");
 
             boolean first = true;
@@ -1144,37 +1183,24 @@ public class GridToStringBuilder {
     public static String toString(String str, String name, @Nullable Object 
val, boolean sens) {
         assert name != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] propNames = tmp.getAdditionalNames();
-        Object[] propVals = tmp.getAdditionalValues();
-        boolean[] propSens = tmp.getAdditionalSensitives();
+        Object[] propNames = new Object[1];
+        Object[] propVals = new Object[1];
+        boolean[] propSens = new boolean[1];
 
         propNames[0] = name;
         propVals[0] = val;
         propSens[0] = sens;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(str, tmp.getStringBuilder(lenLim), propNames, 
propVals, propSens, 1);
+            return toStringImpl(str, sb, propNames, propVals, propSens, 1);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -1211,18 +1237,9 @@ public class GridToStringBuilder {
         assert name0 != null;
         assert name1 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] propNames = tmp.getAdditionalNames();
-        Object[] propVals = tmp.getAdditionalValues();
-        boolean[] propSens = tmp.getAdditionalSensitives();
+        Object[] propNames = new Object[2];
+        Object[] propVals = new Object[2];
+        boolean[] propSens = new boolean[2];
 
         propNames[0] = name0;
         propVals[0] = val0;
@@ -1231,20 +1248,16 @@ public class GridToStringBuilder {
         propVals[1] = val1;
         propSens[1] = sens1;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(str, tmp.getStringBuilder(lenLim), propNames, 
propVals, propSens, 2);
+            return toStringImpl(str, sb, propNames, propVals, propSens, 2);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -1271,18 +1284,9 @@ public class GridToStringBuilder {
         assert name1 != null;
         assert name2 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] propNames = tmp.getAdditionalNames();
-        Object[] propVals = tmp.getAdditionalValues();
-        boolean[] propSens = tmp.getAdditionalSensitives();
+        Object[] propNames = new Object[3];
+        Object[] propVals = new Object[3];
+        boolean[] propSens = new boolean[3];
 
         propNames[0] = name0;
         propVals[0] = val0;
@@ -1294,20 +1298,16 @@ public class GridToStringBuilder {
         propVals[2] = val2;
         propSens[2] = sens2;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(str, tmp.getStringBuilder(lenLim), propNames, 
propVals, propSens, 3);
+            return toStringImpl(str, sb, propNames, propVals, propSens, 3);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -1339,18 +1339,9 @@ public class GridToStringBuilder {
         assert name2 != null;
         assert name3 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] propNames = tmp.getAdditionalNames();
-        Object[] propVals = tmp.getAdditionalValues();
-        boolean[] propSens = tmp.getAdditionalSensitives();
+        Object[] propNames = new Object[4];
+        Object[] propVals = new Object[4];
+        boolean[] propSens = new boolean[4];
 
         propNames[0] = name0;
         propVals[0] = val0;
@@ -1365,20 +1356,16 @@ public class GridToStringBuilder {
         propVals[3] = val3;
         propSens[3] = sens3;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(str, tmp.getStringBuilder(lenLim), propNames, 
propVals, propSens, 4);
+            return toStringImpl(str, sb, propNames, propVals, propSens, 4);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -1415,18 +1402,9 @@ public class GridToStringBuilder {
         assert name3 != null;
         assert name4 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] propNames = tmp.getAdditionalNames();
-        Object[] propVals = tmp.getAdditionalValues();
-        boolean[] propSens = tmp.getAdditionalSensitives();
+        Object[] propNames = new Object[5];
+        Object[] propVals = new Object[5];
+        boolean[] propSens = new boolean[5];
 
         propNames[0] = name0;
         propVals[0] = val0;
@@ -1444,20 +1422,16 @@ public class GridToStringBuilder {
         propVals[4] = val4;
         propSens[4] = sens4;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(str, tmp.getStringBuilder(lenLim), propNames, 
propVals, propSens, 5);
+            return toStringImpl(str, sb, propNames, propVals, propSens, 5);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -1499,18 +1473,9 @@ public class GridToStringBuilder {
         assert name4 != null;
         assert name5 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] propNames = tmp.getAdditionalNames();
-        Object[] propVals = tmp.getAdditionalValues();
-        boolean[] propSens = tmp.getAdditionalSensitives();
+        Object[] propNames = new Object[6];
+        Object[] propVals = new Object[6];
+        boolean[] propSens = new boolean[6];
 
         propNames[0] = name0;
         propVals[0] = val0;
@@ -1531,20 +1496,16 @@ public class GridToStringBuilder {
         propVals[5] = val5;
         propSens[5] = sens5;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(str, tmp.getStringBuilder(lenLim), propNames, 
propVals, propSens, 6);
+            return toStringImpl(str, sb, propNames, propVals, propSens, 6);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -1591,18 +1552,9 @@ public class GridToStringBuilder {
         assert name5 != null;
         assert name6 != null;
 
-        Queue<GridToStringThreadLocal> queue = threadCache.get();
-
-        assert queue != null;
-
-        // Since string() methods can be chain-called from the same thread we
-        // have to keep a list of thread-local objects and remove/add them
-        // in each string() apply.
-        GridToStringThreadLocal tmp = queue.isEmpty() ? new 
GridToStringThreadLocal() : queue.remove();
-
-        Object[] propNames = tmp.getAdditionalNames();
-        Object[] propVals = tmp.getAdditionalValues();
-        boolean[] propSens = tmp.getAdditionalSensitives();
+        Object[] propNames = new Object[7];
+        Object[] propVals = new Object[7];
+        boolean[] propSens = new boolean[7];
 
         propNames[0] = name0;
         propVals[0] = val0;
@@ -1626,20 +1578,16 @@ public class GridToStringBuilder {
         propVals[6] = val6;
         propSens[6] = sens6;
 
-        SBLengthLimit lenLim = threadCurLen.get();
+        SBLimitedLength sb = threadLocSB.get();
 
-        boolean newStr = false;
+        boolean newStr = sb.length() == 0;
 
         try {
-            newStr = lenLim.length() == 0;
-
-            return toStringImpl(str, tmp.getStringBuilder(lenLim), propNames, 
propVals, propSens, 7);
+            return toStringImpl(str, sb, propNames, propVals, propSens, 7);
         }
         finally {
-            queue.offer(tmp);
-
             if (newStr)
-                lenLim.reset();
+                sb.reset();
         }
     }
 
@@ -1841,4 +1789,54 @@ public class GridToStringBuilder {
 
         return sb.toString();
     }
+
+    /**
+     * Checks that object is already saved.
+     * In positive case this method inserts hash to the saved object entry (if 
needed) and name@hash for current entry.
+     * Further toString operations are not needed for current object.
+     *
+     * @param buf String builder buffer.
+     * @param obj Object.
+     * @param svdObjs Map with saved objects to handle recursion.
+     * @return {@code True} if object is already saved and name@hash was added 
to buffer.
+     * {@code False} if it wasn't saved previously and it should be saved.
+     */
+    private static boolean handleRecursion(SBLimitedLength buf, Object obj, 
IdentityHashMap<Object, Integer> svdObjs) {
+        Integer pos = svdObjs.get(obj);
+
+        if (pos == null)
+            return false;
+
+        String name = obj.getClass().getSimpleName();
+        String hash = '@' + Integer.toHexString(System.identityHashCode(obj));
+        String savedName = name + hash;
+
+        if (!buf.isOverflowed() && buf.impl().indexOf(savedName, pos) != pos) {
+            buf.i(pos + name.length(), hash);
+
+            incValues(svdObjs, obj, hash.length());
+        }
+
+        buf.a(savedName);
+
+        return true;
+    }
+
+    /**
+     * Increment positions of already presented objects afterward given object.
+     *
+     * @param svdObjs Map with objects already presented in the buffer.
+     * @param obj Object.
+     * @param hashLen Length of the object's hash.
+     */
+    private static void incValues(IdentityHashMap<Object, Integer> svdObjs, 
Object obj, int hashLen) {
+        Integer baseline = svdObjs.get(obj);
+
+        for (IdentityHashMap.Entry<Object, Integer> entry : 
svdObjs.entrySet()) {
+            Integer pos = entry.getValue();
+
+            if (pos > baseline)
+                entry.setValue(pos + hashLen);
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/d67c5bf4/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringThreadLocal.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringThreadLocal.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringThreadLocal.java
deleted file mode 100644
index 2f62727..0000000
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringThreadLocal.java
+++ /dev/null
@@ -1,66 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.ignite.internal.util.tostring;
-
-/**
- * Helper wrapper containing StringBuilder and additional values. Stored as a 
thread-local variable.
- */
-class GridToStringThreadLocal {
-    /** */
-    private SBLimitedLength sb = new SBLimitedLength(256);
-
-    /** */
-    private Object[] addNames = new Object[7];
-
-    /** */
-    private Object[] addVals = new Object[7];
-
-    /** */
-    private boolean[] addSens = new boolean[7];
-
-    /**
-     * @param len Length limit.
-     * @return String builder.
-     */
-    SBLimitedLength getStringBuilder(SBLengthLimit len) {
-        sb.initLimit(len);
-
-        return sb;
-    }
-
-    /**
-     * @return Additional names.
-     */
-    Object[] getAdditionalNames() {
-        return addNames;
-    }
-
-    /**
-     * @return Additional values.
-     */
-    Object[] getAdditionalValues() {
-        return addVals;
-    }
-
-    /**
-     * @return Additional values.
-     */
-    boolean[] getAdditionalSensitives() {
-        return addSens;
-    }
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/ignite/blob/d67c5bf4/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java
index c47cf40..b524b3d 100644
--- 
a/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java
@@ -51,6 +51,19 @@ public class SBLimitedLength extends GridStringBuilder {
     }
 
     /**
+     * Resets buffer.
+     */
+    public void reset() {
+        super.setLength(0);
+
+        lenLimit.reset();
+
+        if (tail != null)
+            tail.reset();
+    }
+
+
+    /**
      * @return tail string builder.
      */
     public CircularStringBuilder getTail() {
@@ -292,4 +305,11 @@ public class SBLimitedLength extends GridStringBuilder {
             return res.toString();
         }
     }
+
+    /**
+     * @return {@code True} - if buffer limit is reached.
+     */
+    public boolean isOverflowed() {
+        return lenLimit.overflowed(this);
+    }
 }

http://git-wip-us.apache.org/repos/asf/ignite/blob/d67c5bf4/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/GridToStringBuilderSelfTest.java
----------------------------------------------------------------------
diff --git 
a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/GridToStringBuilderSelfTest.java
 
b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/GridToStringBuilderSelfTest.java
index 4ac05fb..eff3349 100644
--- 
a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/GridToStringBuilderSelfTest.java
+++ 
b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/GridToStringBuilderSelfTest.java
@@ -20,19 +20,25 @@ package org.apache.ignite.internal.util.tostring;
 import java.lang.reflect.Array;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.TreeMap;
 import java.util.UUID;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
 import java.util.concurrent.locks.ReadWriteLock;
 import org.apache.ignite.IgniteLogger;
 import org.apache.ignite.IgniteSystemProperties;
-import static 
org.apache.ignite.IgniteSystemProperties.IGNITE_TO_STRING_COLLECTION_LIMIT;
-import static 
org.apache.ignite.IgniteSystemProperties.IGNITE_TO_STRING_MAX_LENGTH;
+import org.apache.ignite.internal.IgniteInternalFuture;
 import org.apache.ignite.internal.util.typedef.internal.S;
+import org.apache.ignite.testframework.GridTestUtils;
 import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest;
 import org.apache.ignite.testframework.junits.common.GridCommonTest;
 
+import static 
org.apache.ignite.IgniteSystemProperties.IGNITE_TO_STRING_COLLECTION_LIMIT;
+import static 
org.apache.ignite.IgniteSystemProperties.IGNITE_TO_STRING_MAX_LENGTH;
+
 /**
  * Tests for {@link GridToStringBuilder}.
  */
@@ -72,32 +78,164 @@ public class GridToStringBuilderSelfTest extends 
GridCommonAbstractTest {
     /**
      * @throws Exception If failed.
      */
-    public void testToStringCheckSimpleRecursionPrevention() throws Exception {
+    public void testToStringCheckSimpleListRecursionPrevention() throws 
Exception {
         ArrayList<Object> list1 = new ArrayList<>();
         ArrayList<Object> list2 = new ArrayList<>();
 
         list2.add(list1);
         list1.add(list2);
 
-
-        GridToStringBuilder.toString(ArrayList.class, list1);
-        GridToStringBuilder.toString(ArrayList.class, list2);
+        info(GridToStringBuilder.toString(ArrayList.class, list1));
+        info(GridToStringBuilder.toString(ArrayList.class, list2));
     }
 
     /**
      * @throws Exception If failed.
      */
-    public void testToStringCheckAdvancedRecursionPrevention() throws 
Exception {
-        fail("https://issues.apache.org/jira/browse/IGNITE-602";);
+    public void testToStringCheckSimpleMapRecursionPrevention() throws 
Exception {
+        HashMap<Object, Object> map1 = new HashMap<>();
+        HashMap<Object, Object> map2 = new HashMap<>();
 
+        map1.put("2", map2);
+        map2.put("1", map1);
+
+        info(GridToStringBuilder.toString(HashMap.class, map1));
+        info(GridToStringBuilder.toString(HashMap.class, map2));
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testToStringCheckListAdvancedRecursionPrevention() throws 
Exception {
         ArrayList<Object> list1 = new ArrayList<>();
         ArrayList<Object> list2 = new ArrayList<>();
 
         list2.add(list1);
         list1.add(list2);
 
-        GridToStringBuilder.toString(ArrayList.class, list1, "name", list2);
-        GridToStringBuilder.toString(ArrayList.class, list2, "name", list1);
+        info(GridToStringBuilder.toString(ArrayList.class, list1, "name", 
list2));
+        info(GridToStringBuilder.toString(ArrayList.class, list2, "name", 
list1));
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testToStringCheckMapAdvancedRecursionPrevention() throws 
Exception {
+        HashMap<Object, Object> map1 = new HashMap<>();
+        HashMap<Object, Object> map2 = new HashMap<>();
+
+        map1.put("2", map2);
+        map2.put("1", map1);
+
+        info(GridToStringBuilder.toString(HashMap.class, map1, "name", map2));
+        info(GridToStringBuilder.toString(HashMap.class, map2, "name", map1));
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testToStringCheckObjectRecursionPrevention() throws Exception {
+        Node n1 = new Node();
+        Node n2 = new Node();
+        Node n3 = new Node();
+        Node n4 = new Node();
+
+        n1.name = "n1";
+        n2.name = "n2";
+        n3.name = "n3";
+        n4.name = "n4";
+
+        n1.next = n2;
+        n2.next = n3;
+        n3.next = n4;
+        n4.next = n3;
+
+        n1.nodes = new Node[4];
+        n2.nodes = n1.nodes;
+        n3.nodes = n1.nodes;
+        n4.nodes = n1.nodes;
+
+        n1.nodes[0] = n1;
+        n1.nodes[1] = n2;
+        n1.nodes[2] = n3;
+        n1.nodes[3] = n4;
+
+        String expN1 = n1.toString();
+        String expN2 = n2.toString();
+        String expN3 = n3.toString();
+        String expN4 = n4.toString();
+
+        info(expN1);
+        info(expN2);
+        info(expN3);
+        info(expN4);
+        info(GridToStringBuilder.toString("Test", "Appended vals", n1));
+
+        CyclicBarrier bar = new CyclicBarrier(4);
+
+        IgniteInternalFuture<String> fut1 = GridTestUtils.runAsync(new 
BarrierCallable(bar, n1, expN1));
+        IgniteInternalFuture<String> fut2 = GridTestUtils.runAsync(new 
BarrierCallable(bar, n2, expN2));
+        IgniteInternalFuture<String> fut3 = GridTestUtils.runAsync(new 
BarrierCallable(bar, n3, expN3));
+        IgniteInternalFuture<String> fut4 = GridTestUtils.runAsync(new 
BarrierCallable(bar, n4, expN4));
+
+        fut1.get(3_000);
+        fut2.get(3_000);
+        fut3.get(3_000);
+        fut4.get(3_000);
+    }
+
+    /**
+     * Test class.
+     */
+    private static class Node {
+        /** */
+        @GridToStringInclude
+        String name;
+
+        /** */
+        @GridToStringInclude
+        Node next;
+
+        /** */
+        @GridToStringInclude
+        Node[] nodes;
+
+        /** {@inheritDoc} */
+        @Override public String toString() {
+            return GridToStringBuilder.toString(Node.class, this);
+        }
+    }
+
+    /**
+     * Test class.
+     */
+    private static class BarrierCallable implements Callable<String> {
+        /** */
+        CyclicBarrier bar;
+
+        /** */
+        Object obj;
+
+        /** Expected value of {@code toString()} method. */
+        String exp;
+
+        /** */
+        private BarrierCallable(CyclicBarrier bar, Object obj, String exp) {
+            this.bar = bar;
+            this.obj = obj;
+            this.exp = exp;
+        }
+
+        /** {@inheritDoc} */
+        @Override public String call() throws Exception {
+            for (int i = 0; i < 10; i++) {
+                bar.await();
+
+                assertEquals(exp, obj.toString());
+            }
+
+            return null;
+        }
     }
 
     /**
@@ -137,6 +275,33 @@ public class GridToStringBuilderSelfTest extends 
GridCommonAbstractTest {
         Arrays.fill(arrOf, v);
         T[] arr = Arrays.copyOf(arrOf, limit);
 
+        checkArrayOverflow(arrOf, arr, limit);
+    }
+
+    /**
+     * Test array print.
+     *
+     * @throws Exception if failed.
+     */
+    public void testArrLimitWithRecursion() throws Exception {
+        int limit = 
IgniteSystemProperties.getInteger(IGNITE_TO_STRING_COLLECTION_LIMIT, 100);
+
+        ArrayList[] arrOf = new ArrayList[limit + 1];
+        Arrays.fill(arrOf, new ArrayList());
+        ArrayList[] arr = Arrays.copyOf(arrOf, limit);
+
+        arrOf[0].add(arrOf);
+        arr[0].add(arr);
+
+        checkArrayOverflow(arrOf, arr, limit);
+    }
+
+    /**
+     * @param arrOf Array.
+     * @param arr Array copy.
+     * @param limit Array limit.
+     */
+    private void checkArrayOverflow(Object[] arrOf, Object[] arr, int limit) {
         String arrStr = GridToStringBuilder.arrayToString(arr.getClass(), arr);
         String arrOfStr = GridToStringBuilder.arrayToString(arrOf.getClass(), 
arrOf);
 
@@ -144,7 +309,11 @@ public class GridToStringBuilderSelfTest extends 
GridCommonAbstractTest {
         StringBuilder resultSB = new StringBuilder(arrStr);
             resultSB.deleteCharAt(resultSB.length()-1);
             resultSB.append("... and ").append(arrOf.length - limit).append(" 
more]");
-            arrStr = resultSB.toString();
+
+        arrStr = resultSB.toString();
+
+        info(arrOfStr);
+        info(arrStr);
 
         assertTrue("Collection limit error in array of type " + 
arrOf.getClass().getName()
             + " error, normal arr: <" + arrStr + ">, overflowed arr: <" + 
arrOfStr + ">", arrStr.equals(arrOfStr));
@@ -228,18 +397,58 @@ public class GridToStringBuilderSelfTest extends 
GridCommonAbstractTest {
             strMap.put("k" + i, "v");
             strList.add("e");
         }
-        String testClassStr = GridToStringBuilder.toString(TestClass1.class, 
testClass);
 
-        strMap.put("kz", "v"); // important to add last element in TreeMap here
-        strList.add("e");
+        checkColAndMap(testClass);
+    }
+
+    /**
+     * @throws Exception If failed.
+     */
+    public void testToStringColAndMapLimitWithRecursion() throws Exception {
+        int limit = 
IgniteSystemProperties.getInteger(IGNITE_TO_STRING_COLLECTION_LIMIT, 100);
+        Map strMap = new TreeMap<>();
+        List strList = new ArrayList<>(limit+1);
+
+        TestClass1 testClass = new TestClass1();
+        testClass.strMap = strMap;
+        testClass.strListIncl = strList;
+
+        Map m = new TreeMap();
+        m.put("m", strMap);
+
+        List l = new ArrayList();
+        l.add(strList);
+
+        strMap.put("k0", m);
+        strList.add(l);
+
+        for (int i = 1; i < limit; i++) {
+            strMap.put("k" + i, "v");
+            strList.add("e");
+        }
+
+        checkColAndMap(testClass);
+    }
+
+    /**
+     * @param testCls Class with collection and map included in toString().
+     */
+    private void checkColAndMap(TestClass1 testCls) {
+        String testClsStr = GridToStringBuilder.toString(TestClass1.class, 
testCls);
+
+        testCls.strMap.put("kz", "v"); // important to add last element in 
TreeMap here
+        testCls.strListIncl.add("e");
 
-        String testClassStrOf = GridToStringBuilder.toString(TestClass1.class, 
testClass);
+        String testClsStrOf = GridToStringBuilder.toString(TestClass1.class, 
testCls);
 
-        String testClassStrOfR = testClassStrOf.replaceAll("... and 1 
more","");
+        String testClsStrOfR = testClsStrOf.replaceAll("... and 1 more","");
 
-        assertTrue("Collection limit error in Map or List, normal: <" + 
testClassStr + ">, overflowed: <"
-            +"testClassStrOf", testClassStr.length() == 
testClassStrOfR.length());
+        info(testClsStr);
+        info(testClsStrOf);
+        info(testClsStrOfR);
 
+        assertTrue("Collection limit error in Map or List, normal: <" + 
testClsStr + ">, overflowed: <"
+            + testClsStrOf + ">", testClsStr.length() == 
testClsStrOfR.length());
     }
 
     /**
@@ -402,4 +611,4 @@ public class GridToStringBuilderSelfTest extends 
GridCommonAbstractTest {
             this.str = str;
         }
     }
-}
\ No newline at end of file
+}

Reply via email to