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