
com.randomnoun.common.ExceptionUtils Maven / Gradle / Ivy
package com.randomnoun.common;
/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
* BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
*/
import java.io.*;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.List;
/**
* Exception utilities class.
*
* This class contains static utility methods for handling and manipulating exceptions;
* the only one you're likely to call being
* {@link #getStackTraceWithRevisions(Throwable, ClassLoader, int, String)},
* which extracts CVS revision information from classes to produce an annotated (and highlighted)
* stack trace.
*
*
When passing in {@link java.lang.ClassLoader}s to these methods, you may want to try one of
*
* - this.getClass().getClassLoader()
*
- Thread.currentThread().getContextClassLoader()
*
*
* @blog http://www.randomnoun.com/wp/2012/12/17/marginally-better-stack-traces/
* @version $Id: ExceptionUtils.java,v 1.2 2013-09-24 02:37:09 knoxg Exp $
* @author knoxg
*/
public class ExceptionUtils {
public static final String _revision = "$Id: ExceptionUtils.java,v 1.2 2013-09-24 02:37:09 knoxg Exp $";
/** Perform no stack trace element highlighting */
public static final int HIGHLIGHT_NONE = 0;
/** Allow stack trace elements to be highlighted, as text */
public static final int HIGHLIGHT_TEXT = 1;
/** Allow stack trace elements to be highlighted, as bold HTML */
public static final int HIGHLIGHT_HTML = 2;
/**
* Private constructor to prevent instantiation of this class
*/
private ExceptionUtils() {
}
/**
* Converts an exception's stack trace to a string. If the exception passed
* to this function is null, returns the empty string.
*
* @param e exception
*
* @return string representation of the exception's stack trace
*/
public static String getStackTrace(Throwable e) {
if (e == null) {
return "";
}
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
return writer.toString();
}
/**
* Converts an exception's stack trace to a string. Each stack trace element
* is annotated to include the CVS revision Id, if it contains a public static
* String element containing this information.
*
* Stack trace elements whose classes begin with the specified highlightPrefix
* are also marked, to allow easier debugging. Highlights used can be text
* (which will insert the string "=>" before relevent stack trace elements), or
* HTML (which will render the stack trace element between <b> and </b> tags.
*
*
If HTML highlighting is enabled, then the exception message is also HTML-escaped.
*
* @param e exception
* @param loader ClassLoader used to read stack trace element revision information
* @param highlight One of the HIGHLIGHT_* constants in this class
* @param highlightPrefix A prefix used to determine which stack trace elements are
* rendered as being 'important'. (e.g. "com.randomnoun.common."). Multiple
* prefixes can be specified, if separated by commas.
*
* @return string representation of the exception's stack trace
*/
public static String getStackTraceWithRevisions(Throwable e, ClassLoader loader, int highlight, String highlightPrefix) {
if (e == null) {
return "(null)";
}
StringBuffer sb = new StringBuffer();
// using reflection to remove runtime dependency on beanshell package
try {
if (e.getClass().getName().equals("bsh.TargetError")) {
Method m;
m = e.getClass().getMethod("getTarget");
e = (Throwable) m.invoke(e);
}
} catch (SecurityException e1) {
// ignore - just use original exception
} catch (NoSuchMethodException e1) {
// ignore - just use original exception
} catch (IllegalArgumentException e1) {
// ignore - just use original exception
} catch (IllegalAccessException e1) {
// ignore - just use original exception
} catch (InvocationTargetException e1) {
// ignore - just use original exception
}
/*
if (e instanceof bsh.TargetError) {
e = ((bsh.TargetError)e).getTarget();
}
*/
String s = e.getClass().getName();
String message = e.getLocalizedMessage();
if (highlight==HIGHLIGHT_HTML) {
sb.append(escapeHtml((message != null) ? (s + ": " + message) : s ));
} else {
sb.append((message != null) ? (s + ": " + message) : s );
}
sb.append('\n');
// dump the stack trace for the top-level exception
StackTraceElement[] trace = null;
trace = e.getStackTrace();
for (int i=0; i < trace.length; i++) {
sb.append(getStackTraceElementWithRevision(trace[i], loader, highlight, highlightPrefix) + "\n");
}
Throwable cause = getCause(e);
if (cause != null) {
sb.append(getStackTraceWithRevisionsAsCause(cause, trace, loader, highlight, highlightPrefix));
}
return sb.toString();
}
/** Returns the 'Caused by...' exception text for a chained exception, performing the
* same stack trace element reduction as performed by the built-in {@link java.lang.Throwable#printStackTrace()}
* class.
*
*
Note that the notion of 'Suppressed' exceptions introduced in Java 7 is not
* supported by this implementation.
*
* @param e the cause of the original exception
* @param causedTrace the original exception trace
* @param loader ClassLoader used to read stack trace element revision information
* @param highlight One of the HIGHLIGHT_* constants in this class
* @param highlightPrefix A prefix used to determine which stack trace elements are
* rendered as being 'important'. (e.g. "com.randomnoun.common."). Multiple
* prefixes can be specified, if separated by commas.
*
* @return the 'caused by' component of a stack trace
*/
private static String getStackTraceWithRevisionsAsCause(Throwable e, StackTraceElement[] causedTrace, ClassLoader loader, int highlight, String highlightPrefix) {
StringBuffer sb = new StringBuffer();
// Compute number of frames in common between this and caused
StackTraceElement[] trace = e.getStackTrace();
int m = trace.length-1;
int n = causedTrace.length-1;
while (m >= 0 && n >=0 && trace[m].equals(causedTrace[n])) {
m--; n--;
}
int framesInCommon = trace.length - 1 - m;
String s = e.getClass().getName();
String message = e.getLocalizedMessage();
sb.append("Caused by: ");
if (highlight==HIGHLIGHT_HTML) {
sb.append(escapeHtml((message != null) ? (s + ": " + message) : s ));
} else {
sb.append((message != null) ? (s + ": " + message) : s );
}
sb.append("\n");
for (int i=0; i <= m; i++) {
sb.append(getStackTraceElementWithRevision(trace[i], loader, highlight, highlightPrefix) + "\n");
}
if (framesInCommon != 0)
sb.append("\t... " + framesInCommon + " more\n");
// Recurse if we have a cause
Throwable ourCause = getCause(e);
if (ourCause != null) {
sb.append(getStackTraceWithRevisionsAsCause(ourCause, trace, loader, highlight, highlightPrefix));
}
return sb.toString();
}
/** Returns a single stack trace element as a String, with highlighting
*
* @param ste the StackTraceElement
* @param loader ClassLoader used to read stack trace element revision information
* @param highlight One of the HIGHLIGHT_* constants in this class
* @param highlightPrefix A prefix used to determine which stack trace elements are
* rendered as being 'important'. (e.g. "com.randomnoun.common."). Multiple
* prefixes can be specified, if separated by commas.
*
* @return the stack trace element as a String, with highlighting
*/
private static String getStackTraceElementWithRevision(StackTraceElement ste, ClassLoader loader,
int highlight, String highlightPrefix)
{
// s should be something like:
// javax.servlet.http.HttpServlet.service(HttpServlet.java:740)
String s;
if (highlightPrefix==null || highlight==HIGHLIGHT_NONE) {
s = " at " + ste.toString();
} else {
boolean isHighlighted = (highlight!=HIGHLIGHT_NONE && isHighlighted(ste.getClassName(), highlightPrefix));
if (isHighlighted && highlight==HIGHLIGHT_HTML) {
s = " at " + ste.toString() + "";
} else if (isHighlighted && highlight==HIGHLIGHT_TEXT) {
s = " => at " + ste.toString();
} else if (!isHighlighted) {
s = " at " + ste.toString();
} else {
throw new IllegalArgumentException("Unknown highlight " + highlight);
}
}
int endLocation = s.lastIndexOf(")");
if (endLocation!=-1) {
try {
// remove inner class info
String className = ste.getClassName();
String revision = getClassRevision(loader, className);
//System.out.println("Class=" + className + ", revision='" + revision + "'");
s = s.substring(0, endLocation) + ", " + revision + s.substring(endLocation);
} catch (Exception e2) {
} catch (NoClassDefFoundError ncdfe) {
}
}
return s;
}
/** Returns true if the provided className matches the highlightPrefix pattern supplied,
* false otherwise
*
* @param className The name of a class (i.e. the class contained in a stack trace element)
* @param highlightPrefix A prefix used to determine which stack trace elements are
* rendered as being 'important'. (e.g. "com.randomnoun.common."). Multiple
* prefixes can be specified, if separated by commas.
*
* @return true if the provided className matches the highlightPrefix pattern supplied,
* false otherwise
*/
private static boolean isHighlighted(String className, String highlightPrefix) {
if (highlightPrefix.contains(",")) {
String[] prefixes = highlightPrefix.split(",");
boolean highlighted = false;
for (int i=0; i getStackTraceSummary(Throwable throwable) {
List result = new ArrayList();
while (throwable!=null) {
result.add(throwable.getMessage());
throwable = getCause(throwable);
// I think some RemoteExceptions have non-standard caused-by chains as well...
if (throwable==null) {
if (throwable instanceof java.sql.SQLException) {
throwable = ((java.sql.SQLException) throwable).getNextException();
}
}
}
return result;
}
/** Returns the cause of an exception, or null if not known.
* If the exception is a a bsh.TargetError
, then the cause is determined
* by calling the getTarget()
method, otherwise this method will
* return the same value returned by the standard Exception
* getCause()
method.
*
* @param e the cause of an exception, or null if not known.
*
* @return the cause of an exception, or null if not known.
*/
private static Throwable getCause(Throwable e) {
Throwable cause = null;
if (e.getClass().getName().equals("bsh.TargetError")) {
try {
if (e.getClass().getName().equals("bsh.TargetError")) {
Method m;
m = e.getClass().getMethod("getTarget");
cause = (Throwable) m.invoke(e);
}
} catch (SecurityException e1) {
// ignore - just use original exception
} catch (NoSuchMethodException e1) {
// ignore - just use original exception
} catch (IllegalArgumentException e1) {
// ignore - just use original exception
} catch (IllegalAccessException e1) {
// ignore - just use original exception
} catch (InvocationTargetException e1) {
// ignore - just use original exception
}
} else {
cause = e.getCause();
}
return cause;
}
/**
* Returns the HTML-escaped form of a string. Any &,
* <, >, and " characters are converted to
* &, <, >, and
* " respectively.
*
* @param string the string to convert
*
* @return the HTML-escaped form of the string
*/
static public String escapeHtml(String string) {
if (string == null) {
return "";
}
char c;
StringBuffer sb = new StringBuffer(string.length());
for (int i = 0; i < string.length(); i++) {
c = string.charAt(i);
switch (c) {
case '&':
sb.append("&");
break;
case '<':
sb.append("<");
break;
case '>':
sb.append(">");
break;
case '\"':
sb.append(""");
break;
default:
sb.append(c);
}
}
return sb.toString();
}
}