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.Serializable;
|
---|
6 | import java.io.StringWriter;
|
---|
7 | import java.util.concurrent.CopyOnWriteArrayList;
|
---|
8 | import java.util.function.Predicate;
|
---|
9 |
|
---|
10 | import org.openstreetmap.josm.actions.ShowStatusReportAction;
|
---|
11 |
|
---|
12 | /**
|
---|
13 | * This class contains utility methods to create and handle a bug report.
|
---|
14 | * <p>
|
---|
15 | * It allows you to configure the format and request to send the bug report.
|
---|
16 | * <p>
|
---|
17 | * It also contains the main entry point for all components to use the bug report system: Call {@link #intercept(Throwable)} to start handling an
|
---|
18 | * exception.
|
---|
19 | * <h1> Handling Exceptions </h1>
|
---|
20 | * In your code, you should add try...catch blocks for any runtime exceptions that might happen. It is fine to catch throwable there.
|
---|
21 | * <p>
|
---|
22 | * 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
|
---|
23 | * or other local variables. Make sure that no excpetions may occur while computing the values. It is best to send plain local variables to
|
---|
24 | * put(...). If you need to do computations, put them into a lambda expression. Then simply throw the throwable you got from the bug report.
|
---|
25 | * The global exception handler will do the rest.
|
---|
26 | * <pre>
|
---|
27 | * int id = ...;
|
---|
28 | * String tag = "...";
|
---|
29 | * try {
|
---|
30 | * ... your code ...
|
---|
31 | * } catch (RuntimeException t) {
|
---|
32 | * throw BugReport.intercept(t).put("id", id).put("tag", () -> x.getTag());
|
---|
33 | * }
|
---|
34 | * </pre>
|
---|
35 | *
|
---|
36 | * Instead of re-throwing, you can call {@link ReportedException#warn()}. This will display a warning to the user and allow it to either report
|
---|
37 | * the exception or ignore it.
|
---|
38 | *
|
---|
39 | * @author Michael Zangl
|
---|
40 | * @since 10285
|
---|
41 | */
|
---|
42 | public final class BugReport implements Serializable {
|
---|
43 | private static final long serialVersionUID = 1L;
|
---|
44 |
|
---|
45 | private boolean includeStatusReport = true;
|
---|
46 | private boolean includeData = true;
|
---|
47 | private boolean includeAllStackTraces;
|
---|
48 | private final ReportedException exception;
|
---|
49 | private final CopyOnWriteArrayList<BugReportListener> listeners = new CopyOnWriteArrayList<>();
|
---|
50 |
|
---|
51 | /**
|
---|
52 | * Create a new bug report
|
---|
53 | * @param e The {@link ReportedException} to use. No more data should be added after creating the report.
|
---|
54 | */
|
---|
55 | public BugReport(ReportedException e) {
|
---|
56 | this.exception = e;
|
---|
57 | includeAllStackTraces = e.mayHaveConcurrentSource();
|
---|
58 | }
|
---|
59 |
|
---|
60 | /**
|
---|
61 | * Determines if this report should include a system status report
|
---|
62 | * @return <code>true</code> to include it.
|
---|
63 | * @since 10597
|
---|
64 | */
|
---|
65 | public boolean isIncludeStatusReport() {
|
---|
66 | return includeStatusReport;
|
---|
67 | }
|
---|
68 |
|
---|
69 | /**
|
---|
70 | * Set if this report should include a system status report
|
---|
71 | * @param includeStatusReport if the status report should be included
|
---|
72 | * @since 10585
|
---|
73 | */
|
---|
74 | public void setIncludeStatusReport(boolean includeStatusReport) {
|
---|
75 | this.includeStatusReport = includeStatusReport;
|
---|
76 | fireChange();
|
---|
77 | }
|
---|
78 |
|
---|
79 | /**
|
---|
80 | * Determines if this report should include the data that was traced.
|
---|
81 | * @return <code>true</code> to include it.
|
---|
82 | * @since 10597
|
---|
83 | */
|
---|
84 | public boolean isIncludeData() {
|
---|
85 | return includeData;
|
---|
86 | }
|
---|
87 |
|
---|
88 | /**
|
---|
89 | * Set if this report should include the data that was traced.
|
---|
90 | * @param includeData if data should be included
|
---|
91 | * @since 10585
|
---|
92 | */
|
---|
93 | public void setIncludeData(boolean includeData) {
|
---|
94 | this.includeData = includeData;
|
---|
95 | fireChange();
|
---|
96 | }
|
---|
97 |
|
---|
98 | /**
|
---|
99 | * 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) { // NOPMD
|
---|
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 | }
|
---|