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