1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.tools.bugreport;
|
---|
3 |
|
---|
4 | import java.io.PrintWriter;
|
---|
5 | import java.io.StringWriter;
|
---|
6 | import java.util.concurrent.CopyOnWriteArrayList;
|
---|
7 |
|
---|
8 | import org.openstreetmap.josm.actions.ShowStatusReportAction;
|
---|
9 |
|
---|
10 | /**
|
---|
11 | * This class contains utility methods to create and handle a bug report.
|
---|
12 | * <p>
|
---|
13 | * It allows you to configure the format and request to send the bug report.
|
---|
14 | * <p>
|
---|
15 | * It also contains the main entry point for all components to use the bug report system: Call {@link #intercept(Throwable)} to start handling an
|
---|
16 | * exception.
|
---|
17 | * <h1> Handling Exceptions </h1>
|
---|
18 | * In your code, you should add try...catch blocks for any runtime exceptions that might happen. It is fine to catch throwable there.
|
---|
19 | * <p>
|
---|
20 | * 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
|
---|
21 | * or other local variables. Make sure that no excpetions may occur while computing the values. It is best to send plain local variables to
|
---|
22 | * put(...). If you need to do computations, put them into a lambda expression. Then simply throw the throwable you got from the bug report.
|
---|
23 | * The global exception handler will do the rest.
|
---|
24 | * <pre>
|
---|
25 | * int id = ...;
|
---|
26 | * String tag = "...";
|
---|
27 | * try {
|
---|
28 | * ... your code ...
|
---|
29 | * } catch (RuntimeException t) {
|
---|
30 | * throw BugReport.intercept(t).put("id", id).put("tag", () -> x.getTag());
|
---|
31 | * }
|
---|
32 | * </pre>
|
---|
33 | *
|
---|
34 | * Instead of re-throwing, you can call {@link ReportedException#warn()}. This will display a warning to the user and allow it to either report
|
---|
35 | * the exception or ignore it.
|
---|
36 | *
|
---|
37 | * @author Michael Zangl
|
---|
38 | * @since 10285
|
---|
39 | */
|
---|
40 | public final class BugReport {
|
---|
41 | private boolean includeStatusReport = true;
|
---|
42 | private boolean includeData = true;
|
---|
43 | private boolean includeAllStackTraces;
|
---|
44 | private ReportedException exception;
|
---|
45 | private final CopyOnWriteArrayList<BugReportListener> listeners = new CopyOnWriteArrayList<>();
|
---|
46 |
|
---|
47 | /**
|
---|
48 | * Create a new bug report
|
---|
49 | * @param e The {@link ReportedException} to use. No more data should be added after creating the report.
|
---|
50 | */
|
---|
51 | BugReport(ReportedException e) {
|
---|
52 | this.exception = e;
|
---|
53 | includeAllStackTraces = e.mayHaveConcurrentSource();
|
---|
54 | }
|
---|
55 |
|
---|
56 | /**
|
---|
57 | * Determines if this report should include a system status report
|
---|
58 | * @return <code>true</code> to include it.
|
---|
59 | * @since 10597
|
---|
60 | */
|
---|
61 | public boolean isIncludeStatusReport() {
|
---|
62 | return includeStatusReport;
|
---|
63 | }
|
---|
64 |
|
---|
65 | /**
|
---|
66 | * Set if this report should include a system status report
|
---|
67 | * @param includeStatusReport if the status report should be included
|
---|
68 | * @since 10585
|
---|
69 | */
|
---|
70 | public void setIncludeStatusReport(boolean includeStatusReport) {
|
---|
71 | this.includeStatusReport = includeStatusReport;
|
---|
72 | fireChange();
|
---|
73 | }
|
---|
74 |
|
---|
75 | /**
|
---|
76 | * Determines if this report should include the data that was traced.
|
---|
77 | * @return <code>true</code> to include it.
|
---|
78 | * @since 10597
|
---|
79 | */
|
---|
80 | public boolean isIncludeData() {
|
---|
81 | return includeData;
|
---|
82 | }
|
---|
83 |
|
---|
84 | /**
|
---|
85 | * Set if this report should include the data that was traced.
|
---|
86 | * @param includeData if data should be included
|
---|
87 | * @since 10585
|
---|
88 | */
|
---|
89 | public void setIncludeData(boolean includeData) {
|
---|
90 | this.includeData = includeData;
|
---|
91 | fireChange();
|
---|
92 | }
|
---|
93 |
|
---|
94 | /**
|
---|
95 | * Determines if this report should include the stack traces for all other threads.
|
---|
96 | * @return <code>true</code> to include it.
|
---|
97 | * @since 10597
|
---|
98 | */
|
---|
99 | public boolean isIncludeAllStackTraces() {
|
---|
100 | return includeAllStackTraces;
|
---|
101 | }
|
---|
102 |
|
---|
103 | /**
|
---|
104 | * Sets if this report should include the stack traces for all other threads.
|
---|
105 | * @param includeAllStackTraces if all stack traces should be included
|
---|
106 | * @since 10585
|
---|
107 | */
|
---|
108 | public void setIncludeAllStackTraces(boolean includeAllStackTraces) {
|
---|
109 | this.includeAllStackTraces = includeAllStackTraces;
|
---|
110 | fireChange();
|
---|
111 | }
|
---|
112 |
|
---|
113 | /**
|
---|
114 | * Gets the full string that should be send as error report.
|
---|
115 | * @return The string.
|
---|
116 | * @since 10585
|
---|
117 | */
|
---|
118 | public String getReportText() {
|
---|
119 | StringWriter stringWriter = new StringWriter();
|
---|
120 | PrintWriter out = new PrintWriter(stringWriter);
|
---|
121 | if (isIncludeStatusReport()) {
|
---|
122 | out.println(ShowStatusReportAction.getReportHeader());
|
---|
123 | }
|
---|
124 | if (isIncludeData()) {
|
---|
125 | exception.printReportDataTo(out);
|
---|
126 | }
|
---|
127 | exception.printReportStackTo(out);
|
---|
128 | if (isIncludeAllStackTraces()) {
|
---|
129 | exception.printReportThreadsTo(out);
|
---|
130 | }
|
---|
131 | return stringWriter.toString().replaceAll("\r", "");
|
---|
132 | }
|
---|
133 |
|
---|
134 | /**
|
---|
135 | * Add a new change listener.
|
---|
136 | * @param listener The listener
|
---|
137 | * @since 10585
|
---|
138 | */
|
---|
139 | public void addChangeListener(BugReportListener listener) {
|
---|
140 | listeners.add(listener);
|
---|
141 | }
|
---|
142 |
|
---|
143 | /**
|
---|
144 | * Remove a change listener.
|
---|
145 | * @param listener The listener
|
---|
146 | * @since 10585
|
---|
147 | */
|
---|
148 | public void removeChangeListener(BugReportListener listener) {
|
---|
149 | listeners.remove(listener);
|
---|
150 | }
|
---|
151 |
|
---|
152 | private void fireChange() {
|
---|
153 | listeners.stream().forEach(l -> l.bugReportChanged(this));
|
---|
154 | }
|
---|
155 |
|
---|
156 | /**
|
---|
157 | * This should be called whenever you want to add more information to a given exception.
|
---|
158 | * @param t The throwable that was thrown.
|
---|
159 | * @return A {@link ReportedException} to which you can add additional information.
|
---|
160 | */
|
---|
161 | public static ReportedException intercept(Throwable t) {
|
---|
162 | ReportedException e;
|
---|
163 | if (t instanceof ReportedException) {
|
---|
164 | e = (ReportedException) t;
|
---|
165 | } else {
|
---|
166 | e = new ReportedException(t);
|
---|
167 | }
|
---|
168 | e.startSection(getCallingMethod(2));
|
---|
169 | return e;
|
---|
170 | }
|
---|
171 |
|
---|
172 | /**
|
---|
173 | * Find the method that called us.
|
---|
174 | *
|
---|
175 | * @param offset
|
---|
176 | * How many methods to look back in the stack trace. 1 gives the method calling this method, 0 gives you getCallingMethod().
|
---|
177 | * @return The method name.
|
---|
178 | */
|
---|
179 | public static String getCallingMethod(int offset) {
|
---|
180 | StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
---|
181 | String className = BugReport.class.getName();
|
---|
182 | for (int i = 0; i < stackTrace.length - offset; i++) {
|
---|
183 | StackTraceElement element = stackTrace[i];
|
---|
184 | if (className.equals(element.getClassName()) && "getCallingMethod".equals(element.getMethodName())) {
|
---|
185 | StackTraceElement toReturn = stackTrace[i + offset];
|
---|
186 | return toReturn.getClassName().replaceFirst(".*\\.", "") + '#' + toReturn.getMethodName();
|
---|
187 | }
|
---|
188 | }
|
---|
189 | return "?";
|
---|
190 | }
|
---|
191 |
|
---|
192 | /**
|
---|
193 | * A listener that listens to changes to this report.
|
---|
194 | * @author Michael Zangl
|
---|
195 | * @since 10585
|
---|
196 | */
|
---|
197 | @FunctionalInterface
|
---|
198 | public interface BugReportListener {
|
---|
199 | /**
|
---|
200 | * Called whenever this bug report was changed, e.g. the data to be included in it.
|
---|
201 | * @param report The report that was changed.
|
---|
202 | */
|
---|
203 | void bugReportChanged(BugReport report);
|
---|
204 | }
|
---|
205 | }
|
---|