PrintStackTrace.java

/*
 * SPDX-FileCopyrightText: 2025 kaumei.io
 * SPDX-License-Identifier: Apache-2.0
 */
package io.kaumei.jdbc.anno.utils;

import java.util.Arrays;
import java.util.IdentityHashMap;
import java.util.Map;

public final class PrintStackTrace {
    private static final StackTraceElement[] EMPTY = new StackTraceElement[0];

    private PrintStackTrace() {
    }

    public static void appendStackTrace(StringBuilder sb, Throwable t) {
        printThrowable(sb, t, "", EMPTY, new IdentityHashMap<>());
    }

    public static String appendStackTrace(Throwable t) {
        var sb = new StringBuilder();
        printThrowable(sb, t, "", EMPTY, new IdentityHashMap<>());
        return sb.toString();
    }

    private static void printThrowable(
            StringBuilder sb,
            Throwable t,
            String prefix,
            StackTraceElement[] parentTrace,
            Map<Throwable, Boolean> seen) {
        // using IdentityHashMap as a Set
        if (seen.put(t, Boolean.TRUE) != null) {
            sb.append(prefix).append("[CIRCULAR REFERENCE: ").append(t).append("]\n");
            return;
        }

        StackTraceElement[] trace = trimCallerSide(t.getStackTrace());
        int commonFrames = countCommonFrames(trace, parentTrace);

        sb.append(prefix).append(t).append("\n");

        for (int i = 0; i < trace.length - commonFrames; i++) {
            sb.append(prefix).append("\tat ").append(trace[i]).append("\n");
        }

        if (commonFrames > 0) {
            sb.append(prefix).append("\t... ").append(commonFrames).append(" more\n");
        }

        for (Throwable suppressed : t.getSuppressed()) {
            sb.append(prefix).append("\tSuppressed: ");
            printThrowable(sb, suppressed, prefix + "\t", trace, seen);
        }

        Throwable cause = t.getCause();
        if (cause != null) {
            sb.append(prefix).append("Caused by: ");
            printThrowable(sb, cause, prefix, trace, seen);
        }
    }

    private static StackTraceElement[] trimCallerSide(StackTraceElement[] trace) {
        for (var i = trace.length - 1; i >= 0; i--) {
            if (trace[i].getClassName().startsWith("io.kaumei")) {
                return Arrays.copyOfRange(trace, 0, i + 1);
            }
        }
        return trace;
    }

    private static int countCommonFrames(StackTraceElement[] trace, StackTraceElement[] parentTrace) {
        int i = trace.length - 1;
        int j = parentTrace.length - 1;
        int count = 0;
        while (i >= 0 && j >= 0 && trace[i].equals(parentTrace[j])) {
            count++;
            i--;
            j--;
        }

        return count;
    }
}