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