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

Last change on this file since 10179 was 10160, checked in by bastiK, 8 years ago

partial revert of [9529] (fixes #12583)

possibly breaking tests

File size: 22.7 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.CookieHandler;
12import java.net.CookieManager;
13import java.net.HttpURLConnection;
14import java.net.URL;
15import java.util.Collections;
16import java.util.List;
17import java.util.Locale;
18import java.util.Map;
19import java.util.Map.Entry;
20import java.util.Scanner;
21import java.util.TreeMap;
22import java.util.regex.Matcher;
23import java.util.regex.Pattern;
24import java.util.zip.GZIPInputStream;
25
26import org.openstreetmap.josm.Main;
27import org.openstreetmap.josm.data.Version;
28import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
29import org.openstreetmap.josm.gui.progress.ProgressMonitor;
30import org.openstreetmap.josm.io.Compression;
31import org.openstreetmap.josm.io.ProgressInputStream;
32import org.openstreetmap.josm.io.ProgressOutputStream;
33import org.openstreetmap.josm.io.UTFInputStreamReader;
34import org.openstreetmap.josm.io.auth.DefaultAuthenticator;
35
36/**
37 * Provides a uniform access for a HTTP/HTTPS server. This class should be used in favour of {@link HttpURLConnection}.
38 * @since 9168
39 */
40public final class HttpClient {
41
42 private URL url;
43 private final String requestMethod;
44 private int connectTimeout = Main.pref.getInteger("socket.timeout.connect", 15) * 1000;
45 private int readTimeout = Main.pref.getInteger("socket.timeout.read", 30) * 1000;
46 private byte[] requestBody;
47 private long ifModifiedSince;
48 private final Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
49 private int maxRedirects = Main.pref.getInteger("socket.maxredirects", 5);
50 private boolean useCache;
51 private String reasonForRequest;
52 private HttpURLConnection connection; // to allow disconnecting before `response` is set
53 private Response response;
54
55 static {
56 CookieHandler.setDefault(new CookieManager());
57 }
58
59 private HttpClient(URL url, String requestMethod) {
60 this.url = url;
61 this.requestMethod = requestMethod;
62 this.headers.put("Accept-Encoding", "gzip");
63 }
64
65 /**
66 * Opens the HTTP connection.
67 * @return HTTP response
68 * @throws IOException if any I/O error occurs
69 */
70 public Response connect() throws IOException {
71 return connect(null);
72 }
73
74 /**
75 * Opens the HTTP connection.
76 * @param progressMonitor progress monitor
77 * @return HTTP response
78 * @throws IOException if any I/O error occurs
79 * @since 9179
80 */
81 public Response connect(ProgressMonitor progressMonitor) throws IOException {
82 if (progressMonitor == null) {
83 progressMonitor = NullProgressMonitor.INSTANCE;
84 }
85 final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
86 this.connection = connection;
87 connection.setRequestMethod(requestMethod);
88 connection.setRequestProperty("User-Agent", Version.getInstance().getFullAgentString());
89 connection.setConnectTimeout(connectTimeout);
90 connection.setReadTimeout(readTimeout);
91 connection.setInstanceFollowRedirects(false); // we do that ourselves
92 if (ifModifiedSince > 0) {
93 connection.setIfModifiedSince(ifModifiedSince);
94 }
95 connection.setUseCaches(useCache);
96 if (!useCache) {
97 connection.setRequestProperty("Cache-Control", "no-cache");
98 }
99 for (Map.Entry<String, String> header : headers.entrySet()) {
100 if (header.getValue() != null) {
101 connection.setRequestProperty(header.getKey(), header.getValue());
102 }
103 }
104
105 progressMonitor.beginTask(tr("Contacting Server..."), 1);
106 progressMonitor.indeterminateSubTask(null);
107
108 if ("PUT".equals(requestMethod) || "POST".equals(requestMethod) || "DELETE".equals(requestMethod)) {
109 Main.info("{0} {1} ({2}) ...", requestMethod, url, Utils.getSizeString(requestBody.length, Locale.getDefault()));
110 connection.setFixedLengthStreamingMode(requestBody.length);
111 connection.setDoOutput(true);
112 try (OutputStream out = new BufferedOutputStream(
113 new ProgressOutputStream(connection.getOutputStream(), requestBody.length, progressMonitor))) {
114 out.write(requestBody);
115 }
116 }
117
118 boolean successfulConnection = false;
119 try {
120 try {
121 connection.connect();
122 final boolean hasReason = reasonForRequest != null && !reasonForRequest.isEmpty();
123 Main.info("{0} {1}{2} -> {3}{4}",
124 requestMethod, url, hasReason ? " (" + reasonForRequest + ")" : "",
125 connection.getResponseCode(),
126 connection.getContentLengthLong() > 0
127 ? " (" + Utils.getSizeString(connection.getContentLengthLong(), Locale.getDefault()) + ")"
128 : ""
129 );
130 if (Main.isDebugEnabled()) {
131 Main.debug("RESPONSE: " + connection.getHeaderFields());
132 }
133 if (DefaultAuthenticator.getInstance().isEnabled() && connection.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
134 DefaultAuthenticator.getInstance().addFailedCredentialHost(url.getHost());
135 }
136 } catch (IOException e) {
137 Main.info("{0} {1} -> !!!", requestMethod, url);
138 Main.warn(e);
139 //noinspection ThrowableResultOfMethodCallIgnored
140 Main.addNetworkError(url, Utils.getRootCause(e));
141 throw e;
142 }
143 if (isRedirect(connection.getResponseCode())) {
144 final String redirectLocation = connection.getHeaderField("Location");
145 if (redirectLocation == null) {
146 /* I18n: argument is HTTP response code */
147 String msg = tr("Unexpected response from HTTP server. Got {0} response without ''Location'' header." +
148 " Can''t redirect. Aborting.", connection.getResponseCode());
149 throw new IOException(msg);
150 } else if (maxRedirects > 0) {
151 url = new URL(url, redirectLocation);
152 maxRedirects--;
153 Main.info(tr("Download redirected to ''{0}''", redirectLocation));
154 return connect();
155 } else if (maxRedirects == 0) {
156 String msg = tr("Too many redirects to the download URL detected. Aborting.");
157 throw new IOException(msg);
158 }
159 }
160 response = new Response(connection, progressMonitor);
161 successfulConnection = true;
162 return response;
163 } finally {
164 if (!successfulConnection) {
165 connection.disconnect();
166 }
167 }
168 }
169
170 /**
171 * Returns the HTTP response which is set only after calling {@link #connect()}.
172 * Calling this method again, returns the identical object (unless another {@link #connect()} is performed).
173 *
174 * @return the HTTP response
175 * @since 9309
176 */
177 public Response getResponse() {
178 return response;
179 }
180
181 /**
182 * A wrapper for the HTTP response.
183 */
184 public static final class Response {
185 private final HttpURLConnection connection;
186 private final ProgressMonitor monitor;
187 private final int responseCode;
188 private final String responseMessage;
189 private boolean uncompress;
190 private boolean uncompressAccordingToContentDisposition;
191
192 private Response(HttpURLConnection connection, ProgressMonitor monitor) throws IOException {
193 CheckParameterUtil.ensureParameterNotNull(connection, "connection");
194 CheckParameterUtil.ensureParameterNotNull(monitor, "monitor");
195 this.connection = connection;
196 this.monitor = monitor;
197 this.responseCode = connection.getResponseCode();
198 this.responseMessage = connection.getResponseMessage();
199 }
200
201 /**
202 * Sets whether {@link #getContent()} should uncompress the input stream if necessary.
203 *
204 * @param uncompress whether the input stream should be uncompressed if necessary
205 * @return {@code this}
206 */
207 public Response uncompress(boolean uncompress) {
208 this.uncompress = uncompress;
209 return this;
210 }
211
212 /**
213 * Sets whether {@link #getContent()} should uncompress the input stream according to {@code Content-Disposition}
214 * HTTP header.
215 * @param uncompressAccordingToContentDisposition whether the input stream should be uncompressed according to
216 * {@code Content-Disposition}
217 * @return {@code this}
218 * @since 9172
219 */
220 public Response uncompressAccordingToContentDisposition(boolean uncompressAccordingToContentDisposition) {
221 this.uncompressAccordingToContentDisposition = uncompressAccordingToContentDisposition;
222 return this;
223 }
224
225 /**
226 * Returns the URL.
227 * @return the URL
228 * @see HttpURLConnection#getURL()
229 * @since 9172
230 */
231 public URL getURL() {
232 return connection.getURL();
233 }
234
235 /**
236 * Returns the request method.
237 * @return the HTTP request method
238 * @see HttpURLConnection#getRequestMethod()
239 * @since 9172
240 */
241 public String getRequestMethod() {
242 return connection.getRequestMethod();
243 }
244
245 /**
246 * Returns an input stream that reads from this HTTP connection, or,
247 * error stream if the connection failed but the server sent useful data.
248 * <p>
249 * Note: the return value can be null, if both the input and the error stream are null.
250 * Seems to be the case if the OSM server replies a 401 Unauthorized, see #3887
251 * @return input or error stream
252 * @throws IOException if any I/O error occurs
253 *
254 * @see HttpURLConnection#getInputStream()
255 * @see HttpURLConnection#getErrorStream()
256 */
257 @SuppressWarnings("resource")
258 public InputStream getContent() throws IOException {
259 InputStream in;
260 try {
261 in = connection.getInputStream();
262 } catch (IOException ioe) {
263 in = connection.getErrorStream();
264 }
265 if (in != null) {
266 in = new ProgressInputStream(in, getContentLength(), monitor);
267 in = "gzip".equalsIgnoreCase(getContentEncoding()) ? new GZIPInputStream(in) : in;
268 Compression compression = Compression.NONE;
269 if (uncompress) {
270 final String contentType = getContentType();
271 Main.debug("Uncompressing input stream according to Content-Type header: {0}", contentType);
272 compression = Compression.forContentType(contentType);
273 }
274 if (uncompressAccordingToContentDisposition && Compression.NONE.equals(compression)) {
275 final String contentDisposition = getHeaderField("Content-Disposition");
276 final Matcher matcher = Pattern.compile("filename=\"([^\"]+)\"").matcher(
277 contentDisposition != null ? contentDisposition : "");
278 if (matcher.find()) {
279 Main.debug("Uncompressing input stream according to Content-Disposition header: {0}", contentDisposition);
280 compression = Compression.byExtension(matcher.group(1));
281 }
282 }
283 in = compression.getUncompressedInputStream(in);
284 }
285 return in;
286 }
287
288 /**
289 * Returns {@link #getContent()} wrapped in a buffered reader.
290 *
291 * Detects Unicode charset in use utilizing {@link UTFInputStreamReader}.
292 * @return buffered reader
293 * @throws IOException if any I/O error occurs
294 */
295 public BufferedReader getContentReader() throws IOException {
296 return new BufferedReader(
297 UTFInputStreamReader.create(getContent())
298 );
299 }
300
301 /**
302 * Fetches the HTTP response as String.
303 * @return the response
304 * @throws IOException if any I/O error occurs
305 */
306 @SuppressWarnings("resource")
307 public String fetchContent() throws IOException {
308 try (Scanner scanner = new Scanner(getContentReader()).useDelimiter("\\A")) {
309 return scanner.hasNext() ? scanner.next() : "";
310 }
311 }
312
313 /**
314 * Gets the response code from this HTTP connection.
315 * @return HTTP response code
316 *
317 * @see HttpURLConnection#getResponseCode()
318 */
319 public int getResponseCode() {
320 return responseCode;
321 }
322
323 /**
324 * Gets the response message from this HTTP connection.
325 * @return HTTP response message
326 *
327 * @see HttpURLConnection#getResponseMessage()
328 * @since 9172
329 */
330 public String getResponseMessage() {
331 return responseMessage;
332 }
333
334 /**
335 * Returns the {@code Content-Encoding} header.
336 * @return {@code Content-Encoding} HTTP header
337 * @see HttpURLConnection#getContentEncoding()
338 */
339 public String getContentEncoding() {
340 return connection.getContentEncoding();
341 }
342
343 /**
344 * Returns the {@code Content-Type} header.
345 * @return {@code Content-Type} HTTP header
346 */
347 public String getContentType() {
348 return connection.getHeaderField("Content-Type");
349 }
350
351 /**
352 * Returns the {@code Expire} header.
353 * @return {@code Expire} HTTP header
354 * @see HttpURLConnection#getExpiration()
355 * @since 9232
356 */
357 public long getExpiration() {
358 return connection.getExpiration();
359 }
360
361 /**
362 * Returns the {@code Last-Modified} header.
363 * @return {@code Last-Modified} HTTP header
364 * @see HttpURLConnection#getLastModified()
365 * @since 9232
366 */
367 public long getLastModified() {
368 return connection.getLastModified();
369 }
370
371 /**
372 * Returns the {@code Content-Length} header.
373 * @return {@code Content-Length} HTTP header
374 * @see HttpURLConnection#getContentLengthLong()
375 */
376 public long getContentLength() {
377 return connection.getContentLengthLong();
378 }
379
380 /**
381 * Returns the value of the named header field.
382 * @param name the name of a header field
383 * @return the value of the named header field, or {@code null} if there is no such field in the header
384 * @see HttpURLConnection#getHeaderField(String)
385 * @since 9172
386 */
387 public String getHeaderField(String name) {
388 return connection.getHeaderField(name);
389 }
390
391 /**
392 * Returns an unmodifiable Map mapping header keys to a List of header values.
393 * As per RFC 2616, section 4.2 header names are case insensitive, so returned map is also case insensitive
394 * @return unmodifiable Map mapping header keys to a List of header values
395 * @see HttpURLConnection#getHeaderFields()
396 * @since 9232
397 */
398 public Map<String, List<String>> getHeaderFields() {
399 // returned map from HttpUrlConnection is case sensitive, use case insensitive TreeMap to conform to RFC 2616
400 Map<String, List<String>> ret = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
401 for (Entry<String, List<String>> e: connection.getHeaderFields().entrySet()) {
402 if (e.getKey() != null) {
403 ret.put(e.getKey(), e.getValue());
404 }
405 }
406 return Collections.unmodifiableMap(ret);
407 }
408
409 /**
410 * @see HttpURLConnection#disconnect()
411 */
412 public void disconnect() {
413 HttpClient.disconnect(connection);
414 }
415 }
416
417 /**
418 * Creates a new instance for the given URL and a {@code GET} request
419 *
420 * @param url the URL
421 * @return a new instance
422 */
423 public static HttpClient create(URL url) {
424 return create(url, "GET");
425 }
426
427 /**
428 * Creates a new instance for the given URL and a {@code GET} request
429 *
430 * @param url the URL
431 * @param requestMethod the HTTP request method to perform when calling
432 * @return a new instance
433 */
434 public static HttpClient create(URL url, String requestMethod) {
435 return new HttpClient(url, requestMethod);
436 }
437
438 /**
439 * Returns the URL set for this connection.
440 * @return the URL
441 * @see #create(URL)
442 * @see #create(URL, String)
443 * @since 9172
444 */
445 public URL getURL() {
446 return url;
447 }
448
449 /**
450 * Returns the request method set for this connection.
451 * @return the HTTP request method
452 * @see #create(URL, String)
453 * @since 9172
454 */
455 public String getRequestMethod() {
456 return requestMethod;
457 }
458
459 /**
460 * Returns the set value for the given {@code header}.
461 * @param header HTTP header name
462 * @return HTTP header value
463 * @since 9172
464 */
465 public String getRequestHeader(String header) {
466 return headers.get(header);
467 }
468
469 /**
470 * Sets whether not to set header {@code Cache-Control=no-cache}
471 *
472 * @param useCache whether not to set header {@code Cache-Control=no-cache}
473 * @return {@code this}
474 * @see HttpURLConnection#setUseCaches(boolean)
475 */
476 public HttpClient useCache(boolean useCache) {
477 this.useCache = useCache;
478 return this;
479 }
480
481 /**
482 * Sets whether not to set header {@code Connection=close}
483 * <p>
484 * This might fix #7640, see
485 * <a href='https://web.archive.org/web/20140118201501/http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive'>here</a>.
486 *
487 * @param keepAlive whether not to set header {@code Connection=close}
488 * @return {@code this}
489 */
490 public HttpClient keepAlive(boolean keepAlive) {
491 return setHeader("Connection", keepAlive ? null : "close");
492 }
493
494 /**
495 * Sets a specified timeout value, in milliseconds, to be used when opening a communications link to the resource referenced
496 * by this URLConnection. If the timeout expires before the connection can be established, a
497 * {@link java.net.SocketTimeoutException} is raised. A timeout of zero is interpreted as an infinite timeout.
498 * @param connectTimeout an {@code int} that specifies the connect timeout value in milliseconds
499 * @return {@code this}
500 * @see HttpURLConnection#setConnectTimeout(int)
501 */
502 public HttpClient setConnectTimeout(int connectTimeout) {
503 this.connectTimeout = connectTimeout;
504 return this;
505 }
506
507 /**
508 * Sets the read timeout to a specified timeout, in milliseconds. A non-zero value specifies the timeout when reading from
509 * input stream when a connection is established to a resource. If the timeout expires before there is data available for
510 * read, a {@link java.net.SocketTimeoutException} is raised. A timeout of zero is interpreted as an infinite timeout.
511 * @param readTimeout an {@code int} that specifies the read timeout value in milliseconds
512 * @return {@code this}
513 * @see HttpURLConnection#setReadTimeout(int)
514 */
515 public HttpClient setReadTimeout(int readTimeout) {
516 this.readTimeout = readTimeout;
517 return this;
518 }
519
520 /**
521 * Sets the {@code Accept} header.
522 * @param accept header value
523 *
524 * @return {@code this}
525 */
526 public HttpClient setAccept(String accept) {
527 return setHeader("Accept", accept);
528 }
529
530 /**
531 * Sets the request body for {@code PUT}/{@code POST} requests.
532 * @param requestBody request body
533 *
534 * @return {@code this}
535 */
536 public HttpClient setRequestBody(byte[] requestBody) {
537 this.requestBody = requestBody;
538 return this;
539 }
540
541 /**
542 * Sets the {@code If-Modified-Since} header.
543 * @param ifModifiedSince header value
544 *
545 * @return {@code this}
546 */
547 public HttpClient setIfModifiedSince(long ifModifiedSince) {
548 this.ifModifiedSince = ifModifiedSince;
549 return this;
550 }
551
552 /**
553 * Sets the maximum number of redirections to follow.
554 *
555 * Set {@code maxRedirects} to {@code -1} in order to ignore redirects, i.e.,
556 * to not throw an {@link IOException} in {@link #connect()}.
557 * @param maxRedirects header value
558 *
559 * @return {@code this}
560 */
561 public HttpClient setMaxRedirects(int maxRedirects) {
562 this.maxRedirects = maxRedirects;
563 return this;
564 }
565
566 /**
567 * Sets an arbitrary HTTP header.
568 * @param key header name
569 * @param value header value
570 *
571 * @return {@code this}
572 */
573 public HttpClient setHeader(String key, String value) {
574 this.headers.put(key, value);
575 return this;
576 }
577
578 /**
579 * Sets arbitrary HTTP headers.
580 * @param headers HTTP headers
581 *
582 * @return {@code this}
583 */
584 public HttpClient setHeaders(Map<String, String> headers) {
585 this.headers.putAll(headers);
586 return this;
587 }
588
589 /**
590 * Sets a reason to show on console. Can be {@code null} if no reason is given.
591 * @param reasonForRequest Reason to show
592 * @return {@code this}
593 * @since 9172
594 */
595 public HttpClient setReasonForRequest(String reasonForRequest) {
596 this.reasonForRequest = reasonForRequest;
597 return this;
598 }
599
600 private static boolean isRedirect(final int statusCode) {
601 switch (statusCode) {
602 case HttpURLConnection.HTTP_MOVED_PERM: // 301
603 case HttpURLConnection.HTTP_MOVED_TEMP: // 302
604 case HttpURLConnection.HTTP_SEE_OTHER: // 303
605 case 307: // TEMPORARY_REDIRECT:
606 case 308: // PERMANENT_REDIRECT:
607 return true;
608 default:
609 return false;
610 }
611 }
612
613 /**
614 * @see HttpURLConnection#disconnect()
615 * @since 9309
616 */
617 public void disconnect() {
618 HttpClient.disconnect(connection);
619 }
620
621 private static void disconnect(final HttpURLConnection connection) {
622 // Fix upload aborts - see #263
623 connection.setConnectTimeout(100);
624 connection.setReadTimeout(100);
625 try {
626 Thread.sleep(100);
627 } catch (InterruptedException ex) {
628 Main.warn("InterruptedException in " + HttpClient.class + " during cancel");
629 }
630 connection.disconnect();
631 }
632}
Note: See TracBrowser for help on using the repository browser.