From 546cc8afee3381612789b70e91cc3ea8265a64b7 Mon Sep 17 00:00:00 2001 From: Egor Baranov Date: Mon, 22 Jun 2026 09:06:45 +0300 Subject: [PATCH 1/2] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Collect toString as tree, avoid extra allocations and problems in recursion resolution --- .../internal/util/GridStringBuilder.java | 1 + .../util/tostring/CircularStringBuilder.java | 159 +++- .../util/tostring/GridToStringArrayNode.java | 82 +++ .../util/tostring/GridToStringBuilder.java | 679 ++---------------- .../tostring/GridToStringCollectionNode.java | 87 +++ .../util/tostring/GridToStringMapNode.java | 92 +++ .../util/tostring/GridToStringNode.java | 145 ++++ .../tostring/GridToStringNodeFactory.java | 156 ++++ .../util/tostring/GridToStringNullNode.java | 32 + .../util/tostring/GridToStringObjectNode.java | 103 +++ .../GridToStringRecursionTerminationNode.java | 58 ++ .../util/tostring/GridToStringValueNode.java | 45 ++ .../util/tostring/LongSequenceSkipRule.java | 51 ++ .../util/tostring/NodeRecursionMonitor.java | 100 +++ .../internal/util/tostring/SBLengthLimit.java | 33 +- .../util/tostring/SBLimitedLength.java | 53 +- .../CircularStringBuilderSelfTest.java | 70 ++ .../tostring/GridToStringBuilderSelfTest.java | 98 ++- 18 files changed, 1349 insertions(+), 695 deletions(-) create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringArrayNode.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringCollectionNode.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringMapNode.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNullNode.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringObjectNode.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringRecursionTerminationNode.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringValueNode.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/LongSequenceSkipRule.java create mode 100644 modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/NodeRecursionMonitor.java diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/GridStringBuilder.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/GridStringBuilder.java index 75f0913b0aaf2..16d7c6238e1f8 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/GridStringBuilder.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/GridStringBuilder.java @@ -68,6 +68,7 @@ public GridStringBuilder(CharSequence seq) { /** * * @param len Length to set. + * @throws UnsupportedOperationException if length limit is not supported by this GridStringBuilder */ public void setLength(int len) { impl.setLength(len); diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java index 1bd54ba879b2d..9b15ccff15d98 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java @@ -17,13 +17,10 @@ package org.apache.ignite.internal.util.tostring; -import java.util.Arrays; - /** * Basic string builder over circular buffer. */ public class CircularStringBuilder { - /** Value */ private final char value[]; @@ -47,16 +44,6 @@ public class CircularStringBuilder { value = new char[capacity]; } - /** - * Reset internal builder state - */ - public void reset() { - Arrays.fill(value, (char)0); - finishAt = -1; - full = false; - skipped = 0; - } - /** * Returns the length (character count). * @@ -170,6 +157,64 @@ private CircularStringBuilder appendNull() { return this; } + /** + * Skip additional count of chars + * @param cnt Count of chars skipped. + */ + public void skip(int cnt) { + skipped += cnt; + } + + /** + * Inserts a string into the buffer at the specified logical offset. + * This method is optimized to minimize the number of elements moved by choosing + * to shift elements from the closest end (left or right) to the insertion point. + * + * @param offset The logical position (accounting for skipped characters) + * at which to insert. + * @param valToInsert The string to be inserted. + * @throws StringIndexOutOfBoundsException if the offset is invalid. + */ + public void insert(int offset, String valToInsert) { + int curLength = length(); + int offsetInsideBuf = offset - skipped; + if (offset < 0 || offsetInsideBuf > curLength) + throw new StringIndexOutOfBoundsException("Offset " + offset + " out of bounds for length " + curLength); + if (valToInsert == null) + valToInsert = "null"; + int insertLength = valToInsert.length(); + if (insertLength == 0) + return; + if (offsetInsideBuf == curLength) { + append(valToInsert); + return; + } + int spareSpace = value.length - curLength; + int insertCnt = Math.min(valToInsert.length(), spareSpace + offsetInsideBuf); + if (insertCnt <= 0) { + skipped += valToInsert.length(); + return; + } + int bufStartShiftedOffset = full ? (finishAt + 1) % value.length : 0; + int shiftedOffset = (bufStartShiftedOffset + offsetInsideBuf) % value.length; + int moveRightCnt = ((shiftedOffset <= finishAt ? 0 : curLength) + finishAt + 1) - shiftedOffset; + if (!full || offset - skipped > curLength / 2) { + shiftRight(insertCnt, moveRightCnt); + int charsToSkip = Math.max(0, insertCnt - spareSpace); + finishAt = (finishAt + insertCnt) % value.length; + shiftedOffset = (shiftedOffset + insertCnt) % value.length; + full = curLength + insertCnt >= value.length; + insertStringTail(valToInsert, shiftedOffset, insertCnt); + skipped += charsToSkip; + } + else { + int moveLeftCnt = (curLength - moveRightCnt - insertCnt); + shiftLeft(insertCnt, moveLeftCnt); + insertStringTail(valToInsert, shiftedOffset, insertCnt); + skipped += valToInsert.length(); + } + } + /** * @return Count of skipped elements. */ @@ -177,6 +222,45 @@ public int getSkipped() { return skipped; } + /** + * Returns a substring from the logical sequence of characters, accounting for + * the circular buffer structure and any skipped characters. + * + *

This method first validates the indices against the total logical length + * (skipped + visible characters). If the requested range is empty or fully within + * the skipped portion, an empty string is returned for efficiency. + * + *

It then calculates the physical indices in the internal array. If the + * substring wraps around the end of the circular buffer, it performs a two-part + * copy operation to assemble the result. + * + * @param beginIdx the beginning index, inclusive. + * @param endIdx the ending index, exclusive. + * @return a new String containing the specified subsequence. + * @throws StringIndexOutOfBoundsException if beginIdx or endIdx are negative, + * or if endIdx is greater than the total logical length. + * @throws IllegalArgumentException if beginIdx is greater than endIdx. + */ + public String substring(int beginIdx, int endIdx) { + if (beginIdx < 0 || endIdx < 0 || endIdx > skipped + length()) + throw new StringIndexOutOfBoundsException( + "Some of indexes is out of bounds. Begind index = " + beginIdx + " end index = " + endIdx); + if (beginIdx > endIdx) + throw new IllegalArgumentException( + "Begin index can not be greater then end index (begin = " + beginIdx + " end = " + endIdx + ")"); + if (endIdx <= skipped || beginIdx == endIdx) return ""; + char resultArr[] = new char[Math.max(skipped, endIdx) - Math.max(skipped, beginIdx)]; + int effectiveBeginIdx = ((full ? (finishAt + 1) : 0) + Math.max(skipped, beginIdx) - skipped) % value.length; + int effectiveEndIdx = ((full ? (finishAt + 1) : 0) + Math.max(skipped, endIdx) - skipped) % value.length; + if (effectiveBeginIdx >= effectiveEndIdx) { + System.arraycopy(value, effectiveBeginIdx, resultArr, 0, length() - effectiveBeginIdx); + System.arraycopy(value, 0, resultArr, length() - effectiveBeginIdx, effectiveEndIdx); + } + else + System.arraycopy(value, effectiveBeginIdx, resultArr, 0, effectiveEndIdx - effectiveBeginIdx); + return new String(resultArr); + } + /** {@inheritDoc} */ @Override public String toString() { // Create a copy, don't share the array @@ -192,4 +276,53 @@ public int getSkipped() { else return new String(value, 0, finishAt + 1); } + + /** + * Performs an in-place rightward shift of elements within the circular buffer. + * This is used to create space for new data by moving a block of existing elements. + *

+ * The shift is executed in reverse order (from the end of the block to the beginning) + * to prevent overwriting source elements before they are copied. + * + * @param shift The starting offset from the 'finishAt' index, defining the beginning + * of the block to be moved. + * @param moveSteps The number of elements to be shifted to the right. + */ + private void shiftRight(int shift, int moveSteps) { + for (int i = 0; i < moveSteps; i++) { + int pointer = (finishAt + shift - i) % value.length; + value[pointer] = value[(value.length + pointer - shift) % value.length]; + } + } + + /** + * Performs a leftward shift of elements in the circular buffer. + * Copies elements from a source position to a destination position, + * effectively overwriting a range of values. + *

+ * @param shift The offset for the source element. + * @param shiftsCnt The count of elements to shift. + */ + private void shiftLeft(int shift, int shiftsCnt) { + for (int i = 0; i < shiftsCnt; i++) { + int pointer = (finishAt + 1 + i) % value.length; + value[pointer] = value[(pointer + shift) % value.length]; + } + } + + /** + * Inserts a substring from the source string into the buffer at the specified tail position. + *

+ * The insertion is performed in reverse order (from the last character to the first) to + * prevent overwriting source data in the buffer before it is copied. This is a common + * technique for in-place buffer manipulation. + * + * @param src The source string to copy characters from. + * @param tailEndOffset The physical index in the buffer where the last character will be placed. + * @param insertCnt The number of characters from the source string to insert. + */ + private void insertStringTail(String src, int tailEndOffset, int insertCnt) { + for (int i = 0; i < insertCnt; i++) + value[(value.length + tailEndOffset - i - 1) % value.length] = src.charAt(src.length() - 1 - i); + } } diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringArrayNode.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringArrayNode.java new file mode 100644 index 0000000000000..df17b422cb77a --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringArrayNode.java @@ -0,0 +1,82 @@ +/* + * 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; + +import org.apache.ignite.internal.util.GridStringBuilder; + +import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT; +import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode; + +/** + * A node that represents an array in the string representation. + * It creates nodes for each element of the array and formats the output with square brackets. + */ +class GridToStringArrayNode extends NodeRecursionMonitor { + /** An array of child nodes, each representing an element of the source array. */ + private final GridToStringNode[] nodes; + + /** The rule for appending a hint about skipped elements if the array is too large. */ + private final LongSequenceSkipRule skipRule; + + /** The class object representing the type of the array. */ + private final Class arrType; + + /** + * Constructs a new array node. + * Iterates over the input array, creates a node for each element, + * and populates the internal array up to the collection size limit. + * @param propName The property name. + * @param arr The source array. + * @param arrType The class object of the array's type. + */ + GridToStringArrayNode(String propName, Object[] arr, Class arrType) { + super(propName, arr); + try { + aqcuireRecursionMonitor(this); + this.arrType = arrType; + skipRule = new LongSequenceSkipRule(() -> arr.length); + nodes = new GridToStringNode[Math.min(COLLECTION_LIMIT, arr.length)]; + for (int i = 0; i < nodes.length; i++) { + final int idx = i; + nodes[idx] = getGridToStringNode(null, () -> arr[idx], () -> arr[idx].getClass()); + } + } + finally { + releaseRecursionMonitor(); + } + } + + /** + * Appends the string representation of the array to the builder. + * The format is: ArrayType [element1, element2, ...]. + * Also appends a hint about skipped elements if necessary. + * @param sb The string builder to append to. + */ + @Override void appendNode(GridStringBuilder sb) { + super.appendNode(sb); + sb.a(arrType.getSimpleName()).a(" ["); + for (int i = 0; i < nodes.length - 2; i++) { + nodes[i].appendNode(sb); + sb.a(", "); + } + if (nodes.length > 0) + nodes[nodes.length - 1].appendNode(sb); + skipRule.appendSkippedCountHint(sb); + sb.a("]"); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java index 22a4053afa03f..c700a786852b9 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringBuilder.java @@ -20,7 +20,9 @@ import java.io.Externalizable; import java.io.InputStream; import java.io.OutputStream; +import java.io.PrintWriter; import java.io.Serializable; +import java.io.StringWriter; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -29,7 +31,6 @@ import java.util.Collection; import java.util.Collections; import java.util.EventListener; -import java.util.IdentityHashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -42,10 +43,8 @@ import java.util.function.Function; import java.util.function.Supplier; import org.apache.ignite.IgniteCommonsSystemProperties; -import org.apache.ignite.IgniteException; import org.apache.ignite.internal.util.typedef.F; import org.apache.ignite.internal.util.typedef.internal.SB; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import static java.util.Objects.nonNull; @@ -127,27 +126,6 @@ public class GridToStringBuilder { public static final int COLLECTION_LIMIT = IgniteCommonsSystemProperties.getInteger(IGNITE_TO_STRING_COLLECTION_LIMIT, DFLT_TO_STRING_COLLECTION_LIMIT); - /** Every thread has its own string builder. */ - private static ThreadLocal threadLocSB = new ThreadLocal() { - @Override protected SBLimitedLength initialValue() { - SBLimitedLength sb = new SBLimitedLength(256); - - sb.initLimit(new SBLengthLimit()); - - return sb; - } - }; - - /** - * Contains objects currently printing in the string builder. - *

- * 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> savedObjects = - ThreadLocal.withInitial(() -> new IdentityHashMap<>()); - /** * Implementation of the @@ -362,17 +340,7 @@ public static String toString(Class cls, T obj, addVals[4] = val4; addSens[4] = sens4; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 5); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(cls, obj, addNames, addVals, addSens, 5); } /** @@ -440,17 +408,7 @@ public static String toString(Class cls, T obj, addVals[5] = val5; addSens[5] = sens5; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 6); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(cls, obj, addNames, addVals, addSens, 6); } /** @@ -526,17 +484,7 @@ public static String toString(Class cls, T obj, addVals[6] = val6; addSens[6] = sens6; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 7); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(cls, obj, addNames, addVals, addSens, 7); } /** @@ -616,17 +564,7 @@ public static String toString(Class cls, T obj, addVals[3] = val3; addSens[3] = sens3; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 4); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(cls, obj, addNames, addVals, addSens, 4); } /** @@ -695,17 +633,7 @@ public static String toString(Class cls, T obj, addVals[2] = val2; addSens[2] = sens2; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 3); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(cls, obj, addNames, addVals, addSens, 3); } /** @@ -759,17 +687,7 @@ public static String toString(Class cls, T obj, addVals[1] = val1; addSens[1] = sens1; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 2); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(cls, obj, addNames, addVals, addSens, 2); } /** @@ -810,17 +728,7 @@ public static String toString(Class cls, T obj, String name, @Nullable Ob addVals[0] = val; addSens[0] = sens; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(cls, sb, obj, addNames, addVals, addSens, 1); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(cls, obj, addNames, addVals, addSens, 1); } /** @@ -835,17 +743,7 @@ public static String toString(Class cls, T obj) { assert cls != null; assert obj != null; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(cls, sb, obj, EMPTY_ARRAY, EMPTY_ARRAY, null, 0); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(cls, obj, EMPTY_ARRAY, EMPTY_ARRAY, null, 0); } /** @@ -861,166 +759,11 @@ public static String toString(Class cls, T obj, String parent) { return parent != null ? toString(cls, obj, "super", parent) : toString(cls, obj); } - /** - * 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) { - toString(buf, null, val); - } - - /** - * Print value with length limitation. - * - * @param buf buffer to print to. - * @param cls value class. - * @param val value to print. - */ - private static void toString(SBLimitedLength buf, Class cls, Object val) { - if (val == null) { - buf.a("null"); - - return; - } - - if (cls == null) - cls = val.getClass(); - - if (cls.isPrimitive()) { - buf.a(val); - - return; - } - - IdentityHashMap svdObjs = savedObjects.get(); - - if (handleRecursion(buf, val, cls, svdObjs)) - return; - - svdObjs.put(val, new EntryReference(buf.length())); - - 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); - } - } - - /** - * 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(obj)); - - 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 void addMap(SBLimitedLength buf, Map map) { - buf.a(map.getClass().getSimpleName()).a(" {"); - - int cnt = 0; - - for (Map.Entry 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 Type of 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. @@ -1030,148 +773,29 @@ private static void handleOverflow(SBLimitedLength buf, int size) { */ private static String toStringImpl( Class cls, - SBLimitedLength buf, T obj, Object[] addNames, Object[] addVals, @Nullable boolean[] addSens, int addLen) { assert cls != null; - assert buf != null; assert obj != null; assert addNames != null; assert addVals != null; assert addNames.length == addVals.length; assert addLen <= addNames.length; - - boolean newStr = buf.length() == 0; - - IdentityHashMap svdObjs = savedObjects.get(); - - if (newStr) - svdObjs.put(obj, new EntryReference(buf.length())); - - try { - int len = buf.length(); - - String s = toStringImpl0(cls, buf, obj, addNames, addVals, addSens, addLen); - - if (newStr) - return s; - - buf.setLength(len); - - return s.substring(len); - } - 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 Type of object. - */ - private static String toStringImpl0( - Class cls, - SBLimitedLength buf, - T obj, - Object[] addNames, - Object[] addVals, - @Nullable boolean[] addSens, - int addLen - ) { + boolean isNew = GridToStringNode.isNew(); try { - GridToStringClassDescriptor cd = getClassDescriptor(cls); - - assert cd != null; - - buf.a(cd.getSimpleClassName()); - - EntryReference ref = savedObjects.get().get(obj); - - if (ref != null && ref.hashNeeded) { - buf.a(identity(obj)); - - ref.hashNeeded = false; - } - - buf.a(" ["); - - boolean first = true; - - for (GridToStringFieldDescriptor fd : cd.getFields()) { - if (!first) - buf.a(", "); - else - first = false; - - buf.a(fd.getName()).a('='); - - switch (fd.type()) { - case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT: - toString(buf, fd.fieldClass(), fd.objectValue(obj)); - - break; - case GridToStringFieldDescriptor.FIELD_TYPE_BYTE: - buf.a(fd.byteValue(obj)); - - break; - case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN: - buf.a(fd.booleanValue(obj)); - - break; - case GridToStringFieldDescriptor.FIELD_TYPE_CHAR: - buf.a(fd.charValue(obj)); - - break; - case GridToStringFieldDescriptor.FIELD_TYPE_SHORT: - buf.a(fd.shortValue(obj)); - - break; - case GridToStringFieldDescriptor.FIELD_TYPE_INT: - buf.a(fd.intField(obj)); - - break; - case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT: - buf.a(fd.floatField(obj)); - - break; - case GridToStringFieldDescriptor.FIELD_TYPE_LONG: - buf.a(fd.longField(obj)); - - break; - case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE: - buf.a(fd.doubleField(obj)); - - break; - } - } - - appendVals(buf, first, addNames, addVals, addSens, addLen); - - buf.a(']'); - - return buf.toString(); + List addNodes = + GridToStringNodeFactory.getNodes(addNames, addVals, addSens, addLen); + GridToStringNode node = GridToStringNode.getRootNode(obj, cls, addNodes); + return isNew ? node.toString() : GridToStringNode.markNode(node); } - // Specifically catching all exceptions. - catch (Exception e) { - // Remove entry from cache to avoid potential memory leak - // in case new class loader got loaded under the same identity hash. - classCache.remove(cls.getName() + System.identityHashCode(cls.getClassLoader())); - - // No other option here. - throw new IgniteException(e); + catch (RuntimeException | StackOverflowError throwable) { + if (isNew) + return handleThrowable(throwable); + else + throw throwable; } } @@ -1281,17 +905,7 @@ public static String toString(String str, String name, @Nullable Object val, boo propVals[0] = val; propSens[0] = sens; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(str, sb, propNames, propVals, propSens, 1); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(str, propNames, propVals, propSens, 1); } /** @@ -1338,17 +952,7 @@ public static String toString(String str, propVals[1] = val1; propSens[1] = sens1; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(str, sb, propNames, propVals, propSens, 2); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(str, propNames, propVals, propSens, 2); } /** @@ -1388,17 +992,7 @@ public static String toString(String str, propVals[2] = val2; propSens[2] = sens2; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(str, sb, propNames, propVals, propSens, 3); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(str, propNames, propVals, propSens, 3); } /** @@ -1446,17 +1040,7 @@ public static String toString(String str, propVals[3] = val3; propSens[3] = sens3; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(str, sb, propNames, propVals, propSens, 4); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(str, propNames, propVals, propSens, 4); } /** @@ -1512,17 +1096,7 @@ public static String toString(String str, propVals[4] = val4; propSens[4] = sens4; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(str, sb, propNames, propVals, propSens, 5); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(str, propNames, propVals, propSens, 5); } /** @@ -1586,17 +1160,7 @@ public static String toString(String str, propVals[5] = val5; propSens[5] = sens5; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(str, sb, propNames, propVals, propSens, 6); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(str, propNames, propVals, propSens, 6); } /** @@ -1668,17 +1232,7 @@ public static String toString(String str, propVals[6] = val6; propSens[6] = sens6; - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(str, sb, propNames, propVals, propSens, 7); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(str, propNames, propVals, propSens, 7); } /** @@ -1713,92 +1267,33 @@ public static String toString(String str, Object... triplets) { propSens[i] = (Boolean)sens; } - - SBLimitedLength sb = threadLocSB.get(); - - boolean newStr = sb.length() == 0; - - try { - return toStringImpl(str, sb, propNames, propVals, propSens, propCnt); - } - finally { - if (newStr) - sb.reset(); - } + return toStringImpl(str, propNames, propVals, propSens, propCnt); } /** * Creates an uniformed string presentation for the binary-like object. * * @param str Output prefix or {@code null} if empty. - * @param buf String builder buffer. * @param propNames Names of object properties. * @param propVals Property values. * @param propSens Sensitive flag of values or {@code null} if all values is not sensitive. * @param propCnt Properties count. * @return String presentation of the object. */ - private static String toStringImpl(String str, SBLimitedLength buf, Object[] propNames, Object[] propVals, + private static String toStringImpl(String str, Object[] propNames, Object[] propVals, boolean[] propSens, int propCnt) { - - boolean newStr = buf.length() == 0; - - if (str != null) - buf.a(str).a(" "); - - buf.a("["); - - appendVals(buf, true, propNames, propVals, propSens, propCnt); - - buf.a(']'); - - if (newStr) - return buf.toString(); - - // Called from another GTSB.toString(), so this string is already in the buffer and shouldn't be returned. - return ""; - } - - /** - * Append additional values to the buffer. - * - * @param buf Buffer. - * @param first First value flag. - * @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. - */ - private static void appendVals(SBLimitedLength buf, - boolean first, - Object[] addNames, - Object[] addVals, - boolean[] addSens, - int addLen - ) { - if (addLen > 0) { - for (int i = 0; i < addLen; i++) { - Object addVal = addVals[i]; - - if (addVal != null) { - if (addSens != null && addSens[i] && !includeSensitive()) - continue; - - GridToStringInclude incAnn = addVal.getClass().getAnnotation(GridToStringInclude.class); - - if (incAnn != null && incAnn.sensitive() && !includeSensitive()) - continue; - } - - if (!first) - buf.a(", "); - else - first = false; - - buf.a(addNames[i]).a('='); - - toString(buf, addVal); - } + boolean isNew = GridToStringNode.isNew(); + try { + List addNodes = + GridToStringNodeFactory.getNodes(propNames, propVals, propSens, propCnt); + GridToStringNode node = GridToStringNode.getRootNode(str, addNodes); + return isNew ? node.toString() : GridToStringNode.markNode(node); + } + catch (RuntimeException | StackOverflowError throwable) { + if (isNew) + return handleThrowable(throwable); + else + throw throwable; } } @@ -1808,7 +1303,7 @@ private static void appendVals(SBLimitedLength buf, * @return Descriptor for the class. */ @SuppressWarnings({"TooBroadScope"}) - private static GridToStringClassDescriptor getClassDescriptor(Class cls) { + static GridToStringClassDescriptor getClassDescriptor(Class cls) { assert cls != null; String key = cls.getName() + System.identityHashCode(cls.getClassLoader()); @@ -1994,87 +1489,13 @@ public static String joinToString( return buf.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 cls Class. - * @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, - @NotNull Class cls, - IdentityHashMap svdObjs - ) { - EntryReference ref = svdObjs.get(obj); - - if (ref == null) - return false; - - int pos = ref.pos; - - String name = cls.getSimpleName(); - String hash = identity(obj); - String savedName = name + hash; - String charsAtPos = buf.impl().substring(pos, pos + savedName.length()); - - if (!buf.isOverflowed() && !savedName.equals(charsAtPos)) { - if (charsAtPos.startsWith(cls.getSimpleName())) { - buf.i(pos + name.length(), hash); - - incValues(svdObjs, obj, hash.length()); - } - else - ref.hashNeeded = true; - } - - 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 svdObjs, Object obj, int hashLen) { - int baseline = svdObjs.get(obj).pos; - - for (IdentityHashMap.Entry entry : svdObjs.entrySet()) { - EntryReference ref = entry.getValue(); - - int pos = ref.pos; - - if (pos > baseline) - ref.pos = pos + hashLen; - } - } - - /** - * - */ - private static class EntryReference { - /** Position. */ - int pos; - - /** First object entry needs hash to be written. */ - boolean hashNeeded; - - /** - * @param pos Position. - */ - private EntryReference(int pos) { - this.pos = pos; - hashNeeded = false; - } + /** */ + private static String handleThrowable(T throwable) { + StringWriter strWriter = new StringWriter(); + PrintWriter printWriter = new PrintWriter(strWriter); + throwable.printStackTrace(printWriter); + printWriter.flush(); + printWriter.close(); + return strWriter.toString(); } } diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringCollectionNode.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringCollectionNode.java new file mode 100644 index 0000000000000..c15137f0496b5 --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringCollectionNode.java @@ -0,0 +1,87 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import org.apache.ignite.internal.util.GridStringBuilder; + +import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT; +import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode; + +/** + * A node that represents a collection (e.g., List, Set) in the string representation. + * It creates nodes for each element and formats the output with square brackets. + */ +class GridToStringCollectionNode extends NodeRecursionMonitor { + /** A list of child nodes, each representing an element of the collection. */ + private final Collection col; + + /** The simple class name of the collection being represented. */ + private final String colSimpleClsName; + + /** The rule for appending a hint about skipped elements if the collection is too large. */ + private final LongSequenceSkipRule skipRule; + + /** + * Constructs a new collection node. + * Iterates over the input collection, creates a node for each element, + * and populates the internal list up to the collection size limit. + * @param propName The property name. + * @param col The collection to represent. + */ + GridToStringCollectionNode(String propName, Collection col) { + super(propName, col); + try { + aqcuireRecursionMonitor(this); + colSimpleClsName = col.getClass().getSimpleName(); + this.col = new ArrayList<>(Math.min(col.size(), COLLECTION_LIMIT)); + skipRule = new LongSequenceSkipRule(col::size); + Iterator iter = col.iterator(); + while (iter.hasNext() && this.col.size() != COLLECTION_LIMIT) { + Object obj = iter.next(); + GridToStringNode node = getGridToStringNode(null, () -> obj, obj::getClass); + this.col.add(node); + } + } + finally { + releaseRecursionMonitor(); + } + } + + /** + * Appends the string representation of the collection to the builder. + * The format is: CollectionClassName [element1, element2, ...]. + * Also appends a hint about skipped elements if necessary. + * @param sb The string builder to append to. + */ + @Override void appendNode(GridStringBuilder sb) { + super.appendNode(sb); + sb.a(colSimpleClsName).a(" ["); + Iterator iter = col.iterator(); + while (iter.hasNext()) { + GridToStringNode next = iter.next(); + next.appendNode(sb); + if (iter.hasNext()) + sb.a(", "); + } + skipRule.appendSkippedCountHint(sb); + sb.a(']'); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringMapNode.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringMapNode.java new file mode 100644 index 0000000000000..1be5193292a26 --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringMapNode.java @@ -0,0 +1,92 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import org.apache.ignite.internal.util.GridStringBuilder; + +import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT; +import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode; + +/** + * A node that represents a map (key-value pairs) in the string representation. + * It creates nodes for each key and value and formats the output with curly braces. + */ +class GridToStringMapNode extends NodeRecursionMonitor { + /** An internal map that stores key-value pairs as GridToStringNode instances. */ + private final Map map; + + /** The simple class name of the map being represented. */ + private final String mapClsSimpleName; + + /** The rule for appending a hint about skipped elements if the map is too large. */ + private final LongSequenceSkipRule skipRule; + + /** + * Constructs a new map node. + * Iterates over the entries of the input map, creates nodes for keys and values, + * and populates the internal map structure up to the collection size limit. + * @param propName The property name. + * @param map The map to represent. + */ + GridToStringMapNode(String propName, Map map) { + super(propName, map); + try { + aqcuireRecursionMonitor(this); + mapClsSimpleName = map.getClass().getSimpleName(); + this.map = new HashMap<>(); + skipRule = new LongSequenceSkipRule(map::size); + Iterator> iter = map.entrySet().iterator(); + while (iter.hasNext() && this.map.size() != COLLECTION_LIMIT) { + Map.Entry entry = iter.next(); + Object key = entry.getKey(); + Object val = entry.getValue(); + GridToStringNode keyNode = getGridToStringNode(null, () -> key, key::getClass); + GridToStringNode valNode = getGridToStringNode(null, () -> val, val::getClass); + this.map.put(keyNode, valNode); + } + } + finally { + releaseRecursionMonitor(); + } + } + + /** + * Appends the string representation of the map to the builder. + * The format is: MapClassName {key1=value1, key2=value2, ...}. + * Also appends a hint about skipped elements if necessary. + * @param sb The string builder to append to. + */ + @Override void appendNode(GridStringBuilder sb) { + super.appendNode(sb); + sb.a(mapClsSimpleName).a(" {"); + Iterator> iter = map.entrySet().iterator(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + entry.getKey().appendNode(sb); + sb.a('='); + entry.getValue().appendNode(sb); + if (iter.hasNext()) + sb.a(", "); + } + skipRule.appendSkippedCountHint(sb); + sb.a('}'); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java new file mode 100644 index 0000000000000..b621e55378403 --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNode.java @@ -0,0 +1,145 @@ +/* + * 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; + +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Optional; +import org.apache.ignite.internal.util.GridStringBuilder; + +/** + * The abstract base class for all nodes in the string representation tree. + * Defines the common interface for appending a node's value to a string builder + * and provides static factory and utility methods for all subclasses. + */ +public abstract class GridToStringNode { + /** + * A thread-local cache for nodes, used to handle references of + * inner toString() calls by mapping temporary markers to actual nodes. + */ + public static final ThreadLocal> CATCHED_NODES = + ThreadLocal.withInitial(IdentityHashMap::new); + + /** The name of the property this node represents. */ + String propName; + + /** + * Base constructor. + * @param propName The name of the property. + */ + GridToStringNode(String propName) { + this.propName = propName; + } + + /** + * Creates a root node for a string value. + * @param str The string value. + * @param addNodes Additional child nodes to include. + * @return A new root node. + */ + public static GridToStringNode getRootNode(String str, List addNodes) { + return new GridToStringObjectNode(str, addNodes); + } + + /** + * Creates a root node for an object. + * Checks for recursion and creates either a termination node or a full object node. + * @param obj The object to represent. + * @param cls The class of the object. + * @param addNodes Additional child nodes to include. + * @return A new root node. + */ + public static GridToStringNode getRootNode(Object obj, Class cls, List addNodes) { + return recursionTermination(obj) + .orElseGet(() -> new GridToStringObjectNode(null, obj, cls, addNodes)); + } + + /** + * Marks a node in the thread-local cache with a unique, empty string key. + * Used to handle references during object graph traversal. + * @param node The node to mark. + * @return The unique marker string. + */ + public static String markNode(GridToStringNode node) { + String result = new String(); + CATCHED_NODES.get().put(result, node); + return result; + } + + /** + * Checks if the current context is new, meaning no recursion or inner calls are in progress. + * @return True if the context is new; false otherwise. + */ + public static boolean isNew() { + return NodeRecursionMonitor.isEmpty() && CATCHED_NODES.get().isEmpty(); + } + + /** + * Creates a node for a null value if the object is null. + * @param obj The object to check. + * @return An Optional containing a null node if the object is null; otherwise, an empty Optional. + */ + static Optional nullNode(Object obj) { + return obj == null ? Optional.of(new GridToStringNullNode(null)) : Optional.empty(); + } + + /** + * Checks if an object is part of a recursive reference and, if so, + * creates a termination node to break the cycle. + * @param obj The object to check. + * @return An Optional containing a termination node if recursion is detected; + * otherwise, an empty Optional. + */ + static Optional recursionTermination(Object obj) { + return Optional.ofNullable(obj) + .flatMap(NodeRecursionMonitor::findRecursionMonitor) + .map(sameObjNode -> + GridToStringRecursionTerminationNode.of(sameObjNode, obj)); + } + + /** + * Appends the property name (if any) and a placeholder for the value to the builder. + * This method is intended to be overridden by subclasses to provide specific value output. + * @param sb The string builder to append to. + */ + void appendNode(GridStringBuilder sb) { + if (propName != null) + sb.a(propName).a("="); + } + + /** + * Clears the thread-local cache of nodes. + */ + void clear() { + CATCHED_NODES.remove(); + } + + /** + * Generates the final string representation of this node. + * Initializes a limited-length string builder, appends the node's content, + * clears the cache, and returns the result. + * @return The string representation of the node. + */ + @Override public String toString() { + SBLimitedLength sb = new SBLimitedLength(256); + sb.initLimit(new SBLengthLimit()); + appendNode(sb); + clear(); + return sb.toString(); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java new file mode 100644 index 0000000000000..bb2c2e460a9b3 --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNodeFactory.java @@ -0,0 +1,156 @@ +/* + * 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; + +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.function.Supplier; + +import static org.apache.ignite.internal.util.tostring.GridToStringNode.CATCHED_NODES; + +/** + * A factory class responsible for creating appropriate GridToStringNode instances + * based on the type and value of the object being processed. + */ +public class GridToStringNodeFactory { + /** + * Creates a list of nodes from arrays of property names and values. + * Handles sensitive data exclusion and node reuse from a thread-local cache. + * @param addNames Array of property names. + * @param addVals Array of property values. + * @param addSens Array of flags indicating if a property is sensitive. + * @param addLen The number of elements to process. + * @return A list of constructed GridToStringNode objects. + */ + public static List getNodes(Object[] addNames, + Object[] addVals, + boolean[] addSens, + int addLen) { + List result = new LinkedList<>(); + boolean includeSensitive = GridToStringBuilder.includeSensitive(); + for (int i = 0; i < addLen; i++) { + GridToStringNode node = CATCHED_NODES.get().remove(addVals[i]); + if (!includeSensitive && shouldBeExcluded(addVals, addSens, i)) + continue; + String propName = String.valueOf(addNames[i]); + final int idx = i; + if (node == null) + node = getGridToStringNode(propName, () -> addVals[idx], () -> addVals[idx].getClass()); + else + node.propName = propName; + result.add(node); + } + return result; + } + + /** + * Creates a node for a field based on its descriptor and the parent object. + * This method acts as a dispatcher, routing the creation logic based on the field's type. + * @param obj The parent object containing the field. + * @param fd The descriptor of the field to be processed. + * @return A new GridToStringNode for the field's value. + */ + static GridToStringNode getGridToStringNode(Object obj, GridToStringFieldDescriptor fd) { + String childPropName = fd.getName(); + if (obj == null) + return new GridToStringNullNode(childPropName); + return switch (fd.type()) { + case GridToStringFieldDescriptor.FIELD_TYPE_OBJECT -> { + Supplier> fieldClsSupplier = () -> Optional.of(fd) + .map(GridToStringFieldDescriptor::fieldClass) + .map(Class.class::cast) + .orElseGet(obj::getClass); + yield getGridToStringNode(childPropName, () -> fd.objectValue(obj), fieldClsSupplier); + } + case GridToStringFieldDescriptor.FIELD_TYPE_BYTE -> + getGridToStringNode(childPropName, () -> fd.byteValue(obj), () -> byte.class); + case GridToStringFieldDescriptor.FIELD_TYPE_BOOLEAN -> + getGridToStringNode(childPropName, () -> fd.booleanValue(obj), () -> boolean.class); + case GridToStringFieldDescriptor.FIELD_TYPE_CHAR -> + getGridToStringNode(childPropName, () -> fd.charValue(obj), () -> char.class); + case GridToStringFieldDescriptor.FIELD_TYPE_SHORT -> + getGridToStringNode(childPropName, () -> fd.shortValue(obj), () -> short.class); + case GridToStringFieldDescriptor.FIELD_TYPE_INT -> + getGridToStringNode(childPropName, () -> fd.intField(obj), () -> int.class); + case GridToStringFieldDescriptor.FIELD_TYPE_FLOAT -> + getGridToStringNode(childPropName, () -> fd.floatField(obj), () -> float.class); + case GridToStringFieldDescriptor.FIELD_TYPE_LONG -> + getGridToStringNode(childPropName, () -> fd.longField(obj), () -> long.class); + case GridToStringFieldDescriptor.FIELD_TYPE_DOUBLE -> + getGridToStringNode(childPropName, () -> fd.doubleField(obj), () -> double.class); + default -> new GridToStringValueNode(childPropName, "toString is not implemented yet"); + }; + } + + /** + * The core factory method that creates a node for a given value and its class. + * It is the central point for determining the correct node type for any object. + * Handles nulls, recursion, primitives, arrays, collections, maps, and standard objects. + * @param childPropName The property name for the new node. + * @param valSupplier A supplier to lazily retrieve the value. + * @param childFieldClsSupplier A supplier to lazily retrieve the class of the value. + * @return A new GridToStringNode appropriate for the value. + */ + static GridToStringNode getGridToStringNode(String childPropName, + Supplier valSupplier, + Supplier> childFieldClsSupplier) { + Object val = valSupplier.get(); + if (val == null) + return new GridToStringNullNode(childPropName); + Optional recursionTermination = NodeRecursionMonitor.findRecursionMonitor(val) + .map(monitor -> GridToStringRecursionTerminationNode.of(monitor, val)); + if (recursionTermination.isPresent()) + return recursionTermination.get(); + Class childFieldCls = childFieldClsSupplier.get(); + if (childFieldCls.isPrimitive()) + return new GridToStringValueNode(childPropName, val); + else if (childFieldCls.isArray()) + return new GridToStringArrayNode(childPropName, (Object[])val, childFieldCls); + else if (val instanceof Collection) + return new GridToStringCollectionNode(childPropName, (Collection)val); + else if (val instanceof Map) + return new GridToStringMapNode(childPropName, (Map)val); + + String toStrResult = val.toString(); + GridToStringNode catchedNode = CATCHED_NODES.get().remove(toStrResult); + if (catchedNode == null) + return new GridToStringValueNode(childPropName, toStrResult); + catchedNode.propName = childPropName; + return catchedNode; + } + + /** + * Determines if a property should be excluded from the output based on its sensitivity. + * Checks if the property is marked as sensitive, or it's class marked as sensitive. + * @param addVals The array of property values. + * @param addSens The array of sensitivity flags. + * @param idx The index of the property to check. + * @return True if the property should be excluded; false otherwise. + */ + private static boolean shouldBeExcluded(Object[] addVals, boolean[] addSens, int idx) { + boolean fieldMarkedAsSensitive = addSens != null && addSens[idx]; + return fieldMarkedAsSensitive || Optional.ofNullable(addVals[idx]) + .map(Object::getClass) + .map(cls -> cls.getAnnotation(GridToStringInclude.class)) + .filter(GridToStringInclude::sensitive) + .isPresent(); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNullNode.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNullNode.java new file mode 100644 index 0000000000000..de2f64c28b3bd --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringNullNode.java @@ -0,0 +1,32 @@ +/* + * 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; + +/** + * A specialized node for representing null values. + * It extends GridToStringValueNode to provide a consistent "null" string output. + */ +class GridToStringNullNode extends GridToStringValueNode { + /** + * Constructs a new null node. + * @param propName The property name associated with this null value. + */ + GridToStringNullNode(String propName) { + super(propName, "null"); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringObjectNode.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringObjectNode.java new file mode 100644 index 0000000000000..d70cfe2b1141a --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringObjectNode.java @@ -0,0 +1,103 @@ +/* + * 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; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import org.apache.ignite.internal.util.GridStringBuilder; + +import static org.apache.ignite.internal.util.tostring.GridToStringNodeFactory.getGridToStringNode; + +/** + * Represents a complex object node that contains and manages child property nodes. + * It is responsible for building the string representation of a user-defined object, + * including its class name and a list of its fields. + */ +class GridToStringObjectNode extends NodeRecursionMonitor { + /** An unmodifiable list of child nodes representing the object's properties. */ + private final List childNodes; + + /** The simple class name of the object being represented. */ + private final String entryName; + + /** + * Constructor for a root node with a custom entry name and pre-defined child nodes. + * This is typically used for the top-level object being stringified. + * @param str The custom entry name to display. + * @param childNodes The pre-constructed list of child nodes to include. + */ + GridToStringObjectNode(String str, List childNodes) { + super(null, null); + entryName = str; + this.childNodes = Collections.unmodifiableList(childNodes); + } + + /** + * Constructor that builds a node for an object by reflecting its fields. + * This constructor acquires a recursion monitor, retrieves field descriptors, + * creates child nodes for each field, and then releases the monitor. + * @param propName The property name of this object in the parent structure. + * @param obj The object to represent. + * @param cls The class of the object. + * @param additionalChildNodes Any additional nodes to include in the output. + */ + GridToStringObjectNode(String propName, Object obj, Class cls, List additionalChildNodes) { + super(propName, obj); + try { + aqcuireRecursionMonitor(this); + List childNodes = new LinkedList<>(); + GridToStringClassDescriptor cd = GridToStringBuilder.getClassDescriptor(cls); + for (GridToStringFieldDescriptor fd : cd.getFields()) { + GridToStringNode childNode = getGridToStringNode(obj, fd); + childNodes.add(childNode); + } + entryName = cd.getSimpleClassName(); + childNodes.addAll(additionalChildNodes); + this.childNodes = Collections.unmodifiableList(childNodes); + } + finally { + releaseRecursionMonitor(); + } + } + + /** + * Appends the object's class name and its property nodes to the string builder. + * The output format is: [ClassName@hashCode] [field1=value1, field2=value2]. + * If a hash code is required (due to recursion), it is appended to the class name. + * @param sb The string builder to append to. + */ + @Override void appendNode(GridStringBuilder sb) { + super.appendNode(sb); + if (entryName != null) { + sb.a(entryName); + appendHashIfRequired(sb); + sb.a(' '); + } + sb.a('['); + Iterator iter = childNodes.iterator(); + while (iter.hasNext()) { + GridToStringNode node = iter.next(); + node.appendNode(sb); + if (iter.hasNext()) + sb.a(", "); + } + sb.a("]"); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringRecursionTerminationNode.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringRecursionTerminationNode.java new file mode 100644 index 0000000000000..d4c65125db6d7 --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringRecursionTerminationNode.java @@ -0,0 +1,58 @@ +/* + * 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; + +import org.apache.ignite.internal.util.GridStringBuilder; + +/** + * A node that terminates recursion by outputting a class name and object identity. + */ +class GridToStringRecursionTerminationNode extends GridToStringNode { + /** The simple name of the class of the terminated object. */ + private final String simpleClsName; + + /** The identity hash code string of the terminated object. */ + private final String identity; + + /** + * Private constructor to create a termination node. + * @param obj The object that caused the recursion. + */ + private GridToStringRecursionTerminationNode(Object obj) { + super(null); + simpleClsName = obj.getClass().getSimpleName(); + identity = GridToStringBuilder.identity(obj); + } + + /** + * Factory method to create a termination node and mark the monitor. + * @param nodeRecursionMonitor The monitor for the current object. + * @param obj The object that is being re-encountered. + * @return A new termination node. + */ + static GridToStringNode of(NodeRecursionMonitor nodeRecursionMonitor, Object obj) { + nodeRecursionMonitor.setHashRequired(); + return new GridToStringRecursionTerminationNode(obj); + } + + /** {@inheritDoc} */ + @Override void appendNode(GridStringBuilder sb) { + super.appendNode(sb); + sb.a(simpleClsName).a(identity); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringValueNode.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringValueNode.java new file mode 100644 index 0000000000000..0bbd33f7f208e --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/GridToStringValueNode.java @@ -0,0 +1,45 @@ +/* + * 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; + +import org.apache.ignite.internal.util.GridStringBuilder; + +/** + * Represents a simple value node + * that appends its string representation to a builder. + */ +class GridToStringValueNode extends GridToStringNode { + /** The string representation of the value. */ + private final String val; + + /** + * Constructs a new value node. + * @param propName Name of the property. + * @param val The value to be stringified. + */ + GridToStringValueNode(String propName, Object val) { + super(propName); + this.val = String.valueOf(val); + } + + /** {@inheritDoc} */ + @Override void appendNode(GridStringBuilder sb) { + super.appendNode(sb); + sb.a(val); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/LongSequenceSkipRule.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/LongSequenceSkipRule.java new file mode 100644 index 0000000000000..966f4fae45b32 --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/LongSequenceSkipRule.java @@ -0,0 +1,51 @@ +/* + * 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; + +import java.util.function.Supplier; +import org.apache.ignite.internal.util.GridStringBuilder; + +import static org.apache.ignite.internal.util.tostring.GridToStringBuilder.COLLECTION_LIMIT; + +/** + * Appends a "skipped elements" hint to a string builder + * when a size exceeds the limit. + */ +class LongSequenceSkipRule { + /** The number of elements that were skipped due to the size limit.*/ + private final int skipped; + + /** + * Constructor. + * Calculates the number of skipped elements based on size and limit. + * @param sizeSupplier Supplier that provides the actual size. + */ + LongSequenceSkipRule(Supplier sizeSupplier) { + skipped = Math.max(0, sizeSupplier.get() - COLLECTION_LIMIT); + } + + /** + * Appends a hint about skipped elements to the string builder + * if any were skipped. + * @param sb The string builder to append the hint to. + */ + void appendSkippedCountHint(GridStringBuilder sb) { + if (skipped != 0) + sb.a("... and ").a(skipped).a(" more"); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/NodeRecursionMonitor.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/NodeRecursionMonitor.java new file mode 100644 index 0000000000000..ee6cd904cc0cb --- /dev/null +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/NodeRecursionMonitor.java @@ -0,0 +1,100 @@ +/* + * 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; + +import java.util.IdentityHashMap; +import java.util.Optional; +import org.apache.ignite.internal.util.GridStringBuilder; + +/** + * Abstract base class for nodes that tracks object references + * to prevent infinite recursion during string representation building. + */ +abstract class NodeRecursionMonitor extends GridToStringNode { + /** Thread-local registry to track objects currently being processed. */ + static final ThreadLocal> OBJECT_REGISTRY = + ThreadLocal.withInitial(IdentityHashMap::new); + + /** The object being monitored for recursive references. */ + private final Object obj; + + /** Flag indicating if the identity hash code should be appended to the output. */ + boolean hashIsRequired; + + /** + * Constructor. + * @param propName Name of the property. + * @param obj Object to monitor for recursion. + */ + NodeRecursionMonitor(String propName, Object obj) { + super(propName); + this.obj = obj; + } + + /** + * Registers the current node in the thread-local registry for the given object. + * @param node The node to register. + */ + void aqcuireRecursionMonitor(NodeRecursionMonitor node) { + OBJECT_REGISTRY.get().put(obj, node); + } + + /** Removes the current node from the thread-local registry. */ + void releaseRecursionMonitor() { + OBJECT_REGISTRY.get().remove(obj); + } + + /** Sets the flag to require appending the identity hash code of the object. */ + void setHashRequired() { + hashIsRequired = true; + } + + /** + * Appends the identity hash code of the object to the string builder if required. + * @param sb The string builder to append to. + */ + void appendHashIfRequired(GridStringBuilder sb) { + if (hashIsRequired && obj != null) + sb.a(GridToStringBuilder.identity(obj)); + } + + /** {@inheritDoc} */ + @Override void clear() { + super.clear(); + OBJECT_REGISTRY.remove(); + } + + /** + * Finds a recursion monitor for a given object in the registry. + * @param obj The object to find. + * @return An optional containing the monitor if found. + */ + static Optional findRecursionMonitor(Object obj) { + return Optional.of(OBJECT_REGISTRY) + .map(ThreadLocal::get) + .map(map -> map.get(obj)); + } + + /** + * Checks if the thread-local registry is empty. + * @return True if no objects are currently being processed; false otherwise. + */ + static boolean isEmpty() { + return OBJECT_REGISTRY.get().isEmpty(); + } +} diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java index 58a070eef3ac3..11a2f80913550 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java @@ -33,45 +33,24 @@ class SBLengthLimit { private static final int TAIL_LEN = MAX_TO_STR_LEN / 10 * 2; /** Length of head part of message. */ - private static final int HEAD_LEN = MAX_TO_STR_LEN - TAIL_LEN; - - /** */ - private int len; - - /** - * @return Current length. - */ - int length() { - return len; - } - - /** - * - */ - void reset() { - len = 0; - } + static final int HEAD_LEN = MAX_TO_STR_LEN - TAIL_LEN; /** * @param sb String builder. * @param writtenLen Written length. */ void onWrite(SBLimitedLength sb, int writtenLen) { - len += writtenLen; - if (overflowed(sb) && (sb.getTail() == null || sb.getTail().length() == 0)) { - CircularStringBuilder tail = getTail(); - - int newSbLen = Math.min(sb.length(), HEAD_LEN + 1); + CircularStringBuilder tail = createTail(); + int newSbLen = Math.min(sb.length(), HEAD_LEN); tail.append(sb.impl().substring(newSbLen)); - sb.setTail(tail); - sb.setLength(newSbLen); + sb.impl().setLength(newSbLen); } } /** */ - CircularStringBuilder getTail() { + CircularStringBuilder createTail() { return new CircularStringBuilder(TAIL_LEN); } @@ -79,6 +58,6 @@ CircularStringBuilder getTail() { * @return {@code True} if reached limit. */ boolean overflowed(SBLimitedLength sb) { - return sb.impl().length() > HEAD_LEN; + return sb.length() >= HEAD_LEN; } } diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java index d478e3cbcc391..3e479555fbc49 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java @@ -45,21 +45,6 @@ public class SBLimitedLength extends GridStringBuilder { */ void initLimit(SBLengthLimit lenLimit) { this.lenLimit = lenLimit; - - if (tail != null) - tail.reset(); - } - - /** - * Resets buffer. - */ - public void reset() { - super.setLength(0); - - lenLimit.reset(); - - if (tail != null) - tail.reset(); } /** @@ -270,6 +255,37 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { return onWrite(curLen); } + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int offset, String str) { + if (offset < SBLengthLimit.HEAD_LEN) { + super.i(offset, str); + if (lenLimit.overflowed(this)) { + String tailCandidate = impl().substring(SBLengthLimit.HEAD_LEN); + if (tail == null) + tail = lenLimit.createTail(); + tail.insert(0, tailCandidate); + tail.skip(str.length() - tailCandidate.length()); + impl().setLength(SBLengthLimit.HEAD_LEN); + } + return this; + } + tail.insert(offset - SBLengthLimit.HEAD_LEN, str); + return this; + } + + /** {@inheritDoc} */ + @Override public int length() { + int length = super.length(); + if (tail != null) + length += tail.getSkipped() + tail.length(); + return length; + } + + /** {@inheritDoc} */ + @Override public void setLength(int len) { + throw new UnsupportedOperationException("setLength is not supported by this imlementation"); + } + /** {@inheritDoc} */ @Override public GridStringBuilder appendCodePoint(int codePoint) { if (lenLimit.overflowed(this)) { @@ -304,11 +320,4 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { return res.toString(); } } - - /** - * @return {@code True} - if buffer limit is reached. - */ - public boolean isOverflowed() { - return lenLimit.overflowed(this); - } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java index f9bf453663793..01317876c493d 100644 --- a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/CircularStringBuilderSelfTest.java @@ -55,6 +55,56 @@ public void testCSBOverflow() throws Exception { testSB(8, "1234", 2, "12341234"); } + /** Assertions to ensure {@link CircularStringBuilder#insert(int, String)} method works */ + @Test + public void testCSBInsert() { + testSBInsert(5, "123456789", 4, "new", "56789"); + testSBInsert(5, "123456789", 5, "new", "w6789"); + testSBInsert(5, "123456789", 6, "new", "ew789"); + testSBInsert(5, "123456789", 7, "new", "new89"); + testSBInsert(5, "123456789", 8, "new", "8new9"); + testSBInsert(5, "123456789", 9, "new", "89new"); + testSBInsert(5, "1", 0, "new", "new1"); + testSBInsert(5, "12", 0, "new", "new12"); + testSBInsert(5, "123", 0, "new", "ew123"); + testSBInsert(5, "1234", 0, "new", "w1234"); + testSBInsert(2, "1", 0, "new", "w1"); + testSBInsert(2, "12", 0, "new", "12"); + testSBInsert(3, "12", 0, "new", "w12"); + testSBInsert(3, "12", 1, "new", "ew2"); + } + + /** Assertions to ensure {@link CircularStringBuilder#substring(int, int)} method works */ + @Test + public void testSubstring() { + CircularStringBuilder circularStrBuilder = new CircularStringBuilder(5); + circularStrBuilder.append("abc"); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + assertEquals("ab", circularStrBuilder.substring(0, 2)); + assertEquals("bc", circularStrBuilder.substring(1, 3)); + circularStrBuilder.append("de"); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + assertEquals("ab", circularStrBuilder.substring(0, 2)); + assertEquals("bc", circularStrBuilder.substring(1, 3)); + assertEquals("abcde", circularStrBuilder.substring(0, 5)); + assertEquals("de", circularStrBuilder.substring(3, 5)); + assertEquals("abc", circularStrBuilder.substring(0, 3)); + circularStrBuilder.append("fg"); + assertEquals("cdefg", circularStrBuilder.substring(2, 7)); + assertEquals("cdef", circularStrBuilder.substring(2, 6)); + assertEquals("defg", circularStrBuilder.substring(3, 7)); + assertEquals("cdefg", circularStrBuilder.substring(0, 7)); + circularStrBuilder.append("hi"); + assertEquals("efg", circularStrBuilder.substring(0, 7)); + assertEquals("efghi", circularStrBuilder.substring(0, 9)); + circularStrBuilder.append("j"); + assertEquals("fghi", circularStrBuilder.substring(0, 9)); + assertEquals("fghij", circularStrBuilder.substring(0, 10)); + assertEquals("ghij", circularStrBuilder.substring(6, 10)); + assertEquals("", circularStrBuilder.substring(0, 5)); + assertEquals("f", circularStrBuilder.substring(0, 6)); + } + /** * @param capacity Capacity. * @param pattern Pattern to add. @@ -69,4 +119,24 @@ private void testSB(int capacity, String pattern, int num, String expected) { assertEquals(expected, csb.toString()); } + + /** + * Test ring buffer method {@link CircularStringBuilder#insert(int, String)} + * @param capacity ring buffer capacity + * @param firstVal value to append to buffer before test + * @param offset insert offset argument + * @param insertSubstring insert substring argument + * @param expectedResult expected ring buffer state + * (to assert it equals to {@link CircularStringBuilder#toString()}) + */ + private void testSBInsert(int capacity, + String firstVal, + int offset, + String insertSubstring, + String expectedResult) { + CircularStringBuilder csb = new CircularStringBuilder(capacity); + csb.append(firstVal); + csb.insert(offset, insertSubstring); + assertEquals(expectedResult, csb.toString()); + } } 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 4b2987c1431b5..a8a8b0d641303 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 @@ -204,6 +204,97 @@ public void testToStringCheckObjectRecursionPrevention() throws Exception { fut4.get(3_000); } + /** */ + @Test + public void testSetLength() { + SBLimitedLength sbLimitedLength = new SBLimitedLength(256); + SBLengthLimit sbLengthLimit = new SBLengthLimit(); + sbLimitedLength.initLimit(sbLengthLimit); + + } + + /** */ + @Test + public void testObjectRecursionPrevention() { + RecursivePayload recursivePayload1 = new RecursivePayload(8_000, null); + RecursivePayload recursivePayload2 = new RecursivePayload(4_000, recursivePayload1); + recursivePayload1.setChild(recursivePayload2); + String result; + result = recursivePayload1.toString(); + info(result); + String identity = identity(recursivePayload1); + assertTrue(result.matches("^.*" + identity + ".*" + identity + ".*$")); + // it's in the middle + RecursivePayload recursivePayload0; + recursivePayload0 = new RecursivePayload(8, recursivePayload1); + result = recursivePayload0.toString(); + info(result); + assertTrue(result.matches("^.*" + identity + ".*" + identity + ".*$")); + // it's in the head + recursivePayload0 = new RecursivePayload(8_000 - 60, recursivePayload1); + result = recursivePayload0.toString(); + info(result); + assertTrue(result.matches("^.*" + identity + ".*" + identity + ".*$")); + // it's in the tail + recursivePayload1 = new RecursivePayload('1', 1, null); + identity = identity(recursivePayload1); + recursivePayload2 = new RecursivePayload('2', 1, recursivePayload1); + recursivePayload1.setChild(recursivePayload2); + recursivePayload0 = new RecursivePayload(8_001, recursivePayload1); + result = recursivePayload0.toString(); + info(result); + assertTrue(result.matches("^.*" + identity + ".*" + identity + ".*$")); + } + + /** */ + private static class RecursivePayload { + /** */ + private final String payload; + + /** */ + private RecursivePayload child; + + /** + * Constructor + * @param payloadLength Payload length. + * @param child Child (nullable) + */ + private RecursivePayload(int payloadLength, RecursivePayload child) { + this('a', payloadLength, child); + } + + /** + * Constructor + * @param payloadChar PayloadChar + * @param payloadLength Payload length. + * @param child Child (nullable) + */ + private RecursivePayload(char payloadChar, int payloadLength, RecursivePayload child) { + payload = String.valueOf(payloadChar).repeat(payloadLength); + this.child = child; + } + + /** */ + public String getPayload() { + return payload; + } + + /** */ + public RecursivePayload getChild() { + return child; + } + + /** */ + public void setChild(RecursivePayload child) { + this.child = child; + } + + /** {@inheritDoc} */ + @Override public String toString() { + return S.toString(RecursivePayload.class, this); + } + } + /** * Test class. */ @@ -552,14 +643,13 @@ public void testHierarchy() { checkHierarchy("Wrapper [p=Child [b=0, pb=Parent[] [null], super=Parent [a=0, pa=Parent[] [null]]]]", w); p.pa[0] = p; - - checkHierarchy("Wrapper [p=Child" + hash + - " [b=0, pb=Parent[] [null], super=Parent [a=0, pa=Parent[] [Child" + hash + "]]]]", w); + checkHierarchy("Wrapper [p=Child [b=0, pb=Parent[] [null], super=Parent" + hash + + " [a=0, pa=Parent[] [Child" + hash + "]]]]", w); ((Child)p).pb[0] = p; checkHierarchy("Wrapper [p=Child" + hash + " [b=0, pb=Parent[] [Child" + hash - + "], super=Parent [a=0, pa=Parent[] [Child" + hash + "]]]]", w); + + "], super=Parent" + hash + " [a=0, pa=Parent[] [Child" + hash + "]]]]", w); } /** From e23ed1d21565a2f5bd0f0f224bdd5096a080bc8d Mon Sep 17 00:00:00 2001 From: Egor Baranov Date: Mon, 22 Jun 2026 16:06:12 +0300 Subject: [PATCH 2/2] IGNITE-28747 GridToStringBuilder#handleRecursion may cause NPE. Override all SBLengthLimit's parent methods (wrong logic). Added new tests. --- .../internal/util/GridStringBuilder.java | 3 + .../util/tostring/CircularStringBuilder.java | 8 - .../internal/util/tostring/SBLengthLimit.java | 27 ++- .../util/tostring/SBLimitedLength.java | 145 +++++++++++++--- .../tostring/SBLimitedLengthSelfTest.java | 160 ++++++++++++++++++ .../testsuites/IgniteUtilSelfTestSuite.java | 2 + 6 files changed, 303 insertions(+), 42 deletions(-) create mode 100644 modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/GridStringBuilder.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/GridStringBuilder.java index 16d7c6238e1f8..19e81896d6f86 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/GridStringBuilder.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/GridStringBuilder.java @@ -256,6 +256,7 @@ public GridStringBuilder appendCodePoint(int codePoint) { * @param start Start position to delete from. * @param end End position. * @return This buffer for chaining method calls. + * @throws UnsupportedOperationException if not supported by this imlementation */ public GridStringBuilder d(int start, int end) { impl.delete(start, end); @@ -267,6 +268,7 @@ public GridStringBuilder d(int start, int end) { * * @param index Index to delete character at. * @return This buffer for chaining method calls. + * @throws UnsupportedOperationException if not supported by this imlementation */ public GridStringBuilder d(int index) { impl.deleteCharAt(index); @@ -291,6 +293,7 @@ public GridStringBuilder nl() { * @param end End position. * @param str String to replace with. * @return This buffer for chaining method calls. + * @throws UnsupportedOperationException if not supported by this imlementation */ public GridStringBuilder r(int start, int end, String str) { impl.replace(start, end, str); diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java index 9b15ccff15d98..af726f9009eca 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/CircularStringBuilder.java @@ -157,14 +157,6 @@ private CircularStringBuilder appendNull() { return this; } - /** - * Skip additional count of chars - * @param cnt Count of chars skipped. - */ - public void skip(int cnt) { - skipped += cnt; - } - /** * Inserts a string into the buffer at the specified logical offset. * This method is optimized to minimize the number of elements moved by choosing diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java index 11a2f80913550..151166c15de58 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLengthLimit.java @@ -33,7 +33,7 @@ class SBLengthLimit { private static final int TAIL_LEN = MAX_TO_STR_LEN / 10 * 2; /** Length of head part of message. */ - static final int HEAD_LEN = MAX_TO_STR_LEN - TAIL_LEN; + private static final int HEAD_LEN = MAX_TO_STR_LEN - TAIL_LEN; /** * @param sb String builder. @@ -42,22 +42,37 @@ class SBLengthLimit { void onWrite(SBLimitedLength sb, int writtenLen) { if (overflowed(sb) && (sb.getTail() == null || sb.getTail().length() == 0)) { CircularStringBuilder tail = createTail(); - int newSbLen = Math.min(sb.length(), HEAD_LEN); + int newSbLen = Math.min(sb.length(), getHeadLengthLimit()); tail.append(sb.impl().substring(newSbLen)); sb.setTail(tail); sb.impl().setLength(newSbLen); } } - /** */ + /** Creates empty tail + * @return empty tail */ CircularStringBuilder createTail() { - return new CircularStringBuilder(TAIL_LEN); + return new CircularStringBuilder(getTailLengthLimit()); } /** - * @return {@code True} if reached limit. + * @return {@code True} if this string builder exceeds limit, false otherwise */ boolean overflowed(SBLimitedLength sb) { - return sb.length() >= HEAD_LEN; + return sb.length() >= getHeadLengthLimit(); + } + + /** + * Returns max available head length + * @return head limit */ + int getHeadLengthLimit() { + return HEAD_LEN; + } + + /** + * + */ + int getTailLengthLimit() { + return TAIL_LEN; } } diff --git a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java index 3e479555fbc49..6f37fa594630b 100644 --- a/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java +++ b/modules/commons/src/main/java/org/apache/ignite/internal/util/tostring/SBLimitedLength.java @@ -17,11 +17,32 @@ package org.apache.ignite.internal.util.tostring; -import java.util.Arrays; +import org.apache.ignite.internal.util.CommonUtils; import org.apache.ignite.internal.util.GridStringBuilder; /** + * A specialized string builder that enforces a maximum length limit + * by splitting the content into two parts: + *
    + *
  1. Head: The main buffer managed by the parent {@link GridStringBuilder}. + * Its size is constrained by a provided {@link SBLengthLimit}.
  2. + *
  3. Tail: An auxiliary {@link CircularStringBuilder} + * that stores any overflow data once the head reaches its capacity.
  4. + *
* + *

This class overrides all mutating methods to redirect new data to the tail + * when the length limit is exceeded, effectively making it an "append-only" structure + * for the head buffer once the limit is reached. + * + *

Important Behavior: + * All operations that can reduce or delete characters from the head buffer + * will throw an {@link UnsupportedOperationException}. + * This design decision ensures the integrity of the length-limited head + * and simplifies internal logic. + * + *

The {@code toString()} method provides + * a unified representation of both head and tail contents, + * optionally indicating skipped characters between them. */ public class SBLimitedLength extends GridStringBuilder { /** */ @@ -144,31 +165,15 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { } /** {@inheritDoc} */ - @Override public GridStringBuilder a(char[] str) { - if (lenLimit.overflowed(this)) { - tail.append(str); - return this; - } - - int curLen = length(); - - super.a(str); - - return onWrite(curLen); + @Override public GridStringBuilder a(char[] arr) { + String str = new String(arr); + return a(str); } /** {@inheritDoc} */ - @Override public GridStringBuilder a(char[] str, int offset, int len) { - if (lenLimit.overflowed(this)) { - tail.append(Arrays.copyOfRange(str, offset, len)); - return this; - } - - int curLen = length(); - - super.a(str, offset, len); - - return onWrite(curLen); + @Override public GridStringBuilder a(char[] arr, int offset, int len) { + String str = new String(arr, offset, len); + return a(str); } /** {@inheritDoc} */ @@ -257,22 +262,106 @@ private GridStringBuilder onWrite(int lenBeforeWrite) { /** {@inheritDoc} */ @Override public GridStringBuilder i(int offset, String str) { - if (offset < SBLengthLimit.HEAD_LEN) { + int headLengthLimit = lenLimit.getHeadLengthLimit(); + if (offset < headLengthLimit) { super.i(offset, str); if (lenLimit.overflowed(this)) { - String tailCandidate = impl().substring(SBLengthLimit.HEAD_LEN); + String tailCandidate = impl().substring(headLengthLimit); if (tail == null) tail = lenLimit.createTail(); tail.insert(0, tailCandidate); - tail.skip(str.length() - tailCandidate.length()); - impl().setLength(SBLengthLimit.HEAD_LEN); + impl().setLength(headLengthLimit); } return this; } - tail.insert(offset - SBLengthLimit.HEAD_LEN, str); + tail.insert(offset - headLengthLimit, str); return this; } + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int idx, char[] str, int off, int len) { + StringBuilder strBuilder = new StringBuilder(); + for (int i = 0; i < len; i++) + strBuilder.append(str[i + off]); + return i(idx, strBuilder.toString()); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, Object obj) { + return i(off, String.valueOf(obj)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, char[] str) { + return i(off, new String(str)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int dstOff, CharSequence s) { + StringBuilder strBuilder = new StringBuilder(); + for (int i = 0; i < s.length(); i++) + strBuilder.append(s.charAt(i)); + return i(dstOff, strBuilder.toString()); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int dstOff, CharSequence s, int start, int end) { + StringBuilder strBuilder = new StringBuilder(); + for (int i = start; i < end; i++) + strBuilder.append(s.charAt(i)); + return i(dstOff, strBuilder.toString()); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, boolean b) { + return super.i(off, String.valueOf(b)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, char c) { + return super.i(off, String.valueOf(c)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, int i) { + return super.i(off, String.valueOf(i)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, long l) { + return super.i(off, String.valueOf(l)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, float f) { + return super.i(off, String.valueOf(f)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder i(int off, double d) { + return super.i(off, String.valueOf(d)); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder d(int start, int end) { + throw new UnsupportedOperationException("Not supported by this implementation"); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder d(int idx) { + throw new UnsupportedOperationException("Not supported by this implementation"); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder r(int start, int end, String str) { + throw new UnsupportedOperationException("Not supported by this implementation"); + } + + /** {@inheritDoc} */ + @Override public GridStringBuilder nl() { + return a(CommonUtils.nl()); + } + /** {@inheritDoc} */ @Override public int length() { int length = super.length(); diff --git a/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java new file mode 100644 index 0000000000000..e07d1e9caa1eb --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/util/tostring/SBLimitedLengthSelfTest.java @@ -0,0 +1,160 @@ +/* + * 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; + +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.apache.ignite.testframework.junits.common.GridCommonTest; +import org.junit.Assert; +import org.junit.Test; + +/** + * Test suite to ensure SBLimitedLength works by design + */ +@GridCommonTest(group = "Utils") +public class SBLimitedLengthSelfTest extends GridCommonAbstractTest { + /** Ensure all append operations are working fine */ + @Test + public void testAppend() { + SBLimitedLength strBuilder = getStrBuilder(5, 50); + strBuilder.a(1); + Assert.assertEquals("1", strBuilder.toString()); + strBuilder.a(2L); + Assert.assertEquals("12", strBuilder.toString()); + strBuilder.a(3f); + Assert.assertEquals("123.0", strBuilder.toString()); + strBuilder.a(4d); + Assert.assertEquals("123.04.0", strBuilder.toString()); + strBuilder.a('5'); + Assert.assertEquals("123.04.05", strBuilder.toString()); + strBuilder.a(true); + Assert.assertEquals("123.04.05true", strBuilder.toString()); + Object obj = "6"; + strBuilder.a(obj); + Assert.assertEquals("123.04.05true6", strBuilder.toString()); + strBuilder.a("7"); + Assert.assertEquals("123.04.05true67", strBuilder.toString()); + strBuilder.a(new StringBuilder().append("8")); + Assert.assertEquals("123.04.05true678", strBuilder.toString()); + CharSequence charSeq = "9"; + strBuilder.a(charSeq); + Assert.assertEquals("123.04.05true6789", strBuilder.toString()); + strBuilder.a(charSeq, 0, 1); + Assert.assertEquals("123.04.05true67899", strBuilder.toString()); + strBuilder.a(new char[]{'a'}); + Assert.assertEquals("123.04.05true67899a", strBuilder.toString()); + strBuilder.a(new char[]{'b', 'c', 'd'}, 0, 2); + Assert.assertEquals("123.04.05true67899abc", strBuilder.toString()); + } + + /** Ensure all insert operations are working fine */ + @Test + public void testInsert() { + SBLimitedLength strBuilder = getStrBuilder(5, 50); + strBuilder.i(0, 1); + Assert.assertEquals("1", strBuilder.toString()); + strBuilder.i(0, 2L); + Assert.assertEquals("21", strBuilder.toString()); + strBuilder.i(0, 3f); + Assert.assertEquals("3.021", strBuilder.toString()); + strBuilder.i(0, 4d); + Assert.assertEquals("4.03.021", strBuilder.toString()); + strBuilder.i(0, true); + Assert.assertEquals("true4.03.021", strBuilder.toString()); + strBuilder.i(0, '5'); + Assert.assertEquals("5true4.03.021", strBuilder.toString()); + strBuilder.i(1, "6"); + Assert.assertEquals("56true4.03.021", strBuilder.toString()); + strBuilder.i(2, new char[] {'a', 'b', 'c', 'd'}); + Assert.assertEquals("56abcdtrue4.03.021", strBuilder.toString()); + strBuilder.i(5, new char[] {'e', 'f', 'g', 'i'}, 0, 3); + Assert.assertEquals("56abcefgdtrue4.03.021", strBuilder.toString()); + Object obj = "h"; + strBuilder.i(6, obj); + Assert.assertEquals("56abcehfgdtrue4.03.021", strBuilder.toString()); + CharSequence charSeq = "ijk"; + strBuilder.i(7, charSeq); + Assert.assertEquals("56abcehijkfgdtrue4.03.021", strBuilder.toString()); + strBuilder.i(8, charSeq, 0, 2); + Assert.assertEquals("56abcehiijjkfgdtrue4.03.021", strBuilder.toString()); + } + + /** Ensure toString works as expected */ + @Test + public void testToString() { + SBLimitedLength strBuilder = getStrBuilder(2, 2); + strBuilder.a("ab"); + Assert.assertEquals("ab", strBuilder.toString()); + strBuilder.a("cd"); + Assert.assertEquals("abcd", strBuilder.toString()); + strBuilder.a("ef"); + Assert.assertEquals("ab... and 4 skipped ...ef", strBuilder.toString()); + } + + /** Ensure all operations that could possibly reduce length are prohibited */ + @Test + public void testLengthReduceOperationsAreProhibited() { + SBLimitedLength strBuilder = getStrBuilder(2, 2); + assertThrows(UnsupportedOperationException.class, () -> strBuilder.d(0)); + assertThrows(UnsupportedOperationException.class, () -> strBuilder.d(0, 0)); + assertThrows(UnsupportedOperationException.class, () -> strBuilder.r(0, 0, "asd")); + assertThrows(UnsupportedOperationException.class, () -> strBuilder.setLength(0)); + } + + /** + * Assert {@link Runnable#run()} will throw specified exception + * @param expectedExceptionClass Expected exception class. + * @param runnable Runnable. + */ + private void assertThrows(Class expectedExceptionClass, Runnable runnable) { + boolean eIsSpotted = false; + try { + runnable.run(); + } + catch (Throwable throwable) { + if (expectedExceptionClass.isAssignableFrom(throwable.getClass())) + eIsSpotted = true; + } + finally { + Assert.assertTrue(eIsSpotted); + } + } + + /** + * Get {@link SBLimitedLength} instance with specific head and tail length + * to simplify test cases + * @param headLength Head length. + * @param tailLength Tail length. + */ + private SBLimitedLength getStrBuilder(int headLength, int tailLength) { + SBLimitedLength sbLimitedLength = new SBLimitedLength(0); + sbLimitedLength.initLimit(new SBLengthLimit() { + + @Override int getHeadLengthLimit() { + return headLength; + } + + @Override int getTailLengthLimit() { + return tailLength; + } + }); + return sbLimitedLength; + } +} + + + diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java index c06cbfe8fd4c8..5b5d98fc87b1c 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/IgniteUtilSelfTestSuite.java @@ -50,6 +50,7 @@ import org.apache.ignite.internal.util.tostring.GridToStringBuilderSelfTest; import org.apache.ignite.internal.util.tostring.IncludeSensitiveAtomicTest; import org.apache.ignite.internal.util.tostring.IncludeSensitiveTransactionalTest; +import org.apache.ignite.internal.util.tostring.SBLimitedLengthSelfTest; import org.apache.ignite.internal.util.tostring.TransactionSensitiveDataTest; import org.apache.ignite.lang.GridByteArrayListSelfTest; import org.apache.ignite.spi.discovery.ClusterMetricsSelfTest; @@ -91,6 +92,7 @@ GridStringBuilderFactorySelfTest.class, GridToStringBuilderSelfTest.class, CircularStringBuilderSelfTest.class, + SBLimitedLengthSelfTest.class, GridByteArrayListSelfTest.class, GridMBeanSelfTest.class, GridMBeanDisableSelfTest.class,