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