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.GridBagConstraints; 007import java.awt.GridBagLayout; 008import java.io.IOException; 009import java.io.InputStream; 010import java.net.URL; 011import java.net.URLEncoder; 012import java.nio.ByteBuffer; 013import java.nio.CharBuffer; 014import java.nio.charset.Charset; 015import java.nio.charset.StandardCharsets; 016 017import javax.swing.JOptionPane; 018import javax.swing.JPanel; 019import javax.swing.SwingUtilities; 020import javax.xml.parsers.ParserConfigurationException; 021import javax.xml.xpath.XPath; 022import javax.xml.xpath.XPathConstants; 023import javax.xml.xpath.XPathExpressionException; 024import javax.xml.xpath.XPathFactory; 025 026import org.openstreetmap.josm.Main; 027import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 028import org.openstreetmap.josm.gui.widgets.UrlLabel; 029import org.openstreetmap.josm.tools.Base64; 030import org.openstreetmap.josm.tools.GBC; 031import org.openstreetmap.josm.tools.HttpClient; 032import org.openstreetmap.josm.tools.HttpClient.Response; 033import org.openstreetmap.josm.tools.OpenBrowser; 034import org.openstreetmap.josm.tools.Utils; 035import org.w3c.dom.Document; 036import org.xml.sax.SAXException; 037 038/** 039 * This class handles sending the bug report to JOSM website. 040 * <p> 041 * Currently, we try to open a browser window for the user that displays the bug report. 042 * 043 * @author Michael Zangl 044 * @since 10055 045 */ 046public class BugReportSender extends Thread { 047 048 private final String statusText; 049 private String errorMessage; 050 051 /** 052 * Creates a new sender. 053 * @param statusText The status text to send. 054 */ 055 protected BugReportSender(String statusText) { 056 super("Bug report sender"); 057 this.statusText = statusText; 058 } 059 060 @Override 061 public void run() { 062 try { 063 // first, send the debug text using post. 064 String debugTextPasteId = pasteDebugText(); 065 066 // then open a browser to display the pasted text. 067 String openBrowserError = OpenBrowser.displayUrl(getJOSMTicketURL() + "?pdata_stored=" + debugTextPasteId); 068 if (openBrowserError != null) { 069 Main.warn(openBrowserError); 070 failed(openBrowserError); 071 } 072 } catch (BugReportSenderException e) { 073 Main.warn(e); 074 failed(e.getMessage()); 075 } 076 } 077 078 /** 079 * Sends the debug text to the server. 080 * @return The token which was returned by the server. We need to pass this on to the ticket system. 081 * @throws BugReportSenderException if sending the report failed. 082 */ 083 private String pasteDebugText() throws BugReportSenderException { 084 try { 085 String text = Utils.strip(statusText); 086 ByteBuffer buffer = Charset.forName("UTF-8").encode(CharBuffer.wrap(text)); 087 String pdata = Base64.encode(buffer, false); 088 String postQuery = "pdata=" + URLEncoder.encode(pdata, "UTF-8"); 089 HttpClient client = HttpClient.create(new URL(getJOSMTicketURL()), "POST") 090 .setHeader("Content-Type", "application/x-www-form-urlencoded") 091 .setRequestBody(postQuery.getBytes(StandardCharsets.UTF_8)); 092 093 Response connection = client.connect(); 094 095 if (connection.getResponseCode() >= 500) { 096 throw new BugReportSenderException("Internal server error."); 097 } 098 099 try (InputStream in = connection.getContent()) { 100 return retrieveDebugToken(Utils.parseSafeDOM(in)); 101 } 102 } catch (IOException | SAXException | ParserConfigurationException | XPathExpressionException t) { 103 throw new BugReportSenderException(t); 104 } 105 } 106 107 private static String getJOSMTicketURL() { 108 return Main.getJOSMWebsite() + "/josmticket"; 109 } 110 111 private static String retrieveDebugToken(Document document) throws XPathExpressionException, BugReportSenderException { 112 XPathFactory factory = XPathFactory.newInstance(); 113 XPath xpath = factory.newXPath(); 114 String status = (String) xpath.compile("/josmticket/@status").evaluate(document, XPathConstants.STRING); 115 if (!"ok".equals(status)) { 116 String message = (String) xpath.compile("/josmticket/error/text()").evaluate(document, 117 XPathConstants.STRING); 118 if (message.isEmpty()) { 119 message = "Error in server response but server did not tell us what happened."; 120 } 121 throw new BugReportSenderException(message); 122 } 123 124 String token = (String) xpath.compile("/josmticket/preparedid/text()") 125 .evaluate(document, XPathConstants.STRING); 126 if (token.isEmpty()) { 127 throw new BugReportSenderException("Server did not respond with a prepared id."); 128 } 129 return token; 130 } 131 132 private void failed(String string) { 133 errorMessage = string; 134 SwingUtilities.invokeLater(new Runnable() { 135 @Override 136 public void run() { 137 JPanel errorPanel = new JPanel(new GridBagLayout()); 138 errorPanel.add(new JMultilineLabel( 139 tr("Opening the bug report failed. Please report manually using this website:")), 140 GBC.eol().fill(GridBagConstraints.HORIZONTAL)); 141 errorPanel.add(new UrlLabel(Main.getJOSMWebsite() + "/newticket", 2), GBC.eop().insets(8, 0, 0, 0)); 142 errorPanel.add(new DebugTextDisplay(statusText)); 143 144 JOptionPane.showMessageDialog(Main.parent, errorPanel, tr("You have encountered a bug in JOSM"), 145 JOptionPane.ERROR_MESSAGE); 146 } 147 }); 148 } 149 150 /** 151 * Returns the error message that could have occured during bug sending. 152 * @return the error message, or {@code null} if successful 153 */ 154 public final String getErrorMessage() { 155 return errorMessage; 156 } 157 158 private static class BugReportSenderException extends Exception { 159 BugReportSenderException(String message) { 160 super(message); 161 } 162 163 BugReportSenderException(Throwable cause) { 164 super(cause); 165 } 166 } 167 168 /** 169 * Opens the bug report window on the JOSM server. 170 * @param statusText The status text to send along to the server. 171 * @return bug report sender started thread 172 */ 173 public static BugReportSender reportBug(String statusText) { 174 BugReportSender sender = new BugReportSender(statusText); 175 sender.start(); 176 return sender; 177 } 178}