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
015package org.apache.tapestry.util.exception;
016
017import java.beans.BeanInfo;
018import java.beans.IntrospectionException;
019import java.beans.Introspector;
020import java.beans.PropertyDescriptor;
021import java.io.CharArrayWriter;
022import java.io.IOException;
023import java.io.LineNumberReader;
024import java.io.PrintStream;
025import java.io.PrintWriter;
026import java.io.StringReader;
027import java.lang.reflect.Method;
028import java.util.ArrayList;
029import 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
037public 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}