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