View Javadoc

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