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("Unable to detect latest version of JOSM: "+ex.getMessage()); 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("Unable to access JOSM website: "+ex.getMessage()); 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}