001//License: GPL. See README for details. 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.BufferedReader; 008import java.io.BufferedWriter; 009import java.io.IOException; 010import java.io.InputStream; 011import java.io.InputStreamReader; 012import java.io.OutputStream; 013import java.io.OutputStreamWriter; 014import java.io.PrintWriter; 015import java.io.StringReader; 016import java.io.StringWriter; 017import java.net.ConnectException; 018import java.net.HttpURLConnection; 019import java.net.MalformedURLException; 020import java.net.SocketTimeoutException; 021import java.net.URL; 022import java.net.UnknownHostException; 023import java.util.Collection; 024import java.util.Collections; 025import java.util.HashMap; 026import java.util.Map; 027 028import javax.xml.parsers.ParserConfigurationException; 029import javax.xml.parsers.SAXParserFactory; 030 031import org.openstreetmap.josm.Main; 032import org.openstreetmap.josm.data.osm.Changeset; 033import org.openstreetmap.josm.data.osm.IPrimitive; 034import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 035import org.openstreetmap.josm.gui.layer.ImageryLayer; 036import org.openstreetmap.josm.gui.layer.Layer; 037import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 038import org.openstreetmap.josm.gui.progress.ProgressMonitor; 039import org.openstreetmap.josm.tools.CheckParameterUtil; 040import org.openstreetmap.josm.tools.Utils; 041import org.xml.sax.Attributes; 042import org.xml.sax.InputSource; 043import org.xml.sax.SAXException; 044import org.xml.sax.SAXParseException; 045import org.xml.sax.helpers.DefaultHandler; 046 047/** 048 * Class that encapsulates the communications with the <a href="http://wiki.openstreetmap.org/wiki/API_v0.6">OSM API</a>.<br/><br/> 049 * 050 * All interaction with the server-side OSM API should go through this class.<br/><br/> 051 * 052 * It is conceivable to extract this into an interface later and create various 053 * classes implementing the interface, to be able to talk to various kinds of servers. 054 * 055 */ 056public class OsmApi extends OsmConnection { 057 058 /** 059 * Maximum number of retries to send a request in case of HTTP 500 errors or timeouts 060 */ 061 static public final int DEFAULT_MAX_NUM_RETRIES = 5; 062 063 /** 064 * Maximum number of concurrent download threads, imposed by 065 * <a href="http://wiki.openstreetmap.org/wiki/API_usage_policy#Technical_Usage_Requirements"> 066 * OSM API usage policy.</a> 067 * @since 5386 068 */ 069 static public final int MAX_DOWNLOAD_THREADS = 2; 070 071 /** 072 * Default URL of the standard OSM API. 073 * @since 5422 074 */ 075 static public final String DEFAULT_API_URL = "http://api.openstreetmap.org/api"; 076 077 // The collection of instantiated OSM APIs 078 private static Map<String, OsmApi> instances = new HashMap<String, OsmApi>(); 079 080 /** 081 * Replies the {@link OsmApi} for a given server URL 082 * 083 * @param serverUrl the server URL 084 * @return the OsmApi 085 * @throws IllegalArgumentException thrown, if serverUrl is null 086 * 087 */ 088 static public OsmApi getOsmApi(String serverUrl) { 089 OsmApi api = instances.get(serverUrl); 090 if (api == null) { 091 api = new OsmApi(serverUrl); 092 instances.put(serverUrl,api); 093 } 094 return api; 095 } 096 097 /** 098 * Replies the {@link OsmApi} for the URL given by the preference <code>osm-server.url</code> 099 * 100 * @return the OsmApi 101 * @throws IllegalStateException thrown, if the preference <code>osm-server.url</code> is not set 102 * 103 */ 104 static public OsmApi getOsmApi() { 105 String serverUrl = Main.pref.get("osm-server.url", DEFAULT_API_URL); 106 if (serverUrl == null) 107 throw new IllegalStateException(tr("Preference ''{0}'' missing. Cannot initialize OsmApi.", "osm-server.url")); 108 return getOsmApi(serverUrl); 109 } 110 111 /** the server URL */ 112 private String serverUrl; 113 114 /** 115 * Object describing current changeset 116 */ 117 private Changeset changeset; 118 119 /** 120 * API version used for server communications 121 */ 122 private String version = null; 123 124 /** the api capabilities */ 125 private Capabilities capabilities = new Capabilities(); 126 127 /** 128 * true if successfully initialized 129 */ 130 private boolean initialized = false; 131 132 /** 133 * A parser for the "capabilities" response XML 134 */ 135 private class CapabilitiesParser extends DefaultHandler { 136 @Override 137 public void startDocument() throws SAXException { 138 capabilities.clear(); 139 } 140 141 @Override public void startElement(String namespaceURI, String localName, String qName, Attributes atts) throws SAXException { 142 for (int i=0; i< atts.getLength(); i++) { 143 capabilities.put(qName, atts.getQName(i), atts.getValue(i)); 144 } 145 } 146 } 147 148 /** 149 * creates an OSM api for a specific server URL 150 * 151 * @param serverUrl the server URL. Must not be null 152 * @throws IllegalArgumentException thrown, if serverUrl is null 153 */ 154 protected OsmApi(String serverUrl) { 155 CheckParameterUtil.ensureParameterNotNull(serverUrl, "serverUrl"); 156 this.serverUrl = serverUrl; 157 } 158 159 /** 160 * Replies the OSM protocol version we use to talk to the server. 161 * @return protocol version, or null if not yet negotiated. 162 */ 163 public String getVersion() { 164 return version; 165 } 166 167 /** 168 * Replies the host name of the server URL. 169 * @return the host name of the server URL, or null if the server URL is malformed. 170 */ 171 public String getHost() { 172 String host = null; 173 try { 174 host = (new URL(serverUrl)).getHost(); 175 } catch (MalformedURLException e) { 176 } 177 return host; 178 } 179 180 private class CapabilitiesCache extends CacheCustomContent<OsmTransferException> { 181 182 ProgressMonitor monitor; 183 boolean fastFail; 184 185 public CapabilitiesCache(ProgressMonitor monitor, boolean fastFail) { 186 super("capabilities" + getBaseUrl().hashCode(), CacheCustomContent.INTERVAL_WEEKLY); 187 this.monitor = monitor; 188 this.fastFail = fastFail; 189 } 190 191 @Override 192 protected byte[] updateData() throws OsmTransferException { 193 return sendRequest("GET", "capabilities", null, monitor, false, fastFail).getBytes(); 194 } 195 } 196 197 /** 198 * Initializes this component by negotiating a protocol version with the server. 199 * 200 * @param monitor the progress monitor 201 * @throws OsmTransferCanceledException If the initialisation has been cancelled by user. 202 * @throws OsmApiInitializationException If any other exception occurs. Use getCause() to get the original exception. 203 */ 204 public void initialize(ProgressMonitor monitor) throws OsmTransferCanceledException, OsmApiInitializationException { 205 initialize(monitor, false); 206 } 207 208 /** 209 * Initializes this component by negotiating a protocol version with the server, with the ability to control the timeout. 210 * 211 * @param monitor the progress monitor 212 * @param fastFail true to request quick initialisation with a small timeout (more likely to throw exception) 213 * @throws OsmTransferCanceledException If the initialisation has been cancelled by user. 214 * @throws OsmApiInitializationException If any other exception occurs. Use getCause() to get the original exception. 215 */ 216 public void initialize(ProgressMonitor monitor, boolean fastFail) throws OsmTransferCanceledException, OsmApiInitializationException { 217 if (initialized) 218 return; 219 cancel = false; 220 try { 221 CapabilitiesCache cache = new CapabilitiesCache(monitor, fastFail); 222 try { 223 initializeCapabilities(cache.updateIfRequiredString()); 224 } catch (SAXParseException parseException) { 225 // XML parsing may fail if JOSM previously stored a corrupted capabilities document (see #8278) 226 // In that case, force update and try again 227 initializeCapabilities(cache.updateForceString()); 228 } 229 if (capabilities.supportsVersion("0.6")) { 230 version = "0.6"; 231 } else { 232 Main.error(tr("This version of JOSM is incompatible with the configured server.")); 233 Main.error(tr("It supports protocol version 0.6, while the server says it supports {0} to {1}.", 234 capabilities.get("version", "minimum"), capabilities.get("version", "maximum"))); 235 initialized = false; // FIXME gets overridden by next assignment 236 } 237 initialized = true; 238 239 /* This is an interim solution for openstreetmap.org not currently 240 * transmitting their imagery blacklist in the capabilities call. 241 * remove this as soon as openstreetmap.org adds blacklists. */ 242 if (this.serverUrl.matches(".*openstreetmap.org/api.*") && capabilities.getImageryBlacklist().isEmpty()) 243 { 244 capabilities.put("blacklist", "regex", ".*\\.google\\.com/.*"); 245 capabilities.put("blacklist", "regex", ".*209\\.85\\.2\\d\\d.*"); 246 capabilities.put("blacklist", "regex", ".*209\\.85\\.1[3-9]\\d.*"); 247 capabilities.put("blacklist", "regex", ".*209\\.85\\.12[89].*"); 248 } 249 250 /* This checks if there are any layers currently displayed that 251 * are now on the blacklist, and removes them. This is a rare 252 * situation - probably only occurs if the user changes the API URL 253 * in the preferences menu. Otherwise they would not have been able 254 * to load the layers in the first place becuase they would have 255 * been disabled! */ 256 if (Main.isDisplayingMapView()) { 257 for (Layer l : Main.map.mapView.getLayersOfType(ImageryLayer.class)) { 258 if (((ImageryLayer) l).getInfo().isBlacklisted()) { 259 Main.info(tr("Removed layer {0} because it is not allowed by the configured API.", l.getName())); 260 Main.main.removeLayer(l); 261 } 262 } 263 } 264 265 } catch (OsmTransferCanceledException e) { 266 throw e; 267 } catch (Exception e) { 268 initialized = false; 269 throw new OsmApiInitializationException(e); 270 } 271 } 272 273 private void initializeCapabilities(String xml) throws SAXException, IOException, ParserConfigurationException { 274 InputSource inputSource = new InputSource(new StringReader(xml)); 275 SAXParserFactory.newInstance().newSAXParser().parse(inputSource, new CapabilitiesParser()); 276 } 277 278 /** 279 * Makes an XML string from an OSM primitive. Uses the OsmWriter class. 280 * @param o the OSM primitive 281 * @param addBody true to generate the full XML, false to only generate the encapsulating tag 282 * @return XML string 283 */ 284 private String toXml(IPrimitive o, boolean addBody) { 285 StringWriter swriter = new StringWriter(); 286 OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(new PrintWriter(swriter), true, version); 287 swriter.getBuffer().setLength(0); 288 osmWriter.setWithBody(addBody); 289 osmWriter.setChangeset(changeset); 290 osmWriter.header(); 291 o.accept(osmWriter); 292 osmWriter.footer(); 293 osmWriter.flush(); 294 return swriter.toString(); 295 } 296 297 /** 298 * Makes an XML string from an OSM primitive. Uses the OsmWriter class. 299 * @param s the changeset 300 * @return XML string 301 */ 302 private String toXml(Changeset s) { 303 StringWriter swriter = new StringWriter(); 304 OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(new PrintWriter(swriter), true, version); 305 swriter.getBuffer().setLength(0); 306 osmWriter.header(); 307 osmWriter.visit(s); 308 osmWriter.footer(); 309 osmWriter.flush(); 310 return swriter.toString(); 311 } 312 313 /** 314 * Returns the base URL for API requests, including the negotiated version number. 315 * @return base URL string 316 */ 317 public String getBaseUrl() { 318 StringBuffer rv = new StringBuffer(serverUrl); 319 if (version != null) { 320 rv.append("/"); 321 rv.append(version); 322 } 323 rv.append("/"); 324 // this works around a ruby (or lighttpd) bug where two consecutive slashes in 325 // an URL will cause a "404 not found" response. 326 int p; while ((p = rv.indexOf("//", 6)) > -1) { rv.delete(p, p + 1); } 327 return rv.toString(); 328 } 329 330 /** 331 * Creates an OSM primitive on the server. The OsmPrimitive object passed in 332 * is modified by giving it the server-assigned id. 333 * 334 * @param osm the primitive 335 * @param monitor the progress monitor 336 * @throws OsmTransferException if something goes wrong 337 */ 338 public void createPrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException { 339 String ret = ""; 340 try { 341 ensureValidChangeset(); 342 initialize(monitor); 343 ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/create", toXml(osm, true),monitor); 344 osm.setOsmId(Long.parseLong(ret.trim()), 1); 345 osm.setChangesetId(getChangeset().getId()); 346 } catch(NumberFormatException e){ 347 throw new OsmTransferException(tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret)); 348 } 349 } 350 351 /** 352 * Modifies an OSM primitive on the server. 353 * 354 * @param osm the primitive. Must not be null. 355 * @param monitor the progress monitor 356 * @throws OsmTransferException if something goes wrong 357 */ 358 public void modifyPrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException { 359 String ret = null; 360 try { 361 ensureValidChangeset(); 362 initialize(monitor); 363 // normal mode (0.6 and up) returns new object version. 364 ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/" + osm.getId(), toXml(osm, true), monitor); 365 osm.setOsmId(osm.getId(), Integer.parseInt(ret.trim())); 366 osm.setChangesetId(getChangeset().getId()); 367 osm.setVisible(true); 368 } catch(NumberFormatException e) { 369 throw new OsmTransferException(tr("Unexpected format of new version of modified primitive ''{0}''. Got ''{1}''.", osm.getId(), ret)); 370 } 371 } 372 373 /** 374 * Deletes an OSM primitive on the server. 375 * @param osm the primitive 376 * @param monitor the progress monitor 377 * @throws OsmTransferException if something goes wrong 378 */ 379 public void deletePrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException { 380 ensureValidChangeset(); 381 initialize(monitor); 382 // can't use a the individual DELETE method in the 0.6 API. Java doesn't allow 383 // submitting a DELETE request with content, the 0.6 API requires it, however. Falling back 384 // to diff upload. 385 // 386 uploadDiff(Collections.singleton(osm), monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 387 } 388 389 /** 390 * Creates a new changeset based on the keys in <code>changeset</code>. If this 391 * method succeeds, changeset.getId() replies the id the server assigned to the new 392 * changeset 393 * 394 * The changeset must not be null, but its key/value-pairs may be empty. 395 * 396 * @param changeset the changeset toe be created. Must not be null. 397 * @param progressMonitor the progress monitor 398 * @throws OsmTransferException signifying a non-200 return code, or connection errors 399 * @throws IllegalArgumentException thrown if changeset is null 400 */ 401 public void openChangeset(Changeset changeset, ProgressMonitor progressMonitor) throws OsmTransferException { 402 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset"); 403 try { 404 progressMonitor.beginTask((tr("Creating changeset..."))); 405 initialize(progressMonitor); 406 String ret = ""; 407 try { 408 ret = sendRequest("PUT", "changeset/create", toXml(changeset),progressMonitor); 409 changeset.setId(Integer.parseInt(ret.trim())); 410 changeset.setOpen(true); 411 } catch(NumberFormatException e){ 412 throw new OsmTransferException(tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret)); 413 } 414 progressMonitor.setCustomText((tr("Successfully opened changeset {0}",changeset.getId()))); 415 } finally { 416 progressMonitor.finishTask(); 417 } 418 } 419 420 /** 421 * Updates a changeset with the keys in <code>changesetUpdate</code>. The changeset must not 422 * be null and id > 0 must be true. 423 * 424 * @param changeset the changeset to update. Must not be null. 425 * @param monitor the progress monitor. If null, uses the {@link NullProgressMonitor#INSTANCE}. 426 * 427 * @throws OsmTransferException if something goes wrong. 428 * @throws IllegalArgumentException if changeset is null 429 * @throws IllegalArgumentException if changeset.getId() <= 0 430 * 431 */ 432 public void updateChangeset(Changeset changeset, ProgressMonitor monitor) throws OsmTransferException { 433 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset"); 434 if (monitor == null) { 435 monitor = NullProgressMonitor.INSTANCE; 436 } 437 if (changeset.getId() <= 0) 438 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId())); 439 try { 440 monitor.beginTask(tr("Updating changeset...")); 441 initialize(monitor); 442 monitor.setCustomText(tr("Updating changeset {0}...", changeset.getId())); 443 sendRequest( 444 "PUT", 445 "changeset/" + changeset.getId(), 446 toXml(changeset), 447 monitor 448 ); 449 } catch(ChangesetClosedException e) { 450 e.setSource(ChangesetClosedException.Source.UPDATE_CHANGESET); 451 throw e; 452 } catch(OsmApiException e) { 453 if (e.getResponseCode() == HttpURLConnection.HTTP_CONFLICT && ChangesetClosedException.errorHeaderMatchesPattern(e.getErrorHeader())) 454 throw new ChangesetClosedException(e.getErrorHeader(), ChangesetClosedException.Source.UPDATE_CHANGESET); 455 throw e; 456 } finally { 457 monitor.finishTask(); 458 } 459 } 460 461 /** 462 * Closes a changeset on the server. Sets changeset.setOpen(false) if this operation 463 * succeeds. 464 * 465 * @param changeset the changeset to be closed. Must not be null. changeset.getId() > 0 required. 466 * @param monitor the progress monitor. If null, uses {@link NullProgressMonitor#INSTANCE} 467 * 468 * @throws OsmTransferException if something goes wrong. 469 * @throws IllegalArgumentException thrown if changeset is null 470 * @throws IllegalArgumentException thrown if changeset.getId() <= 0 471 */ 472 public void closeChangeset(Changeset changeset, ProgressMonitor monitor) throws OsmTransferException { 473 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset"); 474 if (monitor == null) { 475 monitor = NullProgressMonitor.INSTANCE; 476 } 477 if (changeset.getId() <= 0) 478 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId())); 479 try { 480 monitor.beginTask(tr("Closing changeset...")); 481 initialize(monitor); 482 /* send "\r\n" instead of empty string, so we don't send zero payload - works around bugs 483 in proxy software */ 484 sendRequest("PUT", "changeset" + "/" + changeset.getId() + "/close", "\r\n", monitor); 485 changeset.setOpen(false); 486 } finally { 487 monitor.finishTask(); 488 } 489 } 490 491 /** 492 * Uploads a list of changes in "diff" form to the server. 493 * 494 * @param list the list of changed OSM Primitives 495 * @param monitor the progress monitor 496 * @return list of processed primitives 497 * @throws OsmTransferException if something is wrong 498 */ 499 public Collection<IPrimitive> uploadDiff(Collection<? extends IPrimitive> list, ProgressMonitor monitor) throws OsmTransferException { 500 try { 501 monitor.beginTask("", list.size() * 2); 502 if (changeset == null) 503 throw new OsmTransferException(tr("No changeset present for diff upload.")); 504 505 initialize(monitor); 506 507 // prepare upload request 508 // 509 OsmChangeBuilder changeBuilder = new OsmChangeBuilder(changeset); 510 monitor.subTask(tr("Preparing upload request...")); 511 changeBuilder.start(); 512 changeBuilder.append(list); 513 changeBuilder.finish(); 514 String diffUploadRequest = changeBuilder.getDocument(); 515 516 // Upload to the server 517 // 518 monitor.indeterminateSubTask( 519 trn("Uploading {0} object...", "Uploading {0} objects...", list.size(), list.size())); 520 String diffUploadResponse = sendRequest("POST", "changeset/" + changeset.getId() + "/upload", diffUploadRequest,monitor); 521 522 // Process the response from the server 523 // 524 DiffResultProcessor reader = new DiffResultProcessor(list); 525 reader.parse(diffUploadResponse, monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)); 526 return reader.postProcess( 527 getChangeset(), 528 monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false) 529 ); 530 } catch(OsmTransferException e) { 531 throw e; 532 } catch(OsmDataParsingException e) { 533 throw new OsmTransferException(e); 534 } finally { 535 monitor.finishTask(); 536 } 537 } 538 539 private void sleepAndListen(int retry, ProgressMonitor monitor) throws OsmTransferCanceledException { 540 Main.info(tr("Waiting 10 seconds ... ")); 541 for (int i=0; i < 10; i++) { 542 if (monitor != null) { 543 monitor.setCustomText(tr("Starting retry {0} of {1} in {2} seconds ...", getMaxRetries() - retry,getMaxRetries(), 10-i)); 544 } 545 if (cancel) 546 throw new OsmTransferCanceledException(); 547 try { 548 Thread.sleep(1000); 549 } catch (InterruptedException ex) { 550 Main.warn("InterruptedException in "+getClass().getSimpleName()+" during sleep"); 551 } 552 } 553 Main.info(tr("OK - trying again.")); 554 } 555 556 /** 557 * Replies the max. number of retries in case of 5XX errors on the server 558 * 559 * @return the max number of retries 560 */ 561 protected int getMaxRetries() { 562 int ret = Main.pref.getInteger("osm-server.max-num-retries", DEFAULT_MAX_NUM_RETRIES); 563 return Math.max(ret,0); 564 } 565 566 /** 567 * Determines if JOSM is configured to access OSM API via OAuth 568 * @return {@code true} if JOSM is configured to access OSM API via OAuth, {@code false} otherwise 569 * @since 6349 570 */ 571 public static final boolean isUsingOAuth() { 572 return "oauth".equals(Main.pref.get("osm-server.auth-method", "basic")); 573 } 574 575 private String sendRequest(String requestMethod, String urlSuffix,String requestBody, ProgressMonitor monitor) throws OsmTransferException { 576 return sendRequest(requestMethod, urlSuffix, requestBody, monitor, true, false); 577 } 578 579 /** 580 * Generic method for sending requests to the OSM API. 581 * 582 * This method will automatically re-try any requests that are answered with a 5xx 583 * error code, or that resulted in a timeout exception from the TCP layer. 584 * 585 * @param requestMethod The http method used when talking with the server. 586 * @param urlSuffix The suffix to add at the server url, not including the version number, 587 * but including any object ids (e.g. "/way/1234/history"). 588 * @param requestBody the body of the HTTP request, if any. 589 * @param monitor the progress monitor 590 * @param doAuthenticate set to true, if the request sent to the server shall include authentication 591 * credentials; 592 * @param fastFail true to request a short timeout 593 * 594 * @return the body of the HTTP response, if and only if the response code was "200 OK". 595 * @throws OsmTransferException if the HTTP return code was not 200 (and retries have 596 * been exhausted), or rewrapping a Java exception. 597 */ 598 private String sendRequest(String requestMethod, String urlSuffix,String requestBody, ProgressMonitor monitor, boolean doAuthenticate, boolean fastFail) throws OsmTransferException { 599 StringBuffer responseBody = new StringBuffer(); 600 int retries = fastFail ? 0 : getMaxRetries(); 601 602 while(true) { // the retry loop 603 try { 604 URL url = new URL(new URL(getBaseUrl()), urlSuffix); 605 System.out.print(requestMethod + " " + url + "... "); 606 // fix #5369, see http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive 607 activeConnection = Utils.openHttpConnection(url, false); 608 activeConnection.setConnectTimeout(fastFail ? 1000 : Main.pref.getInteger("socket.timeout.connect",15)*1000); 609 if (fastFail) { 610 activeConnection.setReadTimeout(1000); 611 } 612 activeConnection.setRequestMethod(requestMethod); 613 if (doAuthenticate) { 614 addAuth(activeConnection); 615 } 616 617 if (requestMethod.equals("PUT") || requestMethod.equals("POST") || requestMethod.equals("DELETE")) { 618 activeConnection.setDoOutput(true); 619 activeConnection.setRequestProperty("Content-type", "text/xml"); 620 OutputStream out = activeConnection.getOutputStream(); 621 622 // It seems that certain bits of the Ruby API are very unhappy upon 623 // receipt of a PUT/POST message without a Content-length header, 624 // even if the request has no payload. 625 // Since Java will not generate a Content-length header unless 626 // we use the output stream, we create an output stream for PUT/POST 627 // even if there is no payload. 628 if (requestBody != null) { 629 BufferedWriter bwr = new BufferedWriter(new OutputStreamWriter(out, "UTF-8")); 630 try { 631 bwr.write(requestBody); 632 bwr.flush(); 633 } finally { 634 bwr.close(); 635 } 636 } 637 Utils.close(out); 638 } 639 640 activeConnection.connect(); 641 Main.info(activeConnection.getResponseMessage()); 642 int retCode = activeConnection.getResponseCode(); 643 644 if (retCode >= 500) { 645 if (retries-- > 0) { 646 sleepAndListen(retries, monitor); 647 Main.info(tr("Starting retry {0} of {1}.", getMaxRetries() - retries,getMaxRetries())); 648 continue; 649 } 650 } 651 652 // populate return fields. 653 responseBody.setLength(0); 654 655 // If the API returned an error code like 403 forbidden, getInputStream 656 // will fail with an IOException. 657 InputStream i = null; 658 try { 659 i = activeConnection.getInputStream(); 660 } catch (IOException ioe) { 661 i = activeConnection.getErrorStream(); 662 } 663 if (i != null) { 664 // the input stream can be null if both the input and the error stream 665 // are null. Seems to be the case if the OSM server replies a 401 666 // Unauthorized, see #3887. 667 // 668 BufferedReader in = new BufferedReader(new InputStreamReader(i)); 669 String s; 670 try { 671 while((s = in.readLine()) != null) { 672 responseBody.append(s); 673 responseBody.append("\n"); 674 } 675 } finally { 676 in.close(); 677 } 678 } 679 String errorHeader = null; 680 // Look for a detailed error message from the server 681 if (activeConnection.getHeaderField("Error") != null) { 682 errorHeader = activeConnection.getHeaderField("Error"); 683 Main.error("Error header: " + errorHeader); 684 } else if (retCode != 200 && responseBody.length()>0) { 685 Main.error("Error body: " + responseBody); 686 } 687 activeConnection.disconnect(); 688 689 errorHeader = errorHeader == null? null : errorHeader.trim(); 690 String errorBody = responseBody.length() == 0? null : responseBody.toString().trim(); 691 switch(retCode) { 692 case HttpURLConnection.HTTP_OK: 693 return responseBody.toString(); 694 case HttpURLConnection.HTTP_GONE: 695 throw new OsmApiPrimitiveGoneException(errorHeader, errorBody); 696 case HttpURLConnection.HTTP_CONFLICT: 697 if (ChangesetClosedException.errorHeaderMatchesPattern(errorHeader)) 698 throw new ChangesetClosedException(errorBody, ChangesetClosedException.Source.UPLOAD_DATA); 699 else 700 throw new OsmApiException(retCode, errorHeader, errorBody); 701 case HttpURLConnection.HTTP_FORBIDDEN: 702 OsmApiException e = new OsmApiException(retCode, errorHeader, errorBody); 703 e.setAccessedUrl(activeConnection.getURL().toString()); 704 throw e; 705 default: 706 throw new OsmApiException(retCode, errorHeader, errorBody); 707 } 708 } catch (UnknownHostException e) { 709 throw new OsmTransferException(e); 710 } catch (SocketTimeoutException e) { 711 if (retries-- > 0) { 712 continue; 713 } 714 throw new OsmTransferException(e); 715 } catch (ConnectException e) { 716 if (retries-- > 0) { 717 continue; 718 } 719 throw new OsmTransferException(e); 720 } catch(IOException e){ 721 throw new OsmTransferException(e); 722 } catch(OsmTransferCanceledException e){ 723 throw e; 724 } catch(OsmTransferException e) { 725 throw e; 726 } 727 } 728 } 729 730 /** 731 * Replies the API capabilities 732 * 733 * @return the API capabilities, or null, if the API is not initialized yet 734 */ 735 public Capabilities getCapabilities() { 736 return capabilities; 737 } 738 739 /** 740 * Ensures that the current changeset can be used for uploading data 741 * 742 * @throws OsmTransferException thrown if the current changeset can't be used for 743 * uploading data 744 */ 745 protected void ensureValidChangeset() throws OsmTransferException { 746 if (changeset == null) 747 throw new OsmTransferException(tr("Current changeset is null. Cannot upload data.")); 748 if (changeset.getId() <= 0) 749 throw new OsmTransferException(tr("ID of current changeset > 0 required. Current ID is {0}.", changeset.getId())); 750 } 751 752 /** 753 * Replies the changeset data uploads are currently directed to 754 * 755 * @return the changeset data uploads are currently directed to 756 */ 757 public Changeset getChangeset() { 758 return changeset; 759 } 760 761 /** 762 * Sets the changesets to which further data uploads are directed. The changeset 763 * can be null. If it isn't null it must have been created, i.e. id > 0 is required. Furthermore, 764 * it must be open. 765 * 766 * @param changeset the changeset 767 * @throws IllegalArgumentException thrown if changeset.getId() <= 0 768 * @throws IllegalArgumentException thrown if !changeset.isOpen() 769 */ 770 public void setChangeset(Changeset changeset) { 771 if (changeset == null) { 772 this.changeset = null; 773 return; 774 } 775 if (changeset.getId() <= 0) 776 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId())); 777 if (!changeset.isOpen()) 778 throw new IllegalArgumentException(tr("Open changeset expected. Got closed changeset with id {0}.", changeset.getId())); 779 this.changeset = changeset; 780 } 781}