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.lang.reflect.Method;
021import java.net.URL;
022import java.security.CodeSource;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Stack;
027
028import org.apache.logging.log4j.Logger;
029import org.apache.logging.log4j.status.StatusLogger;
030
031/**
032 * Wraps a Throwable to add packaging information about each stack trace element.
033 */
034public class ThrowableProxy implements Serializable {
035
036    private static final long serialVersionUID = -2752771578252251910L;
037
038    private static final Logger LOGGER = StatusLogger.getLogger();
039
040    private static final PrivateSecurityManager SECURITY_MANAGER;
041
042    private static final Method GET_SUPPRESSED;
043
044    private static final Method ADD_SUPPRESSED;
045
046    private final ThrowableProxy proxyCause;
047
048    private final Throwable throwable;
049
050    private final String name;
051
052    private final StackTracePackageElement[] callerPackageData;
053
054    private int commonElementCount;
055
056    static {
057        if (ReflectiveCallerClassUtility.isSupported()) {
058            SECURITY_MANAGER = null;
059        } else {
060            PrivateSecurityManager securityManager;
061            try {
062                securityManager = new PrivateSecurityManager();
063                if (securityManager.getClasses() == null) {
064                    // This shouldn't happen.
065                    securityManager = null;
066                    LOGGER.error("Unable to obtain call stack from security manager.");
067                }
068            } catch (final Exception e) {
069                securityManager = null;
070                LOGGER.debug("Unable to install security manager.", e);
071            }
072            SECURITY_MANAGER = securityManager;
073        }
074
075        Method getSuppressed = null, addSuppressed = null;
076        final Method[] methods = Throwable.class.getMethods();
077        for (final Method method : methods) {
078            if (method.getName().equals("getSuppressed")) {
079                getSuppressed = method;
080            } else if (method.getName().equals("addSuppressed")) {
081                addSuppressed = method;
082            }
083        }
084        GET_SUPPRESSED = getSuppressed;
085        ADD_SUPPRESSED = addSuppressed;
086    }
087
088    /**
089     * Construct the wrapper for the Throwable that includes packaging data.
090     * @param throwable The Throwable to wrap.
091     */
092    public ThrowableProxy(final Throwable throwable) {
093        this.throwable = throwable;
094        this.name = throwable.getClass().getName();
095        final Map<String, CacheEntry> map = new HashMap<String, CacheEntry>();
096        final Stack<Class<?>> stack = getCurrentStack();
097        callerPackageData = resolvePackageData(stack, map, null, throwable.getStackTrace());
098        this.proxyCause = throwable.getCause() == null ? null :
099            new ThrowableProxy(throwable, stack, map, throwable.getCause());
100        setSuppressed(throwable);
101    }
102
103    /**
104     * Constructs the wrapper for a Throwable that is referenced as the cause by another
105     * Throwable.
106     * @param parent The Throwable referencing this Throwable.
107     * @param stack The Class stack.
108     * @param map The cache containing the packaging data.
109     * @param cause The Throwable to wrap.
110     */
111    private ThrowableProxy(final Throwable parent, final Stack<Class<?>> stack, final Map<String, CacheEntry> map,
112                           final Throwable cause) {
113        this.throwable = cause;
114        this.name = cause.getClass().getName();
115        callerPackageData = resolvePackageData(stack, map, parent.getStackTrace(), cause.getStackTrace());
116        this.proxyCause = cause.getCause() == null ? null :
117            new ThrowableProxy(parent, stack, map, cause.getCause());
118        setSuppressed(cause);
119    }
120
121    public Throwable getThrowable() {
122        return throwable;
123    }
124
125    public ThrowableProxy getCause() {
126        return proxyCause;
127    }
128
129    /**
130     * Return the FQCN of the Throwable.
131     * @return The FQCN of the Throwable.
132     */
133    public String getName() {
134        return name;
135    }
136
137    /**
138     * Return the number of elements that are being ommitted because they are common with the parent Throwable's
139     * stack trace.
140     * @return The number of elements ommitted from the stack trace.
141     */
142    public int getCommonElementCount() {
143        return commonElementCount;
144    }
145
146    /**
147     * Return the package data associated with the stack trace.
148     * @return The package data associated with the stack trace.
149     */
150    public StackTracePackageElement[] getPackageData() {
151        return callerPackageData;
152    }
153
154    @Override
155    public String toString() {
156        final String msg = throwable.getMessage();
157        return msg != null ? name + ": " + msg : name;
158    }
159
160    /**
161     * Format the Throwable that is the cause of this Throwable.
162     * @return The formatted Throwable that caused this Throwable.
163     */
164    public String getRootCauseStackTrace() {
165        return getRootCauseStackTrace(null);
166    }
167
168    /**
169     * Format the Throwable that is the cause of this Throwable.
170     * @param packages The List of packages to be suppressed from the trace.
171     * @return The formatted Throwable that caused this Throwable.
172     */
173    public String getRootCauseStackTrace(final List<String> packages) {
174        final StringBuilder sb = new StringBuilder();
175        if (proxyCause != null) {
176            formatWrapper(sb, proxyCause);
177            sb.append("Wrapped by: ");
178        }
179        sb.append(toString());
180        sb.append("\n");
181        formatElements(sb, 0, throwable.getStackTrace(), callerPackageData, packages);
182        return sb.toString();
183    }
184
185    /**
186     * Formats the specified Throwable.
187     * @param sb StringBuilder to contain the formatted Throwable.
188     * @param cause The Throwable to format.
189     */
190    public void formatWrapper(final StringBuilder sb, final ThrowableProxy cause) {
191        formatWrapper(sb, cause, null);
192    }
193
194    /**
195     * Formats the specified Throwable.
196     * @param sb StringBuilder to contain the formatted Throwable.
197     * @param cause The Throwable to format.
198     * @param packages The List of packages to be suppressed from the trace.
199     */
200    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
201    public void formatWrapper(final StringBuilder sb, final ThrowableProxy cause, final List<String> packages) {
202        final Throwable caused = cause.getCause() != null ? cause.getCause().getThrowable() : null;
203        if (caused != null) {
204            formatWrapper(sb, cause.proxyCause);
205            sb.append("Wrapped by: ");
206        }
207        sb.append(cause).append("\n");
208        formatElements(sb, cause.commonElementCount, cause.getThrowable().getStackTrace(), cause.callerPackageData,
209            packages);
210    }
211
212    /**
213     * Format the stack trace including packaging information.
214     * @return The formatted stack trace including packaging information.
215     */
216    public String getExtendedStackTrace() {
217        return getExtendedStackTrace(null);
218    }
219
220    /**
221     * Format the stack trace including packaging information.
222     * @param packages List of packages to be suppressed from the trace.
223     * @return The formatted stack trace including packaging information.
224     */
225    public String getExtendedStackTrace(final List<String> packages) {
226        final StringBuilder sb = new StringBuilder(name);
227        final String msg = throwable.getMessage();
228        if (msg != null) {
229            sb.append(": ").append(throwable.getMessage());
230        }
231        sb.append("\n");
232        formatElements(sb, 0, throwable.getStackTrace(), callerPackageData, packages);
233        if (proxyCause != null) {
234            formatCause(sb, proxyCause, packages);
235        }
236        return sb.toString();
237    }
238
239    /**
240     * Format the suppressed Throwables.
241     * @return The formatted suppressed Throwables.
242     */
243    public String getSuppressedStackTrace() {
244        final ThrowableProxy[] suppressed = getSuppressed();
245        if (suppressed == null || suppressed.length == 0) {
246            return "";
247        }
248        final StringBuilder sb = new StringBuilder("Suppressed Stack Trace Elements:\n");
249        for (final ThrowableProxy proxy : suppressed) {
250            sb.append(proxy.getExtendedStackTrace());
251        }
252        return sb.toString();
253    }
254
255    @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
256    private void formatCause(final StringBuilder sb, final ThrowableProxy cause, final List<String> packages) {
257        sb.append("Caused by: ").append(cause).append("\n");
258        formatElements(sb, cause.commonElementCount, cause.getThrowable().getStackTrace(), cause.callerPackageData,
259            packages);
260        if (cause.getCause() != null) {
261            formatCause(sb, cause.proxyCause, packages);
262        }
263    }
264
265    private void formatElements(final StringBuilder sb, final int commonCount, final StackTraceElement[] causedTrace,
266                                final StackTracePackageElement[] packageData, final List<String> packages) {
267        if (packages == null || packages.size() == 0) {
268            for (int i = 0; i < packageData.length; ++i) {
269                formatEntry(causedTrace[i], packageData[i], sb);
270            }
271        } else {
272            int count = 0;
273            for (int i = 0; i < packageData.length; ++i) {
274                if (!isSuppressed(causedTrace[i], packages)) {
275                    if (count > 0) {
276                        if (count == 1) {
277                            sb.append("\t....\n");
278                        } else {
279                            sb.append("\t... suppressed ").append(count).append(" lines\n");
280                        }
281                        count = 0;
282                    }
283                    formatEntry(causedTrace[i], packageData[i], sb);
284                } else {
285                    ++count;
286                }
287            }
288            if (count > 0) {
289                if (count == 1) {
290                    sb.append("\t...\n");
291                } else {
292                    sb.append("\t... suppressed ").append(count).append(" lines\n");
293                }
294            }
295        }
296        if (commonCount != 0) {
297            sb.append("\t... ").append(commonCount).append(" more").append("\n");
298        }
299    }
300
301    private void formatEntry(final StackTraceElement element, final StackTracePackageElement packageData,
302                             final StringBuilder sb) {
303        sb.append("\tat ");
304        sb.append(element);
305        sb.append(" ");
306        sb.append(packageData);
307        sb.append("\n");
308    }
309
310    private boolean isSuppressed(final StackTraceElement element, final List<String> packages) {
311        final String className = element.getClassName();
312        for (final String pkg : packages) {
313            if (className.startsWith(pkg)) {
314                return true;
315            }
316        }
317        return false;
318    }
319
320    /**
321     * Initialize the cache by resolving everything in the current stack trace via Reflection.getCallerClass
322     * or via the SecurityManager if either are available. These are the only Classes that can be trusted
323     * to be accurate.
324     * @return A Deque containing the current stack of Class objects.
325     */
326    private Stack<Class<?>> getCurrentStack() {
327        if (ReflectiveCallerClassUtility.isSupported()) {
328            final Stack<Class<?>> classes = new Stack<Class<?>>();
329            int index = 1;
330            Class<?> clazz = ReflectiveCallerClassUtility.getCaller(index);
331            while (clazz != null) {
332                classes.push(clazz);
333                clazz = ReflectiveCallerClassUtility.getCaller(++index);
334            }
335            return classes;
336        } else if (SECURITY_MANAGER != null) {
337            final Class<?>[] array = SECURITY_MANAGER.getClasses();
338            final Stack<Class<?>> classes = new Stack<Class<?>>();
339            for (final Class<?> clazz : array) {
340                classes.push(clazz);
341            }
342            return classes;
343        }
344        return new Stack<Class<?>>();
345    }
346
347    /**
348     * Resolve all the stack entries in this stack trace that are not common with the parent.
349     * @param stack The callers Class stack.
350     * @param map The cache of CacheEntry objects.
351     * @param rootTrace The first stack trace resolve or null.
352     * @param stackTrace The stack trace being resolved.
353     * @return The StackTracePackageElement array.
354     */
355    StackTracePackageElement[] resolvePackageData(final Stack<Class<?>> stack, final Map<String,
356                                                          CacheEntry> map,
357                                                          final StackTraceElement[] rootTrace,
358                                                          final StackTraceElement[] stackTrace) {
359        int stackLength;
360        if (rootTrace != null) {
361            int rootIndex = rootTrace.length - 1;
362            int stackIndex = stackTrace.length - 1;
363            while (rootIndex >= 0 && stackIndex >= 0 && rootTrace[rootIndex].equals(stackTrace[stackIndex])) {
364                --rootIndex;
365                --stackIndex;
366            }
367            commonElementCount = stackTrace.length - 1 - stackIndex;
368            stackLength = stackIndex + 1;
369        } else {
370            commonElementCount = 0;
371            stackLength = stackTrace.length;
372        }
373        final StackTracePackageElement[] packageArray = new StackTracePackageElement[stackLength];
374        Class<?> clazz = stack.isEmpty() ? null : stack.peek();
375        ClassLoader lastLoader = null;
376        for (int i = stackLength - 1; i >= 0; --i) {
377            final String className = stackTrace[i].getClassName();
378            // The stack returned from getCurrentStack will be missing entries for  java.lang.reflect.Method.invoke()
379            // and its implementation. The Throwable might also contain stack entries that are no longer
380            // present as those methods have returned.
381            if (clazz != null && className.equals(clazz.getName())) {
382                final CacheEntry entry = resolvePackageElement(clazz, true);
383                packageArray[i] = entry.element;
384                lastLoader = entry.loader;
385                stack.pop();
386                clazz = stack.isEmpty() ? null : stack.peek();
387            } else {
388                if (map.containsKey(className)) {
389                    final CacheEntry entry = map.get(className);
390                    packageArray[i] = entry.element;
391                    if (entry.loader != null) {
392                        lastLoader = entry.loader;
393                    }
394                } else {
395                    final CacheEntry entry = resolvePackageElement(loadClass(lastLoader, className), false);
396                    packageArray[i] = entry.element;
397                    map.put(className, entry);
398                    if (entry.loader != null) {
399                        lastLoader = entry.loader;
400                    }
401                }
402            }
403        }
404        return packageArray;
405    }
406
407
408    /**
409     * Construct the CacheEntry from the Class's information.
410     * @param callerClass The Class.
411     * @param exact True if the class was obtained via Reflection.getCallerClass.
412     * @return The CacheEntry.
413     */
414    private CacheEntry resolvePackageElement(final Class<?> callerClass, final boolean exact) {
415        String location = "?";
416        String version = "?";
417        ClassLoader lastLoader = null;
418        if (callerClass != null) {
419            try {
420                final CodeSource source = callerClass.getProtectionDomain().getCodeSource();
421                if (source != null) {
422                    final URL locationURL = source.getLocation();
423                    if (locationURL != null) {
424                        final String str = locationURL.toString().replace('\\', '/');
425                        int index = str.lastIndexOf("/");
426                        if (index >= 0 && index == str.length() - 1) {
427                            index = str.lastIndexOf("/", index - 1);
428                            location = str.substring(index + 1);
429                        } else {
430                            location = str.substring(index + 1);
431                        }
432                    }
433                }
434            } catch (final Exception ex) {
435                // Ignore the exception.
436            }
437            final Package pkg = callerClass.getPackage();
438            if (pkg != null) {
439                final String ver = pkg.getImplementationVersion();
440                if (ver != null) {
441                    version = ver;
442                }
443            }
444            lastLoader = callerClass.getClassLoader();
445        }
446        return new CacheEntry(new StackTracePackageElement(location, version, exact), lastLoader);
447    }
448
449    /**
450     * Loads classes not located via Reflection.getCallerClass.
451     * @param lastLoader The ClassLoader that loaded the Class that called this Class.
452     * @param className The name of the Class.
453     * @return The Class object for the Class or null if it could not be located.
454     */
455    private Class<?> loadClass(final ClassLoader lastLoader, final String className) {
456        Class<?> clazz;
457        if (lastLoader != null) {
458            try {
459                clazz = lastLoader.loadClass(className);
460                if (clazz != null) {
461                    return clazz;
462                }
463            } catch (final Exception ex) {
464                // Ignore exception.
465            }
466        }
467        try {
468            clazz = Thread.currentThread().getContextClassLoader().loadClass(className);
469        } catch (final ClassNotFoundException e) {
470            try {
471                clazz = Class.forName(className);
472            } catch (final ClassNotFoundException e1) {
473                try {
474                    clazz = getClass().getClassLoader().loadClass(className);
475                } catch (final ClassNotFoundException e2) {
476                    return null;
477                }
478            }
479        }
480        return clazz;
481    }
482
483    public ThrowableProxy[] getSuppressed() {
484        if (GET_SUPPRESSED != null) {
485            try {
486                return (ThrowableProxy[]) GET_SUPPRESSED.invoke(throwable);
487            } catch (final Exception ignore) {
488                return null;
489            }
490        }
491        return null;
492    }
493
494    private void setSuppressed(final Throwable throwable) {
495        if (GET_SUPPRESSED != null && ADD_SUPPRESSED != null) {
496            try {
497                final Throwable[] array = (Throwable[]) GET_SUPPRESSED.invoke(throwable);
498                for (final Throwable t : array) {
499                    ADD_SUPPRESSED.invoke(this, new ThrowableProxy(t));
500                }
501            } catch (final Exception ignore) {
502                //
503            }
504        }
505    }
506
507    /**
508     * Cached StackTracePackageElement and the ClassLoader.
509     */
510    class CacheEntry {
511        private final StackTracePackageElement element;
512        private final ClassLoader loader;
513
514        public CacheEntry(final StackTracePackageElement element, final ClassLoader loader) {
515            this.element = element;
516            this.loader = loader;
517        }
518    }
519
520    /**
521     * Security Manager for accessing the call stack.
522     */
523    private static class PrivateSecurityManager extends SecurityManager {
524        public Class<?>[] getClasses() {
525            return getClassContext();
526        }
527    }
528}