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