package er.extensions.foundation; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Writer; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.util.Enumeration; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSMutableArray; import er.extensions.localization.ERXLocalizer; /** * Provides a set of utilities for displaying and managing exceptions. * * @author mschrag */ public class ERXExceptionUtilities { private static final Logger log = LoggerFactory.getLogger(ERXExceptionUtilities.class); /** * Implemented by any exception that you explicitly want to not appear in * stack dumps. * * @author mschrag */ public static interface WeDontNeedAStackTraceException { } /** * Wraps a root cause, but does not render a stack trace to the given * writer. This is used to intercept old code that handles exceptions in * undesirable ways. * * @author mschrag */ public static class HideStackTraceException extends NSForwardException { /** * Do I need to update serialVersionUID? * See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the * <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a> */ private static final long serialVersionUID = 1L; public HideStackTraceException(Throwable cause) { super(cause); } @Override public void printStackTrace(PrintWriter s) { s.println("[stack trace already printed]"); } } /** * Returns the cause of an exception. This should be modified to be pluggable. * * @param t the original exception * @return the cause of the exception or null of there isn't one */ protected static Throwable getCause(Throwable t) { Throwable cause = null; if (t != null) { cause = t.getCause(); if (cause == null) { try { // Check for OGNL root causes Class ognlExceptionClass = Class.forName("ognl.OgnlException"); if (ognlExceptionClass.isAssignableFrom(t.getClass())) { Method reasonMethod = ognlExceptionClass.getDeclaredMethod("getReason"); cause = (Throwable) reasonMethod.invoke(t); } } catch (Throwable e) { // IGNORE } } } if (t == cause) { cause = null; } return cause; } /** * Returns a paragraph form of the given throwable. * * @param t * the throwable to convert to paragraph form * @return the paragraph string */ public static String toParagraph(Throwable t) { return ERXExceptionUtilities.toParagraph(t, true); } /** * Returns a paragraph form of the given throwable. * * @param t * the throwable to convert to paragraph form * @param removeHtmlTags if true, html tags will be filtered from the error messages (to remove, for instance, bold tags from validation messages) * @return the paragraph string */ public static String toParagraph(Throwable t, boolean removeHtmlTags) { StringBuilder messageBuffer = new StringBuilder(); boolean foundInternalError = false; Throwable throwable = t; while (throwable != null) { if (messageBuffer.length() > 0) { messageBuffer.append(' '); } Throwable oldThrowable = ERXExceptionUtilities.getMeaningfulThrowable(throwable); String message = throwable.getLocalizedMessage(); if (message == null) { if (!foundInternalError) { message = "Your request produced an error."; foundInternalError = true; } else { message = ""; } } if (removeHtmlTags) { message = message.replaceAll("<[^>]+>", ""); } message = message.trim(); messageBuffer.append(message); if (!message.endsWith(".")) { messageBuffer.append('.'); } throwable = ERXExceptionUtilities.getCause(oldThrowable); } return messageBuffer.toString(); } /** * Returns the "meaningful" root cause from a throwable. For instance, an * InvocationTargetException is useless -- it's the cause that matters. * * @param t * the meaningful exception given another throwable * @return the meaningful exception */ public static Throwable getMeaningfulThrowable(Throwable t) { Throwable meaningfulThrowable; if (t instanceof NSForwardException) { meaningfulThrowable = ((NSForwardException) t).originalException(); } else if (t instanceof InvocationTargetException) { meaningfulThrowable = ((InvocationTargetException) t).getCause(); } else if (t instanceof WeDontNeedAStackTraceException && t.getMessage() == null) { meaningfulThrowable = t.getCause(); } else { meaningfulThrowable = t; } if (meaningfulThrowable != t) { meaningfulThrowable = ERXExceptionUtilities.getMeaningfulThrowable(meaningfulThrowable); } return meaningfulThrowable; } /** * Prints a debug stack trace to the console. */ public static void printStackTrace() { Exception e = new Exception("DEBUG"); e.fillInStackTrace(); ERXExceptionUtilities.printStackTrace(e); } /** * Logs a debug stack trace. */ public static void logStackTrace() { Exception e = new Exception("DEBUG"); e.fillInStackTrace(); log.error("", e); } /** * Prints the given throwable to the console (stdout). * * @param t * the throwable to print */ public static void printStackTrace(Throwable t) { ERXExceptionUtilities.printStackTrace(t, System.out); } /** * Prints the given throwable to the given outputstream. * * @param t * the throwable to print * @param os * the stream to print to */ public static void printStackTrace(Throwable t, OutputStream os) { ERXExceptionUtilities.printStackTrace(t, new PrintWriter(os, true), 0); } /** * Prints the given throwable to the given printwriter. * * @param t * the throwable to print * @param writer * the writer to print to */ public static void printStackTrace(Throwable t, Writer writer) { ERXExceptionUtilities.printStackTrace(t, new PrintWriter(writer, true), 0); } /** * Prints the given throwable to the given printwriter. * * @param t * the throwable to print * @param writer * the writer to print to */ public static void printStackTrace(Throwable t, PrintWriter writer) { ERXExceptionUtilities.printStackTrace(t, writer, 0); } private static NSArray<Pattern> _skipPatterns; protected static void _printSingleStackTrace(Throwable t, PrintWriter writer, int exceptionDepth, boolean cleanupStackTrace) { NSArray<Pattern> skipPatterns = ERXExceptionUtilities._skipPatterns; if (cleanupStackTrace && skipPatterns == null) { String skipPatternsFile = ERXProperties.stringForKey("er.extensions.stackTrace.skipPatternsFile"); if (skipPatternsFile != null) { NSMutableArray<Pattern> mutableSkipPatterns = new NSMutableArray<>(); Enumeration<String> frameworksEnum = ERXLocalizer.frameworkSearchPath().reverseObjectEnumerator(); while (frameworksEnum.hasMoreElements()) { String framework = frameworksEnum.nextElement(); URL path = ERXFileUtilities.pathURLForResourceNamed(skipPatternsFile, framework, null); if (path != null) { try { NSArray<String> skipPatternStrings = (NSArray<String>) ERXFileUtilities.readPropertyListFromFileInFramework(skipPatternsFile, framework, (NSArray)null); if (skipPatternStrings != null) { for (String skipPatternString : skipPatternStrings) { try { mutableSkipPatterns.addObject(Pattern.compile(skipPatternString)); } catch (Throwable patternThrowable) { log.error("Skipping invalid exception pattern '{}' in '{}' in the framework '{}' ({})", skipPatternString, skipPatternsFile, framework, ERXExceptionUtilities.toParagraph(patternThrowable)); } } } } catch (Throwable patternThrowable) { log.error("Failed to read pattern file '{}' in the framework '{}' ({})", skipPatternsFile, framework, ERXExceptionUtilities.toParagraph(patternThrowable)); } } } skipPatterns = mutableSkipPatterns; } if (ERXProperties.booleanForKeyWithDefault("er.extensions.stackTrace.cachePatterns", true)) { if (skipPatterns == null) { ERXExceptionUtilities._skipPatterns = NSArray.EmptyArray; } else { ERXExceptionUtilities._skipPatterns = skipPatterns; } } } StackTraceElement[] elements = t.getStackTrace(); ERXStringUtilities.indent(writer, exceptionDepth); if (exceptionDepth > 0) { writer.print("Caused by a "); } if (cleanupStackTrace) { writer.print(t.getClass().getSimpleName()); } else { writer.print(t.getClass().getName()); } String message = t.getLocalizedMessage(); if (message != null) { writer.print(": "); writer.print(message); } writer.println(); int stackDepth = 0; int skippedCount = 0; for (StackTraceElement element : elements) { boolean showElement = true; if (stackDepth > 0 && cleanupStackTrace && skipPatterns != null && !skipPatterns.isEmpty()) { String elementName = element.getClassName() + "." + element.getMethodName(); for (Pattern skipPattern : skipPatterns) { if (skipPattern.matcher(elementName).matches()) { showElement = false; break; } } } if (!showElement) { skippedCount++; } else { if (skippedCount > 0) { ERXStringUtilities.indent(writer, exceptionDepth + 1); writer.println(" ... skipped " + skippedCount + " stack elements"); skippedCount = 0; } ERXStringUtilities.indent(writer, exceptionDepth + 1); writer.print("at "); writer.print(element.getClassName()); writer.print("."); writer.print(element.getMethodName()); writer.print("("); if (element.isNativeMethod()) { writer.print("Native Method"); } else if (element.getLineNumber() < 0) { writer.print(element.getFileName()); writer.print(":Unknown"); } else { writer.print(element.getFileName()); writer.print(":"); writer.print(element.getLineNumber()); } writer.print(")"); writer.println(); } stackDepth++; } if (skippedCount > 0) { ERXStringUtilities.indent(writer, exceptionDepth + 1); writer.println("... skipped " + skippedCount + " stack elements"); } } /** * Prints the given throwable to the given writer with an indent. * * @param t * the throwable to print * @param writer * the writer to print to * @param exceptionDepth * the indent level to use * @property er.extensions.stackTrace.cleanup if true, stack traces are * cleaned up for easier use * @property er.extensions.stackTrace.skipPatternsFile the name the resource * that contains an array of class name and method regexes to skip * in stack traces */ public static void printStackTrace(Throwable t, PrintWriter writer, int exceptionDepth) { try { boolean cleanupStackTrace = ERXProperties.booleanForKeyWithDefault("er.extensions.stackTrace.cleanup", false); Throwable actualThrowable; if (cleanupStackTrace) { actualThrowable = t; } else { actualThrowable = ERXExceptionUtilities.getMeaningfulThrowable(t); } if (actualThrowable == null) { return; } Throwable cause = ERXExceptionUtilities.getCause(actualThrowable); boolean showOnlyBottomException = ERXProperties.booleanForKeyWithDefault("er.extensions.stackTrace.bottomOnly", true); if (!showOnlyBottomException || cause == null) { ERXExceptionUtilities._printSingleStackTrace(actualThrowable, writer, exceptionDepth, cleanupStackTrace); } if (cause != null && cause != actualThrowable) { ERXExceptionUtilities.printStackTrace(cause, writer, exceptionDepth); } } catch (Throwable thisSucks) { writer.println("ERXExceptionUtilities.printStackTrace Failed!"); thisSucks.printStackTrace(writer); } } }