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