001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools.bugreport;
003
004import java.io.PrintWriter;
005import java.io.Serializable;
006import java.lang.reflect.InvocationTargetException;
007import java.util.ArrayList;
008import java.util.Arrays;
009import java.util.Collection;
010import java.util.Collections;
011import java.util.ConcurrentModificationException;
012import java.util.HashMap;
013import java.util.IdentityHashMap;
014import java.util.Iterator;
015import java.util.LinkedList;
016import java.util.Map;
017import java.util.Map.Entry;
018import java.util.NoSuchElementException;
019import java.util.Set;
020import java.util.function.Supplier;
021
022import org.openstreetmap.josm.tools.Logging;
023import org.openstreetmap.josm.tools.StreamUtils;
024
025/**
026 * This is a special exception that cannot be directly thrown.
027 * <p>
028 * It is used to capture more information about an exception that was already thrown.
029 *
030 * @author Michael Zangl
031 * @see BugReport
032 * @since 10285
033 */
034public class ReportedException extends RuntimeException {
035    /**
036     * How many entries of a collection to include in the bug report.
037     */
038    private static final int MAX_COLLECTION_ENTRIES = 30;
039
040    private static final long serialVersionUID = 737333873766201033L;
041
042    /**
043     * We capture all stack traces on exception creation. This allows us to trace synchonization problems better.
044     * We cannot be really sure what happened but we at least see which threads
045     */
046    private final transient Map<Thread, StackTraceElement[]> allStackTraces = new HashMap<>();
047    private final LinkedList<Section> sections = new LinkedList<>();
048    private final transient Thread caughtOnThread;
049    private String methodWarningFrom;
050
051    /**
052     * Constructs a new {@code ReportedException}.
053     * @param exception the cause (which is saved for later retrieval by the {@link #getCause()} method)
054     * @since 14380
055     */
056    public ReportedException(Throwable exception) {
057        this(exception, Thread.currentThread());
058    }
059
060    /**
061     * Constructs a new {@code ReportedException}.
062     * @param exception the cause (which is saved for later retrieval by the {@link #getCause()} method)
063     * @param caughtOnThread thread where the exception was caugth
064     * @since 14380
065     */
066    public ReportedException(Throwable exception, Thread caughtOnThread) {
067        super(exception);
068
069        try {
070            allStackTraces.putAll(Thread.getAllStackTraces());
071        } catch (SecurityException e) {
072            Logging.log(Logging.LEVEL_ERROR, "Unable to get thread stack traces", e);
073        }
074        this.caughtOnThread = caughtOnThread;
075    }
076
077    /**
078     * Displays a warning for this exception. The program can then continue normally. Does not block.
079     */
080    public void warn() {
081        methodWarningFrom = BugReport.getCallingMethod(2);
082        try {
083            BugReportQueue.getInstance().submit(this);
084        } catch (RuntimeException e) { // NOPMD
085            Logging.error(e);
086        }
087    }
088
089    /**
090     * Starts a new debug data section. This normally does not need to be called manually.
091     *
092     * @param sectionName
093     *            The section name.
094     */
095    public void startSection(String sectionName) {
096        sections.add(new Section(sectionName));
097    }
098
099    /**
100     * Prints the captured data of this report to a {@link PrintWriter}.
101     *
102     * @param out
103     *            The writer to print to.
104     */
105    public void printReportDataTo(PrintWriter out) {
106        out.println("=== REPORTED CRASH DATA ===");
107        for (Section s : sections) {
108            s.printSection(out);
109            out.println();
110        }
111
112        if (methodWarningFrom != null) {
113            out.println("Warning issued by: " + methodWarningFrom);
114            out.println();
115        }
116    }
117
118    /**
119     * Prints the stack trace of this report to a {@link PrintWriter}.
120     *
121     * @param out
122     *            The writer to print to.
123     */
124    public void printReportStackTo(PrintWriter out) {
125        out.println("=== STACK TRACE ===");
126        out.println(niceThreadName(caughtOnThread));
127        getCause().printStackTrace(out);
128        out.println();
129    }
130
131    /**
132     * Prints the stack traces for other threads of this report to a {@link PrintWriter}.
133     *
134     * @param out
135     *            The writer to print to.
136     */
137    public void printReportThreadsTo(PrintWriter out) {
138        out.println("=== RUNNING THREADS ===");
139        for (Entry<Thread, StackTraceElement[]> thread : allStackTraces.entrySet()) {
140            out.println(niceThreadName(thread.getKey()));
141            if (caughtOnThread.equals(thread.getKey())) {
142                out.println("Stacktrace see above.");
143            } else {
144                for (StackTraceElement e : thread.getValue()) {
145                    out.println(e);
146                }
147            }
148            out.println();
149        }
150    }
151
152    private static String niceThreadName(Thread thread) {
153        StringBuilder name = new StringBuilder("Thread: ").append(thread.getName()).append(" (").append(thread.getId()).append(')');
154        ThreadGroup threadGroup = thread.getThreadGroup();
155        if (threadGroup != null) {
156            name.append(" of ").append(threadGroup.getName());
157        }
158        return name.toString();
159    }
160
161    /**
162     * Checks if this exception is considered the same as an other exception. This is the case if both have the same cause and message.
163     *
164     * @param e
165     *            The exception to check against.
166     * @return <code>true</code> if they are considered the same.
167     */
168    public boolean isSame(ReportedException e) {
169        if (!getMessage().equals(e.getMessage())) {
170            return false;
171        }
172
173        return hasSameStackTrace(new CauseTraceIterator(), e.getCause());
174    }
175
176    private static boolean hasSameStackTrace(CauseTraceIterator causeTraceIterator, Throwable e2) {
177        if (!causeTraceIterator.hasNext()) {
178            // all done.
179            return true;
180        }
181        Throwable e1 = causeTraceIterator.next();
182        StackTraceElement[] t1 = e1.getStackTrace();
183        StackTraceElement[] t2 = e2.getStackTrace();
184
185        if (!Arrays.equals(t1, t2)) {
186            return false;
187        }
188
189        Throwable c1 = e1.getCause();
190        Throwable c2 = e2.getCause();
191        if ((c1 == null) != (c2 == null)) {
192            return false;
193        } else if (c1 != null) {
194            return hasSameStackTrace(causeTraceIterator, c2);
195        } else {
196            return true;
197        }
198    }
199
200    /**
201     * Adds some debug values to this exception. The value is converted to a string. Errors during conversion are handled.
202     *
203     * @param key
204     *            The key to add this for. Does not need to be unique but it would be nice.
205     * @param value
206     *            The value.
207     * @return This exception for easy chaining.
208     */
209    public ReportedException put(String key, Object value) {
210        return put(key, () -> value);
211    }
212
213    /**
214    * Adds some debug values to this exception. This method automatically catches errors that occur during the production of the value.
215    *
216    * @param key
217    *            The key to add this for. Does not need to be unique but it would be nice.
218    * @param valueSupplier
219    *            A supplier that is called once to get the value.
220    * @return This exception for easy chaining.
221    * @since 10586
222    */
223    public ReportedException put(String key, Supplier<Object> valueSupplier) {
224        String string;
225        try {
226            Object value = valueSupplier.get();
227            if (value == null) {
228                string = "null";
229            } else if (value instanceof Collection) {
230                string = makeCollectionNice((Collection<?>) value);
231            } else if (value.getClass().isArray()) {
232                string = makeCollectionNice(Arrays.asList(value));
233            } else {
234                string = value.toString();
235            }
236        } catch (RuntimeException t) { // NOPMD
237            Logging.warn(t);
238            string = "<Error calling toString()>";
239        }
240        sections.getLast().put(key, string);
241        return this;
242    }
243
244    private static String makeCollectionNice(Collection<?> value) {
245        int lines = 0;
246        StringBuilder str = new StringBuilder(32);
247        for (Object e : value) {
248            str.append("\n    - ");
249            if (lines <= MAX_COLLECTION_ENTRIES) {
250                str.append(e);
251            } else {
252                str.append("\n    ... (")
253                   .append(value.size())
254                   .append(" entries)");
255                break;
256            }
257        }
258        return str.toString();
259    }
260
261    @Override
262    public String toString() {
263        return "ReportedException [thread=" + caughtOnThread + ", exception=" + getCause()
264                + ", methodWarningFrom=" + methodWarningFrom + ']';
265    }
266
267    /**
268     * Check if this exception may be caused by a threading issue.
269     * @return <code>true</code> if it is.
270     * @since 10585
271     */
272    public boolean mayHaveConcurrentSource() {
273        return StreamUtils.toStream(CauseTraceIterator::new)
274                .anyMatch(t -> t instanceof ConcurrentModificationException || t instanceof InvocationTargetException);
275    }
276
277    /**
278     * Check if this is caused by an out of memory situaition
279     * @return <code>true</code> if it is.
280     * @since 10819
281     */
282    public boolean isOutOfMemory() {
283        return StreamUtils.toStream(CauseTraceIterator::new).anyMatch(t -> t instanceof OutOfMemoryError);
284    }
285
286    /**
287     * Iterates over the causes for this exception. Ignores cycles and aborts iteration then.
288     * @author Michal Zangl
289     * @since 10585
290     */
291    private final class CauseTraceIterator implements Iterator<Throwable> {
292        private Throwable current = getCause();
293        private final Set<Throwable> dejaVu = Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
294
295        @Override
296        public boolean hasNext() {
297            return current != null;
298        }
299
300        @Override
301        public Throwable next() {
302            if (!hasNext()) {
303                throw new NoSuchElementException();
304            }
305            Throwable toReturn = current;
306            advance();
307            return toReturn;
308        }
309
310        private void advance() {
311            dejaVu.add(current);
312            current = current.getCause();
313            if (current != null && dejaVu.contains(current)) {
314                current = null;
315            }
316        }
317    }
318
319    private static class SectionEntry implements Serializable {
320
321        private static final long serialVersionUID = 1L;
322
323        private final String key;
324        private final String value;
325
326        SectionEntry(String key, String value) {
327            this.key = key;
328            this.value = value;
329        }
330
331        /**
332         * Prints this entry to the output stream in a line.
333         * @param out The stream to print to.
334         */
335        public void print(PrintWriter out) {
336            out.print(" - ");
337            out.print(key);
338            out.print(": ");
339            out.println(value);
340        }
341    }
342
343    private static class Section implements Serializable {
344
345        private static final long serialVersionUID = 1L;
346
347        private final String sectionName;
348        private final ArrayList<SectionEntry> entries = new ArrayList<>();
349
350        Section(String sectionName) {
351            this.sectionName = sectionName;
352        }
353
354        /**
355         * Add a key/value entry to this section.
356         * @param key The key. Need not be unique.
357         * @param value The value.
358         */
359        public void put(String key, String value) {
360            entries.add(new SectionEntry(key, value));
361        }
362
363        /**
364         * Prints this section to the output stream.
365         * @param out The stream to print to.
366         */
367        public void printSection(PrintWriter out) {
368            out.println(sectionName + ':');
369            if (entries.isEmpty()) {
370                out.println("No data collected.");
371            } else {
372                for (SectionEntry e : entries) {
373                    e.print(out);
374                }
375            }
376        }
377    }
378}