001    package echopoint.util.throwable;
002    /*
003     * This file is part of the Echo Point Project.  This project is a collection
004     * of Components that have extended the Echo Web Application Framework.
005     *
006     * Version: MPL 1.1/GPL 2.0/LGPL 2.1
007     *
008     * The contents of this file are subject to the Mozilla Public License Version
009     * 1.1 (the "License"); you may not use this file except in compliance with
010     * the License. You may obtain a copy of the License at
011     * http://www.mozilla.org/MPL/
012     *
013     * Software distributed under the License is distributed on an "AS IS" basis,
014     * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
015     * for the specific language governing rights and limitations under the
016     * License.
017     *
018     * Alternatively, the contents of this file may be used under the terms of
019     * either the GNU General Public License Version 2 or later (the "GPL"), or
020     * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
021     * in which case the provisions of the GPL or the LGPL are applicable instead
022     * of those above. If you wish to allow use of your version of this file only
023     * under the terms of either the GPL or the LGPL, and not to allow others to
024     * use your version of this file under the terms of the MPL, indicate your
025     * decision by deleting the provisions above and replace them with the notice
026     * and other provisions required by the GPL or the LGPL. If you do not delete
027     * the provisions above, a recipient may use your version of this file under
028     * the terms of any one of the MPL, the GPL or the LGPL.
029     */
030    
031    import echopoint.util.ReflectionKit;
032    
033    import java.io.BufferedReader;
034    import java.io.CharArrayWriter;
035    import java.io.IOException;
036    import java.io.PrintStream;
037    import java.io.PrintWriter;
038    import java.io.StringReader;
039    import java.lang.reflect.Constructor;
040    import java.lang.reflect.InvocationTargetException;
041    import java.lang.reflect.Method;
042    import java.lang.reflect.Modifier;
043    import java.util.ArrayList;
044    import java.util.HashMap;
045    import java.util.List;
046    import java.util.Map;
047    
048    
049    /**
050     * <code>ThrowableKit</code> provides helper methods for handling Exceptions
051     * in Echo web applications.  It is expecially useful for allowing JDK 1.4
052     * exception chaining while still retaining a JDK 1.3 compliant code base.
053     */
054    public class ThrowableKit
055    {
056      /** not instantiable */
057      private ThrowableKit()
058      {
059      }
060    
061      /**
062       * This method creates a Throwable derived object that will attach the root
063       * cause if you are running under JDK 1.4 or later.
064       * <p/>
065       * This method makes it easier in dumps that find the root cause of a
066       * problem.
067       * <p/>
068       * If the contructor(String,Throwable) is not available (JDK 1.3 or below)
069       * then the message + " : " + cause.toString() will be used and the root
070       * cause Throwable stack trace will not be avalable.
071       * <p/>
072       * This method allows you to use the new 1.4 exception chaining if
073       * possible.
074       *
075       * @param throwableClass - must be a class derived from Throwable.class
076       * @param message - a custom error message, can be null.
077       * @param cause - the root cause Throwable
078       * @return a new RuntimeException
079       */
080      public static Throwable makeThrowable( Class throwableClass, String message, Throwable cause )
081      {
082        if ( throwableClass == null || !( Throwable.class.isAssignableFrom( throwableClass ) ) )
083        // for gods sake get it right will you
084        {
085          throw new IllegalArgumentException( "The class you provided :" + throwableClass + " is not derived from Throwable.class" );
086        }
087    
088        // we need to use relection to find out if we have the
089        // RuntimeException(String,Throwable) contructor available
090        try
091        {
092          Constructor c = throwableClass.getDeclaredConstructor( String.class, Throwable.class );
093          //
094          // if we have that contructor then we must have the get/setStackTrace method as well
095          Object t = c.newInstance( message, cause );
096          return (Throwable) t;
097        }
098        catch ( Exception e )
099        {
100          // no good.  Must be JDK 1.4
101        }
102    
103        try
104        {
105          String messageText;
106          // no good, just make it into a message : cause.toString()
107          if ( message == null )
108          {
109            messageText = cause.toString();
110          }
111          else
112          {
113            messageText = message + " : " + cause.toString();
114          }
115    
116          Constructor c = throwableClass.getDeclaredConstructor( String.class );
117          Object t = c.newInstance( messageText );
118          return (Throwable) t;
119        }
120        catch ( Exception e )
121        {
122          throw new IllegalStateException( "A constructor(String) was not available for class :" + throwableClass );
123        }
124      }
125    
126      /**
127       * Short hand method for ThrowableKit.makeThrowable(throwableClass,
128       * null,cause);
129       *
130       * @param throwableClass - must be a class derived from Throwable.class
131       * @param cause - the root cause Throwable
132       * @return a new RuntimeException
133       * @see ThrowableKit#makeThrowable(Class, String, Throwable)
134       */
135      public static Throwable makeThrowable( Class throwableClass, Throwable cause )
136      {
137        return makeThrowable( throwableClass, null, cause );
138      }
139    
140      /**
141       * This method creates a RuntimeException that will attach the root cause if
142       * you are running under JDK 1.4 or later.  This makes it easier in
143       * Exception dumps that find the root cause of a problem.  If the
144       * RuntimeException(String,Throwable) contructor is not available (JDK 1.3
145       * or below) then the message + " : " + cause.toString() will be used and
146       * the root cause Throwable stack trace will not be avalable.
147       * <p/>
148       * This method allows you to use the new 1.4 exception chaining if
149       * possible.
150       *
151       * @param message - a custom error message, can be null.
152       * @param cause - the root cause Throwable
153       * @return a new RuntimeException
154       */
155      public static RuntimeException makeRuntimeException( String message, Throwable cause )
156      {
157        return (RuntimeException) makeThrowable( RuntimeException.class, message, cause );
158      }
159    
160      /**
161       * A short cut method for ThrowableKit.makeRuntimeException(null,cause);
162       *
163       * @param cause the root cause Throwablew to attach
164       * @return the target Throwable t
165       * @see ThrowableKit#makeRuntimeException(String, Throwable)
166       */
167      public static RuntimeException makeRuntimeException( Throwable cause )
168      {
169        return makeRuntimeException( null, cause );
170      }
171    
172      /**
173       * This method attempts to attach the cause Throwable to the provided
174       * Throwable.  It emuluates the Throwable.initCause(Throwable) method found
175       * in JDK 1.4.
176       * <p/>
177       * If this cant be done then nothing is attached, such as when you are
178       * running under JDK 1.3
179       * <p/>
180       * <p>This method can be called at most once on an Throwable.  It is
181       * generally called from within the constructor, or immediately after
182       * creating the throwable.  If this throwable was created with {@link
183       * Throwable#Throwable(Throwable)} or {@link Throwable#Throwable(String,Throwable)},
184       * this method has no effect since the cause has already been set.
185       *
186       * @param throwable the target Throwable
187       * @param cause the root cause Throwablew to attach
188       * @return the target Throwable t
189       */
190      public static Throwable attachCause( Throwable throwable, Throwable cause )
191      {
192        try
193        {
194          Method m = Throwable.class.getDeclaredMethod( "initCause", Throwable.class );
195          throwable = (Throwable) m.invoke( throwable, cause );
196        }
197        catch ( SecurityException e ) { /* */ }
198        catch ( IllegalArgumentException e ) { /* */ }
199        catch ( IllegalAccessException e ) { /* */ }
200        catch ( IllegalStateException e ) { /* */ }
201        catch ( InvocationTargetException e ) { /* */ }
202        catch ( NoSuchMethodException e ) { /* */ }
203        // if we got here then we could not init the Throwable.  Ohh well we tried
204        return throwable;
205      }
206    
207      /**
208       * Short hand method for ThrowableKit.attachCause(throwable, cause);
209       *
210       * @param throwable the target Throwable
211       * @param cause the root cause Throwablew to attach
212       * @return the target Throwable t
213       * @see ThrowableKit#attachCause(Throwable, Throwable)
214       * @see Throwable#initCause(java.lang.Throwable)
215       */
216      public static Throwable initCause( Throwable throwable, Throwable cause )
217      {
218        return attachCause( throwable, cause );
219      }
220    
221    
222      /**
223       * This method will examine a Throwable and return a ThrowableDescriptor
224       * that can then be used to output error details.
225       * <p/>
226       * This works equally well on JDK 1.3 as it does on JDK 1.4.
227       * <p/>
228       * Any circular references in the Exception chaining will be removed.
229       *
230       * @param throwable - the Throwable to examine
231       * @return a ThrowableDescriptor that describes the Throwable
232       */
233      public static ThrowableDescriptor describeThrowable( Throwable throwable )
234      {
235    
236        // we keep track of throwables we have already seen so as
237        // we dont get into q circular reference loop
238        Map seenThrowables = new HashMap();
239    
240        ThrowableDescriptor descriptor = new ThrowableDescriptor( throwable );
241        seenThrowables.put( throwable, throwable );
242    
243        examineThrowable( descriptor, throwable, seenThrowables );
244    
245        return descriptor;
246      }
247    
248      /*
249             * Examines a throwable, finding all its child properties as well
250             * as root causes as well as filling the stack trace
251             */
252      private static void examineThrowable( ThrowableDescriptor descriptor, Throwable throwable, Map seenThrowables )
253      {
254        Method[] properties = ReflectionKit.getAllDeclaredMethods( throwable.getClass() );
255        List propertyList = new ArrayList( properties.length );
256        List causeList = new ArrayList( properties.length );
257        String throwableMessage = throwable.getMessage();
258    
259        for ( Method getter : properties )
260        {
261          if ( !ReflectionKit.isGetter( getter ) )
262          {
263            continue;
264          }
265    
266          String name;
267          Class type;
268          Object value;
269          int modifiers = getter.getModifiers();
270          try
271          {
272            //
273            // try and get access to non public methods
274            getter.setAccessible( true );
275            value = getter.invoke( throwable );
276            type = getter.getReturnType();
277            name = ReflectionKit.decapitalize( getter.getName() );
278            if ( name.equals( "class" ) ||
279                name.equals( "message" ) ||
280                name.equals( "ourStackTrace" ) ||
281                name.equals( "stackTraceDepth" ) ||
282                name.equals( "stackTrace" ) )
283            {
284              continue;
285            }
286          }
287          catch ( IllegalArgumentException e1 )
288          {
289            continue;
290          }
291          catch ( SecurityException e1 )
292          {
293            continue;
294          }
295          catch ( IllegalAccessException e1 )
296          {
297            continue;
298          }
299          catch ( InvocationTargetException e1 )
300          {
301            continue;
302          }
303          if ( Throwable.class.isAssignableFrom( type ) )
304          {
305            if ( value == null )
306            {
307              continue;
308            }
309    
310            // its a Throwable, so lets examine it
311            Throwable child = (Throwable) value;
312            //
313            // if we have seen it before we dont want a circular
314            // reference and hence an infinite sized tree graph
315            //
316            if ( seenThrowables.containsKey( child ) )
317            {
318              continue;
319            }
320            seenThrowables.put( child, child );
321    
322            ThrowableDescriptor childDesc = new ThrowableDescriptor( child );
323            examineThrowable( childDesc, child, seenThrowables );
324    
325            causeList.add( childDesc );
326          }
327          else
328          {
329            if ( value == throwableMessage )
330            {
331              continue;
332            }
333            ThrowablePropertyDescriptor propertyDesc = new ThrowablePropertyDescriptor( type, name, value, modifiers );
334            propertyList.add( propertyDesc );
335          }
336        }
337        descriptor.setCauses( (ThrowableDescriptor[]) causeList.toArray( new ThrowableDescriptor[causeList.size()] ) );
338        descriptor.setProperties( (ThrowablePropertyDescriptor[]) propertyList.toArray( new ThrowablePropertyDescriptor[propertyList.size()] ) );
339        descriptor.setStackTrace( examineStackTrace( throwable ) );
340      }
341    
342      /*
343             * Examines the stack trace of the Throwable and converts it
344             * into an array of Strings.
345             */
346      private static String[] examineStackTrace( Throwable parentThrowable )
347      {
348        CharArrayWriter caw = new CharArrayWriter();
349        PrintWriter pw = new PrintWriter( caw );
350        parentThrowable.printStackTrace( pw );
351    
352        List stackTraceList = new ArrayList();
353        StringReader sr = new StringReader( caw.toString() );
354        BufferedReader reader = new BufferedReader( sr );
355        boolean atStart = true;
356        while ( true )
357        {
358          String line = null;
359          try
360          {
361            line = reader.readLine();
362          }
363          catch ( IOException e ) { /* */ }
364          if ( line == null )
365          {
366            break;
367          }
368          if ( line.startsWith( "Caused by" ) )
369          {
370            break;
371          }
372          // skip the non stack frame bit at the start
373          if ( atStart )
374          {
375            atStart = false;
376            continue;
377          }
378          stackTraceList.add( line );
379        }
380        return (String[]) stackTraceList.toArray( new String[stackTraceList.size()] );
381      }
382    
383    
384      /**
385       * Prints a textual description of a Throwable and its properties, causes
386       * and stack traces.
387       *
388       * @param throwable - the Throwable in question
389       * @param out - the PrintStream to write to
390       * @param indent - control whether tab indents will be used
391       */
392      public static void printThrowableDescription( Throwable throwable, PrintStream out, boolean indent )
393      {
394        if ( throwable == null || out == null )
395        {
396          return;
397        }
398        ThrowableDescriptor desc = describeThrowable( throwable );
399        printThrowableDescription0( desc, out, -1, indent );
400      }
401    
402      /**
403       * Shorthand method for ThrowableKit.printThrowableDescription(throwable,out,true);
404       *
405       * @see ThrowableKit#printThrowableDescription(Throwable, PrintStream,
406       *      boolean)
407       */
408      public static void printThrowableDescription( Throwable throwable, PrintStream out )
409      {
410        printThrowableDescription( throwable, out, true );
411      }
412    
413      /*
414             * Does the printing of the Throwable details
415             */
416      private static void printThrowableDescription0( ThrowableDescriptor desc, PrintStream out, int tabLevel, boolean indent )
417      {
418        if ( indent )
419        {
420          tabLevel++;
421        }
422    
423        boolean doneHeader = false;
424        printTabs( out, tabLevel );
425        out.print( desc.getType().getName() );
426        out.print( " : " );
427        out.print( desc.getMessage() );
428        out.println();
429    
430        ThrowablePropertyDescriptor properties[] = desc.getProperties();
431        for ( ThrowablePropertyDescriptor property : properties )
432        {
433          if ( !doneHeader )
434          {
435            doneHeader = true;
436            printTabs( out, tabLevel );
437            out.println( "Properties : " );
438          }
439    
440          printTabs( out, tabLevel );
441          out.print( '\t' );
442          out.print( property.getName() );
443          out.print( " : " );
444          out.print( property.getValueAsString() );
445          out.print( " (" );
446          out.print( property.getType().getName() );
447          out.print( " " );
448          String modifiers = Modifier.toString( property.getModifiers() );
449          out.print( modifiers == null || modifiers.length() == 0 ? "default" : modifiers );
450          out.print( ")" );
451          out.println();
452        }
453    
454        doneHeader = false;
455        String[] stackTrace = desc.getStackTrace();
456        for ( String aStackTrace : stackTrace )
457        {
458          if ( !doneHeader )
459          {
460            doneHeader = true;
461            printTabs( out, tabLevel );
462            out.println( "Stack Trace : " );
463          }
464          printTabs( out, tabLevel );
465          out.println( aStackTrace );
466        }
467    
468        doneHeader = false;
469        ThrowableDescriptor causes[] = desc.getCauses();
470        for ( ThrowableDescriptor cause : causes )
471        {
472          if ( !doneHeader )
473          {
474            doneHeader = true;
475            printTabs( out, tabLevel );
476            out.println( "Caused By : " );
477          }
478          printThrowableDescription0( cause, out, tabLevel, indent );
479        }
480      }
481    
482      /*
483             * Print tabs!
484             */
485      private static void printTabs( PrintStream out, int tabLevel )
486      {
487        for ( int i = 0; i < tabLevel; i++ )
488          out.print( "\t" );
489      }
490    }
491