001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.tools;
003
004import java.io.BufferedOutputStream;
005import java.io.ByteArrayInputStream;
006import java.io.IOException;
007import java.io.InputStream;
008import java.io.OutputStream;
009import java.net.HttpURLConnection;
010import java.net.URL;
011import java.util.Collections;
012import java.util.List;
013import java.util.Map;
014import java.util.Map.Entry;
015import java.util.Optional;
016import java.util.TreeMap;
017
018import org.openstreetmap.josm.data.Version;
019import org.openstreetmap.josm.gui.progress.ProgressMonitor;
020import org.openstreetmap.josm.io.ProgressOutputStream;
021
022/**
023 * Provides a uniform access for a HTTP/HTTPS 1.0/1.1 server.
024 * @since 15229
025 */
026public final class Http1Client extends HttpClient {
027
028    private HttpURLConnection connection; // to allow disconnecting before `response` is set
029
030    /**
031     * Constructs a new {@code Http1Client}.
032     * @param url URL to access
033     * @param requestMethod HTTP request method (GET, POST, PUT, DELETE...)
034     */
035    public Http1Client(URL url, String requestMethod) {
036        super(url, requestMethod);
037    }
038
039    @Override
040    protected void setupConnection(ProgressMonitor progressMonitor) throws IOException {
041        connection = (HttpURLConnection) getURL().openConnection();
042        connection.setRequestMethod(getRequestMethod());
043        connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
044        connection.setConnectTimeout(getConnectTimeout());
045        connection.setReadTimeout(getReadTimeout());
046        connection.setInstanceFollowRedirects(false); // we do that ourselves
047        if (getIfModifiedSince() > 0) {
048            connection.setIfModifiedSince(getIfModifiedSince());
049        }
050        connection.setUseCaches(isUseCache());
051        if (!isUseCache()) {
052            connection.setRequestProperty("Cache-Control", "no-cache");
053        }
054        for (Map.Entry<String, String> header : getHeaders().entrySet()) {
055            if (header.getValue() != null) {
056                connection.setRequestProperty(header.getKey(), header.getValue());
057            }
058        }
059
060        notifyConnect(progressMonitor);
061
062        if (requiresBody()) {
063            logRequestBody();
064            byte[] body = getRequestBody();
065            connection.setFixedLengthStreamingMode(body.length);
066            connection.setDoOutput(true);
067            try (OutputStream out = new BufferedOutputStream(
068                    new ProgressOutputStream(connection.getOutputStream(), body.length,
069                            progressMonitor, getOutputMessage(), isFinishOnCloseOutput()))) {
070                out.write(body);
071            }
072        }
073    }
074
075    @Override
076    protected ConnectionResponse performConnection() throws IOException {
077        connection.connect();
078        return new ConnectionResponse() {
079            @Override
080            public String getResponseVersion() {
081                return "HTTP_1";
082            }
083
084            @Override
085            public int getResponseCode() throws IOException {
086                return connection.getResponseCode();
087            }
088
089            @Override
090            public String getHeaderField(String name) {
091                return connection.getHeaderField(name);
092            }
093
094            @Override
095            public long getContentLengthLong() {
096                return connection.getContentLengthLong();
097            }
098
099            @Override
100            public Map<String, List<String>> getHeaderFields() {
101                return connection.getHeaderFields();
102            }
103        };
104    }
105
106    @Override
107    protected void performDisconnection() throws IOException {
108        connection.disconnect();
109    }
110
111    @Override
112    protected Response buildResponse(ProgressMonitor progressMonitor) throws IOException {
113        return new Http1Response(connection, progressMonitor);
114    }
115
116    /**
117     * A wrapper for the HTTP 1.x response.
118     */
119    public static final class Http1Response extends Response {
120        private final HttpURLConnection connection;
121
122        private Http1Response(HttpURLConnection connection, ProgressMonitor progressMonitor) throws IOException {
123            super(progressMonitor, connection.getResponseCode(), connection.getResponseMessage());
124            this.connection = connection;
125            debugRedirect();
126        }
127
128        @Override
129        public URL getURL() {
130            return connection.getURL();
131        }
132
133        @Override
134        public String getRequestMethod() {
135            return connection.getRequestMethod();
136        }
137
138        @Override
139        public InputStream getInputStream() throws IOException {
140            InputStream in;
141            try {
142                in = connection.getInputStream();
143            } catch (IOException ioe) {
144                Logging.debug(ioe);
145                in = Optional.ofNullable(connection.getErrorStream()).orElseGet(() -> new ByteArrayInputStream(new byte[]{}));
146            }
147            return in;
148        }
149
150        @Override
151        public String getContentEncoding() {
152            return connection.getContentEncoding();
153        }
154
155        @Override
156        public String getContentType() {
157            return connection.getHeaderField("Content-Type");
158        }
159
160        @Override
161        public long getExpiration() {
162            return connection.getExpiration();
163        }
164
165        @Override
166        public long getLastModified() {
167            return connection.getLastModified();
168        }
169
170        @Override
171        public long getContentLength() {
172            return connection.getContentLengthLong();
173        }
174
175        @Override
176        public String getHeaderField(String name) {
177            return connection.getHeaderField(name);
178        }
179
180        @Override
181        public Map<String, List<String>> getHeaderFields() {
182            // returned map from HttpUrlConnection is case sensitive, use case insensitive TreeMap to conform to RFC 2616
183            Map<String, List<String>> ret = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
184            for (Entry<String, List<String>> e: connection.getHeaderFields().entrySet()) {
185                if (e.getKey() != null) {
186                    ret.put(e.getKey(), e.getValue());
187                }
188            }
189            return Collections.unmodifiableMap(ret);
190        }
191
192        @Override
193        public void disconnect() {
194            Http1Client.disconnect(connection);
195        }
196    }
197
198    /**
199     * @see HttpURLConnection#disconnect()
200     */
201    @Override
202    public void disconnect() {
203        Http1Client.disconnect(connection);
204    }
205
206    private static void disconnect(final HttpURLConnection connection) {
207        if (connection != null) {
208            // Fix upload aborts - see #263
209            connection.setConnectTimeout(100);
210            connection.setReadTimeout(100);
211            try {
212                Thread.sleep(100);
213            } catch (InterruptedException ex) {
214                Logging.warn("InterruptedException in " + Http1Client.class + " during cancel");
215                Thread.currentThread().interrupt();
216            }
217            connection.disconnect();
218        }
219    }
220}