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 }