001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache license, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the license for the specific language governing permissions and 015 * limitations under the license. 016 */ 017package org.apache.logging.log4j.core.impl; 018 019import java.io.Serializable; 020import java.net.URL; 021import java.security.CodeSource; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.Stack; 030 031import org.apache.logging.log4j.core.util.Loader; 032import org.apache.logging.log4j.status.StatusLogger; 033import org.apache.logging.log4j.util.ReflectionUtil; 034import org.apache.logging.log4j.util.Strings; 035 036/** 037 * Wraps a Throwable to add packaging information about each stack trace element. 038 * 039 * <p> 040 * A proxy is used to represent a throwable that may not exist in a different class loader or JVM. When an application 041 * deserializes a ThrowableProxy, the throwable may not be set, but the throwable's information is preserved in other 042 * fields of the proxy like the message and stack trace. 043 * </p> 044 * 045 * <p> 046 * TODO: Move this class to org.apache.logging.log4j.core because it is used from LogEvent. 047 * </p> 048 * <p> 049 * TODO: Deserialize: Try to rebuild Throwable if the target exception is in this class loader? 050 * </p> 051 */ 052public class ThrowableProxy implements Serializable { 053 054 private static final String CAUSED_BY_LABEL = "Caused by: "; 055 private static final String SUPPRESSED_LABEL = "Suppressed: "; 056 private static final String WRAPPED_BY_LABEL = "Wrapped by: "; 057 058 /** 059 * Cached StackTracePackageElement and ClassLoader. 060 * <p> 061 * Consider this class private. 062 * </p> 063 */ 064 static class CacheEntry { 065 private final ExtendedClassInfo element; 066 private final ClassLoader loader; 067 068 public CacheEntry(final ExtendedClassInfo element, final ClassLoader loader) { 069 this.element = element; 070 this.loader = loader; 071 } 072 } 073 074 private static final ThrowableProxy[] EMPTY_THROWABLE_PROXY_ARRAY = new ThrowableProxy[0]; 075 076 private static final char EOL = '\n'; 077 078 private static final long serialVersionUID = -2752771578252251910L; 079 080 private final ThrowableProxy causeProxy; 081 082 private int commonElementCount; 083 084 private final ExtendedStackTraceElement[] extendedStackTrace; 085 086 private final String localizedMessage; 087 088 private final String message; 089 090 private final String name; 091 092 private final ThrowableProxy[] suppressedProxies; 093 094 private final transient Throwable throwable; 095 096 /** 097 * For JSON and XML IO via Jackson. 098 */ 099 @SuppressWarnings("unused") 100 private ThrowableProxy() { 101 this.throwable = null; 102 this.name = null; 103 this.extendedStackTrace = null; 104 this.causeProxy = null; 105 this.message = null; 106 this.localizedMessage = null; 107 this.suppressedProxies = EMPTY_THROWABLE_PROXY_ARRAY; 108 } 109 110 /** 111 * Constructs the wrapper for the Throwable that includes packaging data. 112 * 113 * @param throwable 114 * The Throwable to wrap, must not be null. 115 */ 116 public ThrowableProxy(final Throwable throwable) { 117 this(throwable, null); 118 } 119 120 /** 121 * Constructs the wrapper for the Throwable that includes packaging data. 122 * 123 * @param throwable 124 * The Throwable to wrap, must not be null. 125 * @param visited 126 * The set of visited suppressed exceptions. 127 */ 128 private ThrowableProxy(final Throwable throwable, final Set<Throwable> visited) { 129 this.throwable = throwable; 130 this.name = throwable.getClass().getName(); 131 this.message = throwable.getMessage(); 132 this.localizedMessage = throwable.getLocalizedMessage(); 133 final Map<String, CacheEntry> map = new HashMap<>(); 134 final Stack<Class<?>> stack = ReflectionUtil.getCurrentStackTrace(); 135 this.extendedStackTrace = this.toExtendedStackTrace(stack, map, null, throwable.getStackTrace()); 136 final Throwable throwableCause = throwable.getCause(); 137 final Set<Throwable> causeVisited = new HashSet<>(1); 138 this.causeProxy = throwableCause == null ? null : new ThrowableProxy(throwable, stack, map, throwableCause, visited, causeVisited); 139 this.suppressedProxies = this.toSuppressedProxies(throwable, visited); 140 } 141 142 /** 143 * Constructs the wrapper for a Throwable that is referenced as the cause by another Throwable. 144 * 145 * @param parent 146 * The Throwable referencing this Throwable. 147 * @param stack 148 * The Class stack. 149 * @param map 150 * The cache containing the packaging data. 151 * @param cause 152 * The Throwable to wrap. 153 * @param suppressedVisited TODO 154 * @param causeVisited TODO 155 */ 156 private ThrowableProxy(final Throwable parent, final Stack<Class<?>> stack, final Map<String, CacheEntry> map, 157 final Throwable cause, final Set<Throwable> suppressedVisited, final Set<Throwable> causeVisited) { 158 causeVisited.add(cause); 159 this.throwable = cause; 160 this.name = cause.getClass().getName(); 161 this.message = this.throwable.getMessage(); 162 this.localizedMessage = this.throwable.getLocalizedMessage(); 163 this.extendedStackTrace = this.toExtendedStackTrace(stack, map, parent.getStackTrace(), cause.getStackTrace()); 164 final Throwable causeCause = cause.getCause(); 165 this.causeProxy = causeCause == null || causeVisited.contains(causeCause) ? null : new ThrowableProxy(parent, 166 stack, map, causeCause, suppressedVisited, causeVisited); 167 this.suppressedProxies = this.toSuppressedProxies(cause, suppressedVisited); 168 } 169 170 @Override 171 public boolean equals(final Object obj) { 172 if (this == obj) { 173 return true; 174 } 175 if (obj == null) { 176 return false; 177 } 178 if (this.getClass() != obj.getClass()) { 179 return false; 180 } 181 final ThrowableProxy other = (ThrowableProxy) obj; 182 if (this.causeProxy == null) { 183 if (other.causeProxy != null) { 184 return false; 185 } 186 } else if (!this.causeProxy.equals(other.causeProxy)) { 187 return false; 188 } 189 if (this.commonElementCount != other.commonElementCount) { 190 return false; 191 } 192 if (this.name == null) { 193 if (other.name != null) { 194 return false; 195 } 196 } else if (!this.name.equals(other.name)) { 197 return false; 198 } 199 if (!Arrays.equals(this.extendedStackTrace, other.extendedStackTrace)) { 200 return false; 201 } 202 if (!Arrays.equals(this.suppressedProxies, other.suppressedProxies)) { 203 return false; 204 } 205 return true; 206 } 207 208 private void formatCause(final StringBuilder sb, final String prefix, final ThrowableProxy cause, final List<String> ignorePackages) { 209 formatThrowableProxy(sb, prefix, CAUSED_BY_LABEL, cause, ignorePackages); 210 } 211 212 private void formatThrowableProxy(final StringBuilder sb, final String prefix, final String causeLabel, 213 final ThrowableProxy throwableProxy, final List<String> ignorePackages) { 214 if (throwableProxy == null) { 215 return; 216 } 217 sb.append(prefix).append(causeLabel).append(throwableProxy).append(EOL); 218 this.formatElements(sb, prefix, throwableProxy.commonElementCount, 219 throwableProxy.getStackTrace(), throwableProxy.extendedStackTrace, ignorePackages); 220 this.formatSuppressed(sb, prefix + "\t", throwableProxy.suppressedProxies, ignorePackages); 221 this.formatCause(sb, prefix, throwableProxy.causeProxy, ignorePackages); 222 } 223 224 private void formatSuppressed(final StringBuilder sb, final String prefix, final ThrowableProxy[] suppressedProxies, 225 final List<String> ignorePackages) { 226 if (suppressedProxies == null) { 227 return; 228 } 229 for (final ThrowableProxy suppressedProxy : suppressedProxies) { 230 final ThrowableProxy cause = suppressedProxy; 231 formatThrowableProxy(sb, prefix, SUPPRESSED_LABEL, cause, ignorePackages); 232 } 233 } 234 235 private void formatElements(final StringBuilder sb, final String prefix, final int commonCount, 236 final StackTraceElement[] causedTrace, final ExtendedStackTraceElement[] extStackTrace, 237 final List<String> ignorePackages) { 238 if (ignorePackages == null || ignorePackages.isEmpty()) { 239 for (final ExtendedStackTraceElement element : extStackTrace) { 240 this.formatEntry(element, sb, prefix); 241 } 242 } else { 243 int count = 0; 244 for (int i = 0; i < extStackTrace.length; ++i) { 245 if (!this.ignoreElement(causedTrace[i], ignorePackages)) { 246 if (count > 0) { 247 appendSuppressedCount(sb, prefix, count); 248 count = 0; 249 } 250 this.formatEntry(extStackTrace[i], sb, prefix); 251 } else { 252 ++count; 253 } 254 } 255 if (count > 0) { 256 appendSuppressedCount(sb, prefix, count); 257 } 258 } 259 if (commonCount != 0) { 260 sb.append(prefix).append("\t... ").append(commonCount).append(" more").append(EOL); 261 } 262 } 263 264 private void appendSuppressedCount(final StringBuilder sb, final String prefix, final int count) { 265 sb.append(prefix); 266 if (count == 1) { 267 sb.append("\t....").append(EOL); 268 } else { 269 sb.append("\t... suppressed ").append(count).append(" lines").append(EOL); 270 } 271 } 272 273 private void formatEntry(final ExtendedStackTraceElement extStackTraceElement, final StringBuilder sb, final String prefix) { 274 sb.append(prefix); 275 sb.append("\tat "); 276 sb.append(extStackTraceElement); 277 sb.append(EOL); 278 } 279 280 /** 281 * Formats the specified Throwable. 282 * 283 * @param sb 284 * StringBuilder to contain the formatted Throwable. 285 * @param cause 286 * The Throwable to format. 287 */ 288 public void formatWrapper(final StringBuilder sb, final ThrowableProxy cause) { 289 this.formatWrapper(sb, cause, null); 290 } 291 292 /** 293 * Formats the specified Throwable. 294 * 295 * @param sb 296 * StringBuilder to contain the formatted Throwable. 297 * @param cause 298 * The Throwable to format. 299 * @param packages 300 * The List of packages to be suppressed from the trace. 301 */ 302 @SuppressWarnings("ThrowableResultOfMethodCallIgnored") 303 public void formatWrapper(final StringBuilder sb, final ThrowableProxy cause, final List<String> packages) { 304 final Throwable caused = cause.getCauseProxy() != null ? cause.getCauseProxy().getThrowable() : null; 305 if (caused != null) { 306 this.formatWrapper(sb, cause.causeProxy); 307 sb.append(WRAPPED_BY_LABEL); 308 } 309 sb.append(cause).append(EOL); 310 this.formatElements(sb, "", cause.commonElementCount, 311 cause.getThrowable().getStackTrace(), cause.extendedStackTrace, packages); 312 } 313 314 public ThrowableProxy getCauseProxy() { 315 return this.causeProxy; 316 } 317 318 /** 319 * Format the Throwable that is the cause of this Throwable. 320 * 321 * @return The formatted Throwable that caused this Throwable. 322 */ 323 public String getCauseStackTraceAsString() { 324 return this.getCauseStackTraceAsString(null); 325 } 326 327 /** 328 * Format the Throwable that is the cause of this Throwable. 329 * 330 * @param packages 331 * The List of packages to be suppressed from the trace. 332 * @return The formatted Throwable that caused this Throwable. 333 */ 334 public String getCauseStackTraceAsString(final List<String> packages) { 335 final StringBuilder sb = new StringBuilder(); 336 if (this.causeProxy != null) { 337 this.formatWrapper(sb, this.causeProxy); 338 sb.append(WRAPPED_BY_LABEL); 339 } 340 sb.append(this.toString()); 341 sb.append(EOL); 342 this.formatElements(sb, "", 0, this.throwable.getStackTrace(), this.extendedStackTrace, packages); 343 return sb.toString(); 344 } 345 346 /** 347 * Return the number of elements that are being omitted because they are common with the parent Throwable's stack 348 * trace. 349 * 350 * @return The number of elements omitted from the stack trace. 351 */ 352 public int getCommonElementCount() { 353 return this.commonElementCount; 354 } 355 356 /** 357 * Gets the stack trace including packaging information. 358 * 359 * @return The stack trace including packaging information. 360 */ 361 public ExtendedStackTraceElement[] getExtendedStackTrace() { 362 return this.extendedStackTrace; 363 } 364 365 /** 366 * Format the stack trace including packaging information. 367 * 368 * @return The formatted stack trace including packaging information. 369 */ 370 public String getExtendedStackTraceAsString() { 371 return this.getExtendedStackTraceAsString(null); 372 } 373 374 /** 375 * Format the stack trace including packaging information. 376 * 377 * @param ignorePackages 378 * List of packages to be ignored in the trace. 379 * @return The formatted stack trace including packaging information. 380 */ 381 public String getExtendedStackTraceAsString(final List<String> ignorePackages) { 382 final StringBuilder sb = new StringBuilder(this.name); 383 final String msg = this.message; 384 if (msg != null) { 385 sb.append(": ").append(msg); 386 } 387 sb.append(EOL); 388 final StackTraceElement[] causedTrace = this.throwable != null ? this.throwable.getStackTrace() : null; 389 this.formatElements(sb, "", 0, causedTrace, this.extendedStackTrace, ignorePackages); 390 this.formatSuppressed(sb, "\t", this.suppressedProxies, ignorePackages); 391 this.formatCause(sb, "", this.causeProxy, ignorePackages); 392 return sb.toString(); 393 } 394 395 public String getLocalizedMessage() { 396 return this.localizedMessage; 397 } 398 399 public String getMessage() { 400 return this.message; 401 } 402 403 /** 404 * Return the FQCN of the Throwable. 405 * 406 * @return The FQCN of the Throwable. 407 */ 408 public String getName() { 409 return this.name; 410 } 411 412 public StackTraceElement[] getStackTrace() { 413 return this.throwable == null ? null : this.throwable.getStackTrace(); 414 } 415 416 /** 417 * Gets proxies for suppressed exceptions. 418 * 419 * @return proxies for suppressed exceptions. 420 */ 421 public ThrowableProxy[] getSuppressedProxies() { 422 return this.suppressedProxies; 423 } 424 425 /** 426 * Format the suppressed Throwables. 427 * 428 * @return The formatted suppressed Throwables. 429 */ 430 public String getSuppressedStackTrace() { 431 final ThrowableProxy[] suppressed = this.getSuppressedProxies(); 432 if (suppressed == null || suppressed.length == 0) { 433 return Strings.EMPTY; 434 } 435 final StringBuilder sb = new StringBuilder("Suppressed Stack Trace Elements:").append(EOL); 436 for (final ThrowableProxy proxy : suppressed) { 437 sb.append(proxy.getExtendedStackTraceAsString()); 438 } 439 return sb.toString(); 440 } 441 442 /** 443 * The throwable or null if this object is deserialized from XML or JSON. 444 * 445 * @return The throwable or null if this object is deserialized from XML or JSON. 446 */ 447 public Throwable getThrowable() { 448 return this.throwable; 449 } 450 451 @Override 452 public int hashCode() { 453 final int prime = 31; 454 int result = 1; 455 result = prime * result + (this.causeProxy == null ? 0 : this.causeProxy.hashCode()); 456 result = prime * result + this.commonElementCount; 457 result = prime * result + (this.extendedStackTrace == null ? 0 : Arrays.hashCode(this.extendedStackTrace)); 458 result = prime * result + (this.suppressedProxies == null ? 0 : Arrays.hashCode(this.suppressedProxies)); 459 result = prime * result + (this.name == null ? 0 : this.name.hashCode()); 460 return result; 461 } 462 463 private boolean ignoreElement(final StackTraceElement element, final List<String> ignorePackages) { 464 final String className = element.getClassName(); 465 for (final String pkg : ignorePackages) { 466 if (className.startsWith(pkg)) { 467 return true; 468 } 469 } 470 return false; 471 } 472 473 /** 474 * Loads classes not located via Reflection.getCallerClass. 475 * 476 * @param lastLoader 477 * The ClassLoader that loaded the Class that called this Class. 478 * @param className 479 * The name of the Class. 480 * @return The Class object for the Class or null if it could not be located. 481 */ 482 private Class<?> loadClass(final ClassLoader lastLoader, final String className) { 483 // XXX: this is overly complicated 484 Class<?> clazz; 485 if (lastLoader != null) { 486 try { 487 clazz = Loader.initializeClass(className, lastLoader); 488 if (clazz != null) { 489 return clazz; 490 } 491 } catch (final Throwable ignore) { 492 // Ignore exception. 493 } 494 } 495 try { 496 clazz = Loader.loadClass(className); 497 } catch (final ClassNotFoundException ignored) { 498 return initializeClass(className); 499 } catch (final NoClassDefFoundError ignored) { 500 return initializeClass(className); 501 } 502 return clazz; 503 } 504 505 private Class<?> initializeClass(final String className) { 506 try { 507 return Loader.initializeClass(className, this.getClass().getClassLoader()); 508 } catch (final ClassNotFoundException ignore) { 509 return null; 510 } catch (final NoClassDefFoundError ignore) { 511 return null; 512 } 513 } 514 515 /** 516 * Construct the CacheEntry from the Class's information. 517 * 518 * @param stackTraceElement 519 * The stack trace element 520 * @param callerClass 521 * The Class. 522 * @param exact 523 * True if the class was obtained via Reflection.getCallerClass. 524 * 525 * @return The CacheEntry. 526 */ 527 private CacheEntry toCacheEntry(final StackTraceElement stackTraceElement, final Class<?> callerClass, 528 final boolean exact) { 529 String location = "?"; 530 String version = "?"; 531 ClassLoader lastLoader = null; 532 if (callerClass != null) { 533 try { 534 final CodeSource source = callerClass.getProtectionDomain().getCodeSource(); 535 if (source != null) { 536 final URL locationURL = source.getLocation(); 537 if (locationURL != null) { 538 final String str = locationURL.toString().replace('\\', '/'); 539 int index = str.lastIndexOf("/"); 540 if (index >= 0 && index == str.length() - 1) { 541 index = str.lastIndexOf("/", index - 1); 542 location = str.substring(index + 1); 543 } else { 544 location = str.substring(index + 1); 545 } 546 } 547 } 548 } catch (final Exception ex) { 549 // Ignore the exception. 550 } 551 final Package pkg = callerClass.getPackage(); 552 if (pkg != null) { 553 final String ver = pkg.getImplementationVersion(); 554 if (ver != null) { 555 version = ver; 556 } 557 } 558 lastLoader = callerClass.getClassLoader(); 559 } 560 return new CacheEntry(new ExtendedClassInfo(exact, location, version), lastLoader); 561 } 562 563 /** 564 * Resolve all the stack entries in this stack trace that are not common with the parent. 565 * 566 * @param stack 567 * The callers Class stack. 568 * @param map 569 * The cache of CacheEntry objects. 570 * @param rootTrace 571 * The first stack trace resolve or null. 572 * @param stackTrace 573 * The stack trace being resolved. 574 * @return The StackTracePackageElement array. 575 */ 576 ExtendedStackTraceElement[] toExtendedStackTrace(final Stack<Class<?>> stack, final Map<String, CacheEntry> map, 577 final StackTraceElement[] rootTrace, final StackTraceElement[] stackTrace) { 578 int stackLength; 579 if (rootTrace != null) { 580 int rootIndex = rootTrace.length - 1; 581 int stackIndex = stackTrace.length - 1; 582 while (rootIndex >= 0 && stackIndex >= 0 && rootTrace[rootIndex].equals(stackTrace[stackIndex])) { 583 --rootIndex; 584 --stackIndex; 585 } 586 this.commonElementCount = stackTrace.length - 1 - stackIndex; 587 stackLength = stackIndex + 1; 588 } else { 589 this.commonElementCount = 0; 590 stackLength = stackTrace.length; 591 } 592 final ExtendedStackTraceElement[] extStackTrace = new ExtendedStackTraceElement[stackLength]; 593 Class<?> clazz = stack.isEmpty() ? null : stack.peek(); 594 ClassLoader lastLoader = null; 595 for (int i = stackLength - 1; i >= 0; --i) { 596 final StackTraceElement stackTraceElement = stackTrace[i]; 597 final String className = stackTraceElement.getClassName(); 598 // The stack returned from getCurrentStack may be missing entries for java.lang.reflect.Method.invoke() 599 // and its implementation. The Throwable might also contain stack entries that are no longer 600 // present as those methods have returned. 601 ExtendedClassInfo extClassInfo; 602 if (clazz != null && className.equals(clazz.getName())) { 603 final CacheEntry entry = this.toCacheEntry(stackTraceElement, clazz, true); 604 extClassInfo = entry.element; 605 lastLoader = entry.loader; 606 stack.pop(); 607 clazz = stack.isEmpty() ? null : stack.peek(); 608 } else { 609 final CacheEntry cacheEntry = map.get(className); 610 if (cacheEntry != null) { 611 final CacheEntry entry = cacheEntry; 612 extClassInfo = entry.element; 613 if (entry.loader != null) { 614 lastLoader = entry.loader; 615 } 616 } else { 617 final CacheEntry entry = this.toCacheEntry(stackTraceElement, 618 this.loadClass(lastLoader, className), false); 619 extClassInfo = entry.element; 620 map.put(stackTraceElement.toString(), entry); 621 if (entry.loader != null) { 622 lastLoader = entry.loader; 623 } 624 } 625 } 626 extStackTrace[i] = new ExtendedStackTraceElement(stackTraceElement, extClassInfo); 627 } 628 return extStackTrace; 629 } 630 631 @Override 632 public String toString() { 633 final String msg = this.message; 634 return msg != null ? this.name + ": " + msg : this.name; 635 } 636 637 private ThrowableProxy[] toSuppressedProxies(final Throwable thrown, Set<Throwable> suppressedVisited) { 638 try { 639 final Throwable[] suppressed = thrown.getSuppressed(); 640 if (suppressed == null) { 641 return EMPTY_THROWABLE_PROXY_ARRAY; 642 } 643 final List<ThrowableProxy> proxies = new ArrayList<>(suppressed.length); 644 if (suppressedVisited == null) { 645 suppressedVisited = new HashSet<>(proxies.size()); 646 } 647 for (int i = 0; i < suppressed.length; i++) { 648 final Throwable candidate = suppressed[i]; 649 if (!suppressedVisited.contains(candidate)) { 650 suppressedVisited.add(candidate); 651 proxies.add(new ThrowableProxy(candidate, suppressedVisited)); 652 } 653 } 654 return proxies.toArray(new ThrowableProxy[proxies.size()]); 655 } catch (final Exception e) { 656 StatusLogger.getLogger().error(e); 657 } 658 return null; 659 } 660}