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