001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.io;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.BufferedReader;
007import java.io.IOException;
008import java.io.InputStream;
009import java.io.InputStreamReader;
010import java.net.HttpURLConnection;
011import java.net.MalformedURLException;
012import java.net.URL;
013import java.nio.charset.StandardCharsets;
014import java.util.List;
015import java.util.Map;
016import java.util.zip.GZIPInputStream;
017import java.util.zip.Inflater;
018import java.util.zip.InflaterInputStream;
019
020import org.openstreetmap.josm.Main;
021import org.openstreetmap.josm.data.gpx.GpxData;
022import org.openstreetmap.josm.data.osm.DataSet;
023import org.openstreetmap.josm.gui.progress.ProgressMonitor;
024import org.openstreetmap.josm.tools.Utils;
025
026/**
027 * This DataReader reads directly from the REST API of the osm server.
028 *
029 * It supports plain text transfer as well as gzip or deflate encoded transfers;
030 * if compressed transfers are unwanted, set property osm-server.use-compression
031 * to false.
032 *
033 * @author imi
034 */
035public abstract class OsmServerReader extends OsmConnection {
036    private OsmApi api = OsmApi.getOsmApi();
037    private boolean doAuthenticate = false;
038    protected boolean gpxParsedProperly;
039
040    /**
041     * Open a connection to the given url and return a reader on the input stream
042     * from that connection. In case of user cancel, return <code>null</code>.
043     * Relative URL's are directed to API base URL.
044     * @param urlStr The url to connect to.
045     * @param progressMonitor progress monitoring and abort handler
046     * @return A reader reading the input stream (servers answer) or <code>null</code>.
047     * @throws OsmTransferException thrown if data transfer errors occur
048     */
049    protected InputStream getInputStream(String urlStr, ProgressMonitor progressMonitor) throws OsmTransferException  {
050        return getInputStream(urlStr, progressMonitor, null);
051    }
052
053    /**
054     * Open a connection to the given url and return a reader on the input stream
055     * from that connection. In case of user cancel, return <code>null</code>.
056     * Relative URL's are directed to API base URL.
057     * @param urlStr The url to connect to.
058     * @param progressMonitor progress monitoring and abort handler
059     * @param reason The reason to show on console. Can be {@code null} if no reason is given
060     * @return A reader reading the input stream (servers answer) or <code>null</code>.
061     * @throws OsmTransferException thrown if data transfer errors occur
062     */
063    protected InputStream getInputStream(String urlStr, ProgressMonitor progressMonitor, String reason) throws OsmTransferException  {
064        try {
065            api.initialize(progressMonitor);
066            String url = urlStr.startsWith("http") ? urlStr : (getBaseUrl() + urlStr);
067            return getInputStreamRaw(url, progressMonitor, reason);
068        } finally {
069            progressMonitor.invalidate();
070        }
071    }
072
073    /**
074     * Return the base URL for relative URL requests
075     * @return base url of API
076     */
077    protected String getBaseUrl() {
078        return api.getBaseUrl();
079    }
080
081    /**
082     * Open a connection to the given url and return a reader on the input stream
083     * from that connection. In case of user cancel, return <code>null</code>.
084     * @param urlStr The exact url to connect to.
085     * @param progressMonitor progress monitoring and abort handler
086     * @return An reader reading the input stream (servers answer) or <code>null</code>.
087     * @throws OsmTransferException thrown if data transfer errors occur
088     */
089    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor) throws OsmTransferException {
090        return getInputStreamRaw(urlStr, progressMonitor, null);
091    }
092
093    /**
094     * Open a connection to the given url and return a reader on the input stream
095     * from that connection. In case of user cancel, return <code>null</code>.
096     * @param urlStr The exact url to connect to.
097     * @param progressMonitor progress monitoring and abort handler
098     * @param reason The reason to show on console. Can be {@code null} if no reason is given
099     * @return An reader reading the input stream (servers answer) or <code>null</code>.
100     * @throws OsmTransferException thrown if data transfer errors occur
101     */
102    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason) throws OsmTransferException {
103        return getInputStreamRaw(urlStr, progressMonitor, reason, false);
104    }
105
106    /**
107     * Open a connection to the given url and return a reader on the input stream
108     * from that connection. In case of user cancel, return <code>null</code>.
109     * @param urlStr The exact url to connect to.
110     * @param progressMonitor progress monitoring and abort handler
111     * @param reason The reason to show on console. Can be {@code null} if no reason is given
112     * @param uncompressAccordingToContentDisposition Whether to inspect the HTTP header {@code Content-Disposition}
113     *                                                for {@code filename} and uncompress a gzip/bzip2 stream.
114     * @return An reader reading the input stream (servers answer) or <code>null</code>.
115     * @throws OsmTransferException thrown if data transfer errors occur
116     */
117    @SuppressWarnings("resource")
118    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason, boolean uncompressAccordingToContentDisposition) throws OsmTransferException {
119        try {
120            OnlineResource.JOSM_WEBSITE.checkOfflineAccess(urlStr, Main.getJOSMWebsite());
121            OnlineResource.OSM_API.checkOfflineAccess(urlStr, Main.pref.get("osm-server.url", OsmApi.DEFAULT_API_URL));
122
123            URL url = null;
124            try {
125                url = new URL(urlStr.replace(" ", "%20"));
126            } catch(MalformedURLException e) {
127                throw new OsmTransferException(e);
128            }
129            try {
130                // fix #7640, see http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive
131                activeConnection = Utils.openHttpConnection(url, false);
132            } catch(Exception e) {
133                throw new OsmTransferException(tr("Failed to open connection to API {0}.", url.toExternalForm()), e);
134            }
135            if (cancel) {
136                activeConnection.disconnect();
137                return null;
138            }
139
140            if (doAuthenticate) {
141                addAuth(activeConnection);
142            }
143            if (cancel)
144                throw new OsmTransferCanceledException();
145            if (Main.pref.getBoolean("osm-server.use-compression", true)) {
146                activeConnection.setRequestProperty("Accept-Encoding", "gzip, deflate");
147            }
148
149            activeConnection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
150
151            try {
152                if (reason != null && !reason.isEmpty()) {
153                    Main.info("GET " + url + " (" + reason + ")");
154                } else {
155                    Main.info("GET " + url);
156                }
157                activeConnection.connect();
158            } catch (Exception e) {
159                Main.error(e);
160                OsmTransferException ote = new OsmTransferException(tr("Could not connect to the OSM server. Please check your internet connection."), e);
161                ote.setUrl(url.toString());
162                throw ote;
163            }
164            try {
165                if (Main.isDebugEnabled()) {
166                    Main.debug("RESPONSE: "+activeConnection.getHeaderFields());
167                }
168                if (activeConnection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED)
169                    throw new OsmApiException(HttpURLConnection.HTTP_UNAUTHORIZED,null,null);
170
171                if (activeConnection.getResponseCode() == HttpURLConnection.HTTP_PROXY_AUTH)
172                    throw new OsmTransferCanceledException();
173
174                String encoding = activeConnection.getContentEncoding();
175                if (activeConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
176                    String errorHeader = activeConnection.getHeaderField("Error");
177                    StringBuilder errorBody = new StringBuilder();
178                    try {
179                        InputStream i = fixEncoding(activeConnection.getErrorStream(), encoding);
180                        if (i != null) {
181                            BufferedReader in = new BufferedReader(new InputStreamReader(i, StandardCharsets.UTF_8));
182                            String s;
183                            while((s = in.readLine()) != null) {
184                                errorBody.append(s);
185                                errorBody.append("\n");
186                            }
187                        }
188                    }
189                    catch(Exception e) {
190                        errorBody.append(tr("Reading error text failed."));
191                    }
192
193                    throw new OsmApiException(activeConnection.getResponseCode(), errorHeader, errorBody.toString(), url.toString());
194                }
195
196                InputStream in = new ProgressInputStream(activeConnection, progressMonitor);
197                if (uncompressAccordingToContentDisposition) {
198                    in = uncompressAccordingToContentDisposition(in, activeConnection.getHeaderFields());
199                }
200                return fixEncoding(in, encoding);
201            } catch (OsmTransferException e) {
202                throw e;
203            } catch (Exception e) {
204                throw new OsmTransferException(e);
205            }
206        } finally {
207            progressMonitor.invalidate();
208        }
209    }
210
211    private InputStream fixEncoding(InputStream stream, String encoding) throws IOException {
212        if ("gzip".equalsIgnoreCase(encoding)) {
213            stream = new GZIPInputStream(stream);
214        } else if ("deflate".equalsIgnoreCase(encoding)) {
215            stream = new InflaterInputStream(stream, new Inflater(true));
216        }
217        return stream;
218    }
219
220    private InputStream uncompressAccordingToContentDisposition(InputStream stream, Map<String, List<String>> headerFields) throws IOException {
221        List<String> field = headerFields.get("Content-Disposition");
222        if (field != null && field.toString().contains(".gz\"")) {
223            return Compression.GZIP.getUncompressedInputStream(stream);
224        } else if (field != null && field.toString().contains(".bz2\"")) {
225            return Compression.BZIP2.getUncompressedInputStream(stream);
226        } else {
227            return stream;
228        }
229    }
230
231    /**
232     * Download OSM files from somewhere
233     * @param progressMonitor The progress monitor
234     * @return The corresponding dataset
235     * @throws OsmTransferException if any error occurs
236     */
237    public abstract DataSet parseOsm(final ProgressMonitor progressMonitor) throws OsmTransferException;
238
239    /**
240     * Download OSM Change files from somewhere
241     * @param progressMonitor The progress monitor
242     * @return The corresponding dataset
243     * @throws OsmTransferException if any error occurs
244     */
245    public DataSet parseOsmChange(final ProgressMonitor progressMonitor) throws OsmTransferException {
246        return null;
247    }
248
249    /**
250     * Download BZip2-compressed OSM Change files from somewhere
251     * @param progressMonitor The progress monitor
252     * @return The corresponding dataset
253     * @throws OsmTransferException if any error occurs
254     */
255    public DataSet parseOsmChangeBzip2(final ProgressMonitor progressMonitor) throws OsmTransferException {
256        return null;
257    }
258
259    /**
260     * Download GZip-compressed OSM Change files from somewhere
261     * @param progressMonitor The progress monitor
262     * @return The corresponding dataset
263     * @throws OsmTransferException if any error occurs
264     */
265    public DataSet parseOsmChangeGzip(final ProgressMonitor progressMonitor) throws OsmTransferException {
266        return null;
267    }
268
269    /**
270     * Retrieve raw gps waypoints from the server API.
271     * @param progressMonitor The progress monitor
272     * @return The corresponding GPX tracks
273     * @throws OsmTransferException if any error occurs
274     */
275    public GpxData parseRawGps(final ProgressMonitor progressMonitor) throws OsmTransferException {
276        return null;
277    }
278
279    /**
280     * Retrieve BZip2-compressed GPX files from somewhere.
281     * @param progressMonitor The progress monitor
282     * @return The corresponding GPX tracks
283     * @throws OsmTransferException if any error occurs
284     * @since 6244
285     */
286    public GpxData parseRawGpsBzip2(final ProgressMonitor progressMonitor) throws OsmTransferException {
287        return null;
288    }
289
290    /**
291     * Download BZip2-compressed OSM files from somewhere
292     * @param progressMonitor The progress monitor
293     * @return The corresponding dataset
294     * @throws OsmTransferException if any error occurs
295     */
296    public DataSet parseOsmBzip2(final ProgressMonitor progressMonitor) throws OsmTransferException {
297        return null;
298    }
299
300    /**
301     * Download GZip-compressed OSM files from somewhere
302     * @param progressMonitor The progress monitor
303     * @return The corresponding dataset
304     * @throws OsmTransferException if any error occurs
305     */
306    public DataSet parseOsmGzip(final ProgressMonitor progressMonitor) throws OsmTransferException {
307        return null;
308    }
309
310    /**
311     * Download Zip-compressed OSM files from somewhere
312     * @param progressMonitor The progress monitor
313     * @return The corresponding dataset
314     * @throws OsmTransferException if any error occurs
315     * @since 6882
316     */
317    public DataSet parseOsmZip(final ProgressMonitor progressMonitor) throws OsmTransferException {
318        return null;
319    }
320
321    /**
322     * Returns true if this reader is adding authentication credentials to the read
323     * request sent to the server.
324     *
325     * @return true if this reader is adding authentication credentials to the read
326     * request sent to the server
327     */
328    public boolean isDoAuthenticate() {
329        return doAuthenticate;
330    }
331
332    /**
333     * Sets whether this reader adds authentication credentials to the read
334     * request sent to the server.
335     *
336     * @param doAuthenticate  true if  this reader adds authentication credentials to the read
337     * request sent to the server
338     */
339    public void setDoAuthenticate(boolean doAuthenticate) {
340        this.doAuthenticate = doAuthenticate;
341    }
342
343    /**
344     * Determines if the GPX data has been parsed properly.
345     * @return true if the GPX data has been parsed properly, false otherwise
346     * @see GpxReader#parse
347     */
348    public final boolean isGpxParsedProperly() {
349        return gpxParsedProperly;
350    }
351}