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