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

Last change on this file since 14120 was 14119, checked in by Don-vip, 6 years ago

see #15229 - deprecate all Main methods returning an URL

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