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        // this can also throw java.lang.Errors e.g. 
374        // "Caused by: java.lang.Error: Trampoline must not be defined by the bootstrap classloader
375        // at java.base/sun.reflect.misc.Trampoline.<clinit>(MethodUtil.java:43)"
376        Class<?> clazz;
377        try {
378                clazz = Class.forName(className, true, loader);
379        } catch (Throwable t) {
380                return null;
381        }
382        
383        // this is stored in the build.properties file, which we'll have to get from the JAR containing the class
384        // see http://stackoverflow.com/questions/1983839/determine-which-jar-file-a-class-is-from
385        int revisionMethod = 0;
386        
387        String uri = clazz.getResource('/' + clazz.getName().replace('.', '/') + ".class").toString();
388        if (uri.startsWith("file:")) {
389                // this happens in eclipse; e.g. 
390                //   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
391                // use the current class loader's build.properties if available
392                revisionMethod = 1;
393        } else if (uri.startsWith("jar:file:")) {
394                revisionMethod = 2;
395        } else {
396            // int idx = uri.indexOf(':');
397            // String protocol = idx == -1 ? "(unknown)" : uri.substring(0, idx);
398            // logger.warn("unknown protocol " + protocol + " in classpath uri '" + uri + "'");
399            revisionMethod = -1;
400        }
401
402        JarMetadata md = null;
403        if (revisionMethod == 1) {
404                // get the revision from the supplied class loader's build.properties, if there is one
405                md = new JarMetadata();
406                InputStream is = loader.getResourceAsStream("/build.properties");
407                if (is==null) {
408                        // logger.warn("missing build.properties");
409                        return null;
410                } else {
411                        Properties props = new Properties();
412                        try {
413                                props.load(is);
414                                md.gitRevision = props.getProperty("git.buildNumber");
415                                if ("${buildNumber}".equals(md.gitRevision)) { md.gitRevision = null; }
416                        } catch (IOException ioe) {
417                                // ignore;
418                        }
419                }
420                
421        } else if (revisionMethod == 2) {
422                // get the revision from the containing JAR
423                
424                int idx = uri.indexOf('!');
425                //As far as I know, the if statement below can't ever trigger, so it's more of a sanity check thing.
426                if (idx == -1) {
427                        // logger.warn("unparseable classpath uri '" + uri + "'");
428                        return null;
429                }
430                String fileName;
431                try {
432                    fileName = URLDecoder.decode(uri.substring("jar:file:".length(), idx), Charset.defaultCharset().name());
433                } catch (UnsupportedEncodingException e) {
434                    throw new IllegalStateException("default charset doesn't exist. Your VM is borked.");
435                }
436                
437                // use the cache if it exists
438                if (jarMetadataCache!=null) {
439                        md = jarMetadataCache.get(fileName);
440                }
441                if (md==null) {
442                        md = new JarMetadata();
443                        jarMetadataCache.put(fileName, md);
444                        File f = new File(fileName); // .getAbsolutePath();
445                        
446                        // get the build.properties from this JAR
447                        // logger.debug("Getting build.properties for " + className + " from " + f.getPath());
448                        try {
449                                ZipInputStream zis = new ZipInputStream(new FileInputStream(f));
450                                try {
451                                        ZipEntry ze = zis.getNextEntry();
452                                        while (ze!=null && (md.gitRevision==null || md.version==null)) {
453                                                // logger.debug(ze.getName());
454                                                if (ze.getName().equals("build.properties") || ze.getName().equals("/build.properties")) {
455                                                        // logger.debug(ze.getName());
456                                                        Properties props = new Properties();
457                                                        props.load(zis);
458                                                        md.gitRevision = props.getProperty("git.buildNumber");
459                                                }
460                                                if (ze.getName().endsWith("/pom.properties")) { 
461                                                        // e.g. META-INF/maven/edu.ucla.cs.compilers/jtb/pom.xml, pom.properties
462                                                        // should probably check full path to prevent false positives
463                                                        Properties props = new Properties();
464                                                        props.load(zis);
465                                                        md.groupId = props.getProperty("groupId");
466                                                        md.artifactId = props.getProperty("artifactId");
467                                                        md.version = props.getProperty("version");
468                                                }
469                                                ze = zis.getNextEntry();
470                                        }
471                                } finally {
472                                        zis.close();
473                                }
474                        } catch (IOException ioe) {
475                                // ignore;
476                        }
477                }
478        }
479        return md;
480    }
481    
482    /** Returns a string describing the CVS revision number of a class. 
483     * 
484     * <p>The class is initialised as part of this process, which may cause a number of 
485     * exceptions to be thrown. 
486     * 
487     * @param loader The classLoader to use
488     * @param className The class we wish to retrieve
489     * 
490     * @return The CVS revision string, in the form "ver 1.234"
491     * 
492     * @throws ClassNotFoundException
493     * @throws SecurityException
494     * @throws NoSuchFieldException
495     * @throws IllegalArgumentException
496     * @throws IllegalAccessException
497     */    
498    private static String getClassRevision(ClassLoader loader, String className) 
499                throws ClassNotFoundException, SecurityException, NoSuchFieldException, 
500                IllegalArgumentException, IllegalAccessException 
501    {
502        if (className.indexOf('$')!=-1) {
503            className = className.substring(0, className.indexOf('$'));
504        }
505        
506        // if this is in a CVS repository, each class has it's own _revision public final static String variable
507        Class<?> clazz = Class.forName(className, true, loader);
508        Field field = null;
509        try {
510                field = clazz.getField("_revision");
511        } catch (NoSuchFieldException nsfe) {
512                
513        }
514        String classRevision = null;
515        if (field!=null) {
516                classRevision = (String) field.get(null);
517            
518                // remove rest of $Id$ text 
519                int pos = classRevision.indexOf(",v ");
520                if (pos != -1) {
521                    classRevision = classRevision.substring(pos + 3);
522                    pos = classRevision.indexOf(' ');
523                    classRevision = "ver " + classRevision.substring(0, pos);
524                }
525        }
526        return classRevision;
527    }
528
529
530    /** An alternate implementation of {@link #getClassRevision(ClassLoader, String)},
531     * which searches the raw bytecode of the class, rather than using Java reflection.
532     * May be a bit more robust.
533     * 
534     * @param loader Classloader to use
535     * @param className Class to load
536     * 
537     * @return The CVS revision string, in the form "ver 1.234".
538     * 
539     * @throws ClassNotFoundException
540     * @throws SecurityException
541     * @throws NoSuchFieldException
542     * @throws IllegalArgumentException
543     * @throws IllegalAccessException
544     */
545    /* TODO implement */
546    public static String getClassRevision2(ClassLoader loader, String className) 
547        throws ClassNotFoundException, SecurityException, NoSuchFieldException, 
548        IllegalArgumentException, IllegalAccessException 
549    {
550        if (className.indexOf('$')!=-1) {
551            className = className.substring(0, className.indexOf('$'));
552        }
553        // String file = className.replace('.',  '/') + ".class";
554        // InputStream is = loader.getResourceAsStream(file);
555        
556        // read up to '$Id:' text
557        
558        throw new UnsupportedOperationException("Not implemented");
559    }
560    
561    
562    
563    /** Returns a list of all the messages contained within a exception (following the causal
564     * chain of the supplied exception). This may be more useful to and end-user, since it
565     * should not contain any references to Java class names.
566     * 
567     * @param throwable an exception
568     * 
569     * @return a List of Strings
570     */
571    public static List<String> getStackTraceSummary(Throwable throwable) {
572        List<String> result = new ArrayList<String>();
573        while (throwable!=null) {
574                result.add(throwable.getMessage());
575                throwable = getCause(throwable);
576                // I think some RemoteExceptions have non-standard caused-by chains as well...
577                if (throwable==null) {
578                        if (throwable instanceof java.sql.SQLException) {
579                                throwable = ((java.sql.SQLException) throwable).getNextException();
580                        } 
581                }
582        }
583        return result;
584    }
585
586    /** Returns the cause of an exception, or null if not known.
587     * If the exception is a a <code>bsh.TargetError</code>, then the cause is determined
588     * by calling the <code>getTarget()</code> method, otherwise this method will
589     * return the same value returned by the standard Exception 
590     * <code>getCause()</code> method.
591     * 
592     * @param e the cause of an exception, or null if not known.
593     * 
594     * @return the cause of an exception, or null if not known.
595     */
596    private static Throwable getCause(Throwable e) {
597        Throwable cause = null;
598        if (e.getClass().getName().equals("bsh.TargetError")) {
599            try {
600                if (e.getClass().getName().equals("bsh.TargetError")) {
601                        Method m;
602                                m = e.getClass().getMethod("getTarget");
603                                cause = (Throwable) m.invoke(e);
604                }
605                } catch (SecurityException e1) {
606                        // ignore - just use original exception
607                } catch (NoSuchMethodException e1) {
608                        // ignore - just use original exception
609                } catch (IllegalArgumentException e1) {
610                        // ignore - just use original exception
611                } catch (IllegalAccessException e1) {
612                        // ignore - just use original exception
613                } catch (InvocationTargetException e1) {
614                        // ignore - just use original exception
615                }
616        } else {
617                cause = e.getCause();
618        }
619        return cause;
620    }
621
622    /**
623     * Returns the HTML-escaped form of a string. Any <code>&amp;</code>,
624     * <code>&lt;</code>, <code>&gt;</code>, and <code>"</code> characters are converted to
625     * <code>&amp;amp;</code>, <code>&amp;lt;</code>, <code>&amp;gt;</code>, and
626     * <code>&amp;quot;</code> respectively.
627     *
628     * @param string the string to convert
629     *
630     * @return the HTML-escaped form of the string
631     */
632    static public String escapeHtml(String string) {
633        if (string == null) {
634            return "";
635        }
636
637        char c;
638        StringBuffer sb = new StringBuffer(string.length());
639
640        for (int i = 0; i < string.length(); i++) {
641            c = string.charAt(i);
642
643            switch (c) {
644                case '&':
645                    sb.append("&amp;");
646                    break;
647                case '<':
648                    sb.append("&lt;");
649                    break;
650                case '>':
651                    sb.append("&gt;");
652                    break;
653                case '\"':
654                    sb.append("&quot;");
655                    break;
656                default:
657                    sb.append(c);
658            }
659        }
660
661        return sb.toString();
662    }
663    
664}