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

Last change on this file was 19050, checked in by taylor.smock, 2 days ago

Revert most var changes from r19048, fix most new compile warnings and checkstyle issues

Also, document why various ErrorProne checks were originally disabled and fix
generic SonarLint issues.

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