001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005import static org.openstreetmap.josm.tools.I18n.trn; 006 007import java.io.IOException; 008import java.io.PrintWriter; 009import java.io.StringReader; 010import java.io.StringWriter; 011import java.net.Authenticator.RequestorType; 012import java.net.ConnectException; 013import java.net.HttpURLConnection; 014import java.net.MalformedURLException; 015import java.net.SocketTimeoutException; 016import java.net.URL; 017import java.nio.charset.StandardCharsets; 018import java.util.Collection; 019import java.util.HashMap; 020import java.util.List; 021import java.util.Map; 022import java.util.function.Consumer; 023import java.util.function.UnaryOperator; 024 025import javax.xml.parsers.ParserConfigurationException; 026 027import org.openstreetmap.josm.data.coor.LatLon; 028import org.openstreetmap.josm.data.notes.Note; 029import org.openstreetmap.josm.data.osm.Changeset; 030import org.openstreetmap.josm.data.osm.IPrimitive; 031import org.openstreetmap.josm.data.osm.OsmPrimitive; 032import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 033import org.openstreetmap.josm.data.preferences.BooleanProperty; 034import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 035import org.openstreetmap.josm.gui.progress.ProgressMonitor; 036import org.openstreetmap.josm.io.Capabilities.CapabilitiesParser; 037import org.openstreetmap.josm.io.auth.CredentialsManager; 038import org.openstreetmap.josm.spi.preferences.Config; 039import org.openstreetmap.josm.tools.CheckParameterUtil; 040import org.openstreetmap.josm.tools.HttpClient; 041import org.openstreetmap.josm.tools.ListenerList; 042import org.openstreetmap.josm.tools.Logging; 043import org.openstreetmap.josm.tools.Utils; 044import org.openstreetmap.josm.tools.XmlParsingException; 045import org.xml.sax.InputSource; 046import org.xml.sax.SAXException; 047import org.xml.sax.SAXParseException; 048 049/** 050 * Class that encapsulates the communications with the <a href="http://wiki.openstreetmap.org/wiki/API_v0.6">OSM API</a>.<br><br> 051 * 052 * All interaction with the server-side OSM API should go through this class.<br><br> 053 * 054 * It is conceivable to extract this into an interface later and create various 055 * classes implementing the interface, to be able to talk to various kinds of servers. 056 * @since 1523 057 */ 058public class OsmApi extends OsmConnection { 059 060 /** 061 * Maximum number of retries to send a request in case of HTTP 500 errors or timeouts 062 */ 063 public static final int DEFAULT_MAX_NUM_RETRIES = 5; 064 065 /** 066 * Maximum number of concurrent download threads, imposed by 067 * <a href="http://wiki.openstreetmap.org/wiki/API_usage_policy#Technical_Usage_Requirements"> 068 * OSM API usage policy.</a> 069 * @since 5386 070 */ 071 public static final int MAX_DOWNLOAD_THREADS = 2; 072 073 /** 074 * Defines whether all OSM API requests should be signed with an OAuth token (user-based bandwidth limit instead of IP-based one) 075 */ 076 public static final BooleanProperty USE_OAUTH_FOR_ALL_REQUESTS = new BooleanProperty("oauth.use-for-all-requests", true); 077 078 // The collection of instantiated OSM APIs 079 private static final Map<String, OsmApi> instances = new HashMap<>(); 080 081 private static final ListenerList<OsmApiInitializationListener> listeners = ListenerList.create(); 082 083 private URL url; 084 085 /** 086 * OSM API initialization listener. 087 * @since 12804 088 */ 089 public interface OsmApiInitializationListener { 090 /** 091 * Called when an OSM API instance has been successfully initialized. 092 * @param instance the initialized OSM API instance 093 */ 094 void apiInitialized(OsmApi instance); 095 } 096 097 /** 098 * Adds a new OSM API initialization listener. 099 * @param listener OSM API initialization listener to add 100 * @since 12804 101 */ 102 public static void addOsmApiInitializationListener(OsmApiInitializationListener listener) { 103 listeners.addListener(listener); 104 } 105 106 /** 107 * Removes an OSM API initialization listener. 108 * @param listener OSM API initialization listener to remove 109 * @since 12804 110 */ 111 public static void removeOsmApiInitializationListener(OsmApiInitializationListener listener) { 112 listeners.removeListener(listener); 113 } 114 115 /** 116 * Replies the {@link OsmApi} for a given server URL 117 * 118 * @param serverUrl the server URL 119 * @return the OsmApi 120 * @throws IllegalArgumentException if serverUrl is null 121 * 122 */ 123 public static OsmApi getOsmApi(String serverUrl) { 124 OsmApi api = instances.get(serverUrl); 125 if (api == null) { 126 api = new OsmApi(serverUrl); 127 cacheInstance(api); 128 } 129 return api; 130 } 131 132 protected static void cacheInstance(OsmApi api) { 133 instances.put(api.getServerUrl(), api); 134 } 135 136 private static String getServerUrlFromPref() { 137 return Config.getPref().get("osm-server.url", Config.getUrls().getDefaultOsmApiUrl()); 138 } 139 140 /** 141 * Replies the {@link OsmApi} for the URL given by the preference <code>osm-server.url</code> 142 * 143 * @return the OsmApi 144 */ 145 public static OsmApi getOsmApi() { 146 return getOsmApi(getServerUrlFromPref()); 147 } 148 149 /** Server URL */ 150 private final String serverUrl; 151 152 /** Object describing current changeset */ 153 private Changeset changeset; 154 155 /** API version used for server communications */ 156 private String version; 157 158 /** API capabilities */ 159 private Capabilities capabilities; 160 161 /** true if successfully initialized */ 162 private boolean initialized; 163 164 /** 165 * Constructs a new {@code OsmApi} for a specific server URL. 166 * 167 * @param serverUrl the server URL. Must not be null 168 * @throws IllegalArgumentException if serverUrl is null 169 */ 170 protected OsmApi(String serverUrl) { 171 CheckParameterUtil.ensureParameterNotNull(serverUrl, "serverUrl"); 172 this.serverUrl = serverUrl; 173 } 174 175 /** 176 * Replies the OSM protocol version we use to talk to the server. 177 * @return protocol version, or null if not yet negotiated. 178 */ 179 public String getVersion() { 180 return version; 181 } 182 183 /** 184 * Replies the host name of the server URL. 185 * @return the host name of the server URL, or null if the server URL is malformed. 186 */ 187 public String getHost() { 188 String host = null; 189 try { 190 host = new URL(serverUrl).getHost(); 191 } catch (MalformedURLException e) { 192 Logging.warn(e); 193 } 194 return host; 195 } 196 197 private class CapabilitiesCache extends CacheCustomContent<OsmTransferException> { 198 199 private static final String CAPABILITIES = "capabilities"; 200 201 private final ProgressMonitor monitor; 202 private final boolean fastFail; 203 204 CapabilitiesCache(ProgressMonitor monitor, boolean fastFail) { 205 super(CAPABILITIES + getBaseUrl().hashCode(), CacheCustomContent.INTERVAL_WEEKLY); 206 this.monitor = monitor; 207 this.fastFail = fastFail; 208 } 209 210 @Override 211 protected boolean isOffline() { 212 return NetworkManager.isOffline(OnlineResource.OSM_API); 213 } 214 215 @Override 216 protected byte[] updateData() throws OsmTransferException { 217 return sendRequest("GET", CAPABILITIES, null, monitor, false, fastFail).getBytes(StandardCharsets.UTF_8); 218 } 219 } 220 221 /** 222 * Initializes this component by negotiating a protocol version with the server. 223 * 224 * @param monitor the progress monitor 225 * @throws OsmTransferCanceledException If the initialisation has been cancelled by user. 226 * @throws OsmApiInitializationException If any other exception occurs. Use getCause() to get the original exception. 227 */ 228 public void initialize(ProgressMonitor monitor) throws OsmTransferCanceledException, OsmApiInitializationException { 229 initialize(monitor, false); 230 } 231 232 /** 233 * Initializes this component by negotiating a protocol version with the server, with the ability to control the timeout. 234 * 235 * @param monitor the progress monitor 236 * @param fastFail true to request quick initialisation with a small timeout (more likely to throw exception) 237 * @throws OsmTransferCanceledException If the initialisation has been cancelled by user. 238 * @throws OsmApiInitializationException If any other exception occurs. Use getCause() to get the original exception. 239 */ 240 public void initialize(ProgressMonitor monitor, boolean fastFail) throws OsmTransferCanceledException, OsmApiInitializationException { 241 if (initialized) 242 return; 243 cancel = false; 244 try { 245 CapabilitiesCache cache = new CapabilitiesCache(monitor, fastFail); 246 try { 247 initializeCapabilities(cache.updateIfRequiredString()); 248 } catch (SAXParseException parseException) { 249 Logging.trace(parseException); 250 // XML parsing may fail if JOSM previously stored a corrupted capabilities document (see #8278) 251 // In that case, force update and try again 252 initializeCapabilities(cache.updateForceString()); 253 } catch (SecurityException e) { 254 Logging.log(Logging.LEVEL_ERROR, "Unable to initialize OSM API", e); 255 } 256 if (capabilities == null) { 257 if (NetworkManager.isOffline(OnlineResource.OSM_API)) { 258 Logging.warn(OfflineAccessException.forResource(tr("")).getMessage()); 259 } else { 260 Logging.error(tr("Unable to initialize OSM API.")); 261 } 262 return; 263 } else if (!capabilities.supportsVersion("0.6")) { 264 Logging.error(tr("This version of JOSM is incompatible with the configured server.")); 265 Logging.error(tr("It supports protocol version 0.6, while the server says it supports {0} to {1}.", 266 capabilities.get("version", "minimum"), capabilities.get("version", "maximum"))); 267 return; 268 } else { 269 version = "0.6"; 270 initialized = true; 271 } 272 273 listeners.fireEvent(l -> l.apiInitialized(this)); 274 } catch (OsmTransferCanceledException e) { 275 throw e; 276 } catch (OsmTransferException e) { 277 initialized = false; 278 NetworkManager.addNetworkError(url, Utils.getRootCause(e)); 279 throw new OsmApiInitializationException(e); 280 } catch (SAXException | IOException | ParserConfigurationException e) { 281 initialized = false; 282 throw new OsmApiInitializationException(e); 283 } 284 } 285 286 private synchronized void initializeCapabilities(String xml) throws SAXException, IOException, ParserConfigurationException { 287 if (xml != null) { 288 capabilities = CapabilitiesParser.parse(new InputSource(new StringReader(xml))); 289 } 290 } 291 292 /** 293 * Makes an XML string from an OSM primitive. Uses the OsmWriter class. 294 * @param o the OSM primitive 295 * @param addBody true to generate the full XML, false to only generate the encapsulating tag 296 * @return XML string 297 */ 298 protected final String toXml(IPrimitive o, boolean addBody) { 299 StringWriter swriter = new StringWriter(); 300 try (OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(new PrintWriter(swriter), true, version)) { 301 swriter.getBuffer().setLength(0); 302 osmWriter.setWithBody(addBody); 303 osmWriter.setChangeset(changeset); 304 osmWriter.header(); 305 o.accept(osmWriter); 306 osmWriter.footer(); 307 osmWriter.flush(); 308 } catch (IOException e) { 309 Logging.warn(e); 310 } 311 return swriter.toString(); 312 } 313 314 /** 315 * Makes an XML string from an OSM primitive. Uses the OsmWriter class. 316 * @param s the changeset 317 * @return XML string 318 */ 319 protected final String toXml(Changeset s) { 320 StringWriter swriter = new StringWriter(); 321 try (OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(new PrintWriter(swriter), true, version)) { 322 swriter.getBuffer().setLength(0); 323 osmWriter.header(); 324 osmWriter.visit(s); 325 osmWriter.footer(); 326 osmWriter.flush(); 327 } catch (IOException e) { 328 Logging.warn(e); 329 } 330 return swriter.toString(); 331 } 332 333 private static String getBaseUrl(String serverUrl, String version) { 334 StringBuilder rv = new StringBuilder(serverUrl); 335 if (version != null) { 336 rv.append('/').append(version); 337 } 338 rv.append('/'); 339 // this works around a ruby (or lighttpd) bug where two consecutive slashes in 340 // an URL will cause a "404 not found" response. 341 int p; 342 while ((p = rv.indexOf("//", rv.indexOf("://")+2)) > -1) { 343 rv.delete(p, p + 1); 344 } 345 return rv.toString(); 346 } 347 348 /** 349 * Returns the base URL for API requests, including the negotiated version number. 350 * @return base URL string 351 */ 352 public String getBaseUrl() { 353 return getBaseUrl(serverUrl, version); 354 } 355 356 /** 357 * Returns the server URL 358 * @return the server URL 359 * @since 9353 360 */ 361 public String getServerUrl() { 362 return serverUrl; 363 } 364 365 private void individualPrimitiveModification(String method, String verb, IPrimitive osm, ProgressMonitor monitor, 366 Consumer<String> consumer, UnaryOperator<String> errHandler) throws OsmTransferException { 367 String ret = ""; 368 try { 369 ensureValidChangeset(); 370 initialize(monitor); 371 // Perform request 372 ret = sendRequest(method, OsmPrimitiveType.from(osm).getAPIName() + '/' + verb, toXml(osm, true), monitor); 373 // Unlock dataset if needed 374 boolean locked = false; 375 if (osm instanceof OsmPrimitive) { 376 locked = ((OsmPrimitive) osm).getDataSet().isLocked(); 377 if (locked) { 378 ((OsmPrimitive) osm).getDataSet().unlock(); 379 } 380 } 381 try { 382 // Update local primitive 383 consumer.accept(ret); 384 } finally { 385 // Lock dataset back if needed 386 if (locked) { 387 ((OsmPrimitive) osm).getDataSet().lock(); 388 } 389 } 390 } catch (ChangesetClosedException e) { 391 e.setSource(ChangesetClosedException.Source.UPDATE_CHANGESET); 392 throw e; 393 } catch (NumberFormatException e) { 394 throw new OsmTransferException(errHandler.apply(ret), e); 395 } 396 } 397 398 /** 399 * Creates an OSM primitive on the server. The OsmPrimitive object passed in 400 * is modified by giving it the server-assigned id. 401 * 402 * @param osm the primitive 403 * @param monitor the progress monitor 404 * @throws OsmTransferException if something goes wrong 405 */ 406 public void createPrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException { 407 individualPrimitiveModification("PUT", "create", osm, monitor, ret -> { 408 osm.setOsmId(Long.parseLong(ret.trim()), 1); 409 osm.setChangesetId(getChangeset().getId()); 410 }, ret -> tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret)); 411 } 412 413 /** 414 * Modifies an OSM primitive on the server. 415 * 416 * @param osm the primitive. Must not be null. 417 * @param monitor the progress monitor 418 * @throws OsmTransferException if something goes wrong 419 */ 420 public void modifyPrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException { 421 individualPrimitiveModification("PUT", Long.toString(osm.getId()), osm, monitor, ret -> { 422 // API returns new object version 423 osm.setOsmId(osm.getId(), Integer.parseInt(ret.trim())); 424 osm.setChangesetId(getChangeset().getId()); 425 osm.setVisible(true); 426 }, ret -> tr("Unexpected format of new version of modified primitive ''{0}''. Got ''{1}''.", osm.getId(), ret)); 427 } 428 429 /** 430 * Deletes an OSM primitive on the server. 431 * 432 * @param osm the primitive 433 * @param monitor the progress monitor 434 * @throws OsmTransferException if something goes wrong 435 */ 436 public void deletePrimitive(OsmPrimitive osm, ProgressMonitor monitor) throws OsmTransferException { 437 individualPrimitiveModification("DELETE", Long.toString(osm.getId()), osm, monitor, ret -> { 438 // API returns new object version 439 osm.setOsmId(osm.getId(), Integer.parseInt(ret.trim())); 440 osm.setChangesetId(getChangeset().getId()); 441 osm.setVisible(false); 442 }, ret -> tr("Unexpected format of new version of deleted primitive ''{0}''. Got ''{1}''.", osm.getId(), ret)); 443 } 444 445 /** 446 * Creates a new changeset based on the keys in <code>changeset</code>. If this 447 * method succeeds, changeset.getId() replies the id the server assigned to the new changeset 448 * 449 * The changeset must not be null, but its key/value-pairs may be empty. 450 * 451 * @param changeset the changeset toe be created. Must not be null. 452 * @param progressMonitor the progress monitor 453 * @throws OsmTransferException signifying a non-200 return code, or connection errors 454 * @throws IllegalArgumentException if changeset is null 455 */ 456 public void openChangeset(Changeset changeset, ProgressMonitor progressMonitor) throws OsmTransferException { 457 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset"); 458 try { 459 progressMonitor.beginTask(tr("Creating changeset...")); 460 initialize(progressMonitor); 461 String ret = ""; 462 try { 463 ret = sendPutRequest("changeset/create", toXml(changeset), progressMonitor); 464 changeset.setId(Integer.parseInt(ret.trim())); 465 changeset.setOpen(true); 466 } catch (NumberFormatException e) { 467 throw new OsmTransferException(tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret), e); 468 } 469 progressMonitor.setCustomText(tr("Successfully opened changeset {0}", changeset.getId())); 470 } finally { 471 progressMonitor.finishTask(); 472 } 473 } 474 475 /** 476 * Updates a changeset with the keys in <code>changesetUpdate</code>. The changeset must not 477 * be null and id > 0 must be true. 478 * 479 * @param changeset the changeset to update. Must not be null. 480 * @param monitor the progress monitor. If null, uses the {@link NullProgressMonitor#INSTANCE}. 481 * 482 * @throws OsmTransferException if something goes wrong. 483 * @throws IllegalArgumentException if changeset is null 484 * @throws IllegalArgumentException if changeset.getId() <= 0 485 * 486 */ 487 public void updateChangeset(Changeset changeset, ProgressMonitor monitor) throws OsmTransferException { 488 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset"); 489 if (monitor == null) { 490 monitor = NullProgressMonitor.INSTANCE; 491 } 492 if (changeset.getId() <= 0) 493 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId())); 494 try { 495 monitor.beginTask(tr("Updating changeset...")); 496 initialize(monitor); 497 monitor.setCustomText(tr("Updating changeset {0}...", changeset.getId())); 498 sendPutRequest("changeset/" + changeset.getId(), toXml(changeset), monitor); 499 } catch (ChangesetClosedException e) { 500 e.setSource(ChangesetClosedException.Source.UPDATE_CHANGESET); 501 throw e; 502 } catch (OsmApiException e) { 503 String errorHeader = e.getErrorHeader(); 504 if (e.getResponseCode() == HttpURLConnection.HTTP_CONFLICT && ChangesetClosedException.errorHeaderMatchesPattern(errorHeader)) 505 throw new ChangesetClosedException(errorHeader, ChangesetClosedException.Source.UPDATE_CHANGESET, e); 506 throw e; 507 } finally { 508 monitor.finishTask(); 509 } 510 } 511 512 /** 513 * Closes a changeset on the server. Sets changeset.setOpen(false) if this operation succeeds. 514 * 515 * @param changeset the changeset to be closed. Must not be null. changeset.getId() > 0 required. 516 * @param monitor the progress monitor. If null, uses {@link NullProgressMonitor#INSTANCE} 517 * 518 * @throws OsmTransferException if something goes wrong. 519 * @throws IllegalArgumentException if changeset is null 520 * @throws IllegalArgumentException if changeset.getId() <= 0 521 */ 522 public void closeChangeset(Changeset changeset, ProgressMonitor monitor) throws OsmTransferException { 523 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset"); 524 if (monitor == null) { 525 monitor = NullProgressMonitor.INSTANCE; 526 } 527 if (changeset.getId() <= 0) 528 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId())); 529 try { 530 monitor.beginTask(tr("Closing changeset...")); 531 initialize(monitor); 532 // send "\r\n" instead of empty string, so we don't send zero payload - workaround bugs in proxy software 533 sendPutRequest("changeset/" + changeset.getId() + "/close", "\r\n", monitor); 534 } catch (ChangesetClosedException e) { 535 e.setSource(ChangesetClosedException.Source.CLOSE_CHANGESET); 536 throw e; 537 } finally { 538 changeset.setOpen(false); 539 monitor.finishTask(); 540 } 541 } 542 543 /** 544 * Adds a comment to the discussion of a closed changeset. 545 * 546 * @param changeset the changeset where to add a comment. Must be closed. changeset.getId() > 0 required. 547 * @param comment Text of the comment 548 * @param monitor the progress monitor. If null, uses {@link NullProgressMonitor#INSTANCE} 549 * 550 * @throws OsmTransferException if something goes wrong. 551 * @since 17500 552 */ 553 public void addCommentToChangeset(Changeset changeset, String comment, ProgressMonitor monitor) throws OsmTransferException { 554 if (changeset.isOpen()) 555 throw new IllegalArgumentException(tr("Changeset must be closed in order to add a comment")); 556 else if (changeset.getId() <= 0) 557 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId())); 558 sendRequest("POST", "changeset/" + changeset.getId() + "/comment?text="+ Utils.encodeUrl(comment), 559 null, monitor, "application/x-www-form-urlencoded", true, false); 560 } 561 562 /** 563 * Uploads a list of changes in "diff" form to the server. 564 * 565 * @param list the list of changed OSM Primitives 566 * @param monitor the progress monitor 567 * @return list of processed primitives 568 * @throws OsmTransferException if something is wrong 569 */ 570 public Collection<OsmPrimitive> uploadDiff(Collection<? extends OsmPrimitive> list, ProgressMonitor monitor) 571 throws OsmTransferException { 572 try { 573 ensureValidChangeset(); 574 monitor.beginTask("", list.size() * 2); 575 576 initialize(monitor); 577 578 // prepare upload request 579 // 580 OsmChangeBuilder changeBuilder = new OsmChangeBuilder(changeset); 581 monitor.subTask(tr("Preparing upload request...")); 582 changeBuilder.start(); 583 changeBuilder.append(list); 584 changeBuilder.finish(); 585 String diffUploadRequest = changeBuilder.getDocument(); 586 587 // Upload to the server 588 // 589 monitor.indeterminateSubTask( 590 trn("Uploading {0} object...", "Uploading {0} objects...", list.size(), list.size())); 591 String diffUploadResponse = sendPostRequest("changeset/" + changeset.getId() + "/upload", diffUploadRequest, monitor); 592 593 // Process the response from the server 594 // 595 DiffResultProcessor reader = new DiffResultProcessor(list); 596 reader.parse(diffUploadResponse, monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 597 return reader.postProcess( 598 getChangeset(), 599 monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false) 600 ); 601 } catch (ChangesetClosedException e) { 602 e.setSource(ChangesetClosedException.Source.UPLOAD_DATA); 603 throw e; 604 } catch (XmlParsingException e) { 605 throw new OsmTransferException(e); 606 } finally { 607 monitor.finishTask(); 608 } 609 } 610 611 private void sleepAndListen(int retry, ProgressMonitor monitor) throws OsmTransferCanceledException { 612 Logging.info(tr("Waiting 10 seconds ... ")); 613 for (int i = 0; i < 10; i++) { 614 if (monitor != null) { 615 monitor.setCustomText(tr("Starting retry {0} of {1} in {2} seconds ...", getMaxRetries() - retry, getMaxRetries(), 10-i)); 616 } 617 if (cancel) 618 throw new OsmTransferCanceledException("Operation canceled" + (i > 0 ? " in retry #"+i : "")); 619 try { 620 Thread.sleep(1000); 621 } catch (InterruptedException ex) { 622 Logging.warn("InterruptedException in "+getClass().getSimpleName()+" during sleep"); 623 Thread.currentThread().interrupt(); 624 } 625 } 626 Logging.info(tr("OK - trying again.")); 627 } 628 629 /** 630 * Replies the max. number of retries in case of 5XX errors on the server 631 * 632 * @return the max number of retries 633 */ 634 protected int getMaxRetries() { 635 int ret = Config.getPref().getInt("osm-server.max-num-retries", DEFAULT_MAX_NUM_RETRIES); 636 return Math.max(ret, 0); 637 } 638 639 /** 640 * Determines if JOSM is configured to access OSM API via OAuth 641 * @return {@code true} if JOSM is configured to access OSM API via OAuth, {@code false} otherwise 642 * @since 6349 643 */ 644 public static boolean isUsingOAuth() { 645 return "oauth".equals(getAuthMethod()); 646 } 647 648 /** 649 * Returns the authentication method set in the preferences 650 * @return the authentication method 651 */ 652 public static String getAuthMethod() { 653 return Config.getPref().get("osm-server.auth-method", "oauth"); 654 } 655 656 protected final String sendPostRequest(String urlSuffix, String requestBody, ProgressMonitor monitor) throws OsmTransferException { 657 // Send a POST request that includes authentication credentials 658 return sendRequest("POST", urlSuffix, requestBody, monitor); 659 } 660 661 protected final String sendPutRequest(String urlSuffix, String requestBody, ProgressMonitor monitor) throws OsmTransferException { 662 // Send a PUT request that includes authentication credentials 663 return sendRequest("PUT", urlSuffix, requestBody, monitor); 664 } 665 666 protected final String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor) 667 throws OsmTransferException { 668 return sendRequest(requestMethod, urlSuffix, requestBody, monitor, true, false); 669 } 670 671 protected final String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor, 672 boolean doAuthenticate, boolean fastFail) throws OsmTransferException { 673 return sendRequest(requestMethod, urlSuffix, requestBody, monitor, null, doAuthenticate, fastFail); 674 } 675 676 /** 677 * Generic method for sending requests to the OSM API. 678 * 679 * This method will automatically re-try any requests that are answered with a 5xx 680 * error code, or that resulted in a timeout exception from the TCP layer. 681 * 682 * @param requestMethod The http method used when talking with the server. 683 * @param urlSuffix The suffix to add at the server url, not including the version number, 684 * but including any object ids (e.g. "/way/1234/history"). 685 * @param requestBody the body of the HTTP request, if any. 686 * @param monitor the progress monitor 687 * @param contentType Content-Type to set for PUT/POST/DELETE requests. 688 * Can be set to {@code null}, in that case it means {@code text/xml} 689 * @param doAuthenticate set to true, if the request sent to the server shall include authentication credentials; 690 * @param fastFail true to request a short timeout 691 * 692 * @return the body of the HTTP response, if and only if the response code was "200 OK". 693 * @throws OsmTransferException if the HTTP return code was not 200 (and retries have 694 * been exhausted), or rewrapping a Java exception. 695 */ 696 protected final String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor, 697 String contentType, boolean doAuthenticate, boolean fastFail) throws OsmTransferException { 698 int retries = fastFail ? 0 : getMaxRetries(); 699 700 while (true) { // the retry loop 701 try { 702 url = new URL(new URL(getBaseUrl()), urlSuffix); 703 final HttpClient client = HttpClient.create(url, requestMethod) 704 .keepAlive(false) 705 .setAccept("application/xml, */*;q=0.8"); 706 activeConnection = client; 707 if (fastFail) { 708 client.setConnectTimeout(1000); 709 client.setReadTimeout(1000); 710 } else { 711 // use default connect timeout from org.openstreetmap.josm.tools.HttpClient.connectTimeout 712 client.setReadTimeout(0); 713 } 714 if (doAuthenticate) { 715 addAuth(client); 716 } 717 718 if ("PUT".equals(requestMethod) || "POST".equals(requestMethod) || "DELETE".equals(requestMethod)) { 719 client.setHeader("Content-Type", contentType == null ? "text/xml" : contentType); 720 // It seems that certain bits of the Ruby API are very unhappy upon 721 // receipt of a PUT/POST message without a Content-length header, 722 // even if the request has no payload. 723 // Since Java will not generate a Content-length header unless 724 // we use the output stream, we create an output stream for PUT/POST 725 // even if there is no payload. 726 client.setRequestBody((requestBody != null ? requestBody : "").getBytes(StandardCharsets.UTF_8)); 727 } 728 729 final HttpClient.Response response = client.connect(); 730 Logging.info(response.getResponseMessage()); 731 int retCode = response.getResponseCode(); 732 733 if (retCode >= 500 && retries-- > 0) { 734 sleepAndListen(retries, monitor); 735 Logging.info(tr("Starting retry {0} of {1}.", getMaxRetries() - retries, getMaxRetries())); 736 continue; 737 } 738 739 final String responseBody = response.fetchContent(); 740 741 String errorHeader = null; 742 // Look for a detailed error message from the server 743 if (response.getHeaderField("Error") != null) { 744 errorHeader = response.getHeaderField("Error"); 745 Logging.error("Error header: " + errorHeader); 746 } else if (retCode != HttpURLConnection.HTTP_OK && responseBody.length() > 0) { 747 Logging.error("Error body: " + responseBody); 748 } 749 activeConnection.disconnect(); 750 751 errorHeader = errorHeader == null ? null : errorHeader.trim(); 752 String errorBody = responseBody.length() == 0 ? null : responseBody.trim(); 753 switch(retCode) { 754 case HttpURLConnection.HTTP_OK: 755 return responseBody; 756 case HttpURLConnection.HTTP_GONE: 757 throw new OsmApiPrimitiveGoneException(errorHeader, errorBody); 758 case HttpURLConnection.HTTP_CONFLICT: 759 if (ChangesetClosedException.errorHeaderMatchesPattern(errorHeader)) 760 throw new ChangesetClosedException(errorBody, ChangesetClosedException.Source.UNSPECIFIED); 761 else 762 throw new OsmApiException(retCode, errorHeader, errorBody); 763 case HttpURLConnection.HTTP_UNAUTHORIZED: 764 case HttpURLConnection.HTTP_FORBIDDEN: 765 CredentialsManager.getInstance().purgeCredentialsCache(RequestorType.SERVER); 766 throw new OsmApiException(retCode, errorHeader, errorBody, activeConnection.getURL().toString(), 767 doAuthenticate ? retrieveBasicAuthorizationLogin(client) : null, response.getContentType()); 768 default: 769 throw new OsmApiException(retCode, errorHeader, errorBody); 770 } 771 } catch (SocketTimeoutException | ConnectException e) { 772 if (retries-- > 0) { 773 continue; 774 } 775 throw new OsmTransferException(e); 776 } catch (IOException e) { 777 throw new OsmTransferException(e); 778 } catch (OsmTransferException e) { 779 throw e; 780 } 781 } 782 } 783 784 /** 785 * Replies the API capabilities. 786 * 787 * @return the API capabilities, or null, if the API is not initialized yet 788 */ 789 public synchronized Capabilities getCapabilities() { 790 return capabilities; 791 } 792 793 /** 794 * Ensures that the current changeset can be used for uploading data 795 * 796 * @throws OsmTransferException if the current changeset can't be used for uploading data 797 */ 798 protected void ensureValidChangeset() throws OsmTransferException { 799 if (changeset == null) 800 throw new OsmTransferException(tr("Current changeset is null. Cannot upload data.")); 801 if (changeset.getId() <= 0) 802 throw new OsmTransferException(tr("ID of current changeset > 0 required. Current ID is {0}.", changeset.getId())); 803 } 804 805 /** 806 * Replies the changeset data uploads are currently directed to 807 * 808 * @return the changeset data uploads are currently directed to 809 */ 810 public Changeset getChangeset() { 811 return changeset; 812 } 813 814 /** 815 * Sets the changesets to which further data uploads are directed. The changeset 816 * can be null. If it isn't null it must have been created, i.e. id > 0 is required. Furthermore, 817 * it must be open. 818 * 819 * @param changeset the changeset 820 * @throws IllegalArgumentException if changeset.getId() <= 0 821 * @throws IllegalArgumentException if !changeset.isOpen() 822 */ 823 public void setChangeset(Changeset changeset) { 824 if (changeset == null) { 825 this.changeset = null; 826 return; 827 } 828 if (changeset.getId() <= 0) 829 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId())); 830 if (!changeset.isOpen()) 831 throw new IllegalArgumentException(tr("Open changeset expected. Got closed changeset with id {0}.", changeset.getId())); 832 this.changeset = changeset; 833 } 834 835 private static StringBuilder noteStringBuilder(Note note) { 836 return new StringBuilder().append("notes/").append(note.getId()); 837 } 838 839 /** 840 * Create a new note on the server. 841 * @param latlon Location of note 842 * @param text Comment entered by user to open the note 843 * @param monitor Progress monitor 844 * @return Note as it exists on the server after creation (ID assigned) 845 * @throws OsmTransferException if any error occurs during dialog with OSM API 846 */ 847 public Note createNote(LatLon latlon, String text, ProgressMonitor monitor) throws OsmTransferException { 848 initialize(monitor); 849 String noteUrl = new StringBuilder() 850 .append("notes?lat=") 851 .append(latlon.lat()) 852 .append("&lon=") 853 .append(latlon.lon()) 854 .append("&text=") 855 .append(Utils.encodeUrl(text)).toString(); 856 857 return parseSingleNote(sendPostRequest(noteUrl, null, monitor)); 858 } 859 860 /** 861 * Add a comment to an existing note. 862 * @param note The note to add a comment to 863 * @param comment Text of the comment 864 * @param monitor Progress monitor 865 * @return Note returned by the API after the comment was added 866 * @throws OsmTransferException if any error occurs during dialog with OSM API 867 */ 868 public Note addCommentToNote(Note note, String comment, ProgressMonitor monitor) throws OsmTransferException { 869 initialize(monitor); 870 String noteUrl = noteStringBuilder(note) 871 .append("/comment?text=") 872 .append(Utils.encodeUrl(comment)).toString(); 873 874 return parseSingleNote(sendPostRequest(noteUrl, null, monitor)); 875 } 876 877 /** 878 * Close a note. 879 * @param note Note to close. Must currently be open 880 * @param closeMessage Optional message supplied by the user when closing the note 881 * @param monitor Progress monitor 882 * @return Note returned by the API after the close operation 883 * @throws OsmTransferException if any error occurs during dialog with OSM API 884 */ 885 public Note closeNote(Note note, String closeMessage, ProgressMonitor monitor) throws OsmTransferException { 886 initialize(monitor); 887 String encodedMessage = Utils.encodeUrl(closeMessage); 888 StringBuilder urlBuilder = noteStringBuilder(note) 889 .append("/close"); 890 if (!encodedMessage.trim().isEmpty()) { 891 urlBuilder.append("?text="); 892 urlBuilder.append(encodedMessage); 893 } 894 895 return parseSingleNote(sendPostRequest(urlBuilder.toString(), null, monitor)); 896 } 897 898 /** 899 * Reopen a closed note 900 * @param note Note to reopen. Must currently be closed 901 * @param reactivateMessage Optional message supplied by the user when reopening the note 902 * @param monitor Progress monitor 903 * @return Note returned by the API after the reopen operation 904 * @throws OsmTransferException if any error occurs during dialog with OSM API 905 */ 906 public Note reopenNote(Note note, String reactivateMessage, ProgressMonitor monitor) throws OsmTransferException { 907 initialize(monitor); 908 String encodedMessage = Utils.encodeUrl(reactivateMessage); 909 StringBuilder urlBuilder = noteStringBuilder(note) 910 .append("/reopen"); 911 if (!encodedMessage.trim().isEmpty()) { 912 urlBuilder.append("?text="); 913 urlBuilder.append(encodedMessage); 914 } 915 916 return parseSingleNote(sendPostRequest(urlBuilder.toString(), null, monitor)); 917 } 918 919 /** 920 * Method for parsing API responses for operations on individual notes 921 * @param xml the API response as XML data 922 * @return the resulting Note 923 * @throws OsmTransferException if the API response cannot be parsed 924 */ 925 private static Note parseSingleNote(String xml) throws OsmTransferException { 926 try { 927 List<Note> newNotes = new NoteReader(xml).parse(); 928 if (newNotes.size() == 1) { 929 return newNotes.get(0); 930 } 931 // Shouldn't ever execute. Server will either respond with an error (caught elsewhere) or one note 932 throw new OsmTransferException(tr("Note upload failed")); 933 } catch (SAXException | IOException e) { 934 Logging.error(e); 935 throw new OsmTransferException(tr("Error parsing note response from server"), e); 936 } 937 } 938}