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 }