| 1 | // License: GPL. For details, see LICENSE file. |
|---|
| 2 | package org.openstreetmap.josm.gui.preferences.server; |
|---|
| 3 | |
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr; |
|---|
| 5 | |
|---|
| 6 | import java.awt.Component; |
|---|
| 7 | import java.io.BufferedReader; |
|---|
| 8 | import java.io.IOException; |
|---|
| 9 | import java.io.InputStreamReader; |
|---|
| 10 | import java.net.HttpURLConnection; |
|---|
| 11 | import java.net.MalformedURLException; |
|---|
| 12 | import java.net.URL; |
|---|
| 13 | |
|---|
| 14 | import javax.swing.JOptionPane; |
|---|
| 15 | |
|---|
| 16 | import org.openstreetmap.josm.data.Version; |
|---|
| 17 | import org.openstreetmap.josm.gui.HelpAwareOptionPane; |
|---|
| 18 | import org.openstreetmap.josm.gui.PleaseWaitRunnable; |
|---|
| 19 | import org.openstreetmap.josm.gui.help.HelpUtil; |
|---|
| 20 | import org.openstreetmap.josm.io.OsmTransferException; |
|---|
| 21 | import org.openstreetmap.josm.tools.CheckParameterUtil; |
|---|
| 22 | import org.xml.sax.SAXException; |
|---|
| 23 | |
|---|
| 24 | /** |
|---|
| 25 | * This is an asynchronous task for testing whether an URL points to an OSM API server. |
|---|
| 26 | * It tries to retrieve a list of changesets from the given URL. If it succeeds, the method |
|---|
| 27 | * {@see #isSuccess()} replies true, otherwise false. |
|---|
| 28 | * |
|---|
| 29 | * Note: it fetches a list of changesets instead of the much smaller capabilities because - strangely enough - |
|---|
| 30 | * an OSM server "http://x.y.y/api/0.6" not only responds to "http://x.y.y/api/0.6/capabilities" but also |
|---|
| 31 | * to "http://x.y.y/api/0/capabilities" or "http://x.y.y/a/capabilities" with valid capabilities. If we get |
|---|
| 32 | * valid capabilities with an URL we therefore can't be sure that the base URL is valid API URL. |
|---|
| 33 | * |
|---|
| 34 | */ |
|---|
| 35 | public class ApiUrlTestTask extends PleaseWaitRunnable{ |
|---|
| 36 | |
|---|
| 37 | private String url; |
|---|
| 38 | private boolean canceled; |
|---|
| 39 | private boolean success; |
|---|
| 40 | private Component parent; |
|---|
| 41 | private HttpURLConnection connection; |
|---|
| 42 | |
|---|
| 43 | /** |
|---|
| 44 | * Creates the task |
|---|
| 45 | * |
|---|
| 46 | * @param parent the parent component relative to which the {@see PleaseWaitRunnable}-Dialog is displayed |
|---|
| 47 | * @param url the url. Must not be null. |
|---|
| 48 | * @throws IllegalArgumentException thrown if url is null. |
|---|
| 49 | */ |
|---|
| 50 | public ApiUrlTestTask(Component parent, String url) throws IllegalArgumentException { |
|---|
| 51 | super(parent, tr("Testing OSM API URL ''{0}''", url), false /* don't ignore exceptions */); |
|---|
| 52 | CheckParameterUtil.ensureParameterNotNull(url,"url"); |
|---|
| 53 | this.parent = parent; |
|---|
| 54 | this.url = url; |
|---|
| 55 | } |
|---|
| 56 | |
|---|
| 57 | protected void alertInvalidUrl(String url) { |
|---|
| 58 | HelpAwareOptionPane.showOptionDialog( |
|---|
| 59 | parent, |
|---|
| 60 | tr("<html>" |
|---|
| 61 | + "''{0}'' is not a valid OSM API URL.<br>" |
|---|
| 62 | + "Please check the spelling and validate again." |
|---|
| 63 | + "</html>", |
|---|
| 64 | url |
|---|
| 65 | ), |
|---|
| 66 | tr("Invalid API URL"), |
|---|
| 67 | JOptionPane.ERROR_MESSAGE, |
|---|
| 68 | HelpUtil.ht("/Preferences/Connection#InvalidAPIUrl") |
|---|
| 69 | ); |
|---|
| 70 | } |
|---|
| 71 | |
|---|
| 72 | protected void alertInvalidChangesetUrl(String url) { |
|---|
| 73 | HelpAwareOptionPane.showOptionDialog( |
|---|
| 74 | parent, |
|---|
| 75 | tr("<html>" |
|---|
| 76 | + "Failed to build URL ''{0}'' for validating the OSM API server.<br>" |
|---|
| 77 | + "Please check the spelling of ''{1}'' and validate again." |
|---|
| 78 | +"</html>", |
|---|
| 79 | url, |
|---|
| 80 | getNormalizedApiUrl() |
|---|
| 81 | ), |
|---|
| 82 | tr("Invalid API URL"), |
|---|
| 83 | JOptionPane.ERROR_MESSAGE, |
|---|
| 84 | HelpUtil.ht("/Preferences/Connection#InvalidAPIGetChangesetsUrl") |
|---|
| 85 | ); |
|---|
| 86 | } |
|---|
| 87 | |
|---|
| 88 | protected void alertConnectionFailed() { |
|---|
| 89 | HelpAwareOptionPane.showOptionDialog( |
|---|
| 90 | parent, |
|---|
| 91 | tr("<html>" |
|---|
| 92 | + "Failed to connect to the URL ''{0}''.<br>" |
|---|
| 93 | + "Please check the spelling of ''{1}'' and your Internet connection and validate again." |
|---|
| 94 | +"</html>", |
|---|
| 95 | url, |
|---|
| 96 | getNormalizedApiUrl() |
|---|
| 97 | ), |
|---|
| 98 | tr("Connection to API failed"), |
|---|
| 99 | 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 | } |
|---|