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

Last change on this file since 18283 was 18283, checked in by Don-vip, 3 years ago

fix #21427 - further simplify UploadDialog (patch by marcello, modified)

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