Skip to content

Commit

Permalink
[LOGMGR-133] Extended stack traces are not useful on Java 9+
Browse files Browse the repository at this point in the history
  • Loading branch information
dmlloyd committed Mar 2, 2018
1 parent cfa5545 commit c54c2de
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 0 deletions.
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@
<execution>
<id>default-test</id>
<configuration>
<excludes>
<exclude>**/*$*</exclude>
<!-- This test no longer applies on Java 9+ -->
<exclude>org/jboss/logmanager/formatters/FormattersTests.java</exclude>
</excludes>
<classesDirectory>${project.build.directory}/classes/META-INF/versions/9</classesDirectory>
<additionalClasspathElements>
<additionalClasspathElement>${project.build.directory}/classes</additionalClasspathElement>
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:jperkins@redhat.com">James R. Perkins</a>
*/
class StackTraceFormatter {
private static final String CAUSED_BY_CAPTION = "Caused by: ";
private static final String SUPPRESSED_CAPTION = "Suppressed: ";

private final Set<Throwable> seen = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
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);
}
}

0 comments on commit c54c2de

Please sign in to comment.