001    /*
002     *  Licensed to the Apache Software Foundation (ASF) under one
003     *  or more contributor license agreements.  See the NOTICE file
004     *  distributed with this work for additional information
005     *  regarding copyright ownership.  The ASF licenses this file
006     *  to you under the Apache License, Version 2.0 (the
007     *  "License"); you may not use this file except in compliance
008     *  with the License.  You may obtain a copy of the License at
009     *  
010     *    http://www.apache.org/licenses/LICENSE-2.0
011     *  
012     *  Unless required by applicable law or agreed to in writing,
013     *  software distributed under the License is distributed on an
014     *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015     *  KIND, either express or implied.  See the License for the
016     *  specific language governing permissions and limitations
017     *  under the License. 
018     *  
019     */
020    package org.apache.directory.shared.ldap.util;
021    
022    
023    import java.io.PrintStream;
024    import java.io.PrintWriter;
025    import java.io.StringWriter;
026    import java.lang.reflect.Field;
027    import java.lang.reflect.InvocationTargetException;
028    import java.lang.reflect.Method;
029    import java.sql.SQLException;
030    import java.util.ArrayList;
031    import java.util.Arrays;
032    import java.util.LinkedList;
033    import java.util.List;
034    import java.util.StringTokenizer;
035    
036    
037    /**
038     * <p>
039     * Provides utilities for manipulating and examining <code>Throwable</code>
040     * objects.
041     * </p>
042     * 
043     * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
044     * @version $Rev: 494173 $
045     */
046    public class ExceptionUtils
047    {
048    
049        /**
050         * <p>
051         * Used when printing stack frames to denote the start of a wrapped
052         * exception.
053         * </p>
054         * <p/>
055         * <p>
056         * Package private for accessibility by test suite.
057         * </p>
058         */
059        static final String WRAPPED_MARKER = " [wrapped] ";
060    
061        /**
062         * <p>
063         * The names of methods commonly used to access a wrapped exception.
064         * </p>
065         */
066        private static String[] CAUSE_METHOD_NAMES =
067            { "getCause", "getNextException", "getTargetException", "getException", "getSourceException", "getRootCause",
068                "getCausedByException", "getNested", "getLinkedException", "getNestedException", "getLinkedCause",
069                "getThrowable", };
070    
071        /**
072         * <p>
073         * The Method object for JDK1.4 getCause.
074         * </p>
075         */
076        private static final Method THROWABLE_CAUSE_METHOD;
077    
078        static
079        {
080            Method getCauseMethod;
081            try
082            {
083                getCauseMethod = Throwable.class.getMethod( "getCause", (Class[])null );
084            }
085            catch ( Exception e )
086            {
087                getCauseMethod = null;
088            }
089            THROWABLE_CAUSE_METHOD = getCauseMethod;
090        }
091    
092    
093        /**
094         * <p>
095         * Public constructor allows an instance of <code>ExceptionUtils</code> to
096         * be created, although that is not normally necessary.
097         * </p>
098         */
099        public ExceptionUtils()
100        {
101        }
102    
103    
104        /**
105         * <p>
106         * Checks if a String is not empty ("") and not null.
107         * </p>
108         * 
109         * @param str
110         *            the String to check, may be null
111         * @return <code>true</code> if the String is not empty and not null
112         */
113        private static boolean isNotEmpty( String str )
114        {
115            return ( str != null && str.length() > 0 );
116        }
117    
118    
119        // -----------------------------------------------------------------------
120        /**
121         * <p>
122         * Adds to the list of method names used in the search for
123         * <code>Throwable</code> objects.
124         * </p>
125         * 
126         * @param methodName
127         *            the methodName to add to the list, <code>null</code> and
128         *            empty strings are ignored
129         * @since 2.0
130         */
131        public static void addCauseMethodName( String methodName )
132        {
133            if ( isNotEmpty( methodName ) )
134            {
135                List<String> list = new ArrayList<String>( Arrays.asList( CAUSE_METHOD_NAMES ) );
136                list.add( methodName );
137                CAUSE_METHOD_NAMES = list.toArray( new String[list.size()] );
138            }
139        }
140    
141    
142        /**
143         * <p>
144         * Introspects the <code>Throwable</code> to obtain the cause.
145         * </p>
146         * <p/>
147         * <p>
148         * The method searches for methods with specific names that return a
149         * <code>Throwable</code> object. This will pick up most wrapping
150         * exceptions, including those from JDK 1.4, and The method names can be
151         * added to using {@link #addCauseMethodName(String)}.
152         * </p>
153         * <p/>
154         * <p>
155         * The default list searched for are:
156         * </p>
157         * <ul>
158         * <li><code>getCause()</code></li>
159         * <li><code>getNextException()</code></li>
160         * <li><code>getTargetException()</code></li>
161         * <li><code>getException()</code></li>
162         * <li><code>getSourceException()</code></li>
163         * <li><code>getRootCause()</code></li>
164         * <li><code>getCausedByException()</code></li>
165         * <li><code>getNested()</code></li>
166         * </ul>
167         * <p/>
168         * <p>
169         * In the absence of any such method, the object is inspected for a
170         * <code>detail</code> field assignable to a <code>Throwable</code>.
171         * </p>
172         * <p/>
173         * <p>
174         * If none of the above is found, returns <code>null</code>.
175         * </p>
176         * 
177         * @param throwable
178         *            the throwable to introspect for a cause, may be null
179         * @return the cause of the <code>Throwable</code>, <code>null</code>
180         *         if none found or null throwable input
181         * @since 1.0
182         */
183        public static Throwable getCause( Throwable throwable )
184        {
185            return getCause( throwable, CAUSE_METHOD_NAMES );
186        }
187    
188    
189        /**
190         * <p>
191         * Introspects the <code>Throwable</code> to obtain the cause.
192         * </p>
193         * <p/>
194         * <ol>
195         * <li>Try known exception types.</li>
196         * <li>Try the supplied array of method names.</li>
197         * <li>Try the field 'detail'.</li>
198         * </ol>
199         * <p/>
200         * <p>
201         * A <code>null</code> set of method names means use the default set. A
202         * <code>null</code> in the set of method names will be ignored.
203         * </p>
204         * 
205         * @param throwable
206         *            the throwable to introspect for a cause, may be null
207         * @param methodNames
208         *            the method names, null treated as default set
209         * @return the cause of the <code>Throwable</code>, <code>null</code>
210         *         if none found or null throwable input
211         * @since 1.0
212         */
213        public static Throwable getCause( Throwable throwable, String[] methodNames )
214        {
215            if ( throwable == null )
216            {
217                return null;
218            }
219            Throwable cause = getCauseUsingWellKnownTypes( throwable );
220            if ( cause == null )
221            {
222                if ( methodNames == null )
223                {
224                    methodNames = CAUSE_METHOD_NAMES;
225                }
226                for ( int i = 0; i < methodNames.length; i++ )
227                {
228                    String methodName = methodNames[i];
229                    if ( methodName != null )
230                    {
231                        cause = getCauseUsingMethodName( throwable, methodName );
232                        if ( cause != null )
233                        {
234                            break;
235                        }
236                    }
237                }
238    
239                if ( cause == null )
240                {
241                    cause = getCauseUsingFieldName( throwable, "detail" );
242                }
243            }
244            return cause;
245        }
246    
247    
248        /**
249         * <p>
250         * Introspects the <code>Throwable</code> to obtain the root cause.
251         * </p>
252         * <p/>
253         * <p>
254         * This method walks through the exception chain to the last element, "root"
255         * of the tree, using {@link #getCause(Throwable)}, and returns that
256         * exception.
257         * </p>
258         * 
259         * @param throwable
260         *            the throwable to get the root cause for, may be null
261         * @return the root cause of the <code>Throwable</code>,
262         *         <code>null</code> if none found or null throwable input
263         */
264        public static Throwable getRootCause( Throwable throwable )
265        {
266            Throwable cause = getCause( throwable );
267            if ( cause != null )
268            {
269                throwable = cause;
270                while ( ( throwable = getCause( throwable ) ) != null )
271                {
272                    cause = throwable;
273                }
274            }
275            return cause;
276        }
277    
278    
279        /**
280         * <p>
281         * Finds a <code>Throwable</code> for known types.
282         * </p>
283         * <p/>
284         * <p>
285         * Uses <code>instanceof</code> checks to examine the exception, looking
286         * for well known types which could contain chained or wrapped exceptions.
287         * </p>
288         * 
289         * @param throwable
290         *            the exception to examine
291         * @return the wrapped exception, or <code>null</code> if not found
292         */
293        private static Throwable getCauseUsingWellKnownTypes( Throwable throwable )
294        {
295            if ( throwable instanceof Nestable )
296            {
297                return ( ( Nestable ) throwable ).getCause();
298            }
299            else if ( throwable instanceof SQLException )
300            {
301                return ( ( SQLException ) throwable ).getNextException();
302            }
303            else if ( throwable instanceof InvocationTargetException )
304            {
305                return ( ( InvocationTargetException ) throwable ).getTargetException();
306            }
307            else
308            {
309                return null;
310            }
311        }
312    
313    
314        /**
315         * <p>
316         * Finds a <code>Throwable</code> by method name.
317         * </p>
318         * 
319         * @param throwable
320         *            the exception to examine
321         * @param methodName
322         *            the name of the method to find and invoke
323         * @return the wrapped exception, or <code>null</code> if not found
324         */
325        private static Throwable getCauseUsingMethodName( Throwable throwable, String methodName )
326        {
327            Method method = null;
328            try
329            {
330                method = throwable.getClass().getMethod( methodName, (Class[])null );
331            }
332            catch ( NoSuchMethodException ignored )
333            {
334            }
335            catch ( SecurityException ignored )
336            {
337            }
338    
339            if ( method != null && Throwable.class.isAssignableFrom( method.getReturnType() ) )
340            {
341                try
342                {
343                    return ( Throwable ) method.invoke( throwable, ArrayUtils.EMPTY_OBJECT_ARRAY );
344                }
345                catch ( IllegalAccessException ignored )
346                {
347                }
348                catch ( IllegalArgumentException ignored )
349                {
350                }
351                catch ( InvocationTargetException ignored )
352                {
353                }
354            }
355            return null;
356        }
357    
358    
359        /**
360         * <p>
361         * Finds a <code>Throwable</code> by field name.
362         * </p>
363         * 
364         * @param throwable
365         *            the exception to examine
366         * @param fieldName
367         *            the name of the attribute to examine
368         * @return the wrapped exception, or <code>null</code> if not found
369         */
370        private static Throwable getCauseUsingFieldName( Throwable throwable, String fieldName )
371        {
372            Field field = null;
373            try
374            {
375                field = throwable.getClass().getField( fieldName );
376            }
377            catch ( NoSuchFieldException ignored )
378            {
379            }
380            catch ( SecurityException ignored )
381            {
382            }
383    
384            if ( field != null && Throwable.class.isAssignableFrom( field.getType() ) )
385            {
386                try
387                {
388                    return ( Throwable ) field.get( throwable );
389                }
390                catch ( IllegalAccessException ignored )
391                {
392                }
393                catch ( IllegalArgumentException ignored )
394                {
395                }
396            }
397            return null;
398        }
399    
400    
401        // -----------------------------------------------------------------------
402        /**
403         * <p>
404         * Checks if the Throwable class has a <code>getCause</code> method.
405         * </p>
406         * <p/>
407         * <p>
408         * This is true for JDK 1.4 and above.
409         * </p>
410         * 
411         * @return true if Throwable is nestable
412         * @since 2.0
413         */
414        public static boolean isThrowableNested()
415        {
416            return ( THROWABLE_CAUSE_METHOD != null );
417        }
418    
419    
420        /**
421         * <p>
422         * Checks whether this <code>Throwable</code> class can store a cause.
423         * </p>
424         * <p/>
425         * <p>
426         * This method does <b>not</b> check whether it actually does store a
427         * cause.
428         * <p>
429         * 
430         * @param throwable
431         *            the <code>Throwable</code> to examine, may be null
432         * @return boolean <code>true</code> if nested otherwise
433         *         <code>false</code>
434         * @since 2.0
435         */
436        public static boolean isNestedThrowable( Throwable throwable )
437        {
438            if ( throwable == null )
439            {
440                return false;
441            }
442    
443            if ( throwable instanceof Nestable )
444            {
445                return true;
446            }
447            else if ( throwable instanceof SQLException )
448            {
449                return true;
450            }
451            else if ( throwable instanceof InvocationTargetException )
452            {
453                return true;
454            }
455            else if ( isThrowableNested() )
456            {
457                return true;
458            }
459    
460            Class cls = throwable.getClass();
461            for ( int i = 0, isize = CAUSE_METHOD_NAMES.length; i < isize; i++ )
462            {
463                try
464                {
465                    Method method = cls.getMethod( CAUSE_METHOD_NAMES[i], (Class[])null );
466                    if ( method != null && Throwable.class.isAssignableFrom( method.getReturnType() ) )
467                    {
468                        return true;
469                    }
470                }
471                catch ( NoSuchMethodException ignored )
472                {
473                }
474                catch ( SecurityException ignored )
475                {
476                }
477            }
478    
479            try
480            {
481                Field field = cls.getField( "detail" );
482                if ( field != null )
483                {
484                    return true;
485                }
486            }
487            catch ( NoSuchFieldException ignored )
488            {
489            }
490            catch ( SecurityException ignored )
491            {
492            }
493    
494            return false;
495        }
496    
497    
498        // -----------------------------------------------------------------------
499        /**
500         * <p>
501         * Counts the number of <code>Throwable</code> objects in the exception
502         * chain.
503         * </p>
504         * <p/>
505         * <p>
506         * A throwable without cause will return <code>1</code>. A throwable with
507         * one cause will return <code>2</code> and so on. A <code>null</code>
508         * throwable will return <code>0</code>.
509         * </p>
510         * 
511         * @param throwable
512         *            the throwable to inspect, may be null
513         * @return the count of throwables, zero if null input
514         */
515        public static int getThrowableCount( Throwable throwable )
516        {
517            int count = 0;
518            while ( throwable != null )
519            {
520                count++;
521                throwable = ExceptionUtils.getCause( throwable );
522            }
523            return count;
524        }
525    
526    
527        /**
528         * <p>
529         * Returns the list of <code>Throwable</code> objects in the exception
530         * chain.
531         * </p>
532         * <p/>
533         * <p>
534         * A throwable without cause will return an array containing one element -
535         * the input throwable. A throwable with one cause will return an array
536         * containing two elements. - the input throwable and the cause throwable. A
537         * <code>null</code> throwable will return an array size zero.
538         * </p>
539         * 
540         * @param throwable
541         *            the throwable to inspect, may be null
542         * @return the array of throwables, never null
543         */
544        public static Throwable[] getThrowables( Throwable throwable )
545        {
546            List<Throwable> list = new ArrayList<Throwable>();
547            
548            while ( throwable != null )
549            {
550                list.add( throwable );
551                throwable = ExceptionUtils.getCause( throwable );
552            }
553            
554            return list.toArray( new Throwable[list.size()] );
555        }
556    
557    
558        // -----------------------------------------------------------------------
559        /**
560         * <p>
561         * Returns the (zero based) index of the first <code>Throwable</code> that
562         * matches the specified type in the exception chain.
563         * </p>
564         * <p/>
565         * <p>
566         * A <code>null</code> throwable returns <code>-1</code>. A
567         * <code>null</code> type returns <code>-1</code>. No match in the
568         * chain returns <code>-1</code>.
569         * </p>
570         * 
571         * @param throwable
572         *            the throwable to inspect, may be null
573         * @param type
574         *            the type to search for
575         * @return the index into the throwable chain, -1 if no match or null input
576         */
577        public static int indexOfThrowable( Throwable throwable, Class type )
578        {
579            return indexOfThrowable( throwable, type, 0 );
580        }
581    
582    
583        /**
584         * <p>
585         * Returns the (zero based) index of the first <code>Throwable</code> that
586         * matches the specified type in the exception chain from a specified index.
587         * </p>
588         * <p/>
589         * <p>
590         * A <code>null</code> throwable returns <code>-1</code>. A
591         * <code>null</code> type returns <code>-1</code>. No match in the
592         * chain returns <code>-1</code>. A negative start index is treated as
593         * zero. A start index greater than the number of throwables returns
594         * <code>-1</code>.
595         * </p>
596         * 
597         * @param throwable
598         *            the throwable to inspect, may be null
599         * @param type
600         *            the type to search for
601         * @param fromIndex
602         *            the (zero based) index of the starting position, negative
603         *            treated as zero, larger than chain size returns -1
604         * @return the index into the throwable chain, -1 if no match or null input
605         */
606        public static int indexOfThrowable( Throwable throwable, Class type, int fromIndex )
607        {
608            if ( throwable == null )
609            {
610                return -1;
611            }
612            if ( fromIndex < 0 )
613            {
614                fromIndex = 0;
615            }
616            Throwable[] throwables = ExceptionUtils.getThrowables( throwable );
617            if ( fromIndex >= throwables.length )
618            {
619                return -1;
620            }
621            for ( int i = fromIndex; i < throwables.length; i++ )
622            {
623                if ( throwables[i].getClass().equals( type ) )
624                {
625                    return i;
626                }
627            }
628            return -1;
629        }
630    
631    
632        // -----------------------------------------------------------------------
633        /**
634         * <p>
635         * Prints a compact stack trace for the root cause of a throwable to
636         * <code>System.err</code>.
637         * </p>
638         * <p/>
639         * <p>
640         * The compact stack trace starts with the root cause and prints stack
641         * frames up to the place where it was caught and wrapped. Then it prints
642         * the wrapped exception and continues with stack frames until the wrapper
643         * exception is caught and wrapped again, etc.
644         * </p>
645         * <p/>
646         * <p>
647         * The method is equivalent to <code>printStackTrace</code> for throwables
648         * that don't have nested causes.
649         * </p>
650         * 
651         * @param throwable
652         *            the throwable to output
653         * @since 2.0
654         */
655        public static void printRootCauseStackTrace( Throwable throwable )
656        {
657            printRootCauseStackTrace( throwable, System.err );
658        }
659    
660    
661        /**
662         * <p>
663         * Prints a compact stack trace for the root cause of a throwable.
664         * </p>
665         * <p/>
666         * <p>
667         * The compact stack trace starts with the root cause and prints stack
668         * frames up to the place where it was caught and wrapped. Then it prints
669         * the wrapped exception and continues with stack frames until the wrapper
670         * exception is caught and wrapped again, etc.
671         * </p>
672         * <p/>
673         * <p>
674         * The method is equivalent to <code>printStackTrace</code> for throwables
675         * that don't have nested causes.
676         * </p>
677         * 
678         * @param throwable
679         *            the throwable to output, may be null
680         * @param stream
681         *            the stream to output to, may not be null
682         * @throws IllegalArgumentException
683         *             if the stream is <code>null</code>
684         * @since 2.0
685         */
686        public static void printRootCauseStackTrace( Throwable throwable, PrintStream stream )
687        {
688            if ( throwable == null )
689            {
690                return;
691            }
692            if ( stream == null )
693            {
694                throw new IllegalArgumentException( "The PrintStream must not be null" );
695            }
696            String trace[] = getRootCauseStackTrace( throwable );
697            for ( int i = 0; i < trace.length; i++ )
698            {
699                stream.println( trace[i] );
700            }
701            stream.flush();
702        }
703    
704    
705        /**
706         * <p>
707         * Prints a compact stack trace for the root cause of a throwable.
708         * </p>
709         * <p/>
710         * <p>
711         * The compact stack trace starts with the root cause and prints stack
712         * frames up to the place where it was caught and wrapped. Then it prints
713         * the wrapped exception and continues with stack frames until the wrapper
714         * exception is caught and wrapped again, etc.
715         * </p>
716         * <p/>
717         * <p>
718         * The method is equivalent to <code>printStackTrace</code> for throwables
719         * that don't have nested causes.
720         * </p>
721         * 
722         * @param throwable
723         *            the throwable to output, may be null
724         * @param writer
725         *            the writer to output to, may not be null
726         * @throws IllegalArgumentException
727         *             if the writer is <code>null</code>
728         * @since 2.0
729         */
730        public static void printRootCauseStackTrace( Throwable throwable, PrintWriter writer )
731        {
732            if ( throwable == null )
733            {
734                return;
735            }
736            if ( writer == null )
737            {
738                throw new IllegalArgumentException( "The PrintWriter must not be null" );
739            }
740            String trace[] = getRootCauseStackTrace( throwable );
741            for ( int i = 0; i < trace.length; i++ )
742            {
743                writer.println( trace[i] );
744            }
745            writer.flush();
746        }
747    
748    
749        // -----------------------------------------------------------------------
750        /**
751         * <p>
752         * Creates a compact stack trace for the root cause of the supplied
753         * <code>Throwable</code>.
754         * </p>
755         * 
756         * @param throwable
757         *            the throwable to examine, may be null
758         * @return an array of stack trace frames, never null
759         * @since 2.0
760         */
761        public static String[] getRootCauseStackTrace( Throwable throwable )
762        {
763            if ( throwable == null )
764            {
765                return ArrayUtils.EMPTY_STRING_ARRAY;
766            }
767            
768            Throwable throwables[] = getThrowables( throwable );
769            int count = throwables.length;
770            List<String> frames = new ArrayList<String>();
771            List<String> nextTrace = getStackFrameList( throwables[count - 1] );
772            
773            for ( int i = count; --i >= 0; )
774            {
775                List<String> trace = nextTrace;
776                
777                if ( i != 0 )
778                {
779                    nextTrace = getStackFrameList( throwables[i - 1] );
780                    removeCommonFrames( trace, nextTrace );
781                }
782                if ( i == count - 1 )
783                {
784                    frames.add( throwables[i].toString() );
785                }
786                else
787                {
788                    frames.add( WRAPPED_MARKER + throwables[i].toString() );
789                }
790                for ( int j = 0; j < trace.size(); j++ )
791                {
792                    frames.add( trace.get( j ) );
793                }
794            }
795            return frames.toArray( new String[0] );
796        }
797    
798    
799        /**
800         * <p>
801         * Removes common frames from the cause trace given the two stack traces.
802         * </p>
803         * 
804         * @param causeFrames
805         *            stack trace of a cause throwable
806         * @param wrapperFrames
807         *            stack trace of a wrapper throwable
808         * @throws IllegalArgumentException
809         *             if either argument is null
810         * @since 2.0
811         */
812        public static void removeCommonFrames( List causeFrames, List wrapperFrames )
813        {
814            if ( causeFrames == null || wrapperFrames == null )
815            {
816                throw new IllegalArgumentException( "The List must not be null" );
817            }
818            int causeFrameIndex = causeFrames.size() - 1;
819            int wrapperFrameIndex = wrapperFrames.size() - 1;
820            while ( causeFrameIndex >= 0 && wrapperFrameIndex >= 0 )
821            {
822                // Remove the frame from the cause trace if it is the same
823                // as in the wrapper trace
824                String causeFrame = ( String ) causeFrames.get( causeFrameIndex );
825                String wrapperFrame = ( String ) wrapperFrames.get( wrapperFrameIndex );
826                if ( causeFrame.equals( wrapperFrame ) )
827                {
828                    causeFrames.remove( causeFrameIndex );
829                }
830                causeFrameIndex--;
831                wrapperFrameIndex--;
832            }
833        }
834    
835    
836        // -----------------------------------------------------------------------
837        /**
838         * <p>
839         * Gets the stack trace from a Throwable as a String.
840         * </p>
841         * 
842         * @param throwable
843         *            the <code>Throwable</code> to be examined
844         * @return the stack trace as generated by the exception's
845         *         <code>printStackTrace(PrintWriter)</code> method
846         */
847        public static String getStackTrace( Throwable throwable )
848        {
849            StringWriter sw = new StringWriter();
850            PrintWriter pw = new PrintWriter( sw, true );
851            throwable.printStackTrace( pw );
852            return sw.getBuffer().toString();
853        }
854    
855    
856        /**
857         * <p>
858         * A way to get the entire nested stack-trace of an throwable.
859         * </p>
860         * 
861         * @param throwable
862         *            the <code>Throwable</code> to be examined
863         * @return the nested stack trace, with the root cause first
864         * @since 2.0
865         */
866        public static String getFullStackTrace( Throwable throwable )
867        {
868            StringWriter sw = new StringWriter();
869            PrintWriter pw = new PrintWriter( sw, true );
870            Throwable[] ts = getThrowables( throwable );
871            for ( int i = 0; i < ts.length; i++ )
872            {
873                ts[i].printStackTrace( pw );
874                if ( isNestedThrowable( ts[i] ) )
875                {
876                    break;
877                }
878            }
879            return sw.getBuffer().toString();
880        }
881    
882    
883        // -----------------------------------------------------------------------
884        /**
885         * <p>
886         * Captures the stack trace associated with the specified
887         * <code>Throwable</code> object, decomposing it into a list of stack
888         * frames.
889         * </p>
890         * 
891         * @param throwable
892         *            the <code>Throwable</code> to examine, may be null
893         * @return an array of strings describing each stack frame, never null
894         */
895        public static String[] getStackFrames( Throwable throwable )
896        {
897            if ( throwable == null )
898            {
899                return ArrayUtils.EMPTY_STRING_ARRAY;
900            }
901            return getStackFrames( getStackTrace( throwable ) );
902        }
903    
904    
905        /**
906         * <p>
907         * Functionality shared between the <code>getStackFrames(Throwable)</code>
908         * methods of this and the
909         */
910        static String[] getStackFrames( String stackTrace )
911        {
912            String linebreak = SystemUtils.LINE_SEPARATOR;
913            StringTokenizer frames = new StringTokenizer( stackTrace, linebreak );
914            List<String> list = new LinkedList<String>();
915            
916            while ( frames.hasMoreTokens() )
917            {
918                list.add( frames.nextToken() );
919            }
920            
921            return list.toArray( new String[list.size()] );
922        }
923    
924    
925        /**
926         * <p>
927         * Produces a <code>List</code> of stack frames - the message is not
928         * included.
929         * </p>
930         * <p/>
931         * <p>
932         * This works in most cases - it will only fail if the exception message
933         * contains a line that starts with:
934         * <code>&quot;&nbsp;&nbsp;&nbsp;at&quot;.</code>
935         * </p>
936         * 
937         * @param t
938         *            is any throwable
939         * @return List of stack frames
940         */
941        static List<String> getStackFrameList( Throwable t )
942        {
943            String stackTrace = getStackTrace( t );
944            String linebreak = SystemUtils.LINE_SEPARATOR;
945            StringTokenizer frames = new StringTokenizer( stackTrace, linebreak );
946            List<String> list = new LinkedList<String>();
947            boolean traceStarted = false;
948            
949            while ( frames.hasMoreTokens() )
950            {
951                String token = frames.nextToken();
952                // Determine if the line starts with <whitespace>at
953                int at = token.indexOf( "at" );
954                
955                if ( at != -1 && token.substring( 0, at ).trim().length() == 0 )
956                {
957                    traceStarted = true;
958                    list.add( token );
959                }
960                else if ( traceStarted )
961                {
962                    break;
963                }
964            }
965            return list;
966        }
967    }