001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools.bugreport;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.awt.GraphicsEnvironment;
008import java.awt.GridBagConstraints;
009import java.awt.GridBagLayout;
010import java.io.IOException;
011import java.io.PrintWriter;
012import java.io.StringWriter;
013
014import javax.swing.JButton;
015import javax.swing.JCheckBox;
016import javax.swing.JLabel;
017import javax.swing.JOptionPane;
018import javax.swing.JPanel;
019import javax.swing.SwingUtilities;
020
021import org.openstreetmap.josm.Main;
022import org.openstreetmap.josm.actions.ReportBugAction;
023import org.openstreetmap.josm.actions.ShowStatusReportAction;
024import org.openstreetmap.josm.data.Version;
025import org.openstreetmap.josm.gui.ExtendedDialog;
026import org.openstreetmap.josm.gui.preferences.plugin.PluginPreference;
027import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
028import org.openstreetmap.josm.gui.widgets.UrlLabel;
029import org.openstreetmap.josm.plugins.PluginDownloadTask;
030import org.openstreetmap.josm.plugins.PluginHandler;
031import org.openstreetmap.josm.tools.GBC;
032import org.openstreetmap.josm.tools.WikiReader;
033
034/**
035 * An exception handler that asks the user to send a bug report.
036 *
037 * @author imi
038 * @since 40
039 */
040public final class BugReportExceptionHandler implements Thread.UncaughtExceptionHandler {
041
042    private static boolean handlingInProgress;
043    private static volatile BugReporterThread bugReporterThread;
044    private static int exceptionCounter;
045    private static boolean suppressExceptionDialogs;
046
047    static final class BugReporterThread extends Thread {
048
049        private final class BugReporterWorker implements Runnable {
050            private final PluginDownloadTask pluginDownloadTask;
051
052            private BugReporterWorker(PluginDownloadTask pluginDownloadTask) {
053                this.pluginDownloadTask = pluginDownloadTask;
054            }
055
056            @Override
057            public void run() {
058                // Then ask for submitting a bug report, for exceptions thrown from a plugin too, unless updated to a new version
059                if (pluginDownloadTask == null) {
060                    askForBugReport(e);
061                } else {
062                    // Ask for restart to install new plugin
063                    PluginPreference.notifyDownloadResults(
064                            Main.parent, pluginDownloadTask, !pluginDownloadTask.getDownloadedPlugins().isEmpty());
065                }
066            }
067        }
068
069        private final Throwable e;
070
071        /**
072         * Constructs a new {@code BugReporterThread}.
073         * @param t the exception
074         */
075        private BugReporterThread(Throwable t) {
076            super("Bug Reporter");
077            this.e = t;
078        }
079
080        static void askForBugReport(final Throwable e) {
081            String[] buttonTexts = new String[] {tr("Do nothing"), tr("Report Bug")};
082            String[] buttonIcons = new String[] {"cancel", "bug"};
083            int defaultButtonIdx = 1;
084            String message = tr("An unexpected exception occurred.<br>" +
085                    "This is always a coding error. If you are running the latest<br>" +
086                    "version of JOSM, please consider being kind and file a bug report."
087                    );
088            // Check user is running current tested version, the error may already be fixed
089            int josmVersion = Version.getInstance().getVersion();
090            if (josmVersion != Version.JOSM_UNKNOWN_VERSION) {
091                try {
092                    int latestVersion = Integer.parseInt(new WikiReader().
093                            read(Main.getJOSMWebsite()+"/wiki/TestedVersion?format=txt").trim());
094                    if (latestVersion > josmVersion) {
095                        buttonTexts = new String[] {tr("Do nothing"), tr("Update JOSM"), tr("Report Bug")};
096                        buttonIcons = new String[] {"cancel", "download", "bug"};
097                        defaultButtonIdx = 2;
098                        message = tr("An unexpected exception occurred. This is always a coding error.<br><br>" +
099                                "However, you are running an old version of JOSM ({0}),<br>" +
100                                "instead of using the current tested version (<b>{1}</b>).<br><br>"+
101                                "<b>Please update JOSM</b> before considering to file a bug report.",
102                                String.valueOf(josmVersion), String.valueOf(latestVersion));
103                    }
104                } catch (IOException | NumberFormatException ex) {
105                    Main.warn(ex, "Unable to detect latest version of JOSM:");
106                }
107            }
108            // Build panel
109            JPanel pnl = new JPanel(new GridBagLayout());
110            pnl.add(new JLabel("<html>" + message + "</html>"), GBC.eol());
111            JCheckBox cbSuppress = null;
112            if (exceptionCounter > 1) {
113                cbSuppress = new JCheckBox(tr("Suppress further error dialogs for this session."));
114                pnl.add(cbSuppress, GBC.eol());
115            }
116            if (GraphicsEnvironment.isHeadless()) {
117                return;
118            }
119            // Show dialog
120            ExtendedDialog ed = new ExtendedDialog(Main.parent, tr("Unexpected Exception"), buttonTexts);
121            ed.setButtonIcons(buttonIcons);
122            ed.setIcon(JOptionPane.ERROR_MESSAGE);
123            ed.setCancelButton(1);
124            ed.setDefaultButton(defaultButtonIdx);
125            ed.setContent(pnl);
126            ed.setFocusOnDefaultButton(true);
127            ed.showDialog();
128            if (cbSuppress != null && cbSuppress.isSelected()) {
129                suppressExceptionDialogs = true;
130            }
131            if (ed.getValue() <= 1) {
132                // "Do nothing"
133                return;
134            } else if (ed.getValue() < buttonTexts.length) {
135                // "Update JOSM"
136                try {
137                    Main.platform.openUrl(Main.getJOSMWebsite());
138                } catch (IOException ex) {
139                    Main.warn(ex, "Unable to access JOSM website:");
140                }
141            } else {
142                // "Report bug"
143                try {
144                    JPanel p = buildPanel(e);
145                    JOptionPane.showMessageDialog(Main.parent, p, tr("You have encountered a bug in JOSM"), JOptionPane.ERROR_MESSAGE);
146                } catch (RuntimeException ex) {
147                    Main.error(ex);
148                }
149            }
150        }
151
152        @Override
153        public void run() {
154            // Give the user a chance to deactivate the plugin which threw the exception (if it was thrown from a plugin)
155            SwingUtilities.invokeLater(new BugReporterWorker(PluginHandler.updateOrdisablePluginAfterException(e)));
156        }
157    }
158
159    @Override
160    public void uncaughtException(Thread t, Throwable e) {
161        handleException(e);
162    }
163
164    /**
165     * Handles the given exception
166     * @param e the exception
167     */
168    public static synchronized void handleException(final Throwable e) {
169        if (handlingInProgress || suppressExceptionDialogs)
170            return;                  // we do not handle secondary exceptions, this gets too messy
171        if (bugReporterThread != null && bugReporterThread.isAlive())
172            return;
173        handlingInProgress = true;
174        exceptionCounter++;
175        try {
176            Main.error(e);
177            if (Main.parent != null) {
178                if (e instanceof OutOfMemoryError) {
179                    // do not translate the string, as translation may raise an exception
180                    JOptionPane.showMessageDialog(Main.parent, "JOSM is out of memory. " +
181                            "Strange things may happen.\nPlease restart JOSM with the -Xmx###M option,\n" +
182                            "where ### is the number of MB assigned to JOSM (e.g. 256).\n" +
183                            "Currently, " + Runtime.getRuntime().maxMemory()/1024/1024 + " MB are available to JOSM.",
184                            "Error",
185                            JOptionPane.ERROR_MESSAGE
186                            );
187                    return;
188                }
189
190                bugReporterThread = new BugReporterThread(e);
191                bugReporterThread.start();
192            }
193        } finally {
194            handlingInProgress = false;
195        }
196    }
197
198    static JPanel buildPanel(final Throwable e) {
199        StringWriter stack = new StringWriter();
200        PrintWriter writer = new PrintWriter(stack);
201        if (e instanceof ReportedException) {
202            // Temporary!
203            ((ReportedException) e).printReportDataTo(writer);
204            ((ReportedException) e).printReportStackTo(writer);
205        } else {
206            e.printStackTrace(writer);
207        }
208
209        String text = ShowStatusReportAction.getReportHeader() + stack.getBuffer().toString();
210        text = text.replaceAll("\r", "");
211
212        JPanel p = new JPanel(new GridBagLayout());
213        p.add(new JMultilineLabel(
214                tr("You have encountered an error in JOSM. Before you file a bug report " +
215                        "make sure you have updated to the latest version of JOSM here:")),
216                        GBC.eol().fill(GridBagConstraints.HORIZONTAL));
217        p.add(new UrlLabel(Main.getJOSMWebsite(), 2), GBC.eop().insets(8, 0, 0, 0));
218        p.add(new JMultilineLabel(
219                tr("You should also update your plugins. If neither of those help please " +
220                        "file a bug report in our bugtracker using this link:")),
221                        GBC.eol().fill(GridBagConstraints.HORIZONTAL));
222        p.add(new JButton(new ReportBugAction(text)), GBC.eop().insets(8, 0, 0, 0));
223        p.add(new JMultilineLabel(
224                tr("There the error information provided below should already be " +
225                        "filled in for you. Please include information on how to reproduce " +
226                        "the error and try to supply as much detail as possible.")),
227                        GBC.eop().fill(GridBagConstraints.HORIZONTAL));
228        p.add(new JMultilineLabel(
229                tr("Alternatively, if that does not work you can manually fill in the information " +
230                        "below at this URL:")), GBC.eol().fill(GridBagConstraints.HORIZONTAL));
231        p.add(new UrlLabel(Main.getJOSMWebsite()+"/newticket", 2), GBC.eop().insets(8, 0, 0, 0));
232
233        // Wiki formatting for manual copy-paste
234        DebugTextDisplay textarea = new DebugTextDisplay(text);
235
236        if (textarea.copyToClippboard()) {
237            p.add(new JLabel(tr("(The text has already been copied to your clipboard.)")),
238                    GBC.eop().fill(GridBagConstraints.HORIZONTAL));
239        }
240
241        p.add(textarea, GBC.eop().fill());
242
243        for (Component c: p.getComponents()) {
244            if (c instanceof JMultilineLabel) {
245                ((JMultilineLabel) c).setMaxWidth(400);
246            }
247        }
248        return p;
249    }
250
251    /**
252     * Determines if an exception is currently being handled
253     * @return {@code true} if an exception is currently being handled, {@code false} otherwise
254     */
255    public static boolean exceptionHandlingInProgress() {
256        return handlingInProgress;
257    }
258}