View Javadoc
1   package org.codehaus.plexus.util;
2   
3   /* ====================================================================
4    * The Apache Software License, Version 1.1
5    *
6    * Copyright (c) 2002-2003 The Apache Software Foundation.  All rights
7    * reserved.
8    *
9    * Redistribution and use in source and binary forms, with or without
10   * modification, are permitted provided that the following conditions
11   * are met:
12   *
13   * 1. Redistributions of source code must retain the above copyright
14   *    notice, this list of conditions and the following disclaimer.
15   *
16   * 2. Redistributions in binary form must reproduce the above copyright
17   *    notice, this list of conditions and the following disclaimer in
18   *    the documentation and/or other materials provided with the
19   *    distribution.
20   *
21   * 3. The end-user documentation included with the redistribution, if
22   *    any, must include the following acknowledgement:
23   *       "This product includes software developed by the
24   *        Apache Software Foundation (http://www.codehaus.org/)."
25   *    Alternately, this acknowledgement may appear in the software itself,
26   *    if and wherever such third-party acknowledgements normally appear.
27   *
28   * 4. The names "The Jakarta Project", "Commons", and "Apache Software
29   *    Foundation" must not be used to endorse or promote products derived
30   *    from this software without prior written permission. For written
31   *    permission, please contact codehaus@codehaus.org.
32   *
33   * 5. Products derived from this software may not be called "Apache"
34   *    nor may "Apache" appear in their names without prior written
35   *    permission of the Apache Software Foundation.
36   *
37   * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
38   * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
39   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
40   * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
41   * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
42   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
43   * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
44   * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
45   * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
46   * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
47   * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
48   * SUCH DAMAGE.
49   * ====================================================================
50   *
51   * This software consists of voluntary contributions made by many
52   * individuals on behalf of the Apache Software Foundation.  For more
53   * information on the Apache Software Foundation, please see
54   * <http://www.codehaus.org/>.
55   */
56  
57  import java.io.PrintStream;
58  import java.io.PrintWriter;
59  import java.io.StringWriter;
60  import java.lang.reflect.Field;
61  import java.lang.reflect.InvocationTargetException;
62  import java.lang.reflect.Method;
63  import java.sql.SQLException;
64  import java.util.ArrayList;
65  import java.util.Arrays;
66  import java.util.LinkedList;
67  import java.util.List;
68  import java.util.StringTokenizer;
69  
70  /**
71   * <p>
72   * <code>ExceptionUtils</code> provides utilities for manipulating <code>Throwable</code> objects.
73   * </p>
74   *
75   * @author <a href="mailto:dlr@finemaltcoding.com">Daniel Rall</a>
76   * @author Dmitri Plotnikov
77   * @author Stephen Colebourne
78   * @since 1.0
79   *
80   */
81  public class ExceptionUtils {
82      /**
83       * Used when printing stack frames to denote the start of a wrapped exception. Package private for accessibility by
84       * test suite.
85       */
86      static final String WRAPPED_MARKER = " [wrapped] ";
87  
88      /**
89       * The names of methods commonly used to access a wrapped exception.
90       */
91      protected static String[] CAUSE_METHOD_NAMES = {
92          "getCause",
93          "getNextException",
94          "getTargetException",
95          "getException",
96          "getSourceException",
97          "getRootCause",
98          "getCausedByException",
99          "getNested"
100     };
101 
102     /**
103      * Constructs a new <code>ExceptionUtils</code>. Protected to discourage instantiation.
104      */
105     protected ExceptionUtils() {}
106 
107     /**
108      * <p>
109      * Adds to the list of method names used in the search for <code>Throwable</code> objects.
110      * </p>
111      *
112      * @param methodName the methodName to add to the list, null and empty strings are ignored
113      */
114     public static void addCauseMethodName(String methodName) {
115         if (methodName != null && methodName.length() > 0) {
116             List<String> list = new ArrayList<String>(Arrays.asList(CAUSE_METHOD_NAMES));
117             list.add(methodName);
118             CAUSE_METHOD_NAMES = list.toArray(new String[0]);
119         }
120     }
121 
122     /**
123      * <p>
124      * Introspects the specified <code>Throwable</code> to obtain the cause.
125      * </p>
126      * <p>
127      * The method searches for methods with specific names that return a <code>Throwable</code> object. This will pick
128      * up most wrapping exceptions, including those from JDK 1.4, and The method names can be added to using
129      * {@link #addCauseMethodName(String)}. The default list searched for are:
130      * </p>
131      * <ul>
132      * <li><code>getCause()</code>
133      * <li><code>getNextException()</code>
134      * <li><code>getTargetException()</code>
135      * <li><code>getException()</code>
136      * <li><code>getSourceException()</code>
137      * <li><code>getRootCause()</code>
138      * <li><code>getCausedByException()</code>
139      * <li><code>getNested()</code>
140      * </ul>
141      * <p>
142      * In the absence of any such method, the object is inspected for a <code>detail</code> field assignable to a
143      * <code>Throwable</code>.
144      * </p>
145      * <p>
146      * If none of the above is found, returns <code>null</code>.
147      * </p>
148      *
149      * @param throwable The exception to introspect for a cause.
150      * @return The cause of the <code>Throwable</code>.
151      * @throws NullPointerException if the throwable is null
152      */
153     public static Throwable getCause(Throwable throwable) {
154         return getCause(throwable, CAUSE_METHOD_NAMES);
155     }
156 
157     /**
158      * <p>
159      * Introspects the specified <code>Throwable</code> to obtain the cause using a supplied array of method names.
160      * </p>
161      *
162      * @param throwable The exception to introspect for a cause.
163      * @param methodNames the methods names to match
164      * @return The cause of the <code>Throwable</code>.
165      * @throws NullPointerException if the method names array is null or contains null
166      * @throws NullPointerException if the throwable is null
167      */
168     public static Throwable getCause(Throwable throwable, String[] methodNames) {
169         Throwable cause = getCauseUsingWellKnownTypes(throwable);
170         if (cause == null) {
171             for (String methodName : methodNames) {
172                 cause = getCauseUsingMethodName(throwable, methodName);
173                 if (cause != null) {
174                     break;
175                 }
176             }
177 
178             if (cause == null) {
179                 cause = getCauseUsingFieldName(throwable, "detail");
180             }
181         }
182         return cause;
183     }
184 
185     /**
186      * <p>
187      * Walks through the exception chain to the last element -- the "root" of the tree -- using
188      * {@link #getCause(Throwable)}, and returns that exception.
189      * </p>
190      *
191      * @param throwable the throwable to get the root cause for
192      * @return The root cause of the <code>Throwable</code>.
193      */
194     public static Throwable getRootCause(Throwable throwable) {
195         Throwable cause = getCause(throwable);
196         if (cause != null) {
197             throwable = cause;
198             while ((throwable = getCause(throwable)) != null) {
199                 cause = throwable;
200             }
201         }
202         return cause;
203     }
204 
205     /**
206      * <p>
207      * Uses <code>instanceof</code> checks to examine the exception, looking for well known types which could contain
208      * chained or wrapped exceptions.
209      * </p>
210      *
211      * @param throwable the exception to examine
212      * @return The wrapped exception, or <code>null</code> if not found.
213      */
214     private static Throwable getCauseUsingWellKnownTypes(Throwable throwable) {
215         if (throwable instanceof SQLException) {
216             return ((SQLException) throwable).getNextException();
217         } else if (throwable instanceof InvocationTargetException) {
218             return ((InvocationTargetException) throwable).getTargetException();
219         } else {
220             return null;
221         }
222     }
223 
224     /**
225      * <p>
226      * Find a throwable by method name.
227      * </p>
228      *
229      * @param throwable the exception to examine
230      * @param methodName the name of the method to find and invoke
231      * @return The wrapped exception, or <code>null</code> if not found.
232      */
233     private static Throwable getCauseUsingMethodName(Throwable throwable, String methodName) {
234         Method method = null;
235         try {
236             method = throwable.getClass().getMethod(methodName, null);
237         } catch (NoSuchMethodException ignored) {
238         } catch (SecurityException ignored) {
239         }
240 
241         if (method != null && Throwable.class.isAssignableFrom(method.getReturnType())) {
242             try {
243                 return (Throwable) method.invoke(throwable, new Object[0]);
244             } catch (IllegalAccessException ignored) {
245             } catch (IllegalArgumentException ignored) {
246             } catch (InvocationTargetException ignored) {
247             }
248         }
249         return null;
250     }
251 
252     /**
253      * <p>
254      * Find a throwable by field name.
255      * </p>
256      *
257      * @param throwable the exception to examine
258      * @param fieldName the name of the attribute to examine
259      * @return The wrapped exception, or <code>null</code> if not found.
260      */
261     private static Throwable getCauseUsingFieldName(Throwable throwable, String fieldName) {
262         Field field = null;
263         try {
264             field = throwable.getClass().getField(fieldName);
265         } catch (NoSuchFieldException ignored) {
266         } catch (SecurityException ignored) {
267         }
268 
269         if (field != null && Throwable.class.isAssignableFrom(field.getType())) {
270             try {
271                 return (Throwable) field.get(throwable);
272             } catch (IllegalAccessException ignored) {
273             } catch (IllegalArgumentException ignored) {
274             }
275         }
276         return null;
277     }
278 
279     /**
280      * <p>
281      * Returns the number of <code>Throwable</code> objects in the exception chain.
282      * </p>
283      *
284      * @param throwable the exception to inspect
285      * @return The throwable count.
286      */
287     public static int getThrowableCount(Throwable throwable) {
288         // Count the number of throwables
289         int count = 0;
290         while (throwable != null) {
291             count++;
292             throwable = ExceptionUtils.getCause(throwable);
293         }
294         return count;
295     }
296 
297     /**
298      * <p>
299      * Returns the list of <code>Throwable</code> objects in the exception chain.
300      * </p>
301      *
302      * @param throwable the exception to inspect
303      * @return The list of <code>Throwable</code> objects.
304      */
305     public static Throwable[] getThrowables(Throwable throwable) {
306         List<Throwable> list = new ArrayList<>();
307         while (throwable != null) {
308             list.add(throwable);
309             throwable = getCause(throwable);
310         }
311         return list.toArray(new Throwable[0]);
312     }
313 
314     /**
315      * <p>
316      * Delegates to {@link #indexOfThrowable(Throwable, Class, int)}, starting the search at the beginning of the
317      * exception chain.
318      * </p>
319      * @param throwable the exception to inspect
320      * @param type <code>Class</code> to look for
321      * @return index of the stack matching the type
322      * @see #indexOfThrowable(Throwable, Class, int)
323      */
324     public static int indexOfThrowable(Throwable throwable, Class type) {
325         return indexOfThrowable(throwable, type, 0);
326     }
327 
328     /**
329      * <p>
330      * Returns the (zero based) index, of the first <code>Throwable</code> that matches the specified type in the
331      * exception chain of <code>Throwable</code> objects with an index greater than or equal to the specified index, or
332      * <code>-1</code> if the type is not found.
333      * </p>
334      *
335      * @param throwable the exception to inspect
336      * @param type <code>Class</code> to look for
337      * @param fromIndex the (zero based) index of the starting position in the chain to be searched
338      * @return the first occurrence of the type in the chain, or <code>-1</code> if the type is not found
339      * @throws IndexOutOfBoundsException If the <code>fromIndex</code> argument is negative or not less than the count
340      *             of <code>Throwable</code>s in the chain.
341      */
342     public static int indexOfThrowable(Throwable throwable, Class type, int fromIndex) {
343         if (fromIndex < 0) {
344             throw new IndexOutOfBoundsException("Throwable index out of range: " + fromIndex);
345         }
346         Throwable[] throwables = ExceptionUtils.getThrowables(throwable);
347         if (fromIndex >= throwables.length) {
348             throw new IndexOutOfBoundsException("Throwable index out of range: " + fromIndex);
349         }
350         for (int i = fromIndex; i < throwables.length; i++) {
351             if (throwables[i].getClass().equals(type)) {
352                 return i;
353             }
354         }
355         return -1;
356     }
357 
358     /**
359      * Prints a compact stack trace for the root cause of a throwable. The compact stack trace starts with the root
360      * cause and prints stack frames up to the place where it was caught and wrapped. Then it prints the wrapped
361      * exception and continues with stack frames until the wrapper exception is caught and wrapped again, etc.
362      * <p>
363      * The method is equivalent to t.printStackTrace() for throwables that don't have nested causes.
364      * @param t the exception
365      * @param stream the stream
366      */
367     public static void printRootCauseStackTrace(Throwable t, PrintStream stream) {
368         String trace[] = getRootCauseStackTrace(t);
369         for (String aTrace : trace) {
370             stream.println(aTrace);
371         }
372         stream.flush();
373     }
374 
375     /**
376      * Equivalent to printRootCauseStackTrace(t, System.err)
377      * @param t the exception
378      */
379     public static void printRootCauseStackTrace(Throwable t) {
380         printRootCauseStackTrace(t, System.err);
381     }
382 
383     /**
384      * Same as printRootCauseStackTrace(t, stream), except it takes a PrintWriter as an argument.
385      * @param t the cause
386      * @param writer the writer
387      */
388     public static void printRootCauseStackTrace(Throwable t, PrintWriter writer) {
389         String trace[] = getRootCauseStackTrace(t);
390         for (String aTrace : trace) {
391             writer.println(aTrace);
392         }
393         writer.flush();
394     }
395 
396     /**
397      * Creates a compact stack trace for the root cause of the supplied throwable. See
398      * <code>printRootCauseStackTrace(Throwable t, PrintStream s)</code>
399      * @param t the cause
400      * @return the Stack
401      */
402     public static String[] getRootCauseStackTrace(Throwable t) {
403         Throwable[] throwables = getThrowables(t);
404         int count = throwables.length;
405         ArrayList<String> frames = new ArrayList<>();
406         List<String> nextTrace = getStackFrameList(throwables[count - 1]);
407         for (int i = count; --i >= 0; ) {
408             List<String> trace = nextTrace;
409             if (i != 0) {
410                 nextTrace = getStackFrameList(throwables[i - 1]);
411                 removeCommonFrames(trace, nextTrace);
412             }
413             if (i == (count - 1)) {
414                 frames.add(throwables[i].toString());
415             } else {
416                 frames.add(WRAPPED_MARKER + throwables[i].toString());
417             }
418             for (String aTrace : trace) {
419                 frames.add(aTrace);
420             }
421         }
422         return frames.toArray(new String[0]);
423     }
424 
425     /**
426      * Given two stack traces, removes common frames from the cause trace.
427      *
428      * @param causeFrames stack trace of a cause throwable
429      * @param wrapperFrames stack trace of a wrapper throwable
430      */
431     private static void removeCommonFrames(List<String> causeFrames, List<String> wrapperFrames) {
432         int causeFrameIndex = causeFrames.size() - 1;
433         int wrapperFrameIndex = wrapperFrames.size() - 1;
434         while (causeFrameIndex >= 0 && wrapperFrameIndex >= 0) {
435             // Remove the frame from the cause trace if it is the same
436             // as in the wrapper trace
437             String causeFrame = causeFrames.get(causeFrameIndex);
438             String wrapperFrame = wrapperFrames.get(wrapperFrameIndex);
439             if (causeFrame.equals(wrapperFrame)) {
440                 causeFrames.remove(causeFrameIndex);
441             }
442             causeFrameIndex--;
443             wrapperFrameIndex--;
444         }
445     }
446 
447     /**
448      * A convenient way of extracting the stack trace from an exception.
449      *
450      * @param t The <code>Throwable</code>.
451      * @return The stack trace as generated by the exception's <code>printStackTrace(PrintWriter)</code> method.
452      */
453     public static String getStackTrace(Throwable t) {
454         StringWriter sw = new StringWriter();
455         PrintWriter pw = new PrintWriter(sw, true);
456         t.printStackTrace(pw);
457         return sw.getBuffer().toString();
458     }
459 
460     /**
461      * A way to get the entire nested stack-trace of an throwable.
462      *
463      * @param t The <code>Throwable</code>.
464      * @return The nested stack trace, with the root cause first.
465      */
466     public static String getFullStackTrace(Throwable t) {
467         StringWriter sw = new StringWriter();
468         PrintWriter pw = new PrintWriter(sw, true);
469         Throwable[] ts = getThrowables(t);
470         for (Throwable t1 : ts) {
471             t1.printStackTrace(pw);
472             if (isNestedThrowable(t1)) {
473                 break;
474             }
475         }
476         return sw.getBuffer().toString();
477     }
478 
479     /**
480      * Whether an Throwable is considered nested or not.
481      *
482      * @param throwable The <code>Throwable</code>.
483      * @return boolean true/false
484      */
485     public static boolean isNestedThrowable(Throwable throwable) {
486         if (throwable == null) {
487             return false;
488         }
489 
490         if (throwable instanceof SQLException) {
491             return true;
492         } else if (throwable instanceof InvocationTargetException) {
493             return true;
494         }
495 
496         for (String CAUSE_METHOD_NAME : CAUSE_METHOD_NAMES) {
497             try {
498                 Method method = throwable.getClass().getMethod(CAUSE_METHOD_NAME, null);
499                 if (method != null) {
500                     return true;
501                 }
502             } catch (NoSuchMethodException ignored) {
503             } catch (SecurityException ignored) {
504             }
505         }
506 
507         try {
508             Field field = throwable.getClass().getField("detail");
509             if (field != null) {
510                 return true;
511             }
512         } catch (NoSuchFieldException ignored) {
513         } catch (SecurityException ignored) {
514         }
515 
516         return false;
517     }
518 
519     /**
520      * Captures the stack trace associated with the specified <code>Throwable</code> object, decomposing it into a list
521      * of stack frames.
522      *
523      * @param t The <code>Throwable</code>.
524      * @return An array of strings describing each stack frame.
525      */
526     public static String[] getStackFrames(Throwable t) {
527         return getStackFrames(getStackTrace(t));
528     }
529 
530     /**
531      * Functionality shared between the <code>getStackFrames(Throwable)</code> methods of this and the classes.
532      */
533     static String[] getStackFrames(String stackTrace) {
534         String linebreak = System.getProperty("line.separator");
535         StringTokenizer frames = new StringTokenizer(stackTrace, linebreak);
536         List<String> list = new LinkedList<String>();
537         while (frames.hasMoreTokens()) {
538             list.add(frames.nextToken());
539         }
540         return list.toArray(new String[0]);
541     }
542 
543     /**
544      * Produces a List of stack frames - the message is not included. This works in most cases - it will only fail if
545      * the exception message contains a line that starts with: " at".
546      *
547      * @param t is any throwable
548      * @return List of stack frames
549      */
550     static List<String> getStackFrameList(Throwable t) {
551         String stackTrace = getStackTrace(t);
552         String linebreak = System.getProperty("line.separator");
553         StringTokenizer frames = new StringTokenizer(stackTrace, linebreak);
554         List<String> list = new LinkedList<String>();
555         boolean traceStarted = false;
556         while (frames.hasMoreTokens()) {
557             String token = frames.nextToken();
558             // Determine if the line starts with <whitespace>at
559             int at = token.indexOf("at");
560             if (at != -1 && token.substring(0, at).trim().length() == 0) {
561                 traceStarted = true;
562                 list.add(token);
563             } else if (traceStarted) {
564                 break;
565             }
566         }
567         return list;
568     }
569 }