source: josm/trunk/src/org/openstreetmap/josm/tools/HttpClient.java@ 9171

Last change on this file since 9171 was 9171, checked in by simon04, 8 years ago

see #12231 - Use HttpClient instead of some Utils.openHttpConnection usages

File size: 12.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.tools;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.io.BufferedOutputStream;
7import java.io.BufferedReader;
8import java.io.IOException;
9import java.io.InputStream;
10import java.io.OutputStream;
11import java.net.HttpURLConnection;
12import java.net.URL;
13import java.util.Map;
14import java.util.Scanner;
15import java.util.concurrent.ConcurrentHashMap;
16import java.util.zip.GZIPInputStream;
17
18import org.openstreetmap.josm.Main;
19import org.openstreetmap.josm.data.Version;
20import org.openstreetmap.josm.io.Compression;
21import org.openstreetmap.josm.io.UTFInputStreamReader;
22
23/**
24 * Provides a uniform access for a HTTP/HTTPS server. This class should be used in favour of {@link HttpURLConnection}.
25 */
26public class HttpClient {
27
28 private URL url;
29 private final String requestMethod;
30 private int connectTimeout = Main.pref.getInteger("socket.timeout.connect", 15) * 1000;
31 private int readTimeout = Main.pref.getInteger("socket.timeout.read", 30) * 1000;
32 private String accept;
33 private String contentType;
34 private String acceptEncoding = "gzip";
35 private long contentLength;
36 private byte[] requestBody;
37 private long ifModifiedSince;
38 private final Map<String, String> headers = new ConcurrentHashMap<>();
39 private int maxRedirects = Main.pref.getInteger("socket.maxredirects", 5);
40 private boolean useCache;
41 private boolean keepAlive;
42
43 private HttpClient(URL url, String requestMethod) {
44 this.url = url;
45 this.requestMethod = requestMethod;
46 }
47
48 public Response connect() throws IOException {
49 final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
50 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
51 connection.setConnectTimeout(connectTimeout);
52 connection.setReadTimeout(readTimeout);
53 if (accept != null) {
54 connection.setRequestProperty("Accept", accept);
55 }
56 if (contentType != null) {
57 connection.setRequestProperty("Content-Type", contentType);
58 }
59 if (acceptEncoding != null) {
60 connection.setRequestProperty("Accept-Encoding", acceptEncoding);
61 }
62 if (contentLength > 0) {
63 connection.setRequestProperty("Content-Length", String.valueOf(contentLength));
64 }
65 if ("PUT".equals(requestMethod) || "POST".equals(requestMethod) || "DELETE".equals(requestMethod)) {
66 connection.setDoOutput(true);
67 try (OutputStream out = new BufferedOutputStream(connection.getOutputStream())) {
68 out.write(requestBody);
69 }
70 }
71 if (ifModifiedSince > 0) {
72 connection.setIfModifiedSince(ifModifiedSince);
73 }
74 connection.setUseCaches(useCache);
75 if (!useCache) {
76 connection.setRequestProperty("Cache-Control", "no-cache");
77 }
78 if (!keepAlive) {
79 connection.setRequestProperty("Connection", "close");
80 }
81 for (Map.Entry<String, String> header : headers.entrySet()) {
82 connection.setRequestProperty(header.getKey(), header.getValue());
83 }
84
85 boolean successfulConnection = false;
86 try {
87 try {
88 connection.connect();
89 Main.info("{0} {1} => {2}", requestMethod, url, connection.getResponseCode());
90 } catch (IOException e) {
91 //noinspection ThrowableResultOfMethodCallIgnored
92 Main.addNetworkError(url, Utils.getRootCause(e));
93 throw e;
94 }
95 if (isRedirect(connection.getResponseCode())) {
96 final String redirectLocation = connection.getHeaderField("Location");
97 if (redirectLocation == null) {
98 /* I18n: argument is HTTP response code */
99 String msg = tr("Unexpected response from HTTP server. Got {0} response without ''Location'' header." +
100 " Can''t redirect. Aborting.", connection.getResponseCode());
101 throw new IOException(msg);
102 } else if (maxRedirects > 0) {
103 url = new URL(redirectLocation);
104 maxRedirects--;
105 Main.info(tr("Download redirected to ''{0}''", redirectLocation));
106 return connect();
107 } else {
108 String msg = tr("Too many redirects to the download URL detected. Aborting.");
109 throw new IOException(msg);
110 }
111 }
112 Response response = new Response(connection);
113 successfulConnection = true;
114 return response;
115 } finally {
116 if (!successfulConnection) {
117 connection.disconnect();
118 }
119 }
120 }
121
122 /**
123 * A wrapper for the HTTP response.
124 */
125 public static class Response {
126 private final HttpURLConnection connection;
127 private final int responseCode;
128 private boolean uncompress;
129
130 private Response(HttpURLConnection connection) throws IOException {
131 this.connection = connection;
132 this.responseCode = connection.getResponseCode();
133 }
134
135 /**
136 * Sets whether {@link #getContent()} should uncompress the input stream if necessary.
137 *
138 * @param uncompress whether the input stream should be uncompressed if necessary
139 * @return {@code this}
140 */
141 public Response uncompress(boolean uncompress) {
142 this.uncompress = uncompress;
143 return this;
144 }
145
146 /**
147 * Returns an input stream that reads from this HTTP connection, or,
148 * error stream if the connection failed but the server sent useful data.
149 *
150 * @see HttpURLConnection#getInputStream()
151 * @see HttpURLConnection#getErrorStream()
152 */
153 public InputStream getContent() throws IOException {
154 InputStream in;
155 try {
156 in = connection.getInputStream();
157 } catch (IOException ioe) {
158 in = connection.getErrorStream();
159 }
160 in = "gzip".equalsIgnoreCase(getContentEncoding()) ? new GZIPInputStream(in) : in;
161 if (uncompress) {
162 return Compression.forContentType(getContentType()).getUncompressedInputStream(in);
163 } else {
164 return in;
165 }
166 }
167
168 /**
169 * Returns {@link #getContent()} wrapped in a buffered reader.
170 *
171 * Detects Unicode charset in use utilizing {@link UTFInputStreamReader}.
172 */
173 public BufferedReader getContentReader() throws IOException {
174 return new BufferedReader(
175 UTFInputStreamReader.create(getContent())
176 );
177 }
178
179 /**
180 * Fetches the HTTP response as String.
181 * @return the response
182 * @throws IOException
183 */
184 public String fetchContent() throws IOException {
185 try (Scanner scanner = new Scanner(getContentReader())) {
186 return scanner.useDelimiter("\\A").next();
187 }
188 }
189
190 /**
191 * Gets the response code from this HTTP connection.
192 *
193 * @see HttpURLConnection#getResponseCode()
194 */
195 public int getResponseCode() {
196 return responseCode;
197 }
198
199 /**
200 * Returns the {@code Content-Encoding} header.
201 */
202 public String getContentEncoding() {
203 return connection.getContentEncoding();
204 }
205
206 /**
207 * Returns the {@code Content-Type} header.
208 */
209 public String getContentType() {
210 return connection.getHeaderField("Content-Type");
211 }
212
213 /**
214 * Returns the {@code Content-Length} header.
215 */
216 public long getContentLength() {
217 return connection.getContentLengthLong();
218 }
219
220 /**
221 * @see HttpURLConnection#disconnect()
222 */
223 public void disconnect() {
224 connection.disconnect();
225 }
226 }
227
228 /**
229 * Creates a new instance for the given URL and a {@code GET} request
230 *
231 * @param url the URL
232 * @return a new instance
233 */
234 public static HttpClient create(URL url) {
235 return create(url, "GET");
236 }
237
238 /**
239 * Creates a new instance for the given URL and a {@code GET} request
240 *
241 * @param url the URL
242 * @param requestMethod the HTTP request method to perform when calling
243 * @return a new instance
244 */
245 public static HttpClient create(URL url, String requestMethod) {
246 return new HttpClient(url, requestMethod);
247 }
248
249 /**
250 * Sets whether not to set header {@code Cache-Control=no-cache}
251 *
252 * @param useCache whether not to set header {@code Cache-Control=no-cache}
253 * @return {@code this}
254 * @see HttpURLConnection#setUseCaches(boolean)
255 */
256 public HttpClient useCache(boolean useCache) {
257 this.useCache = useCache;
258 return this;
259 }
260
261 /**
262 * Sets whether not to set header {@code Connection=close}
263 * <p/>
264 * This might fix #7640, see <a href='https://web.archive.org/web/20140118201501/http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive'>here</a>.
265 *
266 * @param keepAlive whether not to set header {@code Connection=close}
267 * @return {@code this}
268 */
269 public HttpClient keepAlive(boolean keepAlive) {
270 this.keepAlive = keepAlive;
271 return this;
272 }
273
274 /**
275 * @return {@code this}
276 * @see HttpURLConnection#setConnectTimeout(int)
277 */
278 public HttpClient setConnectTimeout(int connectTimeout) {
279 this.connectTimeout = connectTimeout;
280 return this;
281 }
282
283 /**
284 * @return {@code this}
285 * @see HttpURLConnection#setReadTimeout(int) (int)
286 */
287
288 public HttpClient setReadTimeout(int readTimeout) {
289 this.readTimeout = readTimeout;
290 return this;
291 }
292
293 /**
294 * Sets the {@code Accept} header.
295 *
296 * @return {@code this}
297 */
298 public HttpClient setAccept(String accept) {
299 this.accept = accept;
300 return this;
301 }
302
303 /**
304 * Sets the {@code Content-Type} header.
305 *
306 * @return {@code this}
307 */
308 public HttpClient setContentType(String contentType) {
309 this.contentType = contentType;
310 return this;
311 }
312
313 /**
314 * Sets the {@code Accept-Encoding} header.
315 *
316 * @return {@code this}
317 */
318 public HttpClient setAcceptEncoding(String acceptEncoding) {
319 this.acceptEncoding = acceptEncoding;
320 return this;
321 }
322
323 /**
324 * Sets the {@code Content-Length} header for {@code PUT}/{@code POST} requests.
325 *
326 * @return {@code this}
327 */
328 public HttpClient setContentLength(long contentLength) {
329 this.contentLength = contentLength;
330 return this;
331 }
332
333 /**
334 * Sets the request body for {@code PUT}/{@code POST} requests.
335 *
336 * @return {@code this}
337 */
338 public HttpClient setRequestBody(byte[] requestBody) {
339 this.requestBody = requestBody;
340 return this;
341 }
342
343 /**
344 * Sets the {@code If-Modified-Since} header.
345 *
346 * @return {@code this}
347 */
348 public HttpClient setIfModifiedSince(long ifModifiedSince) {
349 this.ifModifiedSince = ifModifiedSince;
350 return this;
351 }
352
353 /**
354 * Sets the maximum number of redirections to follow.
355 *
356 * @return {@code this}
357 */
358 public HttpClient setMaxRedirects(int maxRedirects) {
359 this.maxRedirects = maxRedirects;
360 return this;
361 }
362
363 /**
364 * Sets an arbitrary HTTP header.
365 *
366 * @return {@code this}
367 */
368 public HttpClient setHeader(String key, String value) {
369 this.headers.put(key, value);
370 return this;
371 }
372
373 /**
374 * Sets arbitrary HTTP headers.
375 *
376 * @return {@code this}
377 */
378 public HttpClient setHeaders(Map<String, String> headers) {
379 this.headers.putAll(headers);
380 return this;
381 }
382
383 private static boolean isRedirect(final int statusCode) {
384 switch (statusCode) {
385 case HttpURLConnection.HTTP_MOVED_PERM: // 301
386 case HttpURLConnection.HTTP_MOVED_TEMP: // 302
387 case HttpURLConnection.HTTP_SEE_OTHER: // 303
388 case 307: // TEMPORARY_REDIRECT:
389 case 308: // PERMANENT_REDIRECT:
390 return true;
391 default:
392 return false;
393 }
394 }
395
396}
Note: See TracBrowser for help on using the repository browser.