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    }