001 // Copyright 2004, 2005 The Apache Software Foundation
002 //
003 // Licensed under the Apache License, Version 2.0 (the "License");
004 // you may not use this file except in compliance with the License.
005 // You may obtain a copy of the License at
006 //
007 // http://www.apache.org/licenses/LICENSE-2.0
008 //
009 // Unless required by applicable law or agreed to in writing, software
010 // distributed under the License is distributed on an "AS IS" BASIS,
011 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012 // See the License for the specific language governing permissions and
013 // limitations under the License.
014
015 package org.apache.tapestry.util.exception;
016
017 import java.beans.BeanInfo;
018 import java.beans.IntrospectionException;
019 import java.beans.Introspector;
020 import java.beans.PropertyDescriptor;
021 import java.io.*;
022 import java.lang.reflect.Method;
023 import java.util.ArrayList;
024 import java.util.List;
025
026 /**
027 * Analyzes an exception, creating one or more {@link ExceptionDescription}s from it.
028 *
029 * @author Howard Lewis Ship
030 */
031
032 public class ExceptionAnalyzer
033 {
034 private static final int SKIP_LEADING_WHITESPACE = 0;
035
036 private static final int SKIP_T = 1;
037
038 private static final int SKIP_OTHER_WHITESPACE = 2;
039
040 private final List exceptionDescriptions = new ArrayList();
041
042 private final List propertyDescriptions = new ArrayList();
043
044 private final CharArrayWriter writer = new CharArrayWriter();
045
046 private boolean exhaustive = false;
047
048 /**
049 * If true, then stack trace is extracted for each exception. If false, the default, then stack
050 * trace is extracted for only the deepest exception.
051 */
052
053 public boolean isExhaustive()
054 {
055 return exhaustive;
056 }
057
058 public void setExhaustive(boolean value)
059 {
060 exhaustive = value;
061 }
062
063 /**
064 * Analyzes the exceptions. This builds an {@link ExceptionDescription}for the exception. It
065 * also looks for a non-null {@link Throwable}property. If one exists, then a second
066 * {@link ExceptionDescription}is created. This continues until no more nested exceptions can
067 * be found.
068 * <p>
069 * The description includes a set of name/value properties (as {@link ExceptionProperty})
070 * object. This list contains all non-null properties that are not, themselves,
071 * {@link Throwable}.
072 * <p>
073 * The name is the display name (not the logical name) of the property. The value is the
074 * <code>toString()</code> value of the property. Only properties defined in subclasses of
075 * {@link Throwable}are included.
076 * <p>
077 * A future enhancement will be to alphabetically sort the properties by name.
078 */
079
080 public ExceptionDescription[] analyze(Throwable exception)
081 {
082 Throwable thrown = exception;
083 try
084 {
085 while (thrown != null)
086 {
087 thrown = buildDescription(thrown);
088 }
089
090 ExceptionDescription[] result = new ExceptionDescription[exceptionDescriptions.size()];
091
092 return (ExceptionDescription[]) exceptionDescriptions.toArray(result);
093 }
094 finally
095 {
096 exceptionDescriptions.clear();
097 propertyDescriptions.clear();
098
099 writer.reset();
100 }
101 }
102
103 protected Throwable buildDescription(Throwable exception)
104 {
105 BeanInfo info;
106 Class exceptionClass;
107 ExceptionProperty property;
108 PropertyDescriptor[] descriptors;
109 PropertyDescriptor descriptor;
110 Throwable next = null;
111 int i;
112 Object value;
113 Method method;
114 ExceptionProperty[] properties;
115 ExceptionDescription description;
116 String stringValue;
117 String message;
118 String[] stackTrace = null;
119
120 propertyDescriptions.clear();
121
122 message = exception.getMessage();
123 exceptionClass = exception.getClass();
124
125 // Get properties, ignoring those in Throwable and higher
126 // (including the 'message' property).
127
128 try
129 {
130 info = Introspector.getBeanInfo(exceptionClass, Throwable.class);
131 }
132 catch (IntrospectionException e)
133 {
134 return null;
135 }
136
137 descriptors = info.getPropertyDescriptors();
138
139 for (i = 0; i < descriptors.length; i++)
140 {
141 descriptor = descriptors[i];
142
143 method = descriptor.getReadMethod();
144 if (method == null)
145 continue;
146
147 try
148 {
149 value = method.invoke(exception, null);
150 }
151 catch (Exception e)
152 {
153 continue;
154 }
155
156 if (value == null)
157 continue;
158
159 // Some annoying exceptions duplicate the message property
160 // (I'm talking to YOU SAXParseException), so just edit that out.
161
162 if (message != null && message.equals(value))
163 continue;
164
165 // Skip Throwables ... but the first non-null found is the next
166 // exception (unless it refers to the current one - some 3rd party
167 // libaries do this). We kind of count on there being no more
168 // than one Throwable property per Exception.
169
170 if (value instanceof Throwable)
171 {
172 if (next == null && value != exception)
173 next = (Throwable) value;
174
175 continue;
176 }
177
178 stringValue = value.toString().trim();
179
180 if (stringValue.length() == 0)
181 continue;
182
183 property = new ExceptionProperty(descriptor.getDisplayName(), value);
184
185 propertyDescriptions.add(property);
186 }
187
188 // If exhaustive, or in the deepest exception (where there's no next)
189 // the extract the stack trace.
190
191 if (next == null || exhaustive)
192 stackTrace = getStackTrace(exception);
193
194 // Would be nice to sort the properties here.
195
196 properties = new ExceptionProperty[propertyDescriptions.size()];
197
198 ExceptionProperty[] propArray = (ExceptionProperty[]) propertyDescriptions.toArray(properties);
199
200 description = new ExceptionDescription(exceptionClass.getName(), message, propArray, stackTrace);
201
202 exceptionDescriptions.add(description);
203
204 return next;
205 }
206
207 /**
208 * Gets the stack trace for the exception, and converts it into an array of strings.
209 * <p>
210 * This involves parsing the string generated indirectly from
211 * <code>Throwable.printStackTrace(PrintWriter)</code>. This method can get confused if the
212 * message (presumably, the first line emitted by printStackTrace()) spans multiple lines.
213 * <p>
214 * Different JVMs format the exception in different ways.
215 * <p>
216 * A possible expansion would be more flexibility in defining the pattern used. Hopefully all
217 * 'mainstream' JVMs are close enough for this to continue working.
218 */
219
220 protected String[] getStackTrace(Throwable exception)
221 {
222 writer.reset();
223
224 PrintWriter printWriter = new PrintWriter(writer);
225
226 exception.printStackTrace(printWriter);
227
228 printWriter.close();
229
230 String fullTrace = writer.toString();
231
232 writer.reset();
233
234 // OK, the trick is to convert the full trace into an array of stack frames.
235
236 StringReader stringReader = new StringReader(fullTrace);
237 LineNumberReader lineReader = new LineNumberReader(stringReader);
238 int lineNumber = 0;
239 List frames = new ArrayList();
240
241 try
242 {
243 while (true)
244 {
245 String line = lineReader.readLine();
246
247 if (line == null)
248 break;
249
250 // Always ignore the first line.
251
252 if (++lineNumber == 1)
253 continue;
254
255 frames.add(stripFrame(line));
256 }
257
258 lineReader.close();
259 }
260 catch (IOException ex)
261 {
262 // Not likely to happen with this particular set
263 // of readers.
264 }
265
266 String[] result = new String[frames.size()];
267
268 return (String[]) frames.toArray(result);
269 }
270
271 /**
272 * Sun's JVM prefixes each line in the stack trace with " <tab>at</tab> ", other JVMs don't. This
273 * method looks for and strips such stuff.
274 */
275
276 private String stripFrame(String frame)
277 {
278 char[] array = frame.toCharArray();
279
280 int i = 0;
281 int state = SKIP_LEADING_WHITESPACE;
282 boolean more = true;
283
284 while (more)
285 {
286 // Ran out of characters to skip? Return the empty string.
287
288 if (i == array.length)
289 return "";
290
291 char ch = array[i];
292
293 switch (state)
294 {
295 // Ignore whitespace at the start of the line.
296
297 case SKIP_LEADING_WHITESPACE:
298
299 if (Character.isWhitespace(ch))
300 {
301 i++;
302 continue;
303 }
304
305 if (ch == 'a')
306 {
307 state = SKIP_T;
308 i++;
309 continue;
310 }
311
312 // Found non-whitespace, not 'a'
313 more = false;
314 break;
315
316 // Skip over the 't' after an 'a'
317
318 case SKIP_T:
319
320 if (ch == 't')
321 {
322 state = SKIP_OTHER_WHITESPACE;
323 i++;
324 continue;
325 }
326
327 // Back out the skipped-over 'a'
328
329 i--;
330 more = false;
331 break;
332
333 // Skip whitespace between 'at' and the name of the class
334
335 case SKIP_OTHER_WHITESPACE:
336
337 if (Character.isWhitespace(ch))
338 {
339 i++;
340 continue;
341 }
342
343 // Not whitespace
344 more = false;
345 break;
346 }
347
348 }
349
350 // Found nothing to strip out.
351
352 if (i == 0)
353 return frame;
354
355 return frame.substring(i);
356 }
357
358 /**
359 * Produces a text based exception report to the provided stream.
360 */
361
362 public void reportException(Throwable exception, PrintStream stream)
363 {
364 int i;
365 int j;
366 ExceptionDescription[] descriptions;
367 ExceptionProperty[] properties;
368 String[] stackTrace;
369 String message;
370
371 descriptions = analyze(exception);
372
373 for (i = 0; i < descriptions.length; i++)
374 {
375 message = descriptions[i].getMessage();
376
377 if (message == null)
378 stream.println(descriptions[i].getExceptionClassName());
379 else
380 stream.println(descriptions[i].getExceptionClassName() + ": "
381 + descriptions[i].getMessage());
382
383 properties = descriptions[i].getProperties();
384
385 for (j = 0; j < properties.length; j++)
386 stream.println(" " + properties[j].getName() + ": " + properties[j].getValue());
387
388 // Just show the stack trace on the deepest exception.
389
390 if (i + 1 == descriptions.length)
391 {
392 stackTrace = descriptions[i].getStackTrace();
393
394 for (j = 0; j < stackTrace.length; j++)
395 stream.println(stackTrace[j]);
396 }
397 else
398 {
399 stream.println();
400 }
401 }
402 }
403
404 }