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