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