001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.preferences.server;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.io.IOException;
008import java.net.HttpURLConnection;
009import java.net.MalformedURLException;
010import java.net.URL;
011
012import javax.swing.JOptionPane;
013import javax.xml.parsers.ParserConfigurationException;
014
015import org.openstreetmap.josm.Main;
016import org.openstreetmap.josm.gui.HelpAwareOptionPane;
017import org.openstreetmap.josm.gui.PleaseWaitRunnable;
018import org.openstreetmap.josm.gui.help.HelpUtil;
019import org.openstreetmap.josm.io.Capabilities;
020import org.openstreetmap.josm.io.OsmTransferException;
021import org.openstreetmap.josm.tools.CheckParameterUtil;
022import org.openstreetmap.josm.tools.Utils;
023import org.xml.sax.InputSource;
024import org.xml.sax.SAXException;
025
026/**
027 * This is an asynchronous task for testing whether an URL points to an OSM API server.
028 * It tries to retrieve capabilities from the given URL. If it succeeds, the method
029 * {@link #isSuccess()} replies true, otherwise false.
030 * @since 2745
031 */
032public class ApiUrlTestTask extends PleaseWaitRunnable {
033
034    private final String url;
035    private boolean canceled;
036    private boolean success;
037    private final Component parent;
038    private HttpURLConnection connection;
039
040    /**
041     * Constructs a new {@code ApiUrlTestTask}.
042     *
043     * @param parent the parent component relative to which the {@link PleaseWaitRunnable}-Dialog is displayed
044     * @param url the url. Must not be null.
045     * @throws IllegalArgumentException if url is null.
046     */
047    public ApiUrlTestTask(Component parent, String url) {
048        super(parent, tr("Testing OSM API URL ''{0}''", url), false /* don't ignore exceptions */);
049        CheckParameterUtil.ensureParameterNotNull(url, "url");
050        this.parent = parent;
051        this.url = url;
052    }
053
054    protected void alertInvalidUrl(String url) {
055        HelpAwareOptionPane.showMessageDialogInEDT(
056                parent,
057                tr("<html>"
058                        + "''{0}'' is not a valid OSM API URL.<br>"
059                        + "Please check the spelling and validate again."
060                        + "</html>",
061                        url
062                ),
063                tr("Invalid API URL"),
064                JOptionPane.ERROR_MESSAGE,
065                HelpUtil.ht("/Preferences/Connection#InvalidAPIUrl")
066        );
067    }
068
069    protected void alertInvalidCapabilitiesUrl(String url) {
070        HelpAwareOptionPane.showMessageDialogInEDT(
071                parent,
072                tr("<html>"
073                        + "Failed to build URL ''{0}'' for validating the OSM API server.<br>"
074                        + "Please check the spelling of ''{1}'' and validate again."
075                        +"</html>",
076                        url,
077                        getNormalizedApiUrl()
078                ),
079                tr("Invalid API URL"),
080                JOptionPane.ERROR_MESSAGE,
081                HelpUtil.ht("/Preferences/Connection#InvalidAPIGetChangesetsUrl")
082        );
083    }
084
085    protected void alertConnectionFailed() {
086        HelpAwareOptionPane.showMessageDialogInEDT(
087                parent,
088                tr("<html>"
089                        + "Failed to connect to the URL ''{0}''.<br>"
090                        + "Please check the spelling of ''{1}'' and your Internet connection and validate again."
091                        +"</html>",
092                        url,
093                        getNormalizedApiUrl()
094                ),
095                tr("Connection to API failed"),
096                JOptionPane.ERROR_MESSAGE,
097                HelpUtil.ht("/Preferences/Connection#ConnectionToAPIFailed")
098        );
099    }
100
101    protected void alertInvalidServerResult(int retCode) {
102        HelpAwareOptionPane.showMessageDialogInEDT(
103                parent,
104                tr("<html>"
105                        + "Failed to retrieve a list of changesets from the OSM API server at<br>"
106                        + "''{1}''. The server responded with the return code {0} instead of 200.<br>"
107                        + "Please check the spelling of ''{1}'' and validate again."
108                        + "</html>",
109                        retCode,
110                        getNormalizedApiUrl()
111                ),
112                tr("Connection to API failed"),
113                JOptionPane.ERROR_MESSAGE,
114                HelpUtil.ht("/Preferences/Connection#InvalidServerResult")
115        );
116    }
117
118    protected void alertInvalidCapabilities() {
119        HelpAwareOptionPane.showMessageDialogInEDT(
120                parent,
121                tr("<html>"
122                        + "The OSM API server at ''{0}'' did not return a valid response.<br>"
123                        + "It is likely that ''{0}'' is not an OSM API server.<br>"
124                        + "Please check the spelling of ''{0}'' and validate again."
125                        + "</html>",
126                        getNormalizedApiUrl()
127                ),
128                tr("Connection to API failed"),
129                JOptionPane.ERROR_MESSAGE,
130                HelpUtil.ht("/Preferences/Connection#InvalidSettings")
131        );
132    }
133
134    @Override
135    protected void cancel() {
136        canceled = true;
137        synchronized (this) {
138            if (connection != null) {
139                connection.disconnect();
140            }
141        }
142    }
143
144    @Override
145    protected void finish() {}
146
147    /**
148     * Removes leading and trailing whitespace from the API URL and removes trailing '/'.
149     *
150     * @return the normalized API URL
151     */
152    protected String getNormalizedApiUrl() {
153        String apiUrl = url.trim();
154        while (apiUrl.endsWith("/")) {
155            apiUrl = apiUrl.substring(0, apiUrl.lastIndexOf('/'));
156        }
157        return apiUrl;
158    }
159
160    @Override
161    protected void realRun() throws SAXException, IOException, OsmTransferException {
162        try {
163            try {
164                new URL(getNormalizedApiUrl());
165            } catch (MalformedURLException e) {
166                alertInvalidUrl(getNormalizedApiUrl());
167                return;
168            }
169            URL capabilitiesUrl;
170            String getCapabilitiesUrl = getNormalizedApiUrl() + "/0.6/capabilities";
171            try {
172                capabilitiesUrl = new URL(getCapabilitiesUrl);
173            } catch (MalformedURLException e) {
174                alertInvalidCapabilitiesUrl(getCapabilitiesUrl);
175                return;
176            }
177
178            synchronized (this) {
179                connection = Utils.openHttpConnection(capabilitiesUrl);
180            }
181            connection.setDoInput(true);
182            connection.setDoOutput(false);
183            connection.setRequestMethod("GET");
184            connection.connect();
185
186            if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
187                alertInvalidServerResult(connection.getResponseCode());
188                return;
189            }
190
191            try {
192                Capabilities.CapabilitiesParser.parse(new InputSource(connection.getInputStream()));
193            } catch (SAXException | ParserConfigurationException e) {
194                Main.warn(e.getMessage());
195                alertInvalidCapabilities();
196                return;
197            }
198            success = true;
199        } catch (IOException e) {
200            if (canceled)
201                // ignore exceptions
202                return;
203            Main.error(e);
204            alertConnectionFailed();
205            return;
206        }
207    }
208
209    /**
210     * Determines if the test has been canceled.
211     * @return {@code true} if canceled, {@code false} otherwise
212     */
213    public boolean isCanceled() {
214        return canceled;
215    }
216
217    /**
218     * Determines if the test has succeeded.
219     * @return {@code true} if success, {@code false} otherwise
220     */
221    public boolean isSuccess() {
222        return success;
223    }
224}