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.HttpClient;
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 HttpClient 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        // Do nothing
147    }
148
149    /**
150     * Removes leading and trailing whitespace from the API URL and removes trailing '/'.
151     *
152     * @return the normalized API URL
153     */
154    protected String getNormalizedApiUrl() {
155        String apiUrl = url.trim();
156        while (apiUrl.endsWith("/")) {
157            apiUrl = apiUrl.substring(0, apiUrl.lastIndexOf('/'));
158        }
159        return apiUrl;
160    }
161
162    @Override
163    protected void realRun() throws SAXException, IOException, OsmTransferException {
164        try {
165            try {
166                new URL(getNormalizedApiUrl());
167            } catch (MalformedURLException e) {
168                alertInvalidUrl(getNormalizedApiUrl());
169                return;
170            }
171            URL capabilitiesUrl;
172            String getCapabilitiesUrl = getNormalizedApiUrl() + "/0.6/capabilities";
173            try {
174                capabilitiesUrl = new URL(getCapabilitiesUrl);
175            } catch (MalformedURLException e) {
176                alertInvalidCapabilitiesUrl(getCapabilitiesUrl);
177                return;
178            }
179
180            synchronized (this) {
181                connection = HttpClient.create(capabilitiesUrl);
182                connection.connect();
183            }
184
185            if (connection.getResponse().getResponseCode() != HttpURLConnection.HTTP_OK) {
186                alertInvalidServerResult(connection.getResponse().getResponseCode());
187                return;
188            }
189
190            try {
191                Capabilities.CapabilitiesParser.parse(new InputSource(connection.getResponse().getContent()));
192            } catch (SAXException | ParserConfigurationException e) {
193                Main.warn(e);
194                alertInvalidCapabilities();
195                return;
196            }
197            success = true;
198        } catch (IOException e) {
199            if (canceled)
200                // ignore exceptions
201                return;
202            Main.error(e);
203            alertConnectionFailed();
204            return;
205        }
206    }
207
208    /**
209     * Determines if the test has been canceled.
210     * @return {@code true} if canceled, {@code false} otherwise
211     */
212    public boolean isCanceled() {
213        return canceled;
214    }
215
216    /**
217     * Determines if the test has succeeded.
218     * @return {@code true} if success, {@code false} otherwise
219     */
220    public boolean isSuccess() {
221        return success;
222    }
223}