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

Last change on this file since 18877 was 18871, checked in by taylor.smock, 8 months ago

See #23218: Use newer error_prone versions when compiling on Java 11+

error_prone 2.11 dropped support for compiling with Java 8, although it still
supports compiling for Java 8. The "major" new check for us is NotJavadoc since
we used /** in quite a few places which were not javadoc.

Other "new" checks that are of interest:

  • AlreadyChecked: if (foo) { doFoo(); } else if (!foo) { doBar(); }
  • UnnecessaryStringBuilder: Avoid StringBuilder (Java converts + to StringBuilder behind-the-scenes, but may also do something else if it performs better)
  • NonApiType: Avoid specific interface types in function definitions
  • NamedLikeContextualKeyword: Avoid using restricted names for classes and methods
  • UnusedMethod: Unused private methods should be removed

This fixes most of the new error_prone issues and some SonarLint issues.

  • Property svn:eol-style set to native
File size: 42.5 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
54/**
55 * Class that encapsulates the communications with the <a href="http://wiki.openstreetmap.org/wiki/API_v0.6">OSM API</a>.<br><br>
56 *
57 * All interaction with the server-side OSM API should go through this class.<br><br>
58 *
59 * It is conceivable to extract this into an interface later and create various
60 * classes implementing the interface, to be able to talk to various kinds of servers.
61 * @since 1523
62 */
63public class OsmApi extends OsmConnection {
64
65 /**
66 * Maximum number of retries to send a request in case of HTTP 500 errors or timeouts
67 */
68 public static final int DEFAULT_MAX_NUM_RETRIES = 5;
69
70 /**
71 * Maximum number of concurrent download threads, imposed by
72 * <a href="http://wiki.openstreetmap.org/wiki/API_usage_policy#Technical_Usage_Requirements">
73 * OSM API usage policy.</a>
74 * @since 5386
75 */
76 public static final int MAX_DOWNLOAD_THREADS = 2;
77
78 /**
79 * Defines whether all OSM API requests should be signed with an OAuth token (user-based bandwidth limit instead of IP-based one)
80 */
81 public static final BooleanProperty USE_OAUTH_FOR_ALL_REQUESTS = new BooleanProperty("oauth.use-for-all-requests", true);
82
83 // The collection of instantiated OSM APIs
84 private static final Map<String, OsmApi> instances = new HashMap<>();
85
86 private static final ListenerList<OsmApiInitializationListener> listeners = ListenerList.create();
87 /** This is used to make certain we have set osm-server.auth-method to the "right" default */
88 private static boolean oauthCompatibilitySwitch;
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.OAuth10a)
656 || isUsingOAuth(OAuthVersion.OAuth20)
657 || isUsingOAuth(OAuthVersion.OAuth21);
658 }
659
660 /**
661 * Determines if JOSM is configured to access OSM API via OAuth
662 * @param version The OAuth version
663 * @return {@code true} if JOSM is configured to access OSM API via OAuth, {@code false} otherwise
664 * @since 18650
665 */
666 public static boolean isUsingOAuth(OAuthVersion version) {
667 if (version == OAuthVersion.OAuth10a) {
668 return "oauth".equalsIgnoreCase(getAuthMethod());
669 } else if (version == OAuthVersion.OAuth20 || version == OAuthVersion.OAuth21) {
670 return "oauth20".equalsIgnoreCase(getAuthMethod());
671 }
672 return false;
673 }
674
675 /**
676 * Ensure that OAuth is set up
677 * @param api The api for which we need OAuth keys
678 * @return {@code true} if we are using OAuth and there are keys for the specified API
679 */
680 public static boolean isUsingOAuthAndOAuthSetUp(OsmApi api) {
681 if (OsmApi.isUsingOAuth()) {
682 if (OsmApi.isUsingOAuth(OAuthVersion.OAuth10a)) {
683 return OAuthAccessTokenHolder.getInstance().containsAccessToken();
684 }
685 if (OsmApi.isUsingOAuth(OAuthVersion.OAuth20)) {
686 return OAuthAccessTokenHolder.getInstance().getAccessToken(api.getBaseUrl(), OAuthVersion.OAuth20) != null;
687 }
688 if (OsmApi.isUsingOAuth(OAuthVersion.OAuth21)) {
689 return OAuthAccessTokenHolder.getInstance().getAccessToken(api.getBaseUrl(), OAuthVersion.OAuth21) != null;
690 }
691 }
692 return false;
693 }
694
695 /**
696 * Returns the authentication method set in the preferences
697 * @return the authentication method
698 */
699 public static String getAuthMethod() {
700 setCurrentAuthMethod();
701 return Config.getPref().get("osm-server.auth-method", "oauth20");
702 }
703
704 /**
705 * This is a compatibility method for users who currently use OAuth 1.0 -- we are changing the default from oauth to oauth20,
706 * but since oauth was the default, pre-existing users will suddenly be switched to oauth20.
707 * This should be removed whenever {@link OAuthVersion#OAuth10a} support is removed.
708 */
709 private static void setCurrentAuthMethod() {
710 if (!oauthCompatibilitySwitch) {
711 oauthCompatibilitySwitch = true;
712 final String prefKey = "osm-server.auth-method";
713 if ("oauth20".equals(Config.getPref().get(prefKey, "oauth20"))
714 && !isUsingOAuthAndOAuthSetUp(OsmApi.getOsmApi())
715 && OAuthAccessTokenHolder.getInstance().containsAccessToken()) {
716 Config.getPref().put(prefKey, "oauth");
717 }
718 }
719 }
720
721 protected final String sendPostRequest(String urlSuffix, String requestBody, ProgressMonitor monitor) throws OsmTransferException {
722 // Send a POST request that includes authentication credentials
723 return sendRequest("POST", urlSuffix, requestBody, monitor);
724 }
725
726 protected final String sendPutRequest(String urlSuffix, String requestBody, ProgressMonitor monitor) throws OsmTransferException {
727 // Send a PUT request that includes authentication credentials
728 return sendRequest("PUT", urlSuffix, requestBody, monitor);
729 }
730
731 protected final String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor)
732 throws OsmTransferException {
733 return sendRequest(requestMethod, urlSuffix, requestBody, monitor, true, false);
734 }
735
736 protected final String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor,
737 boolean doAuthenticate, boolean fastFail) throws OsmTransferException {
738 return sendRequest(requestMethod, urlSuffix, requestBody, monitor, null, doAuthenticate, fastFail);
739 }
740
741 /**
742 * Generic method for sending requests to the OSM API.
743 * <p>
744 * This method will automatically re-try any requests that are answered with a 5xx
745 * error code, or that resulted in a timeout exception from the TCP layer.
746 *
747 * @param requestMethod The http method used when talking with the server.
748 * @param urlSuffix The suffix to add at the server url, not including the version number,
749 * but including any object ids (e.g. "/way/1234/history").
750 * @param requestBody the body of the HTTP request, if any.
751 * @param monitor the progress monitor
752 * @param contentType Content-Type to set for PUT/POST/DELETE requests.
753 * Can be set to {@code null}, in that case it means {@code text/xml}
754 * @param doAuthenticate set to true, if the request sent to the server shall include authentication credentials;
755 * @param fastFail true to request a short timeout
756 *
757 * @return the body of the HTTP response, if and only if the response code was "200 OK".
758 * @throws OsmTransferException if the HTTP return code was not 200 (and retries have
759 * been exhausted), or rewrapping a Java exception.
760 */
761 protected final String sendRequest(String requestMethod, String urlSuffix, String requestBody, ProgressMonitor monitor,
762 String contentType, boolean doAuthenticate, boolean fastFail) throws OsmTransferException {
763 int retries = fastFail ? 0 : getMaxRetries();
764
765 while (true) { // the retry loop
766 try {
767 url = new URL(new URL(getBaseUrl()), urlSuffix);
768 final HttpClient client = HttpClient.create(url, requestMethod)
769 .keepAlive(false)
770 .setAccept("application/xml, */*;q=0.8");
771 activeConnection = client;
772 if (fastFail) {
773 client.setConnectTimeout(1000);
774 client.setReadTimeout(1000);
775 } else {
776 // use default connect timeout from org.openstreetmap.josm.tools.HttpClient.connectTimeout
777 client.setReadTimeout(0);
778 }
779 if (doAuthenticate) {
780 addAuth(client);
781 }
782
783 if ("PUT".equals(requestMethod) || "POST".equals(requestMethod) || "DELETE".equals(requestMethod)) {
784 client.setHeader("Content-Type", contentType == null ? "text/xml" : contentType);
785 // It seems that certain bits of the Ruby API are very unhappy upon
786 // receipt of a PUT/POST message without a Content-length header,
787 // even if the request has no payload.
788 // Since Java will not generate a Content-length header unless
789 // we use the output stream, we create an output stream for PUT/POST
790 // even if there is no payload.
791 client.setRequestBody((requestBody != null ? requestBody : "").getBytes(StandardCharsets.UTF_8));
792 }
793
794 final HttpClient.Response response = client.connect();
795 Logging.info(response.getResponseMessage());
796 int retCode = response.getResponseCode();
797
798 if (retCode >= 500 && retries-- > 0) {
799 sleepAndListen(retries, monitor);
800 Logging.info(tr("Starting retry {0} of {1}.", getMaxRetries() - retries, getMaxRetries()));
801 continue;
802 }
803
804 final String responseBody = response.fetchContent();
805
806 String errorHeader = null;
807 // Look for a detailed error message from the server
808 if (response.getHeaderField("Error") != null) {
809 errorHeader = response.getHeaderField("Error");
810 Logging.error("Error header: " + errorHeader);
811 } else if (retCode != HttpURLConnection.HTTP_OK && !responseBody.isEmpty()) {
812 Logging.error("Error body: " + responseBody);
813 }
814 activeConnection.disconnect();
815
816 errorHeader = errorHeader == null ? null : errorHeader.trim();
817 String errorBody = responseBody.isEmpty() ? null : responseBody.trim();
818 switch(retCode) {
819 case HttpURLConnection.HTTP_OK:
820 return responseBody;
821 case HttpURLConnection.HTTP_GONE:
822 throw new OsmApiPrimitiveGoneException(errorHeader, errorBody);
823 case HttpURLConnection.HTTP_CONFLICT:
824 if (ChangesetClosedException.errorHeaderMatchesPattern(errorHeader))
825 throw new ChangesetClosedException(errorBody, ChangesetClosedException.Source.UNSPECIFIED);
826 else
827 throw new OsmApiException(retCode, errorHeader, errorBody);
828 case HttpURLConnection.HTTP_UNAUTHORIZED:
829 case HttpURLConnection.HTTP_FORBIDDEN:
830 CredentialsManager.getInstance().purgeCredentialsCache(RequestorType.SERVER);
831 throw new OsmApiException(retCode, errorHeader, errorBody, activeConnection.getURL().toString(),
832 doAuthenticate ? retrieveBasicAuthorizationLogin(client) : null, response.getContentType());
833 default:
834 throw new OsmApiException(retCode, errorHeader, errorBody);
835 }
836 } catch (SocketException | SocketTimeoutException e) {
837 /*
838 * See #22160. While it is only thrown once in JDK sources, it does have subclasses.
839 * We check for those first, the explicit non-child exception, and then for the message.
840 */
841 boolean validException = e instanceof SocketTimeoutException
842 || e instanceof ConnectException
843 || e instanceof NoRouteToHostException
844 || e instanceof PortUnreachableException
845 || (e.getClass().equals(SocketException.class) &&
846 "Unexpected end of file from server".equals(e.getMessage()));
847 if (retries-- > 0 && validException) {
848 continue;
849 }
850 throw new OsmTransferException(e);
851 } catch (IOException e) {
852 throw new OsmTransferException(e);
853 }
854 }
855 }
856
857 /**
858 * Replies the API capabilities.
859 *
860 * @return the API capabilities, or null, if the API is not initialized yet
861 */
862 public synchronized Capabilities getCapabilities() {
863 return capabilities;
864 }
865
866 /**
867 * Ensures that the current changeset can be used for uploading data
868 *
869 * @throws OsmTransferException if the current changeset can't be used for uploading data
870 */
871 protected void ensureValidChangeset() throws OsmTransferException {
872 if (changeset == null)
873 throw new OsmTransferException(tr("Current changeset is null. Cannot upload data."));
874 if (changeset.getId() <= 0)
875 throw new OsmTransferException(tr("ID of current changeset > 0 required. Current ID is {0}.", changeset.getId()));
876 }
877
878 /**
879 * Replies the changeset data uploads are currently directed to
880 *
881 * @return the changeset data uploads are currently directed to
882 */
883 public Changeset getChangeset() {
884 return changeset;
885 }
886
887 /**
888 * Sets the changesets to which further data uploads are directed. The changeset
889 * can be null. If it isn't null it must have been created, i.e. id &gt; 0 is required. Furthermore,
890 * it must be open.
891 *
892 * @param changeset the changeset
893 * @throws IllegalArgumentException if changeset.getId() &lt;= 0
894 * @throws IllegalArgumentException if !changeset.isOpen()
895 */
896 public void setChangeset(Changeset changeset) {
897 if (changeset == null) {
898 this.changeset = null;
899 return;
900 }
901 if (changeset.getId() <= 0)
902 throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId()));
903 if (!changeset.isOpen())
904 throw new IllegalArgumentException(tr("Open changeset expected. Got closed changeset with id {0}.", changeset.getId()));
905 this.changeset = changeset;
906 }
907
908 private static StringBuilder noteStringBuilder(Note note) {
909 return new StringBuilder().append("notes/").append(note.getId());
910 }
911
912 /**
913 * Create a new note on the server.
914 * @param latlon Location of note
915 * @param text Comment entered by user to open the note
916 * @param monitor Progress monitor
917 * @return Note as it exists on the server after creation (ID assigned)
918 * @throws OsmTransferException if any error occurs during dialog with OSM API
919 */
920 public Note createNote(LatLon latlon, String text, ProgressMonitor monitor) throws OsmTransferException {
921 initialize(monitor);
922 String noteUrl = "notes?lat=" +
923 latlon.lat() +
924 "&lon=" +
925 latlon.lon() +
926 "&text=" +
927 Utils.encodeUrl(text);
928
929 return parseSingleNote(sendPostRequest(noteUrl, null, monitor));
930 }
931
932 /**
933 * Add a comment to an existing note.
934 * @param note The note to add a comment to
935 * @param comment Text of the comment
936 * @param monitor Progress monitor
937 * @return Note returned by the API after the comment was added
938 * @throws OsmTransferException if any error occurs during dialog with OSM API
939 */
940 public Note addCommentToNote(Note note, String comment, ProgressMonitor monitor) throws OsmTransferException {
941 initialize(monitor);
942 String noteUrl = noteStringBuilder(note)
943 .append("/comment?text=")
944 .append(Utils.encodeUrl(comment)).toString();
945
946 return parseSingleNote(sendPostRequest(noteUrl, null, monitor));
947 }
948
949 /**
950 * Close a note.
951 * @param note Note to close. Must currently be open
952 * @param closeMessage Optional message supplied by the user when closing the note
953 * @param monitor Progress monitor
954 * @return Note returned by the API after the close operation
955 * @throws OsmTransferException if any error occurs during dialog with OSM API
956 */
957 public Note closeNote(Note note, String closeMessage, ProgressMonitor monitor) throws OsmTransferException {
958 initialize(monitor);
959 String encodedMessage = Utils.encodeUrl(closeMessage);
960 StringBuilder urlBuilder = noteStringBuilder(note)
961 .append("/close");
962 if (!encodedMessage.trim().isEmpty()) {
963 urlBuilder.append("?text=");
964 urlBuilder.append(encodedMessage);
965 }
966
967 return parseSingleNote(sendPostRequest(urlBuilder.toString(), null, monitor));
968 }
969
970 /**
971 * Reopen a closed note
972 * @param note Note to reopen. Must currently be closed
973 * @param reactivateMessage Optional message supplied by the user when reopening the note
974 * @param monitor Progress monitor
975 * @return Note returned by the API after the reopen operation
976 * @throws OsmTransferException if any error occurs during dialog with OSM API
977 */
978 public Note reopenNote(Note note, String reactivateMessage, ProgressMonitor monitor) throws OsmTransferException {
979 initialize(monitor);
980 String encodedMessage = Utils.encodeUrl(reactivateMessage);
981 StringBuilder urlBuilder = noteStringBuilder(note)
982 .append("/reopen");
983 if (!encodedMessage.trim().isEmpty()) {
984 urlBuilder.append("?text=");
985 urlBuilder.append(encodedMessage);
986 }
987
988 return parseSingleNote(sendPostRequest(urlBuilder.toString(), null, monitor));
989 }
990
991 /**
992 * Method for parsing API responses for operations on individual notes
993 * @param xml the API response as XML data
994 * @return the resulting Note
995 * @throws OsmTransferException if the API response cannot be parsed
996 */
997 private static Note parseSingleNote(String xml) throws OsmTransferException {
998 try {
999 List<Note> newNotes = new NoteReader(xml).parse();
1000 if (newNotes.size() == 1) {
1001 return newNotes.get(0);
1002 }
1003 // Shouldn't ever execute. Server will either respond with an error (caught elsewhere) or one note
1004 throw new OsmTransferException(tr("Note upload failed"));
1005 } catch (SAXException | IOException e) {
1006 Logging.error(e);
1007 throw new OsmTransferException(tr("Error parsing note response from server"), e);
1008 }
1009 }
1010}
Note: See TracBrowser for help on using the repository browser.