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 }