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    }