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 "=>" before relevent stack trace elements), or 090 * HTML (which will render the stack trace element between <b> and </b> 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>&</code>, 617 * <code><</code>, <code>></code>, and <code>"</code> characters are converted to 618 * <code>&amp;</code>, <code>&lt;</code>, <code>&gt;</code>, and 619 * <code>&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("&"); 639 break; 640 case '<': 641 sb.append("<"); 642 break; 643 case '>': 644 sb.append(">"); 645 break; 646 case '\"': 647 sb.append("""); 648 break; 649 default: 650 sb.append(c); 651 } 652 } 653 654 return sb.toString(); 655 } 656 657}