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}