View Javadoc
1   package com.randomnoun.common;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   
7   import java.io.*;
8   import java.lang.reflect.*;
9   import java.net.URLDecoder;
10  import java.nio.charset.Charset;
11  import java.util.ArrayList;
12  import java.util.HashMap;
13  import java.util.List;
14  import java.util.Map;
15  import java.util.Properties;
16  import java.util.zip.ZipEntry;
17  import java.util.zip.ZipInputStream;
18  
19  import org.apache.log4j.Logger;
20  
21  /**
22   * Exception utilities class.
23   *
24   * <p>This class contains static utility methods for handling and manipulating exceptions;
25   * the only one you're likely to call being 
26   * {@link #getStackTraceWithRevisions(Throwable, ClassLoader, int, String)},
27   * which extracts CVS revision information from classes to produce an annotated (and highlighted)
28   * stack trace.
29   * 
30   * <p>When passing in {@link java.lang.ClassLoader}s to these methods, you may want to try one of 
31   * <ul>
32   * <li>this.getClass().getClassLoader()
33   * <li>Thread.currentThread().getContextClassLoader()
34   * </ul>
35   * 
36   * @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>
37   * @author knoxg
38   */
39  public class ExceptionUtil {
40      
41  
42      /** Perform no stack trace element highlighting */
43      public static final int HIGHLIGHT_NONE = 0;
44      
45      /** Allow stack trace elements to be highlighted, as text */
46      public static final int HIGHLIGHT_TEXT = 1;
47      
48      /** Allow stack trace elements to be highlighted, as bold HTML */
49      public static final int HIGHLIGHT_HTML = 2;
50  
51      /** Allow stack trace elements to be highlighted, with span'ed CSS elements */
52      public static final int HIGHLIGHT_HTML_WITH_CSS = 3;
53  
54      /** The number of characters of the git revision to include in non-CSS stack traces (from the left side of the String) */
55      public static final int NUM_CHARACTERS_GIT_REVISION = 8;
56      
57      static Logger logger = Logger.getLogger(ExceptionUtil.class);
58      
59      /**
60       * Private constructor to prevent instantiation of this class
61       */
62      private ExceptionUtil() {
63      }
64  
65      /**
66       * Converts an exception's stack trace to a string. If the exception passed
67       * to this function is null, returns the empty string.
68       *
69       * @param e exception
70       * 
71       * @return string representation of the exception's stack trace
72       */
73      public static String getStackTrace(Throwable e) {
74          if (e == null) {
75              return "";
76          }
77          StringWriter writer = new StringWriter();
78          e.printStackTrace(new PrintWriter(writer));
79          return writer.toString();
80      }
81  
82      /**
83       * Converts an exception's stack trace to a string. Each stack trace element
84       * is annotated to include the CVS revision Id, if it contains a public static
85       * String element containing this information. 
86       * 
87       * <p>Stack trace elements whose classes begin with the specified highlightPrefix
88       * are also marked, to allow easier debugging. Highlights used can be text
89       * (which will insert the string "=&gt;" before relevent stack trace elements), or 
90       * HTML (which will render the stack trace element between &lt;b&gt; and &lt;/b&gt; tags.
91       * 
92       * <p>If HTML highlighting is enabled, then the exception message is also HTML-escaped.
93       *
94       * @param e exception
95       * @param loader ClassLoader used to read stack trace element revision information
96       * @param highlight One of the HIGHLIGHT_* constants in this class
97       * @param highlightPrefix A prefix used to determine which stack trace elements are
98       *   rendered as being 'important'. (e.g. "<code>com.randomnoun.common.</code>"). Multiple
99       *   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 }