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    }