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            URL url = null;
121            try {
122                url = new URL(urlStr.replace(" ", "%20"));
123            } catch(MalformedURLException e) {
124                throw new OsmTransferException(e);
125            }
126            try {
127                // fix #7640, see http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive
128                activeConnection = Utils.openHttpConnection(url, false);
129            } catch(Exception e) {
130                throw new OsmTransferException(tr("Failed to open connection to API {0}.", url.toExternalForm()), e);
131            }
132            if (cancel) {
133                activeConnection.disconnect();
134                return null;
135            }
136
137            if (doAuthenticate) {
138                addAuth(activeConnection);
139            }
140            if (cancel)
141                throw new OsmTransferCanceledException();
142            if (Main.pref.getBoolean("osm-server.use-compression", true)) {
143                activeConnection.setRequestProperty("Accept-Encoding", "gzip, deflate");
144            }
145
146            activeConnection.setConnectTimeout(Main.pref.getInteger("socket.timeout.connect",15)*1000);
147
148            try {
149                if (reason != null && !reason.isEmpty()) {
150                    Main.info("GET " + url + " (" + reason + ")");
151                } else {
152                    Main.info("GET " + url);
153                }
154                activeConnection.connect();
155            } catch (Exception e) {
156                Main.error(e);
157                OsmTransferException ote = new OsmTransferException(tr("Could not connect to the OSM server. Please check your internet connection."), e);
158                ote.setUrl(url.toString());
159                throw ote;
160            }
161            try {
162                if (Main.isDebugEnabled()) {
163                    Main.debug("RESPONSE: "+activeConnection.getHeaderFields());
164                }
165                if (activeConnection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED)
166                    throw new OsmApiException(HttpURLConnection.HTTP_UNAUTHORIZED,null,null);
167
168                if (activeConnection.getResponseCode() == HttpURLConnection.HTTP_PROXY_AUTH)
169                    throw new OsmTransferCanceledException();
170
171                String encoding = activeConnection.getContentEncoding();
172                if (activeConnection.getResponseCode() != HttpURLConnection.HTTP_OK) {
173                    String errorHeader = activeConnection.getHeaderField("Error");
174                    StringBuilder errorBody = new StringBuilder();
175                    try {
176                        InputStream i = fixEncoding(activeConnection.getErrorStream(), encoding);
177                        if (i != null) {
178                            BufferedReader in = new BufferedReader(new InputStreamReader(i, StandardCharsets.UTF_8));
179                            String s;
180                            while((s = in.readLine()) != null) {
181                                errorBody.append(s);
182                                errorBody.append("\n");
183                            }
184                        }
185                    }
186                    catch(Exception e) {
187                        errorBody.append(tr("Reading error text failed."));
188                    }
189
190                    throw new OsmApiException(activeConnection.getResponseCode(), errorHeader, errorBody.toString(), url.toString());
191                }
192
193                InputStream in = new ProgressInputStream(activeConnection, progressMonitor);
194                if (uncompressAccordingToContentDisposition) {
195                    in = uncompressAccordingToContentDisposition(in, activeConnection.getHeaderFields());
196                }
197                return fixEncoding(in, encoding);
198            } catch (OsmTransferException e) {
199                throw e;
200            } catch (Exception e) {
201                throw new OsmTransferException(e);
202            }
203        } finally {
204            progressMonitor.invalidate();
205        }
206    }
207
208    private InputStream fixEncoding(InputStream stream, String encoding) throws IOException {
209        if ("gzip".equalsIgnoreCase(encoding)) {
210            stream = new GZIPInputStream(stream);
211        } else if ("deflate".equalsIgnoreCase(encoding)) {
212            stream = new InflaterInputStream(stream, new Inflater(true));
213        }
214        return stream;
215    }
216
217    private InputStream uncompressAccordingToContentDisposition(InputStream stream, Map<String, List<String>> headerFields) throws IOException {
218        List<String> field = headerFields.get("Content-Disposition");
219        if (field != null && field.toString().contains(".gz\"")) {
220            return Compression.GZIP.getUncompressedInputStream(stream);
221        } else if (field != null && field.toString().contains(".bz2\"")) {
222            return Compression.BZIP2.getUncompressedInputStream(stream);
223        } else {
224            return stream;
225        }
226    }
227
228    /**
229     * Download OSM files from somewhere
230     * @param progressMonitor The progress monitor
231     * @return The corresponding dataset
232     * @throws OsmTransferException if any error occurs
233     */
234    public abstract DataSet parseOsm(final ProgressMonitor progressMonitor) throws OsmTransferException;
235
236    /**
237     * Download OSM Change files from somewhere
238     * @param progressMonitor The progress monitor
239     * @return The corresponding dataset
240     * @throws OsmTransferException if any error occurs
241     */
242    public DataSet parseOsmChange(final ProgressMonitor progressMonitor) throws OsmTransferException {
243        return null;
244    }
245
246    /**
247     * Download BZip2-compressed OSM Change files from somewhere
248     * @param progressMonitor The progress monitor
249     * @return The corresponding dataset
250     * @throws OsmTransferException if any error occurs
251     */
252    public DataSet parseOsmChangeBzip2(final ProgressMonitor progressMonitor) throws OsmTransferException {
253        return null;
254    }
255
256    /**
257     * Download GZip-compressed OSM Change files from somewhere
258     * @param progressMonitor The progress monitor
259     * @return The corresponding dataset
260     * @throws OsmTransferException if any error occurs
261     */
262    public DataSet parseOsmChangeGzip(final ProgressMonitor progressMonitor) throws OsmTransferException {
263        return null;
264    }
265
266    /**
267     * Retrieve raw gps waypoints from the server API.
268     * @param progressMonitor The progress monitor
269     * @return The corresponding GPX tracks
270     * @throws OsmTransferException if any error occurs
271     */
272    public GpxData parseRawGps(final ProgressMonitor progressMonitor) throws OsmTransferException {
273        return null;
274    }
275
276    /**
277     * Retrieve BZip2-compressed GPX files from somewhere.
278     * @param progressMonitor The progress monitor
279     * @return The corresponding GPX tracks
280     * @throws OsmTransferException if any error occurs
281     * @since 6244
282     */
283    public GpxData parseRawGpsBzip2(final ProgressMonitor progressMonitor) throws OsmTransferException {
284        return null;
285    }
286
287    /**
288     * Download BZip2-compressed OSM files from somewhere
289     * @param progressMonitor The progress monitor
290     * @return The corresponding dataset
291     * @throws OsmTransferException if any error occurs
292     */
293    public DataSet parseOsmBzip2(final ProgressMonitor progressMonitor) throws OsmTransferException {
294        return null;
295    }
296
297    /**
298     * Download GZip-compressed OSM files from somewhere
299     * @param progressMonitor The progress monitor
300     * @return The corresponding dataset
301     * @throws OsmTransferException if any error occurs
302     */
303    public DataSet parseOsmGzip(final ProgressMonitor progressMonitor) throws OsmTransferException {
304        return null;
305    }
306
307    /**
308     * Download Zip-compressed OSM files from somewhere
309     * @param progressMonitor The progress monitor
310     * @return The corresponding dataset
311     * @throws OsmTransferException if any error occurs
312     * @since 6882
313     */
314    public DataSet parseOsmZip(final ProgressMonitor progressMonitor) throws OsmTransferException {
315        return null;
316    }
317
318    /**
319     * Returns true if this reader is adding authentication credentials to the read
320     * request sent to the server.
321     *
322     * @return true if this reader is adding authentication credentials to the read
323     * request sent to the server
324     */
325    public boolean isDoAuthenticate() {
326        return doAuthenticate;
327    }
328
329    /**
330     * Sets whether this reader adds authentication credentials to the read
331     * request sent to the server.
332     *
333     * @param doAuthenticate  true if  this reader adds authentication credentials to the read
334     * request sent to the server
335     */
336    public void setDoAuthenticate(boolean doAuthenticate) {
337        this.doAuthenticate = doAuthenticate;
338    }
339
340    /**
341     * Determines if the GPX data has been parsed properly.
342     * @return true if the GPX data has been parsed properly, false otherwise
343     * @see GpxReader#parse
344     */
345    public final boolean isGpxParsedProperly() {
346        return gpxParsedProperly;
347    }
348}