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