blob: 6a36cc841a2cd3efd9764871ada308dfdaa3708c [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.directory.shared.ldap.util;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import org.apache.directory.shared.i18n.I18n;
/**
* <p>
* Provides utilities for manipulating and examining <code>Throwable</code>
* objects.
* </p>
*
* @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
* @version $Rev$
*/
public class ExceptionUtils
{
/**
* <p>
* Used when printing stack frames to denote the start of a wrapped
* exception.
* </p>
* <p/>
* <p>
* Package private for accessibility by test suite.
* </p>
*/
static final String WRAPPED_MARKER = " [wrapped] ";
/**
* <p>
* The names of methods commonly used to access a wrapped exception.
* </p>
*/
private static String[] CAUSE_METHOD_NAMES =
{ "getCause", "getNextException", "getTargetException", "getException", "getSourceException", "getRootCause",
"getCausedByException", "getNested", "getLinkedException", "getNestedException", "getLinkedCause",
"getThrowable", };
/**
* <p>
* The Method object for JDK1.4 getCause.
* </p>
*/
private static final Method THROWABLE_CAUSE_METHOD;
static
{
Method getCauseMethod;
try
{
getCauseMethod = Throwable.class.getMethod( "getCause", (Class[])null );
}
catch ( Exception e )
{
getCauseMethod = null;
}
THROWABLE_CAUSE_METHOD = getCauseMethod;
}
/**
* <p>
* Public constructor allows an instance of <code>ExceptionUtils</code> to
* be created, although that is not normally necessary.
* </p>
*/
public ExceptionUtils()
{
}
/**
* <p>
* Checks if a String is not empty ("") and not null.
* </p>
*
* @param str
* the String to check, may be null
* @return <code>true</code> if the String is not empty and not null
*/
private static boolean isNotEmpty( String str )
{
return ( str != null && str.length() > 0 );
}
// -----------------------------------------------------------------------
/**
* <p>
* Adds to the list of method names used in the search for
* <code>Throwable</code> objects.
* </p>
*
* @param methodName
* the methodName to add to the list, <code>null</code> and
* empty strings are ignored
* @since 2.0
*/
public static void addCauseMethodName( String methodName )
{
if ( isNotEmpty( methodName ) )
{
List<String> list = new ArrayList<String>( Arrays.asList( CAUSE_METHOD_NAMES ) );
list.add( methodName );
CAUSE_METHOD_NAMES = list.toArray( new String[list.size()] );
}
}
/**
* <p>
* Introspects the <code>Throwable</code> to obtain the cause.
* </p>
* <p/>
* <p>
* The method searches for methods with specific names that return a
* <code>Throwable</code> object. This will pick up most wrapping
* exceptions, including those from JDK 1.4, and The method names can be
* added to using {@link #addCauseMethodName(String)}.
* </p>
* <p/>
* <p>
* The default list searched for are:
* </p>
* <ul>
* <li><code>getCause()</code></li>
* <li><code>getNextException()</code></li>
* <li><code>getTargetException()</code></li>
* <li><code>getException()</code></li>
* <li><code>getSourceException()</code></li>
* <li><code>getRootCause()</code></li>
* <li><code>getCausedByException()</code></li>
* <li><code>getNested()</code></li>
* </ul>
* <p/>
* <p>
* In the absence of any such method, the object is inspected for a
* <code>detail</code> field assignable to a <code>Throwable</code>.
* </p>
* <p/>
* <p>
* If none of the above is found, returns <code>null</code>.
* </p>
*
* @param throwable
* the throwable to introspect for a cause, may be null
* @return the cause of the <code>Throwable</code>, <code>null</code>
* if none found or null throwable input
* @since 1.0
*/
public static Throwable getCause( Throwable throwable )
{
return getCause( throwable, CAUSE_METHOD_NAMES );
}
/**
* <p>
* Introspects the <code>Throwable</code> to obtain the cause.
* </p>
* <p/>
* <ol>
* <li>Try known exception types.</li>
* <li>Try the supplied array of method names.</li>
* <li>Try the field 'detail'.</li>
* </ol>
* <p/>
* <p>
* A <code>null</code> set of method names means use the default set. A
* <code>null</code> in the set of method names will be ignored.
* </p>
*
* @param throwable
* the throwable to introspect for a cause, may be null
* @param methodNames
* the method names, null treated as default set
* @return the cause of the <code>Throwable</code>, <code>null</code>
* if none found or null throwable input
* @since 1.0
*/
public static Throwable getCause( Throwable throwable, String[] methodNames )
{
if ( throwable == null )
{
return null;
}
Throwable cause = getCauseUsingWellKnownTypes( throwable );
if ( cause == null )
{
if ( methodNames == null )
{
methodNames = CAUSE_METHOD_NAMES;
}
for ( int i = 0; i < methodNames.length; i++ )
{
String methodName = methodNames[i];
if ( methodName != null )
{
cause = getCauseUsingMethodName( throwable, methodName );
if ( cause != null )
{
break;
}
}
}
if ( cause == null )
{
cause = getCauseUsingFieldName( throwable, "detail" );
}
}
return cause;
}
/**
* <p>
* Introspects the <code>Throwable</code> to obtain the root cause.
* </p>
* <p/>
* <p>
* This method walks through the exception chain to the last element, "root"
* of the tree, using {@link #getCause(Throwable)}, and returns that
* exception.
* </p>
*
* @param throwable
* the throwable to get the root cause for, may be null
* @return the root cause of the <code>Throwable</code>,
* <code>null</code> if none found or null throwable input
*/
public static Throwable getRootCause( Throwable throwable )
{
Throwable cause = getCause( throwable );
if ( cause != null )
{
throwable = cause;
while ( ( throwable = getCause( throwable ) ) != null )
{
cause = throwable;
}
}
return cause;
}
/**
* <p>
* Finds a <code>Throwable</code> for known types.
* </p>
* <p/>
* <p>
* Uses <code>instanceof</code> checks to examine the exception, looking
* for well known types which could contain chained or wrapped exceptions.
* </p>
*
* @param throwable
* the exception to examine
* @return the wrapped exception, or <code>null</code> if not found
*/
private static Throwable getCauseUsingWellKnownTypes( Throwable throwable )
{
if ( throwable instanceof Nestable )
{
return ( ( Nestable ) throwable ).getCause();
}
else if ( throwable instanceof SQLException )
{
return ( ( SQLException ) throwable ).getNextException();
}
else if ( throwable instanceof InvocationTargetException )
{
return ( ( InvocationTargetException ) throwable ).getTargetException();
}
else
{
return null;
}
}
/**
* <p>
* Finds a <code>Throwable</code> by method name.
* </p>
*
* @param throwable
* the exception to examine
* @param methodName
* the name of the method to find and invoke
* @return the wrapped exception, or <code>null</code> if not found
*/
// This will suppress PMD.EmptyCatchBlock warnings in this method
@SuppressWarnings("PMD.EmptyCatchBlock")
private static Throwable getCauseUsingMethodName( Throwable throwable, String methodName )
{
Method method = null;
try
{
method = throwable.getClass().getMethod( methodName, (Class[])null );
}
catch ( NoSuchMethodException ignored )
{
}
catch ( SecurityException ignored )
{
}
if ( method != null && Throwable.class.isAssignableFrom( method.getReturnType() ) )
{
try
{
return ( Throwable ) method.invoke( throwable, ArrayUtils.EMPTY_OBJECT_ARRAY );
}
catch ( IllegalAccessException ignored )
{
}
catch ( IllegalArgumentException ignored )
{
}
catch ( InvocationTargetException ignored )
{
}
}
return null;
}
/**
* <p>
* Finds a <code>Throwable</code> by field name.
* </p>
*
* @param throwable
* the exception to examine
* @param fieldName
* the name of the attribute to examine
* @return the wrapped exception, or <code>null</code> if not found
*/
// This will suppress PMD.EmptyCatchBlock warnings in this method
@SuppressWarnings("PMD.EmptyCatchBlock")
private static Throwable getCauseUsingFieldName( Throwable throwable, String fieldName )
{
Field field = null;
try
{
field = throwable.getClass().getField( fieldName );
}
catch ( NoSuchFieldException ignored )
{
}
catch ( SecurityException ignored )
{
}
if ( field != null && Throwable.class.isAssignableFrom( field.getType() ) )
{
try
{
return ( Throwable ) field.get( throwable );
}
catch ( IllegalAccessException ignored )
{
}
catch ( IllegalArgumentException ignored )
{
}
}
return null;
}
// -----------------------------------------------------------------------
/**
* <p>
* Checks if the Throwable class has a <code>getCause</code> method.
* </p>
* <p/>
* <p>
* This is true for JDK 1.4 and above.
* </p>
*
* @return true if Throwable is nestable
* @since 2.0
*/
public static boolean isThrowableNested()
{
return ( THROWABLE_CAUSE_METHOD != null );
}
/**
* <p>
* Checks whether this <code>Throwable</code> class can store a cause.
* </p>
* <p/>
* <p>
* This method does <b>not</b> check whether it actually does store a
* cause.
* <p>
*
* @param throwable
* the <code>Throwable</code> to examine, may be null
* @return boolean <code>true</code> if nested otherwise
* <code>false</code>
* @since 2.0
*/
// This will suppress PMD.EmptyCatchBlock warnings in this method
@SuppressWarnings("PMD.EmptyCatchBlock")
public static boolean isNestedThrowable( Throwable throwable )
{
if ( throwable == null )
{
return false;
}
if ( throwable instanceof Nestable )
{
return true;
}
else if ( throwable instanceof SQLException )
{
return true;
}
else if ( throwable instanceof InvocationTargetException )
{
return true;
}
else if ( isThrowableNested() )
{
return true;
}
Class cls = throwable.getClass();
for ( int i = 0, isize = CAUSE_METHOD_NAMES.length; i < isize; i++ )
{
try
{
Method method = cls.getMethod( CAUSE_METHOD_NAMES[i], (Class[])null );
if ( method != null && Throwable.class.isAssignableFrom( method.getReturnType() ) )
{
return true;
}
}
catch ( NoSuchMethodException ignored )
{
}
catch ( SecurityException ignored )
{
}
}
try
{
Field field = cls.getField( "detail" );
if ( field != null )
{
return true;
}
}
catch ( NoSuchFieldException ignored )
{
}
catch ( SecurityException ignored )
{
}
return false;
}
// -----------------------------------------------------------------------
/**
* <p>
* Counts the number of <code>Throwable</code> objects in the exception
* chain.
* </p>
* <p/>
* <p>
* A throwable without cause will return <code>1</code>. A throwable with
* one cause will return <code>2</code> and so on. A <code>null</code>
* throwable will return <code>0</code>.
* </p>
*
* @param throwable
* the throwable to inspect, may be null
* @return the count of throwables, zero if null input
*/
public static int getThrowableCount( Throwable throwable )
{
int count = 0;
while ( throwable != null )
{
count++;
throwable = ExceptionUtils.getCause( throwable );
}
return count;
}
/**
* <p>
* Returns the list of <code>Throwable</code> objects in the exception
* chain.
* </p>
* <p/>
* <p>
* A throwable without cause will return an array containing one element -
* the input throwable. A throwable with one cause will return an array
* containing two elements. - the input throwable and the cause throwable. A
* <code>null</code> throwable will return an array size zero.
* </p>
*
* @param throwable
* the throwable to inspect, may be null
* @return the array of throwables, never null
*/
public static Throwable[] getThrowables( Throwable throwable )
{
List<Throwable> list = new ArrayList<Throwable>();
while ( throwable != null )
{
list.add( throwable );
throwable = ExceptionUtils.getCause( throwable );
}
return list.toArray( new Throwable[list.size()] );
}
// -----------------------------------------------------------------------
/**
* <p>
* Returns the (zero based) index of the first <code>Throwable</code> that
* matches the specified type in the exception chain.
* </p>
* <p/>
* <p>
* A <code>null</code> throwable returns <code>-1</code>. A
* <code>null</code> type returns <code>-1</code>. No match in the
* chain returns <code>-1</code>.
* </p>
*
* @param throwable
* the throwable to inspect, may be null
* @param type
* the type to search for
* @return the index into the throwable chain, -1 if no match or null input
*/
public static int indexOfThrowable( Throwable throwable, Class type )
{
return indexOfThrowable( throwable, type, 0 );
}
/**
* <p>
* Returns the (zero based) index of the first <code>Throwable</code> that
* matches the specified type in the exception chain from a specified index.
* </p>
* <p/>
* <p>
* A <code>null</code> throwable returns <code>-1</code>. A
* <code>null</code> type returns <code>-1</code>. No match in the
* chain returns <code>-1</code>. A negative start index is treated as
* zero. A start index greater than the number of throwables returns
* <code>-1</code>.
* </p>
*
* @param throwable
* the throwable to inspect, may be null
* @param type
* the type to search for
* @param fromIndex
* the (zero based) index of the starting position, negative
* treated as zero, larger than chain size returns -1
* @return the index into the throwable chain, -1 if no match or null input
*/
public static int indexOfThrowable( Throwable throwable, Class type, int fromIndex )
{
if ( throwable == null )
{
return -1;
}
if ( fromIndex < 0 )
{
fromIndex = 0;
}
Throwable[] throwables = ExceptionUtils.getThrowables( throwable );
if ( fromIndex >= throwables.length )
{
return -1;
}
for ( int i = fromIndex; i < throwables.length; i++ )
{
if ( throwables[i].getClass().equals( type ) )
{
return i;
}
}
return -1;
}
// -----------------------------------------------------------------------
/**
* <p>
* Prints a compact stack trace for the root cause of a throwable to
* <code>System.err</code>.
* </p>
* <p/>
* <p>
* The compact stack trace starts with the root cause and prints stack
* frames up to the place where it was caught and wrapped. Then it prints
* the wrapped exception and continues with stack frames until the wrapper
* exception is caught and wrapped again, etc.
* </p>
* <p/>
* <p>
* The method is equivalent to <code>printStackTrace</code> for throwables
* that don't have nested causes.
* </p>
*
* @param throwable
* the throwable to output
* @since 2.0
*/
public static void printRootCauseStackTrace( Throwable throwable )
{
printRootCauseStackTrace( throwable, System.err );
}
/**
* <p>
* Prints a compact stack trace for the root cause of a throwable.
* </p>
* <p/>
* <p>
* The compact stack trace starts with the root cause and prints stack
* frames up to the place where it was caught and wrapped. Then it prints
* the wrapped exception and continues with stack frames until the wrapper
* exception is caught and wrapped again, etc.
* </p>
* <p/>
* <p>
* The method is equivalent to <code>printStackTrace</code> for throwables
* that don't have nested causes.
* </p>
*
* @param throwable
* the throwable to output, may be null
* @param stream
* the stream to output to, may not be null
* @throws IllegalArgumentException
* if the stream is <code>null</code>
* @since 2.0
*/
public static void printRootCauseStackTrace( Throwable throwable, PrintStream stream )
{
if ( throwable == null )
{
return;
}
if ( stream == null )
{
throw new IllegalArgumentException( "The PrintStream must not be null" );
}
String trace[] = getRootCauseStackTrace( throwable );
for ( int i = 0; i < trace.length; i++ )
{
stream.println( trace[i] );
}
stream.flush();
}
/**
* <p>
* Prints a compact stack trace for the root cause of a throwable.
* </p>
* <p/>
* <p>
* The compact stack trace starts with the root cause and prints stack
* frames up to the place where it was caught and wrapped. Then it prints
* the wrapped exception and continues with stack frames until the wrapper
* exception is caught and wrapped again, etc.
* </p>
* <p/>
* <p>
* The method is equivalent to <code>printStackTrace</code> for throwables
* that don't have nested causes.
* </p>
*
* @param throwable
* the throwable to output, may be null
* @param writer
* the writer to output to, may not be null
* @throws IllegalArgumentException
* if the writer is <code>null</code>
* @since 2.0
*/
public static void printRootCauseStackTrace( Throwable throwable, PrintWriter writer )
{
if ( throwable == null )
{
return;
}
if ( writer == null )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_04356 ) );
}
String trace[] = getRootCauseStackTrace( throwable );
for ( int i = 0; i < trace.length; i++ )
{
writer.println( trace[i] );
}
writer.flush();
}
// -----------------------------------------------------------------------
/**
* <p>
* Creates a compact stack trace for the root cause of the supplied
* <code>Throwable</code>.
* </p>
*
* @param throwable
* the throwable to examine, may be null
* @return an array of stack trace frames, never null
* @since 2.0
*/
public static String[] getRootCauseStackTrace( Throwable throwable )
{
if ( throwable == null )
{
return ArrayUtils.EMPTY_STRING_ARRAY;
}
Throwable throwables[] = getThrowables( throwable );
int count = throwables.length;
List<String> frames = new ArrayList<String>();
List<String> nextTrace = getStackFrameList( throwables[count - 1] );
for ( int i = count; --i >= 0; )
{
List<String> trace = nextTrace;
if ( i != 0 )
{
nextTrace = getStackFrameList( throwables[i - 1] );
removeCommonFrames( trace, nextTrace );
}
if ( i == count - 1 )
{
frames.add( throwables[i].toString() );
}
else
{
frames.add( WRAPPED_MARKER + throwables[i].toString() );
}
for ( int j = 0; j < trace.size(); j++ )
{
frames.add( trace.get( j ) );
}
}
return frames.toArray( new String[0] );
}
/**
* <p>
* Removes common frames from the cause trace given the two stack traces.
* </p>
*
* @param causeFrames
* stack trace of a cause throwable
* @param wrapperFrames
* stack trace of a wrapper throwable
* @throws IllegalArgumentException
* if either argument is null
* @since 2.0
*/
public static void removeCommonFrames( List causeFrames, List wrapperFrames )
{
if ( causeFrames == null || wrapperFrames == null )
{
throw new IllegalArgumentException( I18n.err( I18n.ERR_04357 ) );
}
int causeFrameIndex = causeFrames.size() - 1;
int wrapperFrameIndex = wrapperFrames.size() - 1;
while ( causeFrameIndex >= 0 && wrapperFrameIndex >= 0 )
{
// Remove the frame from the cause trace if it is the same
// as in the wrapper trace
String causeFrame = ( String ) causeFrames.get( causeFrameIndex );
String wrapperFrame = ( String ) wrapperFrames.get( wrapperFrameIndex );
if ( causeFrame.equals( wrapperFrame ) )
{
causeFrames.remove( causeFrameIndex );
}
causeFrameIndex--;
wrapperFrameIndex--;
}
}
// -----------------------------------------------------------------------
/**
* <p>
* Gets the stack trace from a Throwable as a String.
* </p>
*
* @param throwable
* the <code>Throwable</code> to be examined
* @return the stack trace as generated by the exception's
* <code>printStackTrace(PrintWriter)</code> method
*/
public static String getStackTrace( Throwable throwable )
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter( sw, true );
throwable.printStackTrace( pw );
return sw.getBuffer().toString();
}
/**
* <p>
* A way to get the entire nested stack-trace of an throwable.
* </p>
*
* @param throwable
* the <code>Throwable</code> to be examined
* @return the nested stack trace, with the root cause first
* @since 2.0
*/
public static String getFullStackTrace( Throwable throwable )
{
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter( sw, true );
Throwable[] ts = getThrowables( throwable );
for ( int i = 0; i < ts.length; i++ )
{
ts[i].printStackTrace( pw );
if ( isNestedThrowable( ts[i] ) )
{
break;
}
}
return sw.getBuffer().toString();
}
// -----------------------------------------------------------------------
/**
* <p>
* Captures the stack trace associated with the specified
* <code>Throwable</code> object, decomposing it into a list of stack
* frames.
* </p>
*
* @param throwable
* the <code>Throwable</code> to examine, may be null
* @return an array of strings describing each stack frame, never null
*/
public static String[] getStackFrames( Throwable throwable )
{
if ( throwable == null )
{
return ArrayUtils.EMPTY_STRING_ARRAY;
}
return getStackFrames( getStackTrace( throwable ) );
}
/**
* <p>
* Functionality shared between the <code>getStackFrames(Throwable)</code>
* methods of this and the
*/
static String[] getStackFrames( String stackTrace )
{
String linebreak = SystemUtils.LINE_SEPARATOR;
StringTokenizer frames = new StringTokenizer( stackTrace, linebreak );
List<String> list = new LinkedList<String>();
while ( frames.hasMoreTokens() )
{
list.add( frames.nextToken() );
}
return list.toArray( new String[list.size()] );
}
/**
* <p>
* Produces a <code>List</code> of stack frames - the message is not
* included.
* </p>
* <p/>
* <p>
* This works in most cases - it will only fail if the exception message
* contains a line that starts with:
* <code>&quot;&nbsp;&nbsp;&nbsp;at&quot;.</code>
* </p>
*
* @param t
* is any throwable
* @return List of stack frames
*/
static List<String> getStackFrameList( Throwable t )
{
String stackTrace = getStackTrace( t );
String linebreak = SystemUtils.LINE_SEPARATOR;
StringTokenizer frames = new StringTokenizer( stackTrace, linebreak );
List<String> list = new LinkedList<String>();
boolean traceStarted = false;
while ( frames.hasMoreTokens() )
{
String token = frames.nextToken();
// Determine if the line starts with <whitespace>at
int at = token.indexOf( "at" );
if ( at != -1 && token.substring( 0, at ).trim().length() == 0 )
{
traceStarted = true;
list.add( token );
}
else if ( traceStarted )
{
break;
}
}
return list;
}
public static String printErrors( List<Throwable> errors )
{
StringBuilder sb = new StringBuilder();
for ( Throwable error:errors )
{
sb.append( "Error : " ).append( error.getMessage() ).append( "\n" );
}
return sb.toString();
}
}