source: josm/trunk/src/org/openstreetmap/josm/io/OsmApi.java@ 12671

Last change on this file since 12671 was 12636, checked in by Don-vip, 7 years ago

see #15182 - deprecate Main.getLayerManager(). Replacement: gui.MainApplication.getLayerManager()

  • Property svn:eol-style set to native
File size: 35.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.io;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5import static org.openstreetmap.josm.tools.I18n.trn;
6
7import java.io.IOException;
8import java.io.PrintWriter;
9import java.io.StringReader;
10import java.io.StringWriter;
11import java.net.ConnectException;
12import java.net.HttpURLConnection;
13import java.net.MalformedURLException;
14import java.net.SocketTimeoutException;
15import java.net.URL;
16import java.nio.charset.StandardCharsets;
17import java.util.Collection;
18import java.util.Collections;
19import java.util.HashMap;
20import java.util.List;
21import java.util.Map;
22
23import javax.xml.parsers.ParserConfigurationException;
24
25import org.openstreetmap.josm.Main;
26import org.openstreetmap.josm.data.coor.LatLon;
27import org.openstreetmap.josm.data.notes.Note;
28import org.openstreetmap.josm.data.osm.Changeset;
29import org.openstreetmap.josm.data.osm.IPrimitive;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
32import org.openstreetmap.josm.gui.MainApplication;
33import org.openstreetmap.josm.gui.layer.ImageryLayer;
34import org.openstreetmap.josm.gui.layer.Layer;
35import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
36import org.openstreetmap.josm.gui.progress.ProgressMonitor;
37import org.openstreetmap.josm.io.Capabilities.CapabilitiesParser;
38import org.openstreetmap.josm.tools.CheckParameterUtil;
39import org.openstreetmap.josm.tools.HttpClient;
40import org.openstreetmap.josm.tools.Logging;
41import org.openstreetmap.josm.tools.Utils;
42import org.openstreetmap.josm.tools.XmlParsingException;
43import org.xml.sax.InputSource;
44import org.xml.sax.SAXException;
45import org.xml.sax.SAXParseException;
46
47/**
48 * Class that encapsulates the communications with the <a href="http://wiki.openstreetmap.org/wiki/API_v0.6">OSM API</a>.<br><br>
49 *
50 * All interaction with the server-side OSM API should go through this class.<br><br>
51 *
52 * It is conceivable to extract this into an interface later and create various
53 * classes implementing the interface, to be able to talk to various kinds of servers.
54 *
55 */
56public class OsmApi extends OsmConnection {
57
58 /**
59 * Maximum number of retries to send a request in case of HTTP 500 errors or timeouts
60 */
61 public static final int DEFAULT_MAX_NUM_RETRIES = 5;
62
63 /**
64 * Maximum number of concurrent download threads, imposed by
65 * <a href="http://wiki.openstreetmap.org/wiki/API_usage_policy#Technical_Usage_Requirements">
66 * OSM API usage policy.</a>
67 * @since 5386
68 */
69 public static final int MAX_DOWNLOAD_THREADS = 2;
70
71 /**
72 * Default URL of the standard OSM API.
73 * @since 5422
74 */
75 public static final String DEFAULT_API_URL = "https://api.openstreetmap.org/api";
76
77 // The collection of instantiated OSM APIs
78 private static Map<String, OsmApi> instances = new HashMap<>();
79
80 private URL url;
81
82 /**
83 * Replies the {@link OsmApi} for a given server URL
84 *
85 * @param serverUrl the server URL
86 * @return the OsmApi
87 * @throws IllegalArgumentException if serverUrl is null
88 *
89 */
90 public static OsmApi getOsmApi(String serverUrl) {
91 OsmApi api = instances.get(serverUrl);
92 if (api == null) {
93 api = new OsmApi(serverUrl);
94 cacheInstance(api);
95 }
96 return api;
97 }
98
99 protected static void cacheInstance(OsmApi api) {
100 instances.put(api.getServerUrl(), api);
101 }
102
103 private static String getServerUrlFromPref() {
104 return Main.pref.get("osm-server.url", DEFAULT_API_URL);
105 }
106
107 /**
108 * Replies the {@link OsmApi} for the URL given by the preference <code>osm-server.url</code>
109 *
110 * @return the OsmApi
111 */
112 public static OsmApi getOsmApi() {
113 return getOsmApi(getServerUrlFromPref());
114 }
115
116 /** Server URL */
117 private final String serverUrl;
118
119 /** Object describing current changeset */
120 private Changeset changeset;
121
122 /** API version used for server communications */
123 private String version;
124
125 /** API capabilities */
126 private Capabilities capabilities;
127
128 /** true if successfully initialized */
129 private boolean initialized;
130
131 /**
132 * Constructs a new {@code OsmApi} for a specific server URL.
133 *
134 * @param serverUrl the server URL. Must not be null
135 * @throws IllegalArgumentException if serverUrl is null
136 */
137 protected OsmApi(String serverUrl) {
138 CheckParameterUtil.ensureParameterNotNull(serverUrl, "serverUrl");
139 this.serverUrl = serverUrl;
140 }
141
142 /**
143 * Replies the OSM protocol version we use to talk to the server.
144 * @return protocol version, or null if not yet negotiated.
145 */
146 public String getVersion() {
147 return version;
148 }
149
150 /**
151 * Replies the host name of the server URL.
152 * @return the host name of the server URL, or null if the server URL is malformed.
153 */
154 public String getHost() {
155 String host = null;
156 try {
157 host = (new URL(serverUrl)).getHost();
158 } catch (MalformedURLException e) {
159 Logging.warn(e);
160 }
161 return host;
162 }
163
164 private class CapabilitiesCache extends CacheCustomContent<OsmTransferException> {
165
166 private static final String CAPABILITIES = "capabilities";
167
168 private final ProgressMonitor monitor;
169 private final boolean fastFail;
170
171 CapabilitiesCache(ProgressMonitor monitor, boolean fastFail) {
172 super(CAPABILITIES + getBaseUrl().hashCode(), CacheCustomContent.INTERVAL_WEEKLY);
173 this.monitor = monitor;
174 this.fastFail = fastFail;
175 }
176
177 @Override
178 protected void checkOfflineAccess() {
179 OnlineResource.OSM_API.checkOfflineAccess(getBaseUrl(getServerUrlFromPref(), "0.6")+CAPABILITIES, getServerUrlFromPref());
180 }
181
182 @Override
183 protected byte[] updateData() throws OsmTransferException {
184 return sendRequest("GET", CAPABILITIES, null, monitor, false, fastFail).getBytes(StandardCharsets.UTF_8);
185 }
186 }
187
188 /**
189 * Initializes this component by negotiating a protocol version with the server.
190 *
191 * @param monitor the progress monitor
192 * @throws OsmTransferCanceledException If the initialisation has been cancelled by user.
193 * @throws OsmApiInitializationException If any other exception occurs. Use getCause() to get the original exception.
194 */
195 public void initialize(ProgressMonitor monitor) throws OsmTransferCanceledException, OsmApiInitializationException {
196 initialize(monitor, false);
197 }
198
199 /**
200 * Initializes this component by negotiating a protocol version with the server, with the ability to control the timeout.
201 *
202 * @param monitor the progress monitor
203 * @param fastFail true to request quick initialisation with a small timeout (more likely to throw exception)
204 * @throws OsmTransferCanceledException If the initialisation has been cancelled by user.
205 * @throws OsmApiInitializationException If any other exception occurs. Use getCause() to get the original exception.
206 */
207 public void initialize(ProgressMonitor monitor, boolean fastFail) throws OsmTransferCanceledException, OsmApiInitializationException {
208 if (initialized)
209 return;
210 cancel = false;
211 try {
212 CapabilitiesCache cache = new CapabilitiesCache(monitor, fastFail);
213 try {
214 initializeCapabilities(cache.updateIfRequiredString());
215 } catch (SAXParseException parseException) {
216 Logging.trace(parseException);
217 // XML parsing may fail if JOSM previously stored a corrupted capabilities document (see #8278)
218 // In that case, force update and try again
219 initializeCapabilities(cache.updateForceString());
220 }
221 if (capabilities == null) {
222 if (Main.isOffline(OnlineResource.OSM_API)) {
223 Logging.warn(tr("{0} not available (offline mode)", tr("OSM API")));
224 } else {
225 Logging.error(tr("Unable to initialize OSM API."));
226 }
227 return;
228 } else if (!capabilities.supportsVersion("0.6")) {
229 Logging.error(tr("This version of JOSM is incompatible with the configured server."));
230 Logging.error(tr("It supports protocol version 0.6, while the server says it supports {0} to {1}.",
231 capabilities.get("version", "minimum"), capabilities.get("version", "maximum")));
232 return;
233 } else {
234 version = "0.6";
235 initialized = true;
236 }
237
238 /* This checks if there are any layers currently displayed that
239 * are now on the blacklist, and removes them. This is a rare
240 * situation - probably only occurs if the user changes the API URL
241 * in the preferences menu. Otherwise they would not have been able
242 * to load the layers in the first place because they would have
243 * been disabled! */
244 if (MainApplication.isDisplayingMapView()) {
245 for (Layer l : MainApplication.getLayerManager().getLayersOfType(ImageryLayer.class)) {
246 if (((ImageryLayer) l).getInfo().isBlacklisted()) {
247 Logging.info(tr("Removed layer {0} because it is not allowed by the configured API.", l.getName()));
248 MainApplication.getLayerManager().removeLayer(l);
249 }
250 }
251 }
252
253 } catch (OsmTransferCanceledException e) {
254 throw e;
255 } catch (OsmTransferException e) {
256 initialized = false;
257 Main.addNetworkError(url, Utils.getRootCause(e));
258 throw new OsmApiInitializationException(e);
259 } catch (SAXException | IOException | ParserConfigurationException e) {
260 initialized = false;
261 throw new OsmApiInitializationException(e);
262 }
263 }
264
265 private synchronized void initializeCapabilities(String xml) throws SAXException, IOException, ParserConfigurationException {
266 if (xml != null) {
267 capabilities = CapabilitiesParser.parse(new InputSource(new StringReader(xml)));
268 }
269 }
270
271 /**
272 * Makes an XML string from an OSM primitive. Uses the OsmWriter class.
273 * @param o the OSM primitive
274 * @param addBody true to generate the full XML, false to only generate the encapsulating tag
275 * @return XML string
276 */
277 protected final String toXml(IPrimitive o, boolean addBody) {
278 StringWriter swriter = new StringWriter();
279 try (OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(new PrintWriter(swriter), true, version)) {
280 swriter.getBuffer().setLength(0);
281 osmWriter.setWithBody(addBody);
282 osmWriter.setChangeset(changeset);
283 osmWriter.header();
284 o.accept(osmWriter);
285 osmWriter.footer();
286 osmWriter.flush();
287 } catch (IOException e) {
288 Logging.warn(e);
289 }
290 return swriter.toString();
291 }
292
293 /**
294 * Makes an XML string from an OSM primitive. Uses the OsmWriter class.
295 * @param s the changeset
296 * @return XML string
297 */
298 protected final String toXml(Changeset s) {
299 StringWriter swriter = new StringWriter();
300 try (OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(new PrintWriter(swriter), true, version)) {
301 swriter.getBuffer().setLength(0);
302 osmWriter.header();
303 osmWriter.visit(s);
304 osmWriter.footer();
305 osmWriter.flush();
306 } catch (IOException e) {
307 Logging.warn(e);
308 }
309 return swriter.toString();
310 }
311
312 private static String getBaseUrl(String serverUrl, String version) {
313 StringBuilder rv = new StringBuilder(serverUrl);
314 if (version != null) {
315 rv.append('/').append(version);
316 }
317 rv.append('/');
318 // this works around a ruby (or lighttpd) bug where two consecutive slashes in
319 // an URL will cause a "404 not found" response.
320 int p;
321 while ((p = rv.indexOf("//", rv.indexOf("://")+2)) > -1) {
322 rv.delete(p, p + 1);
323 }
324 return rv.toString();
325 }
326
327 /**
328 * Returns the base URL for API requests, including the negotiated version number.
329 * @return base URL string
330 */
331 public String getBaseUrl() {
332 return getBaseUrl(serverUrl, version);
333 }
334
335 /**
336 * Returns the server URL
337 * @return the server URL
338 * @since 9353
339 */
340 public String getServerUrl() {
341 return serverUrl;
342 }
343
344 /**
345 * Creates an OSM primitive on the server. The OsmPrimitive object passed in
346 * is modified by giving it the server-assigned id.
347 *
348 * @param osm the primitive
349 * @param monitor the progress monitor
350 * @throws OsmTransferException if something goes wrong
351 */
352 public void createPrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException {
353 String ret = "";
354 try {
355 ensureValidChangeset();
356 initialize(monitor);
357 ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/create", toXml(osm, true), monitor);
358 osm.setOsmId(Long.parseLong(ret.trim()), 1);
359 osm.setChangesetId(getChangeset().getId());
360 } catch (NumberFormatException e) {
361 throw new OsmTransferException(tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret), e);
362 }
363 }
364
365 /**
366 * Modifies an OSM primitive on the server.
367 *
368 * @param osm the primitive. Must not be null.
369 * @param monitor the progress monitor
370 * @throws OsmTransferException if something goes wrong
371 */
372 public void modifyPrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException {
373 String ret = null;
374 try {
375 ensureValidChangeset();
376 initialize(monitor);
377 // normal mode (0.6 and up) returns new object version.
378 ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+'/' + osm.getId(), toXml(osm, true), monitor);
379 osm.setOsmId(osm.getId(), Integer.parseInt(ret.trim()));
380 osm.setChangesetId(getChangeset().getId());
381 osm.setVisible(true);
382 } catch (NumberFormatException e) {
383 throw new OsmTransferException(tr("Unexpected format of new version of modified primitive ''{0}''. Got ''{1}''.",
384 osm.getId(), ret), e);
385 }
386 }
387
388 /**
389 * Deletes an OSM primitive on the server.
390 * @param osm the primitive
391 * @param monitor the progress monitor
392 * @throws OsmTransferException if something goes wrong
393 */
394 public void deletePrimitive(OsmPrimitive osm, ProgressMonitor monitor) throws OsmTransferException {
395 ensureValidChangeset();
396 initialize(monitor);
397 // can't use a the individual DELETE method in the 0.6 API. Java doesn't allow
398 // submitting a DELETE request with content, the 0.6 API requires it, however. Falling back
399 // to diff upload.
400 //
401 uploadDiff(Collections.singleton(osm), monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
402 }
403
404 /**
405 * Creates a new changeset based on the keys in <code>changeset</code>. If this
406 * method succeeds, changeset.getId() replies the id the server assigned to the new
407 * changeset
408 *
409 * The changeset must not be null, but its key/value-pairs may be empty.
410 *
411 * @param changeset the changeset toe be created. Must not be null.
412 * @param progressMonitor the progress monitor
413 * @throws OsmTransferException signifying a non-200 return code, or connection errors
414 * @throws IllegalArgumentException if changeset is null
415 */
416 public void openChangeset(Changeset changeset, ProgressMonitor progressMonitor) throws OsmTransferException {
417 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset");
418 try {
419 progressMonitor.beginTask(tr("Creating changeset..."));
420 initialize(progressMonitor);
421 String ret = "";
422 try {
423 ret = sendRequest("PUT", "changeset/create", toXml(changeset), progressMonitor);
424 changeset.setId(Integer.parseInt(ret.trim()));
425 changeset.setOpen(true);
426 } catch (NumberFormatException e) {
427 throw new OsmTransferException(tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret), e);
428 }
429 progressMonitor.setCustomText(tr("Successfully opened changeset {0}", changeset.getId()));
430 } finally {
431 progressMonitor.finishTask();
432 }
433 }
434
435 /**
436 * Updates a changeset with the keys in <code>changesetUpdate</code>. The changeset must not
437 * be null and id &gt; 0 must be true.
438 *
439 * @param changeset the changeset to update. Must not be null.
440 * @param monitor the progress monitor. If null, uses the {@link NullProgressMonitor#INSTANCE}.
441 *
442 * @throws OsmTransferException if something goes wrong.
443 * @throws IllegalArgumentException if changeset is null
444 * @throws IllegalArgumentException if changeset.getId() &lt;= 0
445 *
446 */
447 public void updateChangeset(Changeset changeset, ProgressMonitor monitor) throws OsmTransferException {
448 CheckParameterUtil.ensureParameterNotNull(changeset, "changeset");
449 if (monitor == null) {
450 monitor = NullProgressMonitor.INSTANCE;
451 }
452 if (changeset.getId() <= 0)
453 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId()));
454 try {
455 monitor.beginTask(tr("Updating changeset..."));
456 initialize(monitor);
457 monitor.setCustomText(tr("Updating changeset {0}...", changeset.getId()));
458 sendRequest(
459 "PUT",
460 "changeset/" + changeset.getId(),
461 toXml(changeset),
462 monitor
463 );
464 } catch (ChangesetClosedException e) {
465 e.setSource(ChangesetClosedException.Source.UPDATE_CHANGESET);
466 throw e;
467 } catch (OsmApiException e) {
468 String errorHeader = e.getErrorHeader();
469 if (e.getResponseCode() == HttpURLConnection.HTTP_CONFLICT && ChangesetClosedException.errorHeaderMatchesPattern(errorHeader))
470 throw new ChangesetClosedException(errorHeader, ChangesetClosedException.Source.UPDATE_CHANGESET);
471 throw e;
472 } finally {
473 monitor.finishTask();
474 }
475 }
476
477 /**
478 * Closes a changeset on the server. Sets changeset.setOpen(false) if this operation succeeds.
479 *
480 * @param changeset the changeset to be closed. Must not be null. changeset.getId() &gt; 0 required.
481 * @param monitor the progress monitor. If null, uses {@link NullProgressMonitor#INSTANCE}
482 *
483 * @throws OsmTransferException if something goes wrong.
484 * @throws IllegalArgumentException if changeset is null
485 * @throws IllegalArgumentException if changeset.getId() &lt;= 0
486 */
487 public void closeChangeset(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("Closing changeset..."));
496 initialize(monitor);
497 /* send "\r\n" instead of empty string, so we don't send zero payload - works around bugs
498 in proxy software */
499 sendRequest("PUT", "changeset" + "/" + changeset.getId() + "/close", "\r\n", monitor);
500 changeset.setOpen(false);
501 } finally {
502 monitor.finishTask();
503 }
504 }
505
506 /**
507 * Uploads a list of changes in "diff" form to the server.
508 *
509 * @param list the list of changed OSM Primitives
510 * @param monitor the progress monitor
511 * @return list of processed primitives
512 * @throws OsmTransferException if something is wrong
513 */
514 public Collection<OsmPrimitive> uploadDiff(Collection<? extends OsmPrimitive> list, ProgressMonitor monitor)
515 throws OsmTransferException {
516 try {
517 monitor.beginTask("", list.size() * 2);
518 if (changeset == null)
519 throw new OsmTransferException(tr("No changeset present for diff upload."));
520
521 initialize(monitor);
522
523 // prepare upload request
524 //
525 OsmChangeBuilder changeBuilder = new OsmChangeBuilder(changeset);
526 monitor.subTask(tr("Preparing upload request..."));
527 changeBuilder.start();
528 changeBuilder.append(list);
529 changeBuilder.finish();
530 String diffUploadRequest = changeBuilder.getDocument();
531
532 // Upload to the server
533 //
534 monitor.indeterminateSubTask(
535 trn("Uploading {0} object...", "Uploading {0} objects...", list.size(), list.size()));
536 String diffUploadResponse = sendRequest("POST", "changeset/" + changeset.getId() + "/upload", diffUploadRequest, monitor);
537
538 // Process the response from the server
539 //
540 DiffResultProcessor reader = new DiffResultProcessor(list);
541 reader.parse(diffUploadResponse, monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
542 return reader.postProcess(
543 getChangeset(),
544 monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)
545 );
546 } catch (OsmTransferException e) {
547 throw e;
548 } catch (XmlParsingException e) {
549 throw new OsmTransferException(e);
550 } finally {
551 monitor.finishTask();
552 }
553 }
554
555 private void sleepAndListen(int retry, ProgressMonitor monitor) throws OsmTransferCanceledException {
556 Logging.info(tr("Waiting 10 seconds ... "));
557 for (int i = 0; i < 10; i++) {
558 if (monitor != null) {
559 monitor.setCustomText(tr("Starting retry {0} of {1} in {2} seconds ...", getMaxRetries() - retry, getMaxRetries(), 10-i));
560 }
561 if (cancel)
562 throw new OsmTransferCanceledException("Operation canceled" + (i > 0 ? " in retry #"+i : ""));
563 try {
564 Thread.sleep(1000);
565 } catch (InterruptedException ex) {
566 Logging.warn("InterruptedException in "+getClass().getSimpleName()+" during sleep");
567 Thread.currentThread().interrupt();
568 }
569 }
570 Logging.info(tr("OK - trying again."));
571 }
572
573 /**
574 * Replies the max. number of retries in case of 5XX errors on the server
575 *
576 * @return the max number of retries
577 */
578 protected int getMaxRetries() {
579 int ret = Main.pref.getInteger("osm-server.max-num-retries", DEFAULT_MAX_NUM_RETRIES);
580 return Math.max(ret, 0);
581 }
582
583 /**
584 * Determines if JOSM is configured to access OSM API via OAuth
585 * @return {@code true} if JOSM is configured to access OSM API via OAuth, {@code false} otherwise
586 * @since 6349
587 */
588 public static boolean isUsingOAuth() {
589 return "oauth".equals(getAuthMethod());
590 }
591
592 /**
593 * Returns the authentication method set in the preferences
594 * @return the authentication method
595 */
596 public static String getAuthMethod() {
597 return Main.pref.get("osm-server.auth-method", "oauth");
598 }
599
600 protected final String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor)
601 throws OsmTransferException {
602 return sendRequest(requestMethod, urlSuffix, requestBody, monitor, true, false);
603 }
604
605 /**
606 * Generic method for sending requests to the OSM API.
607 *
608 * This method will automatically re-try any requests that are answered with a 5xx
609 * error code, or that resulted in a timeout exception from the TCP layer.
610 *
611 * @param requestMethod The http method used when talking with the server.
612 * @param urlSuffix The suffix to add at the server url, not including the version number,
613 * but including any object ids (e.g. "/way/1234/history").
614 * @param requestBody the body of the HTTP request, if any.
615 * @param monitor the progress monitor
616 * @param doAuthenticate set to true, if the request sent to the server shall include authentication
617 * credentials;
618 * @param fastFail true to request a short timeout
619 *
620 * @return the body of the HTTP response, if and only if the response code was "200 OK".
621 * @throws OsmTransferException if the HTTP return code was not 200 (and retries have
622 * been exhausted), or rewrapping a Java exception.
623 */
624 protected final String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor,
625 boolean doAuthenticate, boolean fastFail) throws OsmTransferException {
626 int retries = fastFail ? 0 : getMaxRetries();
627
628 while (true) { // the retry loop
629 try {
630 url = new URL(new URL(getBaseUrl()), urlSuffix);
631 final HttpClient client = HttpClient.create(url, requestMethod).keepAlive(false);
632 activeConnection = client;
633 if (fastFail) {
634 client.setConnectTimeout(1000);
635 client.setReadTimeout(1000);
636 } else {
637 // use default connect timeout from org.openstreetmap.josm.tools.HttpClient.connectTimeout
638 client.setReadTimeout(0);
639 }
640 if (doAuthenticate) {
641 addAuth(client);
642 }
643
644 if ("PUT".equals(requestMethod) || "POST".equals(requestMethod) || "DELETE".equals(requestMethod)) {
645 client.setHeader("Content-Type", "text/xml");
646 // It seems that certain bits of the Ruby API are very unhappy upon
647 // receipt of a PUT/POST message without a Content-length header,
648 // even if the request has no payload.
649 // Since Java will not generate a Content-length header unless
650 // we use the output stream, we create an output stream for PUT/POST
651 // even if there is no payload.
652 client.setRequestBody((requestBody != null ? requestBody : "").getBytes(StandardCharsets.UTF_8));
653 }
654
655 final HttpClient.Response response = client.connect();
656 Logging.info(response.getResponseMessage());
657 int retCode = response.getResponseCode();
658
659 if (retCode >= 500 && retries-- > 0) {
660 sleepAndListen(retries, monitor);
661 Logging.info(tr("Starting retry {0} of {1}.", getMaxRetries() - retries, getMaxRetries()));
662 continue;
663 }
664
665 final String responseBody = response.fetchContent();
666
667 String errorHeader = null;
668 // Look for a detailed error message from the server
669 if (response.getHeaderField("Error") != null) {
670 errorHeader = response.getHeaderField("Error");
671 Logging.error("Error header: " + errorHeader);
672 } else if (retCode != HttpURLConnection.HTTP_OK && responseBody.length() > 0) {
673 Logging.error("Error body: " + responseBody);
674 }
675 activeConnection.disconnect();
676
677 errorHeader = errorHeader == null ? null : errorHeader.trim();
678 String errorBody = responseBody.length() == 0 ? null : responseBody.trim();
679 switch(retCode) {
680 case HttpURLConnection.HTTP_OK:
681 return responseBody;
682 case HttpURLConnection.HTTP_GONE:
683 throw new OsmApiPrimitiveGoneException(errorHeader, errorBody);
684 case HttpURLConnection.HTTP_CONFLICT:
685 if (ChangesetClosedException.errorHeaderMatchesPattern(errorHeader))
686 throw new ChangesetClosedException(errorBody, ChangesetClosedException.Source.UPLOAD_DATA);
687 else
688 throw new OsmApiException(retCode, errorHeader, errorBody);
689 case HttpURLConnection.HTTP_FORBIDDEN:
690 OsmApiException e = new OsmApiException(retCode, errorHeader, errorBody);
691 e.setAccessedUrl(activeConnection.getURL().toString());
692 throw e;
693 default:
694 throw new OsmApiException(retCode, errorHeader, errorBody);
695 }
696 } catch (SocketTimeoutException | ConnectException e) {
697 if (retries-- > 0) {
698 continue;
699 }
700 throw new OsmTransferException(e);
701 } catch (IOException e) {
702 throw new OsmTransferException(e);
703 } catch (OsmTransferException e) {
704 throw e;
705 }
706 }
707 }
708
709 /**
710 * Replies the API capabilities.
711 *
712 * @return the API capabilities, or null, if the API is not initialized yet
713 */
714 public synchronized Capabilities getCapabilities() {
715 return capabilities;
716 }
717
718 /**
719 * Ensures that the current changeset can be used for uploading data
720 *
721 * @throws OsmTransferException if the current changeset can't be used for uploading data
722 */
723 protected void ensureValidChangeset() throws OsmTransferException {
724 if (changeset == null)
725 throw new OsmTransferException(tr("Current changeset is null. Cannot upload data."));
726 if (changeset.getId() <= 0)
727 throw new OsmTransferException(tr("ID of current changeset > 0 required. Current ID is {0}.", changeset.getId()));
728 }
729
730 /**
731 * Replies the changeset data uploads are currently directed to
732 *
733 * @return the changeset data uploads are currently directed to
734 */
735 public Changeset getChangeset() {
736 return changeset;
737 }
738
739 /**
740 * Sets the changesets to which further data uploads are directed. The changeset
741 * can be null. If it isn't null it must have been created, i.e. id &gt; 0 is required. Furthermore,
742 * it must be open.
743 *
744 * @param changeset the changeset
745 * @throws IllegalArgumentException if changeset.getId() &lt;= 0
746 * @throws IllegalArgumentException if !changeset.isOpen()
747 */
748 public void setChangeset(Changeset changeset) {
749 if (changeset == null) {
750 this.changeset = null;
751 return;
752 }
753 if (changeset.getId() <= 0)
754 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId()));
755 if (!changeset.isOpen())
756 throw new IllegalArgumentException(tr("Open changeset expected. Got closed changeset with id {0}.", changeset.getId()));
757 this.changeset = changeset;
758 }
759
760 private static StringBuilder noteStringBuilder(Note note) {
761 return new StringBuilder().append("notes/").append(note.getId());
762 }
763
764 /**
765 * Create a new note on the server.
766 * @param latlon Location of note
767 * @param text Comment entered by user to open the note
768 * @param monitor Progress monitor
769 * @return Note as it exists on the server after creation (ID assigned)
770 * @throws OsmTransferException if any error occurs during dialog with OSM API
771 */
772 public Note createNote(LatLon latlon, String text, ProgressMonitor monitor) throws OsmTransferException {
773 initialize(monitor);
774 String noteUrl = new StringBuilder()
775 .append("notes?lat=")
776 .append(latlon.lat())
777 .append("&lon=")
778 .append(latlon.lon())
779 .append("&text=")
780 .append(Utils.encodeUrl(text)).toString();
781
782 String response = sendRequest("POST", noteUrl, null, monitor, true, false);
783 return parseSingleNote(response);
784 }
785
786 /**
787 * Add a comment to an existing note.
788 * @param note The note to add a comment to
789 * @param comment Text of the comment
790 * @param monitor Progress monitor
791 * @return Note returned by the API after the comment was added
792 * @throws OsmTransferException if any error occurs during dialog with OSM API
793 */
794 public Note addCommentToNote(Note note, String comment, ProgressMonitor monitor) throws OsmTransferException {
795 initialize(monitor);
796 String noteUrl = noteStringBuilder(note)
797 .append("/comment?text=")
798 .append(Utils.encodeUrl(comment)).toString();
799
800 String response = sendRequest("POST", noteUrl, null, monitor, true, false);
801 return parseSingleNote(response);
802 }
803
804 /**
805 * Close a note.
806 * @param note Note to close. Must currently be open
807 * @param closeMessage Optional message supplied by the user when closing the note
808 * @param monitor Progress monitor
809 * @return Note returned by the API after the close operation
810 * @throws OsmTransferException if any error occurs during dialog with OSM API
811 */
812 public Note closeNote(Note note, String closeMessage, ProgressMonitor monitor) throws OsmTransferException {
813 initialize(monitor);
814 String encodedMessage = Utils.encodeUrl(closeMessage);
815 StringBuilder urlBuilder = noteStringBuilder(note)
816 .append("/close");
817 if (!encodedMessage.trim().isEmpty()) {
818 urlBuilder.append("?text=");
819 urlBuilder.append(encodedMessage);
820 }
821
822 String response = sendRequest("POST", urlBuilder.toString(), null, monitor, true, false);
823 return parseSingleNote(response);
824 }
825
826 /**
827 * Reopen a closed note
828 * @param note Note to reopen. Must currently be closed
829 * @param reactivateMessage Optional message supplied by the user when reopening the note
830 * @param monitor Progress monitor
831 * @return Note returned by the API after the reopen operation
832 * @throws OsmTransferException if any error occurs during dialog with OSM API
833 */
834 public Note reopenNote(Note note, String reactivateMessage, ProgressMonitor monitor) throws OsmTransferException {
835 initialize(monitor);
836 String encodedMessage = Utils.encodeUrl(reactivateMessage);
837 StringBuilder urlBuilder = noteStringBuilder(note)
838 .append("/reopen");
839 if (!encodedMessage.trim().isEmpty()) {
840 urlBuilder.append("?text=");
841 urlBuilder.append(encodedMessage);
842 }
843
844 String response = sendRequest("POST", urlBuilder.toString(), null, monitor, true, false);
845 return parseSingleNote(response);
846 }
847
848 /**
849 * Method for parsing API responses for operations on individual notes
850 * @param xml the API response as XML data
851 * @return the resulting Note
852 * @throws OsmTransferException if the API response cannot be parsed
853 */
854 private static Note parseSingleNote(String xml) throws OsmTransferException {
855 try {
856 List<Note> newNotes = new NoteReader(xml).parse();
857 if (newNotes.size() == 1) {
858 return newNotes.get(0);
859 }
860 // Shouldn't ever execute. Server will either respond with an error (caught elsewhere) or one note
861 throw new OsmTransferException(tr("Note upload failed"));
862 } catch (SAXException | IOException e) {
863 Logging.error(e);
864 throw new OsmTransferException(tr("Error parsing note response from server"), e);
865 }
866 }
867}
Note: See TracBrowser for help on using the repository browser.