From c54c2de9a0877f5fdebfc38191d7420d8555cfa1 Mon Sep 17 00:00:00 2001 From: "David M. Lloyd" Date: Fri, 2 Mar 2018 11:18:48 -0600 Subject: [PATCH] [LOGMGR-133] Extended stack traces are not useful on Java 9+ --- pom.xml | 5 + .../formatters/StackTraceFormatter.java | 192 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 src/main/java9/org/jboss/logmanager/formatters/StackTraceFormatter.java diff --git a/pom.xml b/pom.xml index 51e2c1d3..b9cc9fc7 100644 --- a/pom.xml +++ b/pom.xml @@ -232,6 +232,11 @@ default-test + + **/*$* + + org/jboss/logmanager/formatters/FormattersTests.java + ${project.build.directory}/classes/META-INF/versions/9 ${project.build.directory}/classes diff --git a/src/main/java9/org/jboss/logmanager/formatters/StackTraceFormatter.java b/src/main/java9/org/jboss/logmanager/formatters/StackTraceFormatter.java new file mode 100644 index 00000000..7045799c --- /dev/null +++ b/src/main/java9/org/jboss/logmanager/formatters/StackTraceFormatter.java @@ -0,0 +1,192 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2018 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.jboss.logmanager.formatters; + +import java.net.URL; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** + * Formatter used to format the stack trace of an exception. + * + * @author James R. Perkins + */ +class StackTraceFormatter { + private static final String CAUSED_BY_CAPTION = "Caused by: "; + private static final String SUPPRESSED_CAPTION = "Suppressed: "; + + private final Set seen = Collections.newSetFromMap(new IdentityHashMap()); + private final StringBuilder builder; + private final int suppressedDepth; + private int suppressedCount; + + private StackTraceFormatter(final StringBuilder builder, final int suppressedDepth) { + this.builder = builder; + this.suppressedDepth = suppressedDepth; + } + + /** + * Writes the stack trace into the builder. + * + * @param builder the string builder ot append the stack trace to + * @param t the throwable to render + * @param extended ignored + * @param suppressedDepth the number of suppressed messages to include + */ + static void renderStackTrace(final StringBuilder builder, final Throwable t, @SuppressWarnings("unused") final boolean extended, final int suppressedDepth) { + new StackTraceFormatter(builder, suppressedDepth).renderStackTrace(t); + } + + private void renderStackTrace(final Throwable t) { + // Reset the suppression count + suppressedCount = 0; + // Write the exception message + builder.append(": ").append(t); + newLine(); + + // Write the stack trace for this message + final StackTraceElement[] stackTrace = t.getStackTrace(); + for (StackTraceElement element : stackTrace) { + renderTrivial("", element); + } + + // Write any suppressed messages, if required + if (suppressedDepth != 0) { + for (Throwable se : t.getSuppressed()) { + if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) { + renderStackTrace(stackTrace, se, SUPPRESSED_CAPTION, "\t"); + } + } + } + + // Print cause if there is one + final Throwable ourCause = t.getCause(); + if (ourCause != null) { + renderStackTrace(stackTrace, ourCause, CAUSED_BY_CAPTION, ""); + } + } + + private void renderStackTrace(final StackTraceElement[] parentStack, final Throwable child, final String caption, final String prefix) { + if (seen.contains(child)) { + builder.append("\t[CIRCULAR REFERENCE:") + .append(child) + .append(']'); + newLine(); + } else { + seen.add(child); + // Find the unique frames suppressing duplicates + final StackTraceElement[] causeStack = child.getStackTrace(); + int m = causeStack.length - 1; + int n = parentStack.length - 1; + while (m >= 0 && n >= 0 && causeStack[m].equals(parentStack[n])) { + m--; + n--; + } + final int framesInCommon = causeStack.length - 1 - m; + + // Print our stack trace + builder.append(prefix) + .append(caption) + .append(child); + newLine(); + for (int i = 0; i <= m; i++) { + renderTrivial(prefix, causeStack[i]); + } + if (framesInCommon != 0) { + builder.append(prefix) + .append("\t... ") + .append(framesInCommon) + .append(" more"); + newLine(); + } + + // Print suppressed exceptions, if any + if (suppressedDepth != 0) { + for (Throwable se : child.getSuppressed()) { + if (suppressedDepth < 0 || suppressedDepth > suppressedCount++) { + renderStackTrace(causeStack, se, SUPPRESSED_CAPTION, prefix + "\t"); + } + } + } + + // Print cause, if any + Throwable ourCause = child.getCause(); + if (ourCause != null) { + renderStackTrace(causeStack, ourCause, CAUSED_BY_CAPTION, prefix); + } + } + } + + private void renderTrivial(final String prefix, final StackTraceElement element) { + builder.append(prefix) + .append("\tat ") + .append(element); + newLine(); + } + + private void newLine() { + builder.append(System.lineSeparator()); + } + + /** + * Attempts to parse the JAR name from the resource URL. + * + * @param resource the URL for the resource + * @param classResourceName the name of the name of the class within the resource + * + * @return the name of the JAR or module + */ + static String getJarName(URL resource, String classResourceName) { + if (resource == null) { + return null; + } + + final String path = resource.getPath(); + final String protocol = resource.getProtocol(); + + if ("jar".equals(protocol)) { + // the last path segment before "!/" should be the JAR name + final int sepIdx = path.lastIndexOf("!/"); + if (sepIdx != -1) { + // hit! + final String firstPart = path.substring(0, sepIdx); + // now find the last file separator before the JAR separator + final int lsIdx = Math.max(firstPart.lastIndexOf('/'), firstPart.lastIndexOf('\\')); + return firstPart.substring(lsIdx + 1); + } + } else if ("module".equals(protocol)) { + return resource.getPath(); + } + + // OK, that would have been too easy. Next let's just grab the last piece before the class name + for (int endIdx = path.lastIndexOf(classResourceName); endIdx >= 0; endIdx--) { + char ch = path.charAt(endIdx); + if (ch == '/' || ch == '\\' || ch == '?') { + String firstPart = path.substring(0, endIdx); + int lsIdx = Math.max(firstPart.lastIndexOf('/'), firstPart.lastIndexOf('\\')); + return firstPart.substring(lsIdx + 1); + } + } + + // OK, just use the last segment + final int endIdx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); + return path.substring(endIdx + 1); + } +}