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 "=>" before relevent stack trace elements), or
90 * HTML (which will render the stack trace element between <b> and </b> 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>&</code>,
624 * <code><</code>, <code>></code>, and <code>"</code> characters are converted to
625 * <code>&amp;</code>, <code>&lt;</code>, <code>&gt;</code>, and
626 * <code>&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("&");
646 break;
647 case '<':
648 sb.append("<");
649 break;
650 case '>':
651 sb.append(">");
652 break;
653 case '\"':
654 sb.append(""");
655 break;
656 default:
657 sb.append(c);
658 }
659 }
660
661 return sb.toString();
662 }
663
664 }