diff --git a/src/main/java/org/jboss/logmanager/formatters/BasicStackTraceFormatter.java b/src/main/java/org/jboss/logmanager/formatters/BasicStackTraceFormatter.java new file mode 100644 index 00000000..bdbeedb3 --- /dev/null +++ b/src/main/java/org/jboss/logmanager/formatters/BasicStackTraceFormatter.java @@ -0,0 +1,162 @@ +/* + * JBoss, Home of Professional Open Source. + * + * Copyright 2017 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.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +/** + * Formatter used to format the stack trace of an exception. + * + * @author James R. Perkins + */ +final class BasicStackTraceFormatter { + 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 BasicStackTraceFormatter(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 suppressedDepth the number of suppressed messages to include + */ + public static void renderStackTrace(final StringBuilder builder, final Throwable t, final int suppressedDepth) { + renderStackTrace(builder, t, false, 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 BasicStackTraceFormatter(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(prefix) + .append(caption) + .append("[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()); + } +} diff --git a/src/main/java/org/jboss/logmanager/formatters/Formatters.java b/src/main/java/org/jboss/logmanager/formatters/Formatters.java index 499ebec5..ac9c0e78 100644 --- a/src/main/java/org/jboss/logmanager/formatters/Formatters.java +++ b/src/main/java/org/jboss/logmanager/formatters/Formatters.java @@ -23,6 +23,7 @@ import static java.lang.Math.min; import static java.security.AccessController.doPrivileged; +import java.io.IOException; import java.io.PrintWriter; import java.security.PrivilegedAction; import java.time.Duration; @@ -884,7 +885,8 @@ public ItemType getItemType() { */ public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth, final int maximumWidth, final boolean extended) { - return exceptionFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, null, extended); + return exceptionFormatStep(leftJustify, minimumWidth, DEFAULT_TRUNCATE_BEGINNING, maximumWidth, null, extended, + StackTraceFormatter.instance()); } /** @@ -895,42 +897,71 @@ public static FormatStep exceptionFormatStep(final boolean leftJustify, final in * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none * @param extended {@code true} if the stack trace should attempt to include extended JAR version information + * @param formatter the stack trace formatter to use (must not be {@code null}) * @return the format step */ public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth, - final boolean truncateBeginning, final int maximumWidth, final String argument, final boolean extended) { + final boolean truncateBeginning, final int maximumWidth, final String argument, final boolean extended, + final StackTraceFormatter formatter) { + int depth = -1; + try { + depth = Integer.parseInt(argument); + } catch (NumberFormatException ignored) { + } + final int finalDepth = depth; + StackTraceFormatter.Parameters params = new StackTraceFormatter.Parameters() { + public boolean extended() { + return extended; + } + + public int suppressedDepth() { + return finalDepth; + } + }; return new JustifyingFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth) { // not really correct but doesn't matter for now public ItemType getItemType() { return ItemType.EXCEPTION_TRACE; } - public void renderRaw(Formatter formatter, final StringBuilder builder, final ExtLogRecord record) { + public void renderRaw(Formatter fmt, final StringBuilder builder, final ExtLogRecord record) { if (System.getSecurityManager() != null) doPrivileged(new PrivilegedAction() { public Void run() { - doExceptionFormatStep(builder, record, argument, extended); + doExceptionFormatStep(builder, record, argument, extended, formatter, params); return null; } }); else - doExceptionFormatStep(builder, record, argument, extended); + doExceptionFormatStep(builder, record, argument, extended, formatter, params); } }; } + /** + * Create a format step which emits the stack trace of an exception with the given justification rules. + * + * @param leftJustify {@code true} to left justify, {@code false} to right justify + * @param minimumWidth the minimum field width, or 0 for none + * @param truncateBeginning {@code true} to truncate the beginning, otherwise {@code false} to truncate the end + * @param maximumWidth the maximum field width (must be greater than {@code minimumFieldWidth}), or 0 for none + * @param extended {@code true} if the stack trace should attempt to include extended JAR version information + * @return the format step + */ + public static FormatStep exceptionFormatStep(final boolean leftJustify, final int minimumWidth, + final boolean truncateBeginning, final int maximumWidth, final String argument, final boolean extended) { + return exceptionFormatStep(leftJustify, minimumWidth, truncateBeginning, maximumWidth, argument, extended, + StackTraceFormatter.instance()); + } + private static void doExceptionFormatStep(final StringBuilder builder, final ExtLogRecord record, final String argument, - final boolean extended) { + final boolean extended, final StackTraceFormatter formatter, final StackTraceFormatter.Parameters params) { final Throwable t = record.getThrown(); if (t != null) { - int depth = -1; - if (argument != null) { - try { - depth = Integer.parseInt(argument); - } catch (NumberFormatException ignore) { - } + try { + formatter.render(t, builder, params); + } catch (IOException ignored) { } - StackTraceFormatter.renderStackTrace(builder, t, extended, depth); } } diff --git a/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatter.java b/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatter.java index 1ad838a0..a92df3b3 100644 --- a/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatter.java +++ b/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatter.java @@ -1,162 +1,50 @@ -/* - * JBoss, Home of Professional Open Source. - * - * Copyright 2017 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.util.Collections; -import java.util.IdentityHashMap; -import java.util.Set; +import java.io.IOException; +import java.util.ServiceLoader; /** - * Formatter used to format the stack trace of an exception. - * - * @author James R. Perkins + * A stack trace formatter. + * An implementation of this interface is located using {@link ServiceLoader} to determine how to format + * stack traces. */ -public 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; - } - +public interface StackTraceFormatter { /** - * Writes the stack trace into the builder. + * Render an exception stack trace to the given output. * - * @param builder the string builder ot append the stack trace to - * @param t the throwable to render - * @param suppressedDepth the number of suppressed messages to include + * @param t the root exception (not {@code null}) + * @param output the output to which the exception stack trace should be written (not {@code null}) + * @param parameters the format parameters (not {@code null}) */ - public static void renderStackTrace(final StringBuilder builder, final Throwable t, final int suppressedDepth) { - renderStackTrace(builder, t, false, suppressedDepth); - } + void render(Throwable t, Appendable output, Parameters parameters) throws IOException; /** - * 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 + * Parameters for a stack trace formatting request. */ - 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, ""); + interface Parameters { + /** + * {@return true if the "extended" hint is given, or false otherwise} + * Implementations may ignore this parameter; it is only a hint. + */ + default boolean extended() { + return false; } - } - - private void renderStackTrace(final StackTraceElement[] parentStack, final Throwable child, final String caption, - final String prefix) { - if (seen.contains(child)) { - builder.append(prefix) - .append(caption) - .append("[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); - } + /** + * {@return the maximum depth of nested or suppressed exceptions to render} + * Implementations may ignore this parameter; it is only a hint. + */ + default int suppressedDepth() { + return Integer.MAX_VALUE; } } - private void renderTrivial(final String prefix, final StackTraceElement element) { - builder.append(prefix) - .append("\tat ") - .append(element); - newLine(); - } - - private void newLine() { - builder.append(System.lineSeparator()); + /** + * Get the singleton stack trace formatter instance. + * + * @return the stack trace formatter instance (not {@code null}) + */ + static StackTraceFormatter instance() { + return StackTraceFormatterHolder.INSTANCE; } } diff --git a/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatterHolder.java b/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatterHolder.java new file mode 100644 index 00000000..90e20045 --- /dev/null +++ b/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatterHolder.java @@ -0,0 +1,68 @@ +package org.jboss.logmanager.formatters; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; + +/** + * + */ +final class StackTraceFormatterHolder { + private StackTraceFormatterHolder() { + } + + static final StackTraceFormatter INSTANCE; + + static { + StackTraceFormatter instance = null; + // we can't use service loader here, because of course it logs, causing things to fail weirdly. + // so, do our own thing + try { + ClassLoader cl = StackTraceFormatterHolder.class.getClassLoader(); + Enumeration e = cl.getResources("META-INF/services/" + StackTraceFormatter.class.getName()); + out: while (e.hasMoreElements()) { + try { + URL url = e.nextElement(); + try (InputStream is = url.openStream(); + InputStreamReader isr = new InputStreamReader(is, StandardCharsets.UTF_8); + BufferedReader br = new BufferedReader(isr)) { + String line; + while ((line = br.readLine()) != null) { + int idx = line.indexOf('#'); + if (idx != -1) { + line = line.substring(idx); + } + line = line.trim(); + if (!line.isBlank()) { + try { + Class stfClass = Class.forName(line, false, cl) + .asSubclass(StackTraceFormatter.class); + MethodHandle ctor = MethodHandles.publicLookup().findConstructor(stfClass, + MethodType.methodType(void.class)); + instance = (StackTraceFormatter) ctor.invoke(); + break out; + } catch (Throwable ignored) { + // just try the next line + } + } + } + } + } catch (Throwable ignored) { + // try the next file + } + } + } catch (Throwable ignored) { + // can't get the resources at all; give up + } + if (instance == null) { + instance = StackTraceFormatterImpl.INSTANCE; + } + INSTANCE = instance; + } +} diff --git a/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatterImpl.java b/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatterImpl.java new file mode 100644 index 00000000..2ba39810 --- /dev/null +++ b/src/main/java/org/jboss/logmanager/formatters/StackTraceFormatterImpl.java @@ -0,0 +1,28 @@ +package org.jboss.logmanager.formatters; + +import java.io.IOException; + +/** + * The default implementation of {@link StackTraceFormatter}. + */ +final class StackTraceFormatterImpl implements StackTraceFormatter { + /** + * Construct a new instance. + */ + private StackTraceFormatterImpl() { + } + + static final StackTraceFormatterImpl INSTANCE = new StackTraceFormatterImpl(); + + public void render(final Throwable t, final Appendable output, final Parameters parameters) throws IOException { + StringBuilder sb; + if (output instanceof StringBuilder) { + sb = (StringBuilder) output; + BasicStackTraceFormatter.renderStackTrace(sb, t, parameters.suppressedDepth()); + } else { + sb = new StringBuilder(); + BasicStackTraceFormatter.renderStackTrace(sb, t, parameters.suppressedDepth()); + output.append(sb); + } + } +} diff --git a/src/main/java/org/jboss/logmanager/formatters/StructuredFormatter.java b/src/main/java/org/jboss/logmanager/formatters/StructuredFormatter.java index 67e8ebf3..02fb6410 100644 --- a/src/main/java/org/jboss/logmanager/formatters/StructuredFormatter.java +++ b/src/main/java/org/jboss/logmanager/formatters/StructuredFormatter.java @@ -245,7 +245,7 @@ public final synchronized String format(final ExtLogRecord record) { if (isFormattedExceptionOutputType()) { final StringBuilder sb = new StringBuilder(); - StackTraceFormatter.renderStackTrace(sb, thrown, -1); + BasicStackTraceFormatter.renderStackTrace(sb, thrown, -1); generator.add(getKey(Key.STACK_TRACE), sb.toString()); } } diff --git a/src/test/java/org/jboss/logmanager/formatters/StackTraceFormatterTests.java b/src/test/java/org/jboss/logmanager/formatters/StackTraceFormatterTests.java index c1861cd8..0583d222 100644 --- a/src/test/java/org/jboss/logmanager/formatters/StackTraceFormatterTests.java +++ b/src/test/java/org/jboss/logmanager/formatters/StackTraceFormatterTests.java @@ -40,7 +40,7 @@ public void compareSimpleStackTrace() { e.printStackTrace(new PrintWriter(writer)); final StringBuilder sb = new StringBuilder(); - StackTraceFormatter.renderStackTrace(sb, e, false, -1); + BasicStackTraceFormatter.renderStackTrace(sb, e, false, -1); Assertions.assertEquals(writer.toString(), sanitize(sb.toString())); } @@ -53,7 +53,7 @@ public void compareCauseStackTrace() { e.printStackTrace(new PrintWriter(writer)); final StringBuilder sb = new StringBuilder(); - StackTraceFormatter.renderStackTrace(sb, e, false, -1); + BasicStackTraceFormatter.renderStackTrace(sb, e, false, -1); Assertions.assertEquals(writer.toString(), sanitize(sb.toString())); } @@ -73,7 +73,7 @@ public void compareSuppressedAndCauseStackTrace() { cause.printStackTrace(new PrintWriter(writer)); final StringBuilder sb = new StringBuilder(); - StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + BasicStackTraceFormatter.renderStackTrace(sb, cause, false, -1); Assertions.assertEquals(writer.toString(), sanitize(sb.toString())); } @@ -97,7 +97,7 @@ public void compareNestedSuppressedStackTrace() { cause.printStackTrace(new PrintWriter(writer)); final StringBuilder sb = new StringBuilder(); - StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + BasicStackTraceFormatter.renderStackTrace(sb, cause, false, -1); Assertions.assertEquals(writer.toString(), sanitize(sb.toString())); } @@ -111,7 +111,7 @@ public void compareMultiNestedSuppressedStackTrace() { cause.printStackTrace(new PrintWriter(writer)); final StringBuilder sb = new StringBuilder(); - StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + BasicStackTraceFormatter.renderStackTrace(sb, cause, false, -1); Assertions.assertEquals(writer.toString(), sanitize(sb.toString())); } @@ -126,7 +126,7 @@ public void compareMultiNestedSuppressedAndNestedCauseStackTrace() { cause.printStackTrace(new PrintWriter(writer)); final StringBuilder sb = new StringBuilder(); - StackTraceFormatter.renderStackTrace(sb, cause, false, -1); + BasicStackTraceFormatter.renderStackTrace(sb, cause, false, -1); Assertions.assertEquals(writer.toString(), sanitize(sb.toString())); } @@ -145,7 +145,7 @@ private void testDepth(final int depth) { final Throwable cause = createMultiNestedCause(); final StringBuilder sb = new StringBuilder(); - StackTraceFormatter.renderStackTrace(sb, cause, false, depth); + BasicStackTraceFormatter.renderStackTrace(sb, cause, false, depth); String msg = sb.toString();