001//License: GPL. See README for details.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005import static org.openstreetmap.josm.tools.I18n.trn;
006
007import java.io.BufferedReader;
008import java.io.BufferedWriter;
009import java.io.IOException;
010import java.io.InputStream;
011import java.io.InputStreamReader;
012import java.io.OutputStream;
013import java.io.OutputStreamWriter;
014import java.io.PrintWriter;
015import java.io.StringReader;
016import java.io.StringWriter;
017import java.net.ConnectException;
018import java.net.HttpURLConnection;
019import java.net.MalformedURLException;
020import java.net.SocketTimeoutException;
021import java.net.URL;
022import java.nio.charset.StandardCharsets;
023import java.util.Collection;
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.Map;
027
028import javax.xml.parsers.ParserConfigurationException;
029
030import org.openstreetmap.josm.Main;
031import org.openstreetmap.josm.data.osm.Changeset;
032import org.openstreetmap.josm.data.osm.IPrimitive;
033import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
034import org.openstreetmap.josm.gui.layer.ImageryLayer;
035import org.openstreetmap.josm.gui.layer.Layer;
036import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
037import org.openstreetmap.josm.gui.progress.ProgressMonitor;
038import org.openstreetmap.josm.io.Capabilities.CapabilitiesParser;
039import org.openstreetmap.josm.tools.CheckParameterUtil;
040import org.openstreetmap.josm.tools.Utils;
041import org.openstreetmap.josm.tools.XmlParsingException;
042import org.xml.sax.InputSource;
043import org.xml.sax.SAXException;
044import org.xml.sax.SAXParseException;
045
046/**
047 * Class that encapsulates the communications with the <a href="http://wiki.openstreetmap.org/wiki/API_v0.6">OSM API</a>.<br><br>
048 *
049 * All interaction with the server-side OSM API should go through this class.<br><br>
050 *
051 * It is conceivable to extract this into an interface later and create various
052 * classes implementing the interface, to be able to talk to various kinds of servers.
053 *
054 */
055public class OsmApi extends OsmConnection {
056
057    /**
058     * Maximum number of retries to send a request in case of HTTP 500 errors or timeouts
059     */
060    public static final int DEFAULT_MAX_NUM_RETRIES = 5;
061
062    /**
063     * Maximum number of concurrent download threads, imposed by
064     * <a href="http://wiki.openstreetmap.org/wiki/API_usage_policy#Technical_Usage_Requirements">
065     * OSM API usage policy.</a>
066     * @since 5386
067     */
068    public static final int MAX_DOWNLOAD_THREADS = 2;
069
070    /**
071     * Default URL of the standard OSM API.
072     * @since 5422
073     */
074    public static final String DEFAULT_API_URL = "https://api.openstreetmap.org/api";
075
076    // The collection of instantiated OSM APIs
077    private static Map<String, OsmApi> instances = new HashMap<>();
078
079    private URL url = null;
080
081    /**
082     * Replies the {@link OsmApi} for a given server URL
083     *
084     * @param serverUrl  the server URL
085     * @return the OsmApi
086     * @throws IllegalArgumentException thrown, if serverUrl is null
087     *
088     */
089    public static OsmApi getOsmApi(String serverUrl) {
090        OsmApi api = instances.get(serverUrl);
091        if (api == null) {
092            api = new OsmApi(serverUrl);
093            instances.put(serverUrl,api);
094        }
095        return api;
096    }
097
098    private static String getServerUrlFromPref() {
099        return Main.pref.get("osm-server.url", DEFAULT_API_URL);
100    }
101
102    /**
103     * Replies the {@link OsmApi} for the URL given by the preference <code>osm-server.url</code>
104     *
105     * @return the OsmApi
106     */
107    public static OsmApi getOsmApi() {
108        return getOsmApi(getServerUrlFromPref());
109    }
110
111    /** Server URL */
112    private String serverUrl;
113
114    /** Object describing current changeset */
115    private Changeset changeset;
116
117    /** API version used for server communications */
118    private String version = null;
119
120    /** API capabilities */
121    private Capabilities capabilities = null;
122
123    /** true if successfully initialized */
124    private boolean initialized = false;
125
126    /**
127     * Constructs a new {@code OsmApi} for a specific server URL.
128     *
129     * @param serverUrl the server URL. Must not be null
130     * @throws IllegalArgumentException thrown, if serverUrl is null
131     */
132    protected OsmApi(String serverUrl)  {
133        CheckParameterUtil.ensureParameterNotNull(serverUrl, "serverUrl");
134        this.serverUrl = serverUrl;
135    }
136
137    /**
138     * Replies the OSM protocol version we use to talk to the server.
139     * @return protocol version, or null if not yet negotiated.
140     */
141    public String getVersion() {
142        return version;
143    }
144
145    /**
146     * Replies the host name of the server URL.
147     * @return the host name of the server URL, or null if the server URL is malformed.
148     */
149    public String getHost() {
150        String host = null;
151        try {
152            host = (new URL(serverUrl)).getHost();
153        } catch (MalformedURLException e) {
154            Main.warn(e);
155        }
156        return host;
157    }
158
159    private class CapabilitiesCache extends CacheCustomContent<OsmTransferException> {
160
161        private static final String CAPABILITIES = "capabilities";
162
163        ProgressMonitor monitor;
164        boolean fastFail;
165
166        public CapabilitiesCache(ProgressMonitor monitor, boolean fastFail) {
167            super(CAPABILITIES + getBaseUrl().hashCode(), CacheCustomContent.INTERVAL_WEEKLY);
168            this.monitor = monitor;
169            this.fastFail = fastFail;
170        }
171
172        @Override
173        protected void checkOfflineAccess() {
174            OnlineResource.OSM_API.checkOfflineAccess(getBaseUrl(getServerUrlFromPref(), "0.6")+CAPABILITIES, getServerUrlFromPref());
175        }
176
177        @Override
178        protected byte[] updateData() throws OsmTransferException {
179            return sendRequest("GET", CAPABILITIES, null, monitor, false, fastFail).getBytes(StandardCharsets.UTF_8);
180        }
181    }
182
183    /**
184     * Initializes this component by negotiating a protocol version with the server.
185     *
186     * @param monitor the progress monitor
187     * @throws OsmTransferCanceledException If the initialisation has been cancelled by user.
188     * @throws OsmApiInitializationException If any other exception occurs. Use getCause() to get the original exception.
189     */
190    public void initialize(ProgressMonitor monitor) throws OsmTransferCanceledException, OsmApiInitializationException {
191        initialize(monitor, false);
192    }
193
194    /**
195     * Initializes this component by negotiating a protocol version with the server, with the ability to control the timeout.
196     *
197     * @param monitor the progress monitor
198     * @param fastFail true to request quick initialisation with a small timeout (more likely to throw exception)
199     * @throws OsmTransferCanceledException If the initialisation has been cancelled by user.
200     * @throws OsmApiInitializationException If any other exception occurs. Use getCause() to get the original exception.
201     */
202    public void initialize(ProgressMonitor monitor, boolean fastFail) throws OsmTransferCanceledException, OsmApiInitializationException {
203        if (initialized)
204            return;
205        cancel = false;
206        try {
207            CapabilitiesCache cache = new CapabilitiesCache(monitor, fastFail);
208            try {
209                initializeCapabilities(cache.updateIfRequiredString());
210            } catch (SAXParseException parseException) {
211                // XML parsing may fail if JOSM previously stored a corrupted capabilities document (see #8278)
212                // In that case, force update and try again
213                initializeCapabilities(cache.updateForceString());
214            }
215            if (capabilities == null) {
216                if (Main.isOffline(OnlineResource.OSM_API)) {
217                    Main.warn(tr("{0} not available (offline mode)", tr("OSM API")));
218                } else {
219                    Main.error(tr("Unable to initialize OSM API."));
220                }
221                return;
222            } else if (!capabilities.supportsVersion("0.6")) {
223                Main.error(tr("This version of JOSM is incompatible with the configured server."));
224                Main.error(tr("It supports protocol version 0.6, while the server says it supports {0} to {1}.",
225                        capabilities.get("version", "minimum"), capabilities.get("version", "maximum")));
226                return;
227            } else {
228                version = "0.6";
229                initialized = true;
230            }
231
232            /* This is an interim solution for openstreetmap.org not currently
233             * transmitting their imagery blacklist in the capabilities call.
234             * remove this as soon as openstreetmap.org adds blacklists.
235             * If you want to update this list, please ask for update of
236             * http://trac.openstreetmap.org/ticket/5024
237             * This list should not be maintained by each OSM editor (see #9210) */
238            if (this.serverUrl.matches(".*openstreetmap.org/api.*") && capabilities.getImageryBlacklist().isEmpty()) {
239                capabilities.put("blacklist", "regex", ".*\\.google\\.com/.*");
240                capabilities.put("blacklist", "regex", ".*209\\.85\\.2\\d\\d.*");
241                capabilities.put("blacklist", "regex", ".*209\\.85\\.1[3-9]\\d.*");
242                capabilities.put("blacklist", "regex", ".*209\\.85\\.12[89].*");
243            }
244
245            /* This checks if there are any layers currently displayed that
246             * are now on the blacklist, and removes them. This is a rare
247             * situation - probably only occurs if the user changes the API URL
248             * in the preferences menu. Otherwise they would not have been able
249             * to load the layers in the first place because they would have
250             * been disabled! */
251            if (Main.isDisplayingMapView()) {
252                for (Layer l : Main.map.mapView.getLayersOfType(ImageryLayer.class)) {
253                    if (((ImageryLayer) l).getInfo().isBlacklisted()) {
254                        Main.info(tr("Removed layer {0} because it is not allowed by the configured API.", l.getName()));
255                        Main.main.removeLayer(l);
256                    }
257                }
258            }
259
260        } catch (OsmTransferCanceledException e) {
261            throw e;
262        } catch (OsmTransferException e) {
263            initialized = false;
264            Main.addNetworkError(url, Utils.getRootCause(e));
265            throw new OsmApiInitializationException(e);
266        } catch (Exception e) {
267            initialized = false;
268            throw new OsmApiInitializationException(e);
269        }
270    }
271
272    private synchronized void initializeCapabilities(String xml) throws SAXException, IOException, ParserConfigurationException {
273        if (xml != null) {
274            capabilities = CapabilitiesParser.parse(new InputSource(new StringReader(xml)));
275        }
276    }
277
278    /**
279     * Makes an XML string from an OSM primitive. Uses the OsmWriter class.
280     * @param o the OSM primitive
281     * @param addBody true to generate the full XML, false to only generate the encapsulating tag
282     * @return XML string
283     */
284    private String toXml(IPrimitive o, boolean addBody) {
285        StringWriter swriter = new StringWriter();
286        try (OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(new PrintWriter(swriter), true, version)) {
287            swriter.getBuffer().setLength(0);
288            osmWriter.setWithBody(addBody);
289            osmWriter.setChangeset(changeset);
290            osmWriter.header();
291            o.accept(osmWriter);
292            osmWriter.footer();
293            osmWriter.flush();
294        } catch (IOException e) {
295            Main.warn(e);
296        }
297        return swriter.toString();
298    }
299
300    /**
301     * Makes an XML string from an OSM primitive. Uses the OsmWriter class.
302     * @param s the changeset
303     * @return XML string
304     */
305    private String toXml(Changeset s) {
306        StringWriter swriter = new StringWriter();
307        try (OsmWriter osmWriter = OsmWriterFactory.createOsmWriter(new PrintWriter(swriter), true, version)) {
308            swriter.getBuffer().setLength(0);
309            osmWriter.header();
310            osmWriter.visit(s);
311            osmWriter.footer();
312            osmWriter.flush();
313        } catch (IOException e) {
314            Main.warn(e);
315        }
316        return swriter.toString();
317    }
318
319    private static String getBaseUrl(String serverUrl, String version) {
320        StringBuilder rv = new StringBuilder(serverUrl);
321        if (version != null) {
322            rv.append("/");
323            rv.append(version);
324        }
325        rv.append("/");
326        // this works around a ruby (or lighttpd) bug where two consecutive slashes in
327        // an URL will cause a "404 not found" response.
328        int p; while ((p = rv.indexOf("//", rv.indexOf("://")+2)) > -1) { rv.delete(p, p + 1); }
329        return rv.toString();
330    }
331
332    /**
333     * Returns the base URL for API requests, including the negotiated version number.
334     * @return base URL string
335     */
336    public String getBaseUrl() {
337        return getBaseUrl(serverUrl, version);
338    }
339
340    /**
341     * Creates an OSM primitive on the server. The OsmPrimitive object passed in
342     * is modified by giving it the server-assigned id.
343     *
344     * @param osm the primitive
345     * @param monitor the progress monitor
346     * @throws OsmTransferException if something goes wrong
347     */
348    public void createPrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException {
349        String ret = "";
350        try {
351            ensureValidChangeset();
352            initialize(monitor);
353            ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/create", toXml(osm, true),monitor);
354            osm.setOsmId(Long.parseLong(ret.trim()), 1);
355            osm.setChangesetId(getChangeset().getId());
356        } catch(NumberFormatException e){
357            throw new OsmTransferException(tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret));
358        }
359    }
360
361    /**
362     * Modifies an OSM primitive on the server.
363     *
364     * @param osm the primitive. Must not be null.
365     * @param monitor the progress monitor
366     * @throws OsmTransferException if something goes wrong
367     */
368    public void modifyPrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException {
369        String ret = null;
370        try {
371            ensureValidChangeset();
372            initialize(monitor);
373            // normal mode (0.6 and up) returns new object version.
374            ret = sendRequest("PUT", OsmPrimitiveType.from(osm).getAPIName()+"/" + osm.getId(), toXml(osm, true), monitor);
375            osm.setOsmId(osm.getId(), Integer.parseInt(ret.trim()));
376            osm.setChangesetId(getChangeset().getId());
377            osm.setVisible(true);
378        } catch(NumberFormatException e) {
379            throw new OsmTransferException(tr("Unexpected format of new version of modified primitive ''{0}''. Got ''{1}''.", osm.getId(), ret));
380        }
381    }
382
383    /**
384     * Deletes an OSM primitive on the server.
385     * @param osm the primitive
386     * @param monitor the progress monitor
387     * @throws OsmTransferException if something goes wrong
388     */
389    public void deletePrimitive(IPrimitive osm, ProgressMonitor monitor) throws OsmTransferException {
390        ensureValidChangeset();
391        initialize(monitor);
392        // can't use a the individual DELETE method in the 0.6 API. Java doesn't allow
393        // submitting a DELETE request with content, the 0.6 API requires it, however. Falling back
394        // to diff upload.
395        //
396        uploadDiff(Collections.singleton(osm), monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
397    }
398
399    /**
400     * Creates a new changeset based on the keys in <code>changeset</code>. If this
401     * method succeeds, changeset.getId() replies the id the server assigned to the new
402     * changeset
403     *
404     * The changeset must not be null, but its key/value-pairs may be empty.
405     *
406     * @param changeset the changeset toe be created. Must not be null.
407     * @param progressMonitor the progress monitor
408     * @throws OsmTransferException signifying a non-200 return code, or connection errors
409     * @throws IllegalArgumentException thrown if changeset is null
410     */
411    public void openChangeset(Changeset changeset, ProgressMonitor progressMonitor) throws OsmTransferException {
412        CheckParameterUtil.ensureParameterNotNull(changeset, "changeset");
413        try {
414            progressMonitor.beginTask((tr("Creating changeset...")));
415            initialize(progressMonitor);
416            String ret = "";
417            try {
418                ret = sendRequest("PUT", "changeset/create", toXml(changeset),progressMonitor);
419                changeset.setId(Integer.parseInt(ret.trim()));
420                changeset.setOpen(true);
421            } catch(NumberFormatException e){
422                throw new OsmTransferException(tr("Unexpected format of ID replied by the server. Got ''{0}''.", ret));
423            }
424            progressMonitor.setCustomText((tr("Successfully opened changeset {0}",changeset.getId())));
425        } finally {
426            progressMonitor.finishTask();
427        }
428    }
429
430    /**
431     * Updates a changeset with the keys in  <code>changesetUpdate</code>. The changeset must not
432     * be null and id &gt; 0 must be true.
433     *
434     * @param changeset the changeset to update. Must not be null.
435     * @param monitor the progress monitor. If null, uses the {@link NullProgressMonitor#INSTANCE}.
436     *
437     * @throws OsmTransferException if something goes wrong.
438     * @throws IllegalArgumentException if changeset is null
439     * @throws IllegalArgumentException if changeset.getId() &lt;= 0
440     *
441     */
442    public void updateChangeset(Changeset changeset, ProgressMonitor monitor) throws OsmTransferException {
443        CheckParameterUtil.ensureParameterNotNull(changeset, "changeset");
444        if (monitor == null) {
445            monitor = NullProgressMonitor.INSTANCE;
446        }
447        if (changeset.getId() <= 0)
448            throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId()));
449        try {
450            monitor.beginTask(tr("Updating changeset..."));
451            initialize(monitor);
452            monitor.setCustomText(tr("Updating changeset {0}...", changeset.getId()));
453            sendRequest(
454                    "PUT",
455                    "changeset/" + changeset.getId(),
456                    toXml(changeset),
457                    monitor
458            );
459        } catch(ChangesetClosedException e) {
460            e.setSource(ChangesetClosedException.Source.UPDATE_CHANGESET);
461            throw e;
462        } catch(OsmApiException e) {
463            if (e.getResponseCode() == HttpURLConnection.HTTP_CONFLICT && ChangesetClosedException.errorHeaderMatchesPattern(e.getErrorHeader()))
464                throw new ChangesetClosedException(e.getErrorHeader(), ChangesetClosedException.Source.UPDATE_CHANGESET);
465            throw e;
466        } finally {
467            monitor.finishTask();
468        }
469    }
470
471    /**
472     * Closes a changeset on the server. Sets changeset.setOpen(false) if this operation succeeds.
473     *
474     * @param changeset the changeset to be closed. Must not be null. changeset.getId() &gt; 0 required.
475     * @param monitor the progress monitor. If null, uses {@link NullProgressMonitor#INSTANCE}
476     *
477     * @throws OsmTransferException if something goes wrong.
478     * @throws IllegalArgumentException thrown if changeset is null
479     * @throws IllegalArgumentException thrown if changeset.getId() &lt;= 0
480     */
481    public void closeChangeset(Changeset changeset, ProgressMonitor monitor) throws OsmTransferException {
482        CheckParameterUtil.ensureParameterNotNull(changeset, "changeset");
483        if (monitor == null) {
484            monitor = NullProgressMonitor.INSTANCE;
485        }
486        if (changeset.getId() <= 0)
487            throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId()));
488        try {
489            monitor.beginTask(tr("Closing changeset..."));
490            initialize(monitor);
491            /* send "\r\n" instead of empty string, so we don't send zero payload - works around bugs
492               in proxy software */
493            sendRequest("PUT", "changeset" + "/" + changeset.getId() + "/close", "\r\n", monitor);
494            changeset.setOpen(false);
495        } finally {
496            monitor.finishTask();
497        }
498    }
499
500    /**
501     * Uploads a list of changes in "diff" form to the server.
502     *
503     * @param list the list of changed OSM Primitives
504     * @param  monitor the progress monitor
505     * @return list of processed primitives
506     * @throws OsmTransferException if something is wrong
507     */
508    public Collection<IPrimitive> uploadDiff(Collection<? extends IPrimitive> list, ProgressMonitor monitor) throws OsmTransferException {
509        try {
510            monitor.beginTask("", list.size() * 2);
511            if (changeset == null)
512                throw new OsmTransferException(tr("No changeset present for diff upload."));
513
514            initialize(monitor);
515
516            // prepare upload request
517            //
518            OsmChangeBuilder changeBuilder = new OsmChangeBuilder(changeset);
519            monitor.subTask(tr("Preparing upload request..."));
520            changeBuilder.start();
521            changeBuilder.append(list);
522            changeBuilder.finish();
523            String diffUploadRequest = changeBuilder.getDocument();
524
525            // Upload to the server
526            //
527            monitor.indeterminateSubTask(
528                    trn("Uploading {0} object...", "Uploading {0} objects...", list.size(), list.size()));
529            String diffUploadResponse = sendRequest("POST", "changeset/" + changeset.getId() + "/upload", diffUploadRequest,monitor);
530
531            // Process the response from the server
532            //
533            DiffResultProcessor reader = new DiffResultProcessor(list);
534            reader.parse(diffUploadResponse, monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false));
535            return reader.postProcess(
536                    getChangeset(),
537                    monitor.createSubTaskMonitor(ProgressMonitor.ALL_TICKS, false)
538            );
539        } catch(OsmTransferException e) {
540            throw e;
541        } catch(XmlParsingException e) {
542            throw new OsmTransferException(e);
543        } finally {
544            monitor.finishTask();
545        }
546    }
547
548    private void sleepAndListen(int retry, ProgressMonitor monitor) throws OsmTransferCanceledException {
549        Main.info(tr("Waiting 10 seconds ... "));
550        for (int i=0; i < 10; i++) {
551            if (monitor != null) {
552                monitor.setCustomText(tr("Starting retry {0} of {1} in {2} seconds ...", getMaxRetries() - retry,getMaxRetries(), 10-i));
553            }
554            if (cancel)
555                throw new OsmTransferCanceledException();
556            try {
557                Thread.sleep(1000);
558            } catch (InterruptedException ex) {
559                Main.warn("InterruptedException in "+getClass().getSimpleName()+" during sleep");
560            }
561        }
562        Main.info(tr("OK - trying again."));
563    }
564
565    /**
566     * Replies the max. number of retries in case of 5XX errors on the server
567     *
568     * @return the max number of retries
569     */
570    protected int getMaxRetries() {
571        int ret = Main.pref.getInteger("osm-server.max-num-retries", DEFAULT_MAX_NUM_RETRIES);
572        return Math.max(ret,0);
573    }
574
575    /**
576     * Determines if JOSM is configured to access OSM API via OAuth
577     * @return {@code true} if JOSM is configured to access OSM API via OAuth, {@code false} otherwise
578     * @since 6349
579     */
580    public static final boolean isUsingOAuth() {
581        return "oauth".equals(Main.pref.get("osm-server.auth-method", "basic"));
582    }
583
584    protected final String sendRequest(String requestMethod, String urlSuffix,String requestBody, ProgressMonitor monitor) throws OsmTransferException {
585        return sendRequest(requestMethod, urlSuffix, requestBody, monitor, true, false);
586    }
587
588    /**
589     * Generic method for sending requests to the OSM API.
590     *
591     * This method will automatically re-try any requests that are answered with a 5xx
592     * error code, or that resulted in a timeout exception from the TCP layer.
593     *
594     * @param requestMethod The http method used when talking with the server.
595     * @param urlSuffix The suffix to add at the server url, not including the version number,
596     *    but including any object ids (e.g. "/way/1234/history").
597     * @param requestBody the body of the HTTP request, if any.
598     * @param monitor the progress monitor
599     * @param doAuthenticate  set to true, if the request sent to the server shall include authentication
600     * credentials;
601     * @param fastFail true to request a short timeout
602     *
603     * @return the body of the HTTP response, if and only if the response code was "200 OK".
604     * @throws OsmTransferException if the HTTP return code was not 200 (and retries have
605     *    been exhausted), or rewrapping a Java exception.
606     */
607    protected final String sendRequest(String requestMethod, String urlSuffix,String requestBody, ProgressMonitor monitor, boolean doAuthenticate, boolean fastFail) throws OsmTransferException {
608        StringBuilder responseBody = new StringBuilder();
609        int retries = fastFail ? 0 : getMaxRetries();
610
611        while(true) { // the retry loop
612            try {
613                url = new URL(new URL(getBaseUrl()), urlSuffix);
614                Main.info(requestMethod + " " + url + "... ");
615                Main.debug(requestBody);
616                // fix #5369, see http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive
617                activeConnection = Utils.openHttpConnection(url, false);
618                activeConnection.setConnectTimeout(fastFail ? 1000 : Main.pref.getInteger("socket.timeout.connect",15)*1000);
619                if (fastFail) {
620                    activeConnection.setReadTimeout(1000);
621                }
622                activeConnection.setRequestMethod(requestMethod);
623                if (doAuthenticate) {
624                    addAuth(activeConnection);
625                }
626
627                if ("PUT".equals(requestMethod) || "POST".equals(requestMethod) || "DELETE".equals(requestMethod)) {
628                    activeConnection.setDoOutput(true);
629                    activeConnection.setRequestProperty("Content-type", "text/xml");
630                    try (OutputStream out = activeConnection.getOutputStream()) {
631                        // It seems that certain bits of the Ruby API are very unhappy upon
632                        // receipt of a PUT/POST message without a Content-length header,
633                        // even if the request has no payload.
634                        // Since Java will not generate a Content-length header unless
635                        // we use the output stream, we create an output stream for PUT/POST
636                        // even if there is no payload.
637                        if (requestBody != null) {
638                            try (BufferedWriter bwr = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
639                                bwr.write(requestBody);
640                                bwr.flush();
641                            }
642                        }
643                    }
644                }
645
646                activeConnection.connect();
647                Main.info(activeConnection.getResponseMessage());
648                int retCode = activeConnection.getResponseCode();
649
650                if (retCode >= 500) {
651                    if (retries-- > 0) {
652                        sleepAndListen(retries, monitor);
653                        Main.info(tr("Starting retry {0} of {1}.", getMaxRetries() - retries,getMaxRetries()));
654                        continue;
655                    }
656                }
657
658                // populate return fields.
659                responseBody.setLength(0);
660
661                // If the API returned an error code like 403 forbidden, getInputStream
662                // will fail with an IOException.
663                InputStream i = getConnectionStream();
664                if (i != null) {
665                    // the input stream can be null if both the input and the error stream
666                    // are null. Seems to be the case if the OSM server replies a 401
667                    // Unauthorized, see #3887.
668                    //
669                    String s;
670                    try (BufferedReader in = new BufferedReader(new InputStreamReader(i, StandardCharsets.UTF_8))) {
671                        while((s = in.readLine()) != null) {
672                            responseBody.append(s);
673                            responseBody.append("\n");
674                        }
675                    }
676                }
677                String errorHeader = null;
678                // Look for a detailed error message from the server
679                if (activeConnection.getHeaderField("Error") != null) {
680                    errorHeader = activeConnection.getHeaderField("Error");
681                    Main.error("Error header: " + errorHeader);
682                } else if (retCode != HttpURLConnection.HTTP_OK && responseBody.length()>0) {
683                    Main.error("Error body: " + responseBody);
684                }
685                activeConnection.disconnect();
686
687                if (Main.isDebugEnabled()) {
688                    Main.debug("RESPONSE: "+ activeConnection.getHeaderFields());
689                }
690
691                errorHeader = errorHeader == null? null : errorHeader.trim();
692                String errorBody = responseBody.length() == 0? null : responseBody.toString().trim();
693                switch(retCode) {
694                case HttpURLConnection.HTTP_OK:
695                    return responseBody.toString();
696                case HttpURLConnection.HTTP_GONE:
697                    throw new OsmApiPrimitiveGoneException(errorHeader, errorBody);
698                case HttpURLConnection.HTTP_CONFLICT:
699                    if (ChangesetClosedException.errorHeaderMatchesPattern(errorHeader))
700                        throw new ChangesetClosedException(errorBody, ChangesetClosedException.Source.UPLOAD_DATA);
701                    else
702                        throw new OsmApiException(retCode, errorHeader, errorBody);
703                case HttpURLConnection.HTTP_FORBIDDEN:
704                    OsmApiException e = new OsmApiException(retCode, errorHeader, errorBody);
705                    e.setAccessedUrl(activeConnection.getURL().toString());
706                    throw e;
707                default:
708                    throw new OsmApiException(retCode, errorHeader, errorBody);
709                }
710            } catch (SocketTimeoutException | ConnectException e) {
711                if (retries-- > 0) {
712                    continue;
713                }
714                throw new OsmTransferException(e);
715            } catch(IOException e) {
716                throw new OsmTransferException(e);
717            } catch(OsmTransferException e) {
718                throw e;
719            }
720        }
721    }
722
723    private InputStream getConnectionStream() {
724        try {
725            return activeConnection.getInputStream();
726        } catch (IOException ioe) {
727            Main.warn(ioe);
728            return activeConnection.getErrorStream();
729        }
730    }
731
732    /**
733     * Replies the API capabilities.
734     *
735     * @return the API capabilities, or null, if the API is not initialized yet
736     */
737    public synchronized Capabilities getCapabilities() {
738        return capabilities;
739    }
740
741    /**
742     * Ensures that the current changeset can be used for uploading data
743     *
744     * @throws OsmTransferException thrown if the current changeset can't be used for
745     * uploading data
746     */
747    protected void ensureValidChangeset() throws OsmTransferException {
748        if (changeset == null)
749            throw new OsmTransferException(tr("Current changeset is null. Cannot upload data."));
750        if (changeset.getId() <= 0)
751            throw new OsmTransferException(tr("ID of current changeset > 0 required. Current ID is {0}.", changeset.getId()));
752    }
753
754    /**
755     * Replies the changeset data uploads are currently directed to
756     *
757     * @return the changeset data uploads are currently directed to
758     */
759    public Changeset getChangeset() {
760        return changeset;
761    }
762
763    /**
764     * Sets the changesets to which further data uploads are directed. The changeset
765     * can be null. If it isn't null it must have been created, i.e. id &gt; 0 is required. Furthermore,
766     * it must be open.
767     *
768     * @param changeset the changeset
769     * @throws IllegalArgumentException thrown if changeset.getId() &lt;= 0
770     * @throws IllegalArgumentException thrown if !changeset.isOpen()
771     */
772    public void setChangeset(Changeset changeset) {
773        if (changeset == null) {
774            this.changeset = null;
775            return;
776        }
777        if (changeset.getId() <= 0)
778            throw new IllegalArgumentException(tr("Changeset ID > 0 expected. Got {0}.", changeset.getId()));
779        if (!changeset.isOpen())
780            throw new IllegalArgumentException(tr("Open changeset expected. Got closed changeset with id {0}.", changeset.getId()));
781        this.changeset = changeset;
782    }
783}