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.io.StringWriter;
007import java.util.concurrent.CopyOnWriteArrayList;
008import java.util.function.Predicate;
009
010import org.openstreetmap.josm.actions.ShowStatusReportAction;
011
012/**
013 * This class contains utility methods to create and handle a bug report.
014 * <p>
015 * It allows you to configure the format and request to send the bug report.
016 * <p>
017 * It also contains the main entry point for all components to use the bug report system: Call {@link #intercept(Throwable)} to start handling an
018 * exception.
019 * <h1> Handling Exceptions </h1>
020 * In your code, you should add try...catch blocks for any runtime exceptions that might happen. It is fine to catch throwable there.
021 * <p>
022 * You should then add some debug information there. This can be the OSM ids that caused the error, information on the data you were working on
023 * or other local variables. Make sure that no excpetions may occur while computing the values. It is best to send plain local variables to
024 * put(...). If you need to do computations, put them into a lambda expression. Then simply throw the throwable you got from the bug report.
025 * The global exception handler will do the rest.
026 * <pre>
027 * int id = ...;
028 * String tag = "...";
029 * try {
030 *   ... your code ...
031 * } catch (RuntimeException t) {
032 *   throw BugReport.intercept(t).put("id", id).put("tag", () -&gt; x.getTag());
033 * }
034 * </pre>
035 *
036 * Instead of re-throwing, you can call {@link ReportedException#warn()}. This will display a warning to the user and allow it to either report
037 * the exception or ignore it.
038 *
039 * @author Michael Zangl
040 * @since 10285
041 */
042public final class BugReport implements Serializable {
043    private static final long serialVersionUID = 1L;
044
045    private boolean includeStatusReport = true;
046    private boolean includeData = true;
047    private boolean includeAllStackTraces;
048    private final ReportedException exception;
049    private final CopyOnWriteArrayList<BugReportListener> listeners = new CopyOnWriteArrayList<>();
050
051    /**
052     * Create a new bug report
053     * @param e The {@link ReportedException} to use. No more data should be added after creating the report.
054     */
055    BugReport(ReportedException e) {
056        this.exception = e;
057        includeAllStackTraces = e.mayHaveConcurrentSource();
058    }
059
060    /**
061     * Determines if this report should include a system status report
062     * @return <code>true</code> to include it.
063     * @since 10597
064     */
065    public boolean isIncludeStatusReport() {
066        return includeStatusReport;
067    }
068
069    /**
070     * Set if this report should include a system status report
071     * @param includeStatusReport if the status report should be included
072     * @since 10585
073     */
074    public void setIncludeStatusReport(boolean includeStatusReport) {
075        this.includeStatusReport = includeStatusReport;
076        fireChange();
077    }
078
079    /**
080     * Determines if this report should include the data that was traced.
081     * @return <code>true</code> to include it.
082     * @since 10597
083     */
084    public boolean isIncludeData() {
085        return includeData;
086    }
087
088    /**
089     * Set if this report should include the data that was traced.
090     * @param includeData if data should be included
091     * @since 10585
092     */
093    public void setIncludeData(boolean includeData) {
094        this.includeData = includeData;
095        fireChange();
096    }
097
098    /**
099     * Determines if this report should include the stack traces for all other threads.
100     * @return <code>true</code> to include it.
101     * @since 10597
102     */
103    public boolean isIncludeAllStackTraces() {
104        return includeAllStackTraces;
105    }
106
107    /**
108     * Sets if this report should include the stack traces for all other threads.
109     * @param includeAllStackTraces if all stack traces should be included
110     * @since 10585
111     */
112    public void setIncludeAllStackTraces(boolean includeAllStackTraces) {
113        this.includeAllStackTraces = includeAllStackTraces;
114        fireChange();
115    }
116
117    /**
118     * Gets the full string that should be send as error report.
119     * @return The string.
120     * @since 10585
121     */
122    public String getReportText() {
123        StringWriter stringWriter = new StringWriter();
124        PrintWriter out = new PrintWriter(stringWriter);
125        if (isIncludeStatusReport()) {
126            try {
127                out.println(ShowStatusReportAction.getReportHeader());
128            } catch (RuntimeException e) {
129                out.println("Could not generate status report: " + e.getMessage());
130            }
131        }
132        if (isIncludeData()) {
133            exception.printReportDataTo(out);
134        }
135        exception.printReportStackTo(out);
136        if (isIncludeAllStackTraces()) {
137            exception.printReportThreadsTo(out);
138        }
139        return stringWriter.toString().replaceAll("\r", "");
140    }
141
142    /**
143     * Add a new change listener.
144     * @param listener The listener
145     * @since 10585
146     */
147    public void addChangeListener(BugReportListener listener) {
148        listeners.add(listener);
149    }
150
151    /**
152     * Remove a change listener.
153     * @param listener The listener
154     * @since 10585
155     */
156    public void removeChangeListener(BugReportListener listener) {
157        listeners.remove(listener);
158    }
159
160    private void fireChange() {
161        listeners.stream().forEach(l -> l.bugReportChanged(this));
162    }
163
164    /**
165     * This should be called whenever you want to add more information to a given exception.
166     * @param t The throwable that was thrown.
167     * @return A {@link ReportedException} to which you can add additional information.
168     */
169    public static ReportedException intercept(Throwable t) {
170        ReportedException e;
171        if (t instanceof ReportedException) {
172            e = (ReportedException) t;
173        } else {
174            e = new ReportedException(t);
175        }
176        e.startSection(getCallingMethod(2));
177        return e;
178    }
179
180    /**
181     * Find the method that called us.
182     *
183     * @param offset
184     *            How many methods to look back in the stack trace. 1 gives the method calling this method, 0 gives you getCallingMethod().
185     * @return The method name.
186     */
187    public static String getCallingMethod(int offset) {
188        StackTraceElement found = getCallingMethod(offset + 1, BugReport.class.getName(), "getCallingMethod"::equals);
189        if (found != null) {
190            return found.getClassName().replaceFirst(".*\\.", "") + '#' + found.getMethodName();
191        } else {
192            return "?";
193        }
194    }
195
196    /**
197     * Find the method that called the given method on the current stack trace.
198     * @param offset
199     *           How many methods to look back in the stack trace.
200     *           1 gives the method calling this method, 0 gives you the method with the given name..
201     * @param className The name of the class to search for
202     * @param methodName The name of the method to search for
203     * @return The class and method name or null if it is unknown.
204     */
205    public static StackTraceElement getCallingMethod(int offset, String className, Predicate<String> methodName) {
206        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
207        for (int i = 0; i < stackTrace.length - offset; i++) {
208            StackTraceElement element = stackTrace[i];
209            if (className.equals(element.getClassName()) && methodName.test(element.getMethodName())) {
210                return stackTrace[i + offset];
211            }
212        }
213        return null;
214    }
215
216    /**
217     * A listener that listens to changes to this report.
218     * @author Michael Zangl
219     * @since 10585
220     */
221    @FunctionalInterface
222    public interface BugReportListener {
223        /**
224         * Called whenever this bug report was changed, e.g. the data to be included in it.
225         * @param report The report that was changed.
226         */
227        void bugReportChanged(BugReport report);
228    }
229}