001package com.randomnoun.common;
002
003/* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
004 * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
005 */
006
007import java.io.*;
008import java.lang.reflect.*;
009import java.net.URLDecoder;
010import java.nio.charset.Charset;
011import java.util.ArrayList;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015import java.util.Properties;
016import java.util.zip.ZipEntry;
017import java.util.zip.ZipInputStream;
018
019import org.apache.log4j.Logger;
020
021/**
022 * Exception utilities class.
023 *
024 * <p>This class contains static utility methods for handling and manipulating exceptions;
025 * the only one you're likely to call being 
026 * {@link #getStackTraceWithRevisions(Throwable, ClassLoader, int, String)},
027 * which extracts CVS revision information from classes to produce an annotated (and highlighted)
028 * stack trace.
029 * 
030 * <p>When passing in {@link java.lang.ClassLoader}s to these methods, you may want to try one of 
031 * <ul>
032 * <li>this.getClass().getClassLoader()
033 * <li>Thread.currentThread().getContextClassLoader()
034 * </ul>
035 * 
036 * @see <a href="http://www.randomnoun.com/wp/2012/12/17/marginally-better-stack-traces/">http://www.randomnoun.com/wp/2012/12/17/marginally-better-stack-traces/</a>
037 * @author knoxg
038 */
039public class ExceptionUtil {
040    
041
042    /** Perform no stack trace element highlighting */
043    public static final int HIGHLIGHT_NONE = 0;
044    
045    /** Allow stack trace elements to be highlighted, as text */
046    public static final int HIGHLIGHT_TEXT = 1;
047    
048    /** Allow stack trace elements to be highlighted, as bold HTML */
049    public static final int HIGHLIGHT_HTML = 2;
050
051    /** Allow stack trace elements to be highlighted, with span'ed CSS elements */
052    public static final int HIGHLIGHT_HTML_WITH_CSS = 3;
053
054    /** The number of characters of the git revision to include in non-CSS stack traces (from the left side of the String) */
055    public static final int NUM_CHARACTERS_GIT_REVISION = 8;
056    
057    static Logger logger = Logger.getLogger(ExceptionUtil.class);
058    
059    /**
060     * Private constructor to prevent instantiation of this class
061     */
062    private ExceptionUtil() {
063    }
064
065    /**
066     * Converts an exception's stack trace to a string. If the exception passed
067     * to this function is null, returns the empty string.
068     *
069     * @param e exception
070     * 
071     * @return string representation of the exception's stack trace
072     */
073    public static String getStackTrace(Throwable e) {
074        if (e == null) {
075            return "";
076        }
077        StringWriter writer = new StringWriter();
078        e.printStackTrace(new PrintWriter(writer));
079        return writer.toString();
080    }
081
082    /**
083     * Converts an exception's stack trace to a string. Each stack trace element
084     * is annotated to include the CVS revision Id, if it contains a public static
085     * String element containing this information. 
086     * 
087     * <p>Stack trace elements whose classes begin with the specified highlightPrefix
088     * are also marked, to allow easier debugging. Highlights used can be text
089     * (which will insert the string "=&gt;" before relevent stack trace elements), or 
090     * HTML (which will render the stack trace element between &lt;b&gt; and &lt;/b&gt; tags.
091     * 
092     * <p>If HTML highlighting is enabled, then the exception message is also HTML-escaped.
093     *
094     * @param e exception
095     * @param loader ClassLoader used to read stack trace element revision information
096     * @param highlight One of the HIGHLIGHT_* constants in this class
097     * @param highlightPrefix A prefix used to determine which stack trace elements are
098     *   rendered as being 'important'. (e.g. "<code>com.randomnoun.common.</code>"). Multiple
099     *   prefixes can be specified, if separated by commas.   
100     * 
101     * @return string representation of the exception's stack trace
102     */
103    public static String getStackTraceWithRevisions(Throwable e, ClassLoader loader, int highlight, String highlightPrefix) {
104        if (e == null) {
105            return "(null)";
106        }
107        StringBuffer sb = new StringBuffer();
108        String additionalMessageSource = null;
109        String additionalMessage = null; // because some Exceptions are <strike>retarded</strike> of questionable quality
110
111        // was replacing 'e' here with e.getTarget() if e was a bsh.TargetError, 
112        // but should probably just tack this onto the .getCause() chain stead
113                
114        try {
115                if (e.getClass().getName().equals("com.spotify.docker.client.exceptions.DockerRequestException")) {
116                        Method m;
117                                m = e.getClass().getMethod("message");
118                                additionalMessageSource = "DockerRequestException.message()=";
119                                additionalMessage = (String) m.invoke(e);
120                                if (additionalMessage!=null) { additionalMessage = additionalMessage.trim(); }
121                }
122                } catch (SecurityException e1) {
123                        // ignore - just use original exception
124                } catch (NoSuchMethodException e1) {
125                        // ignore - just use original exception
126                } catch (IllegalArgumentException e1) {
127                        // ignore - just use original exception
128                } catch (IllegalAccessException e1) {
129                        // ignore - just use original exception
130                } catch (InvocationTargetException e1) {
131                        // ignore - just use original exception
132                }
133        
134        String s = e.getClass().getName();
135        String message = e.getLocalizedMessage();
136        if (highlight==HIGHLIGHT_HTML || highlight==HIGHLIGHT_HTML_WITH_CSS) {
137                sb.append(escapeHtml((message != null) ? (s + ": " + message) : s ));
138        } else {
139                sb.append((message != null) ? (s + ": " + message) : s );
140        }
141        
142        if (additionalMessage!=null) {
143                sb.append('\n');
144            if (highlight==HIGHLIGHT_HTML || highlight==HIGHLIGHT_HTML_WITH_CSS) {
145                sb.append(escapeHtml(additionalMessageSource));
146                sb.append(escapeHtml(additionalMessage));
147            } else {
148                sb.append(additionalMessageSource);
149                sb.append(additionalMessage);
150            }
151        }
152        sb.append('\n');
153        
154        // dump the stack trace for the top-level exception
155        StackTraceElement[] trace = null;
156        trace = e.getStackTrace();
157        
158        Map<String, JarMetadata> jarMetadataCache = new HashMap<String, JarMetadata>();
159        
160        for (int i=0; i < trace.length; i++) {
161            sb.append(getStackTraceElementWithRevision(jarMetadataCache, trace[i], loader, highlight, highlightPrefix) + "\n");
162        }
163        Throwable cause = getCause(e);
164        if (cause != null) {
165            sb.append(getStackTraceWithRevisionsAsCause(jarMetadataCache, cause, trace, loader, highlight, highlightPrefix));
166        }
167        return sb.toString();
168    }
169
170    /** Returns the 'Caused by...' exception text for a chained exception, performing the
171     * same stack trace element reduction as performed by the built-in {@link java.lang.Throwable#printStackTrace()}
172     * class.
173     * 
174     * <p>Note that the notion of 'Suppressed' exceptions introduced in Java 7 is not 
175     * supported by this implementation.
176     *  
177     * @param e the cause of the original exception
178     * @param causedTrace the original exception trace
179     * @param loader ClassLoader used to read stack trace element revision information
180     * @param highlight One of the HIGHLIGHT_* constants in this class
181     * @param highlightPrefix A prefix used to determine which stack trace elements are
182     *   rendered as being 'important'. (e.g. "<tt>com.randomnoun.common.</tt>"). Multiple
183     *   prefixes can be specified, if separated by commas.
184     *   
185     * @return the 'caused by' component of a stack trace
186     */
187    private static String getStackTraceWithRevisionsAsCause(Map<String, JarMetadata> jarMetadataCache, Throwable e, StackTraceElement[] causedTrace, ClassLoader loader, int highlight, String highlightPrefix) {
188        
189        StringBuffer sb = new StringBuffer();
190        
191        // Compute number of frames in common between this and caused
192        StackTraceElement[] trace = e.getStackTrace();
193        int m = trace.length-1;
194        int n = causedTrace.length-1;
195        while (m >= 0 && n >=0 && trace[m].equals(causedTrace[n])) {
196            m--; n--;
197        }
198        int framesInCommon = trace.length - 1 - m;
199
200        String s = e.getClass().getName();
201        String message = e.getLocalizedMessage();
202        if (message==null) {  // org.json.simple.parser.ParseException doesn't set message 
203                message = e.toString(); 
204                if (s.equals(message)) { message = null; }
205        } 
206        sb.append("Caused by: ");
207        if (highlight==HIGHLIGHT_HTML || highlight==HIGHLIGHT_HTML_WITH_CSS) {
208                sb.append(escapeHtml((message != null) ? (s + ": " + message) : s ));
209        } else {
210                sb.append((message != null) ? (s + ": " + message) : s );
211        }
212        sb.append("\n");
213        
214        for (int i=0; i <= m; i++) {
215            sb.append(getStackTraceElementWithRevision(jarMetadataCache, trace[i], loader, highlight, highlightPrefix) + "\n");
216        }
217        if (framesInCommon != 0)
218            sb.append("\t... " + framesInCommon + " more\n");
219
220        // Recurse if we have a cause
221        Throwable ourCause = getCause(e);
222        if (ourCause != null) {
223            sb.append(getStackTraceWithRevisionsAsCause(jarMetadataCache, ourCause, trace, loader, highlight, highlightPrefix));
224        }
225        return sb.toString();
226    }
227    
228    /** Returns a single stack trace element as a String, with highlighting
229     * 
230     * @param jarMetadataCache jar-level metadata cached during the traversal of the stackTraceElements.
231     * @param ste the StackTraceElement
232     * @param loader ClassLoader used to read stack trace element revision information
233     * @param highlight One of the HIGHLIGHT_* constants in this class
234     * @param highlightPrefix A prefix used to determine which stack trace elements are
235     *   rendered as being 'important'. (e.g. "<tt>com.randomnoun.common.</tt>"). Multiple
236     *   prefixes can be specified, separated by commas.
237     *   
238     * @return the stack trace element as a String, with highlighting
239     */
240    private static String getStackTraceElementWithRevision(Map<String, JarMetadata> jarMetadataCache, 
241        StackTraceElement ste, ClassLoader loader,
242        int highlight, String highlightPrefix) 
243    {
244        // s should be something like:
245        // jakarta.servlet.http.HttpServlet.service(HttpServlet.java:740) or
246        // java.net.Socket.<init>(Socket.java:425), which should be HTML escaped
247        String s;
248        if (highlightPrefix==null || highlight==HIGHLIGHT_NONE) {
249                s = "    at " + ste.toString();
250        } else {
251                boolean isHighlighted = (highlight!=HIGHLIGHT_NONE && isHighlighted(ste.getClassName(), highlightPrefix));
252                if (isHighlighted) {
253                        if (highlight==HIGHLIGHT_HTML) {
254                            s = "    at <b>" + escapeHtml(ste.toString()) + "</b>";  
255                        } else if (isHighlighted && highlight==HIGHLIGHT_HTML_WITH_CSS) {
256                                s = "    at <span class=\"stackTrace-highlight\">" + escapeHtml(ste.toString()) + "</span>";
257                        } else if (isHighlighted && highlight==HIGHLIGHT_TEXT) {
258                            s = " => at " + ste.toString();
259                        } else { 
260                            throw new IllegalArgumentException("Unknown highlight " + highlight);
261                        }
262                } else { // !isHighlighted
263                        if (highlight==HIGHLIGHT_HTML || highlight==HIGHLIGHT_HTML_WITH_CSS) {
264                                s = "    at " + escapeHtml(ste.toString());
265                        } else {
266                                s = "    at " + ste.toString();
267                        }
268                } 
269        }
270        
271        int openBracketPos = s.lastIndexOf("(");
272        int closeBracketPos = s.lastIndexOf(")");
273        if (openBracketPos!=-1 && closeBracketPos!=-1) {
274            try {
275                // add maven, git and cvs revision info
276                String className = ste.getClassName();
277                JarMetadata jarMetadata = getJarMetadata(jarMetadataCache, loader, className);
278                String revision = getClassRevision(loader, className);
279                if (revision != null) {
280                        if (highlight==HIGHLIGHT_HTML_WITH_CSS) {
281                                s = s.substring(0, closeBracketPos) + "<span class=\"stackTrace-revision\">" + revision + "</span> " + s.substring(closeBracketPos);
282                        } else {
283                                s = s.substring(0, closeBracketPos) + ", " + revision + s.substring(closeBracketPos);
284                        }
285                }
286                if (jarMetadata != null) {
287                        if (jarMetadata.groupId!=null && jarMetadata.artifactId!=null) {
288                                if (highlight==HIGHLIGHT_HTML_WITH_CSS) {
289                                        s = s.substring(0, openBracketPos + 1) + "<span class=\"stackTrace-maven\">" + jarMetadata.groupId + ":" + jarMetadata.artifactId + ":" + jarMetadata.version + "</span> " + s.substring(openBracketPos + 1);
290                                } else {
291                                        s = s.substring(0, openBracketPos + 1) + jarMetadata.groupId + ":" + jarMetadata.artifactId + ":" + jarMetadata.version + ", " + s.substring(openBracketPos + 1);
292                                }
293                        }
294                        if (jarMetadata.gitRevision!=null) {
295                                if (highlight==HIGHLIGHT_HTML_WITH_CSS) {
296                                        s = s.substring(0, openBracketPos + 1) + "<span class=\"stackTrace-git\">" + jarMetadata.gitRevision + "</span> " + s.substring(openBracketPos + 1);
297                                } else {
298                                        s = s.substring(0, openBracketPos + 1) + jarMetadata.gitRevision.substring(0, NUM_CHARACTERS_GIT_REVISION) + ", " + s.substring(openBracketPos + 1);
299                                }
300                        }
301                }
302            } catch (Exception e2) {
303                // logger.error(e2);
304            } catch (NoClassDefFoundError ncdfe) {
305            }
306        }
307        return s;
308    }
309    
310    /** Returns true if the provided className matches the highlightPrefix pattern supplied, 
311     * false otherwise
312     *  
313     * @param className The name of a class (i.e. the class contained in a stack trace element)
314     * @param highlightPrefix A prefix used to determine which stack trace elements are
315     *   rendered as being 'important'. (e.g. "<tt>com.randomnoun.common.</tt>"). Multiple
316     *   prefixes can be specified, separated by commas.
317     *   
318     * @return true if the provided className matches the highlightPrefix pattern supplied, 
319     *   false otherwise
320     */
321    private static boolean isHighlighted(String className, String highlightPrefix) {
322        if (highlightPrefix.contains(",")) {
323                String[] prefixes = highlightPrefix.split(",");
324                boolean highlighted = false; 
325                for (int i=0; i<prefixes.length && !highlighted; i++) {
326                        highlighted = className.startsWith(prefixes[i]);
327                }
328                return highlighted;
329        } else {
330                return className.startsWith(highlightPrefix);
331        }
332    }
333
334    /** Return the number of elements in the current thread's stack trace (i.e. the height
335     * of the call stack). 
336     * 
337     * @return the height of the call stack 
338     */
339    public static int getStackDepth() {
340        Throwable t = new Throwable();
341        return t.getStackTrace().length;
342    }
343    
344    /** Jar-level metadata is cached for the duration of a single ExceptionUtils method call */
345    private static class JarMetadata {
346        String gitRevision;
347        String groupId;
348        String artifactId;
349        String version;
350    }
351    
352    
353    /** Returns the JAR metadata (including the git revision and mvn groupId:artifactId co-ordinates) of the JAR containing a class. 
354     * 
355     * @param loader The classLoader to use
356     * @param className The class we wish to retrieve
357     * 
358     * @return A JarMetadata class, or null
359     * 
360     * @throws ClassNotFoundException
361     * @throws SecurityException
362     * @throws NoSuchFieldException
363     * @throws IllegalArgumentException
364     * @throws IllegalAccessException
365     */    
366    private static JarMetadata getJarMetadata(Map<String, JarMetadata> jarMetadataCache, ClassLoader loader, String className) 
367                throws ClassNotFoundException, SecurityException, NoSuchFieldException, 
368                IllegalArgumentException, IllegalAccessException 
369    {
370        if (className.indexOf('$')!=-1) {
371            className = className.substring(0, className.indexOf('$'));
372        }
373        
374        Class<?> clazz = Class.forName(className, true, loader);
375        
376        // this is stored in the build.properties file, which we'll have to get from the JAR containing the class
377        // see http://stackoverflow.com/questions/1983839/determine-which-jar-file-a-class-is-from
378        int revisionMethod = 0;
379        
380        String uri = clazz.getResource('/' + clazz.getName().replace('.', '/') + ".class").toString();
381        if (uri.startsWith("file:")) {
382                // this happens in eclipse; e.g. 
383                //   uri=file:/C:/Users/knoxg/workspace-4.4.1-sts-3.6.2/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/jacobi-web/WEB-INF/classes/com/jacobistrategies/web/struts/Struts1Shim.class
384                // use the current class loader's build.properties if available
385                revisionMethod = 1;
386        } else if (uri.startsWith("jar:file:")) {
387                revisionMethod = 2;
388        } else {
389            // int idx = uri.indexOf(':');
390            // String protocol = idx == -1 ? "(unknown)" : uri.substring(0, idx);
391            // logger.warn("unknown protocol " + protocol + " in classpath uri '" + uri + "'");
392            revisionMethod = -1;
393        }
394
395        JarMetadata md = null;
396        if (revisionMethod == 1) {
397                // get the revision from the supplied class loader's build.properties, if there is one
398                md = new JarMetadata();
399                InputStream is = loader.getResourceAsStream("/build.properties");
400                if (is==null) {
401                        // logger.warn("missing build.properties");
402                        return null;
403                } else {
404                        Properties props = new Properties();
405                        try {
406                                props.load(is);
407                                md.gitRevision = props.getProperty("git.buildNumber");
408                                if ("${buildNumber}".equals(md.gitRevision)) { md.gitRevision = null; }
409                        } catch (IOException ioe) {
410                                // ignore;
411                        }
412                }
413                
414        } else if (revisionMethod == 2) {
415                // get the revision from the containing JAR
416                
417                int idx = uri.indexOf('!');
418                //As far as I know, the if statement below can't ever trigger, so it's more of a sanity check thing.
419                if (idx == -1) {
420                        // logger.warn("unparseable classpath uri '" + uri + "'");
421                        return null;
422                }
423                String fileName;
424                try {
425                    fileName = URLDecoder.decode(uri.substring("jar:file:".length(), idx), Charset.defaultCharset().name());
426                } catch (UnsupportedEncodingException e) {
427                    throw new IllegalStateException("default charset doesn't exist. Your VM is borked.");
428                }
429                
430                // use the cache if it exists
431                if (jarMetadataCache!=null) {
432                        md = jarMetadataCache.get(fileName);
433                }
434                if (md==null) {
435                        md = new JarMetadata();
436                        jarMetadataCache.put(fileName, md);
437                        File f = new File(fileName); // .getAbsolutePath();
438                        
439                        // get the build.properties from this JAR
440                        // logger.debug("Getting build.properties for " + className + " from " + f.getPath());
441                        try {
442                                ZipInputStream zis = new ZipInputStream(new FileInputStream(f));
443                                try {
444                                        ZipEntry ze = zis.getNextEntry();
445                                        while (ze!=null && (md.gitRevision==null || md.version==null)) {
446                                                // logger.debug(ze.getName());
447                                                if (ze.getName().equals("build.properties") || ze.getName().equals("/build.properties")) {
448                                                        // logger.debug(ze.getName());
449                                                        Properties props = new Properties();
450                                                        props.load(zis);
451                                                        md.gitRevision = props.getProperty("git.buildNumber");
452                                                }
453                                                if (ze.getName().endsWith("/pom.properties")) { 
454                                                        // e.g. META-INF/maven/edu.ucla.cs.compilers/jtb/pom.xml, pom.properties
455                                                        // should probably check full path to prevent false positives
456                                                        Properties props = new Properties();
457                                                        props.load(zis);
458                                                        md.groupId = props.getProperty("groupId");
459                                                        md.artifactId = props.getProperty("artifactId");
460                                                        md.version = props.getProperty("version");
461                                                }
462                                                ze = zis.getNextEntry();
463                                        }
464                                } finally {
465                                        zis.close();
466                                }
467                        } catch (IOException ioe) {
468                                // ignore;
469                        }
470                }
471        }
472        return md;
473    }
474    
475    /** Returns a string describing the CVS revision number of a class. 
476     * 
477     * <p>The class is initialised as part of this process, which may cause a number of 
478     * exceptions to be thrown. 
479     * 
480     * @param loader The classLoader to use
481     * @param className The class we wish to retrieve
482     * 
483     * @return The CVS revision string, in the form "ver 1.234"
484     * 
485     * @throws ClassNotFoundException
486     * @throws SecurityException
487     * @throws NoSuchFieldException
488     * @throws IllegalArgumentException
489     * @throws IllegalAccessException
490     */    
491    private static String getClassRevision(ClassLoader loader, String className) 
492                throws ClassNotFoundException, SecurityException, NoSuchFieldException, 
493                IllegalArgumentException, IllegalAccessException 
494    {
495        if (className.indexOf('$')!=-1) {
496            className = className.substring(0, className.indexOf('$'));
497        }
498        
499        // if this is in a CVS repository, each class has it's own _revision public final static String variable
500        Class<?> clazz = Class.forName(className, true, loader);
501        Field field = null;
502        try {
503                field = clazz.getField("_revision");
504        } catch (NoSuchFieldException nsfe) {
505                
506        }
507        String classRevision = null;
508        if (field!=null) {
509                classRevision = (String) field.get(null);
510            
511                // remove rest of $Id$ text 
512                int pos = classRevision.indexOf(",v ");
513                if (pos != -1) {
514                    classRevision = classRevision.substring(pos + 3);
515                    pos = classRevision.indexOf(' ');
516                    classRevision = "ver " + classRevision.substring(0, pos);
517                }
518        }
519        return classRevision;
520    }
521
522
523    /** An alternate implementation of {@link #getClassRevision(ClassLoader, String)},
524     * which searches the raw bytecode of the class, rather than using Java reflection.
525     * May be a bit more robust.
526     * 
527     * @param loader Classloader to use
528     * @param className Class to load
529     * 
530     * @return The CVS revision string, in the form "ver 1.234".
531     * 
532     * @throws ClassNotFoundException
533     * @throws SecurityException
534     * @throws NoSuchFieldException
535     * @throws IllegalArgumentException
536     * @throws IllegalAccessException
537     */
538    /* TODO implement */
539    public static String getClassRevision2(ClassLoader loader, String className) 
540        throws ClassNotFoundException, SecurityException, NoSuchFieldException, 
541        IllegalArgumentException, IllegalAccessException 
542    {
543        if (className.indexOf('$')!=-1) {
544            className = className.substring(0, className.indexOf('$'));
545        }
546        // String file = className.replace('.',  '/') + ".class";
547        // InputStream is = loader.getResourceAsStream(file);
548        
549        // read up to '$Id:' text
550        
551        throw new UnsupportedOperationException("Not implemented");
552    }
553    
554    
555    
556    /** Returns a list of all the messages contained within a exception (following the causal
557     * chain of the supplied exception). This may be more useful to and end-user, since it
558     * should not contain any references to Java class names.
559     * 
560     * @param throwable an exception
561     * 
562     * @return a List of Strings
563     */
564    public static List<String> getStackTraceSummary(Throwable throwable) {
565        List<String> result = new ArrayList<String>();
566        while (throwable!=null) {
567                result.add(throwable.getMessage());
568                throwable = getCause(throwable);
569                // I think some RemoteExceptions have non-standard caused-by chains as well...
570                if (throwable==null) {
571                        if (throwable instanceof java.sql.SQLException) {
572                                throwable = ((java.sql.SQLException) throwable).getNextException();
573                        } 
574                }
575        }
576        return result;
577    }
578
579    /** Returns the cause of an exception, or null if not known.
580     * If the exception is a a <code>bsh.TargetError</code>, then the cause is determined
581     * by calling the <code>getTarget()</code> method, otherwise this method will
582     * return the same value returned by the standard Exception 
583     * <code>getCause()</code> method.
584     * 
585     * @param e the cause of an exception, or null if not known.
586     * 
587     * @return the cause of an exception, or null if not known.
588     */
589    private static Throwable getCause(Throwable e) {
590        Throwable cause = null;
591        if (e.getClass().getName().equals("bsh.TargetError")) {
592            try {
593                if (e.getClass().getName().equals("bsh.TargetError")) {
594                        Method m;
595                                m = e.getClass().getMethod("getTarget");
596                                cause = (Throwable) m.invoke(e);
597                }
598                } catch (SecurityException e1) {
599                        // ignore - just use original exception
600                } catch (NoSuchMethodException e1) {
601                        // ignore - just use original exception
602                } catch (IllegalArgumentException e1) {
603                        // ignore - just use original exception
604                } catch (IllegalAccessException e1) {
605                        // ignore - just use original exception
606                } catch (InvocationTargetException e1) {
607                        // ignore - just use original exception
608                }
609        } else {
610                cause = e.getCause();
611        }
612        return cause;
613    }
614
615    /**
616     * Returns the HTML-escaped form of a string. Any <code>&amp;</code>,
617     * <code>&lt;</code>, <code>&gt;</code>, and <code>"</code> characters are converted to
618     * <code>&amp;amp;</code>, <code>&amp;lt;</code>, <code>&amp;gt;</code>, and
619     * <code>&amp;quot;</code> respectively.
620     *
621     * @param string the string to convert
622     *
623     * @return the HTML-escaped form of the string
624     */
625    static public String escapeHtml(String string) {
626        if (string == null) {
627            return "";
628        }
629
630        char c;
631        StringBuffer sb = new StringBuffer(string.length());
632
633        for (int i = 0; i < string.length(); i++) {
634            c = string.charAt(i);
635
636            switch (c) {
637                case '&':
638                    sb.append("&amp;");
639                    break;
640                case '<':
641                    sb.append("&lt;");
642                    break;
643                case '>':
644                    sb.append("&gt;");
645                    break;
646                case '\"':
647                    sb.append("&quot;");
648                    break;
649                default:
650                    sb.append(c);
651            }
652        }
653
654        return sb.toString();
655    }
656    
657}