001    // License: GPL. For details, see LICENSE file.
002    package org.openstreetmap.josm.gui.preferences.server;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.Component;
007    import java.io.BufferedReader;
008    import java.io.IOException;
009    import java.io.InputStreamReader;
010    import java.net.HttpURLConnection;
011    import java.net.MalformedURLException;
012    import java.net.URL;
013    
014    import javax.swing.JOptionPane;
015    
016    import org.openstreetmap.josm.data.Version;
017    import org.openstreetmap.josm.gui.HelpAwareOptionPane;
018    import org.openstreetmap.josm.gui.PleaseWaitRunnable;
019    import org.openstreetmap.josm.gui.help.HelpUtil;
020    import org.openstreetmap.josm.io.OsmTransferException;
021    import org.openstreetmap.josm.tools.CheckParameterUtil;
022    import org.xml.sax.SAXException;
023    
024    /**
025     * This is an asynchronous task for testing whether an URL points to an OSM API server.
026     * It tries to retrieve a list of changesets from the given URL. If it succeeds, the method
027     * {@link #isSuccess()} replies true, otherwise false.
028     *
029     * Note: it fetches a list of changesets instead of the much smaller capabilities because - strangely enough -
030     * an OSM server "http://x.y.y/api/0.6" not only responds to  "http://x.y.y/api/0.6/capabilities" but also
031     * to "http://x.y.y/api/0/capabilities" or "http://x.y.y/a/capabilities" with valid capabilities. If we get
032     * valid capabilities with an URL we therefore can't be sure that the base URL is valid API URL.
033     *
034     */
035    public class ApiUrlTestTask extends PleaseWaitRunnable{
036    
037        private String url;
038        private boolean canceled;
039        private boolean success;
040        private Component parent;
041        private HttpURLConnection connection;
042    
043        /**
044         * Creates the task
045         *
046         * @param parent the parent component relative to which the {@link PleaseWaitRunnable}-Dialog is displayed
047         * @param url the url. Must not be null.
048         * @throws IllegalArgumentException thrown if url is null.
049         */
050        public ApiUrlTestTask(Component parent, String url) throws IllegalArgumentException {
051            super(parent, tr("Testing OSM API URL ''{0}''", url), false /* don't ignore exceptions */);
052            CheckParameterUtil.ensureParameterNotNull(url,"url");
053            this.parent = parent;
054            this.url = url;
055        }
056    
057        protected void alertInvalidUrl(String url) {
058            HelpAwareOptionPane.showOptionDialog(
059                    parent,
060                    tr("<html>"
061                            + "''{0}'' is not a valid OSM API URL.<br>"
062                            + "Please check the spelling and validate again."
063                            + "</html>",
064                            url
065                    ),
066                    tr("Invalid API URL"),
067                    JOptionPane.ERROR_MESSAGE,
068                    HelpUtil.ht("/Preferences/Connection#InvalidAPIUrl")
069            );
070        }
071    
072        protected void alertInvalidChangesetUrl(String url) {
073            HelpAwareOptionPane.showOptionDialog(
074                    parent,
075                    tr("<html>"
076                            + "Failed to build URL ''{0}'' for validating the OSM API server.<br>"
077                            + "Please check the spelling of ''{1}'' and validate again."
078                            +"</html>",
079                            url,
080                            getNormalizedApiUrl()
081                    ),
082                    tr("Invalid API URL"),
083                    JOptionPane.ERROR_MESSAGE,
084                    HelpUtil.ht("/Preferences/Connection#InvalidAPIGetChangesetsUrl")
085            );
086        }
087    
088        protected void alertConnectionFailed() {
089            HelpAwareOptionPane.showOptionDialog(
090                    parent,
091                    tr("<html>"
092                            + "Failed to connect to the URL ''{0}''.<br>"
093                            + "Please check the spelling of ''{1}'' and your Internet connection and validate again."
094                            +"</html>",
095                            url,
096                            getNormalizedApiUrl()
097                    ),
098                    tr("Connection to API failed"),
099                    JOptionPane.ERROR_MESSAGE,
100                    HelpUtil.ht("/Preferences/Connection#ConnectionToAPIFailed")
101            );
102        }
103    
104        protected void alertInvalidServerResult(int retCode) {
105            HelpAwareOptionPane.showOptionDialog(
106                    parent,
107                    tr("<html>"
108                            + "Failed to retrieve a list of changesets from the OSM API server at<br>"
109                            + "''{1}''. The server responded with the return code {0} instead of 200.<br>"
110                            + "Please check the spelling of ''{1}'' and validate again."
111                            + "</html>",
112                            retCode,
113                            getNormalizedApiUrl()
114                    ),
115                    tr("Connection to API failed"),
116                    JOptionPane.ERROR_MESSAGE,
117                    HelpUtil.ht("/Preferences/Connection#InvalidServerResult")
118            );
119        }
120    
121        protected void alertInvalidChangesetList() {
122            HelpAwareOptionPane.showOptionDialog(
123                    parent,
124                    tr("<html>"
125                            + "The OSM API server at ''{0}'' did not return a valid response.<br>"
126                            + "It is likely that ''{0}'' is not an OSM API server.<br>"
127                            + "Please check the spelling of ''{0}'' and validate again."
128                            + "</html>",
129                            getNormalizedApiUrl()
130                    ),
131                    tr("Connection to API failed"),
132                    JOptionPane.ERROR_MESSAGE,
133                    HelpUtil.ht("/Preferences/Connection#InvalidSettings")
134            );
135        }
136    
137        @Override
138        protected void cancel() {
139            canceled = true;
140            synchronized(this) {
141                if (connection != null) {
142                    connection.disconnect();
143                }
144            }
145        }
146    
147        @Override
148        protected void finish() {}
149    
150        /**
151         * Removes leading and trailing whitespace from the API URL and removes trailing
152         * '/'.
153         *
154         * @return the normalized API URL
155         */
156        protected String getNormalizedApiUrl() {
157            String apiUrl = url.trim();
158            while(apiUrl.endsWith("/")) {
159                apiUrl = apiUrl.substring(0, apiUrl.lastIndexOf("/"));
160            }
161            return apiUrl;
162        }
163    
164        @Override
165        protected void realRun() throws SAXException, IOException, OsmTransferException {
166            BufferedReader bin = null;
167            try {
168                try {
169                    new URL(getNormalizedApiUrl());
170                } catch(MalformedURLException e) {
171                    alertInvalidUrl(getNormalizedApiUrl());
172                    return;
173                }
174                URL capabilitiesUrl;
175                String getChangesetsUrl = getNormalizedApiUrl() + "/0.6/changesets";
176                try {
177                    capabilitiesUrl = new URL(getChangesetsUrl);
178                } catch(MalformedURLException e) {
179                    alertInvalidChangesetUrl(getChangesetsUrl);
180                    return;
181                }
182    
183                synchronized(this) {
184                    connection = (HttpURLConnection)capabilitiesUrl.openConnection();
185                }
186                connection.setDoInput(true);
187                connection.setDoOutput(false);
188                connection.setRequestMethod("GET");
189                connection.setRequestProperty("User-Agent", Version.getInstance().getAgentString());
190                connection.setRequestProperty("Host", connection.getURL().getHost());
191                connection.connect();
192    
193                if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
194                    alertInvalidServerResult(connection.getResponseCode());
195                    return;
196                }
197                StringBuilder changesets = new StringBuilder();
198                bin = new BufferedReader(new InputStreamReader(connection.getInputStream()));
199                String line;
200                while ((line = bin.readLine()) != null) {
201                    changesets.append(line).append("\n");
202                }
203                if (! (changesets.toString().contains("<osm") && changesets.toString().contains("</osm>"))) {
204                    // heuristic: if there isn't an opening and closing "<osm>" tag in the returned content,
205                    // then we didn't get a list of changesets in return. Could be replaced by explicitly parsing
206                    // the result but currently not worth the effort.
207                    alertInvalidChangesetList();
208                    return;
209                }
210                success = true;
211            } catch(IOException e) {
212                if (canceled)
213                    // ignore exceptions
214                    return;
215                e.printStackTrace();
216                alertConnectionFailed();
217                return;
218            } finally {
219                if (bin != null) {
220                    try {
221                        bin.close();
222                    } catch(IOException e){/* ignore */}
223                }
224            }
225        }
226    
227        public boolean isCanceled() {
228            return canceled;
229        }
230    
231        public boolean isSuccess() {
232            return success;
233        }
234    }