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     * <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     */
050    public 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    }