001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.tools.bugreport; 003 004import java.io.IOException; 005import java.io.InputStream; 006import java.net.URL; 007import java.net.URLEncoder; 008import java.nio.charset.StandardCharsets; 009import java.util.Base64; 010import java.util.Objects; 011 012import javax.xml.parsers.ParserConfigurationException; 013import javax.xml.xpath.XPath; 014import javax.xml.xpath.XPathConstants; 015import javax.xml.xpath.XPathExpressionException; 016import javax.xml.xpath.XPathFactory; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.tools.HttpClient; 020import org.openstreetmap.josm.tools.HttpClient.Response; 021import org.openstreetmap.josm.tools.Logging; 022import org.openstreetmap.josm.tools.OpenBrowser; 023import org.openstreetmap.josm.tools.Utils; 024import org.w3c.dom.Document; 025import org.xml.sax.SAXException; 026 027/** 028 * This class handles sending the bug report to JOSM website. 029 * <p> 030 * Currently, we try to open a browser window for the user that displays the bug report. 031 * 032 * @author Michael Zangl 033 * @since 10055 034 */ 035public class BugReportSender extends Thread { 036 037 /** 038 * Called during bug submission to JOSM bugtracker. Completes the bug report submission and handles errors. 039 * @since 12790 040 */ 041 public interface BugReportSendingHandler { 042 /** 043 * Called when a bug is sent to JOSM bugtracker. 044 * @param bugUrl URL to visit to effectively submit the bug report to JOSM website 045 * @param statusText the status text being sent 046 * @return <code>null</code> for success or a string in case of an error 047 */ 048 String sendingBugReport(String bugUrl, String statusText); 049 050 /** 051 * Called when a bug failed to be sent to JOSM bugtracker. 052 * @param errorMessage the error message 053 * @param statusText the status text being sent 054 */ 055 void failed(String errorMessage, String statusText); 056 } 057 058 /** 059 * The fallback bug report sending handler if none is set. 060 * @since 12790 061 */ 062 public static final BugReportSendingHandler FALLBACK_BUGREPORT_SENDING_HANDLER = new BugReportSendingHandler() { 063 @Override 064 public String sendingBugReport(String bugUrl, String statusText) { 065 return OpenBrowser.displayUrl(bugUrl); 066 } 067 068 @Override 069 public void failed(String errorMessage, String statusText) { 070 Logging.error("Unable to send bug report: {0}\n{1}", errorMessage, statusText); 071 } 072 }; 073 074 private static volatile BugReportSendingHandler handler = FALLBACK_BUGREPORT_SENDING_HANDLER; 075 076 private final String statusText; 077 private String errorMessage; 078 079 /** 080 * Creates a new sender. 081 * @param statusText The status text to send. 082 */ 083 protected BugReportSender(String statusText) { 084 super("Bug report sender"); 085 this.statusText = statusText; 086 } 087 088 @Override 089 public void run() { 090 try { 091 // first, send the debug text using post. 092 String debugTextPasteId = pasteDebugText(); 093 String bugUrl = getJOSMTicketURL() + "?pdata_stored=" + debugTextPasteId; 094 095 // then notify handler 096 errorMessage = handler.sendingBugReport(bugUrl, statusText); 097 if (errorMessage != null) { 098 Logging.warn(errorMessage); 099 handler.failed(errorMessage, statusText); 100 } 101 } catch (BugReportSenderException e) { 102 Logging.warn(e); 103 errorMessage = e.getMessage(); 104 handler.failed(errorMessage, statusText); 105 } 106 } 107 108 /** 109 * Sends the debug text to the server. 110 * @return The token which was returned by the server. We need to pass this on to the ticket system. 111 * @throws BugReportSenderException if sending the report failed. 112 */ 113 private String pasteDebugText() throws BugReportSenderException { 114 try { 115 String text = Utils.strip(statusText); 116 String pdata = Base64.getEncoder().encodeToString(text.getBytes(StandardCharsets.UTF_8)); 117 String postQuery = "pdata=" + URLEncoder.encode(pdata, "UTF-8"); 118 HttpClient client = HttpClient.create(new URL(getJOSMTicketURL()), "POST") 119 .setHeader("Content-Type", "application/x-www-form-urlencoded") 120 .setRequestBody(postQuery.getBytes(StandardCharsets.UTF_8)); 121 122 Response connection = client.connect(); 123 124 if (connection.getResponseCode() >= 500) { 125 throw new BugReportSenderException("Internal server error."); 126 } 127 128 try (InputStream in = connection.getContent()) { 129 return retrieveDebugToken(Utils.parseSafeDOM(in)); 130 } 131 } catch (IOException | SAXException | ParserConfigurationException | XPathExpressionException t) { 132 throw new BugReportSenderException(t); 133 } 134 } 135 136 private static String getJOSMTicketURL() { 137 return Main.getJOSMWebsite() + "/josmticket"; 138 } 139 140 private static String retrieveDebugToken(Document document) throws XPathExpressionException, BugReportSenderException { 141 XPathFactory factory = XPathFactory.newInstance(); 142 XPath xpath = factory.newXPath(); 143 String status = (String) xpath.compile("/josmticket/@status").evaluate(document, XPathConstants.STRING); 144 if (!"ok".equals(status)) { 145 String message = (String) xpath.compile("/josmticket/error/text()").evaluate(document, 146 XPathConstants.STRING); 147 if (message.isEmpty()) { 148 message = "Error in server response but server did not tell us what happened."; 149 } 150 throw new BugReportSenderException(message); 151 } 152 153 String token = (String) xpath.compile("/josmticket/preparedid/text()") 154 .evaluate(document, XPathConstants.STRING); 155 if (token.isEmpty()) { 156 throw new BugReportSenderException("Server did not respond with a prepared id."); 157 } 158 return token; 159 } 160 161 /** 162 * Returns the error message that could have occured during bug sending. 163 * @return the error message, or {@code null} if successful 164 */ 165 public final String getErrorMessage() { 166 return errorMessage; 167 } 168 169 private static class BugReportSenderException extends Exception { 170 BugReportSenderException(String message) { 171 super(message); 172 } 173 174 BugReportSenderException(Throwable cause) { 175 super(cause); 176 } 177 } 178 179 /** 180 * Opens the bug report window on the JOSM server. 181 * @param statusText The status text to send along to the server. 182 * @return bug report sender started thread 183 */ 184 public static BugReportSender reportBug(String statusText) { 185 BugReportSender sender = new BugReportSender(statusText); 186 sender.start(); 187 return sender; 188 } 189 190 /** 191 * Sets the {@link BugReportSendingHandler} for bug report sender. 192 * @param bugReportSendingHandler the handler in charge of completing the bug report submission and handle errors. Must not be null 193 * @since 12790 194 */ 195 public static void setBugReportSendingHandler(BugReportSendingHandler bugReportSendingHandler) { 196 handler = Objects.requireNonNull(bugReportSendingHandler, "bugReportSendingHandler"); 197 } 198}