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