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

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

see #16204 - Allow to start and close JOSM in WebStart sandbox mode (where every external access is denied). This was very useful to reproduce some very tricky bugs that occured in real life but were almost impossible to diagnose.

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