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

Last change on this file since 15742 was 15742, checked in by simon04, 4 years ago

HttpClient: add support for Content-Encoding: deflate

File size: 30.6 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.BufferedReader;
7import java.io.IOException;
8import java.io.InputStream;
9import java.net.CookieHandler;
10import java.net.CookieManager;
11import java.net.HttpURLConnection;
12import java.net.MalformedURLException;
13import java.net.URL;
14import java.nio.charset.StandardCharsets;
15import java.util.List;
16import java.util.Locale;
17import java.util.Map;
18import java.util.Objects;
19import java.util.Scanner;
20import java.util.TreeMap;
21import java.util.concurrent.TimeUnit;
22import java.util.regex.Matcher;
23import java.util.regex.Pattern;
24import java.util.zip.GZIPInputStream;
25import java.util.zip.InflaterInputStream;
26
27import org.openstreetmap.josm.data.validation.routines.DomainValidator;
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.NetworkManager;
32import org.openstreetmap.josm.io.ProgressInputStream;
33import org.openstreetmap.josm.io.UTFInputStreamReader;
34import org.openstreetmap.josm.io.auth.DefaultAuthenticator;
35import org.openstreetmap.josm.spi.preferences.Config;
36
37/**
38 * Provides a uniform access for a HTTP/HTTPS server. This class should be used in favour of {@link HttpURLConnection}.
39 * @since 9168
40 */
41public abstract class HttpClient {
42
43 /**
44 * HTTP client factory.
45 * @since 15229
46 */
47 @FunctionalInterface
48 public interface HttpClientFactory {
49 /**
50 * Creates a new instance for the given URL and a {@code GET} request
51 *
52 * @param url the URL
53 * @param requestMethod the HTTP request method to perform when calling
54 * @return a new instance
55 */
56 HttpClient create(URL url, String requestMethod);
57 }
58
59 private URL url;
60 private final String requestMethod;
61 private int connectTimeout = (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.connect", 15));
62 private int readTimeout = (int) TimeUnit.SECONDS.toMillis(Config.getPref().getInt("socket.timeout.read", 30));
63 private byte[] requestBody;
64 private long ifModifiedSince;
65 private final Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
66 private int maxRedirects = Config.getPref().getInt("socket.maxredirects", 5);
67 private boolean useCache = true;
68 private String reasonForRequest;
69 private String outputMessage = tr("Uploading data ...");
70 private Response response;
71 private boolean finishOnCloseOutput = true;
72 private boolean debug;
73
74 // Pattern to detect Tomcat error message. Be careful with change of format:
75 // CHECKSTYLE.OFF: LineLength
76 // https://svn.apache.org/viewvc/tomcat/trunk/java/org/apache/catalina/valves/ErrorReportValve.java?r1=1740707&r2=1779641&pathrev=1779641&diff_format=h
77 // CHECKSTYLE.ON: LineLength
78 private static final Pattern TOMCAT_ERR_MESSAGE = Pattern.compile(
79 ".*<p><b>[^<]+</b>[^<]+</p><p><b>[^<]+</b> (?:<u>)?([^<]*)(?:</u>)?</p><p><b>[^<]+</b> (?:<u>)?[^<]*(?:</u>)?</p>.*",
80 Pattern.CASE_INSENSITIVE);
81
82 private static HttpClientFactory factory;
83
84 static {
85 try {
86 CookieHandler.setDefault(new CookieManager());
87 } catch (SecurityException e) {
88 Logging.log(Logging.LEVEL_ERROR, "Unable to set default cookie handler", e);
89 }
90 }
91
92 /**
93 * Registers a new HTTP client factory.
94 * @param newFactory new HTTP client factory
95 * @since 15229
96 */
97 public static void setFactory(HttpClientFactory newFactory) {
98 factory = Objects.requireNonNull(newFactory);
99 }
100
101 /**
102 * Constructs a new {@code HttpClient}.
103 * @param url URL to access
104 * @param requestMethod HTTP request method (GET, POST, PUT, DELETE...)
105 */
106 protected HttpClient(URL url, String requestMethod) {
107 try {
108 String host = url.getHost();
109 String asciiHost = DomainValidator.unicodeToASCII(host);
110 this.url = asciiHost.equals(host) ? url : new URL(url.getProtocol(), asciiHost, url.getPort(), url.getFile());
111 } catch (MalformedURLException e) {
112 throw new JosmRuntimeException(e);
113 }
114 this.requestMethod = requestMethod;
115 this.headers.put("Accept-Encoding", "gzip, deflate");
116 }
117
118 /**
119 * Opens the HTTP connection.
120 * @return HTTP response
121 * @throws IOException if any I/O error occurs
122 */
123 public final Response connect() throws IOException {
124 return connect(null);
125 }
126
127 /**
128 * Opens the HTTP connection.
129 * @param progressMonitor progress monitor
130 * @return HTTP response
131 * @throws IOException if any I/O error occurs
132 * @since 9179
133 */
134 public final Response connect(ProgressMonitor progressMonitor) throws IOException {
135 if (progressMonitor == null) {
136 progressMonitor = NullProgressMonitor.INSTANCE;
137 }
138 setupConnection(progressMonitor);
139
140 boolean successfulConnection = false;
141 try {
142 ConnectionResponse cr;
143 try {
144 cr = performConnection();
145 final boolean hasReason = reasonForRequest != null && !reasonForRequest.isEmpty();
146 logRequest("{0} {1}{2} -> {3} {4}{5}",
147 getRequestMethod(), getURL(), hasReason ? (" (" + reasonForRequest + ')') : "",
148 cr.getResponseVersion(), cr.getResponseCode(),
149 cr.getContentLengthLong() > 0
150 ? (" (" + Utils.getSizeString(cr.getContentLengthLong(), Locale.getDefault()) + ')')
151 : ""
152 );
153 if (Logging.isDebugEnabled()) {
154 try {
155 Logging.debug("RESPONSE: {0}", cr.getHeaderFields());
156 } catch (IllegalArgumentException e) {
157 Logging.warn(e);
158 }
159 }
160 if (DefaultAuthenticator.getInstance().isEnabled() && cr.getResponseCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
161 DefaultAuthenticator.getInstance().addFailedCredentialHost(url.getHost());
162 }
163 } catch (IOException | RuntimeException e) {
164 logRequest("{0} {1} -> !!!", requestMethod, url);
165 Logging.warn(e);
166 //noinspection ThrowableResultOfMethodCallIgnored
167 NetworkManager.addNetworkError(url, Utils.getRootCause(e));
168 throw e;
169 }
170 if (isRedirect(cr.getResponseCode())) {
171 final String redirectLocation = cr.getHeaderField("Location");
172 if (redirectLocation == null) {
173 /* I18n: argument is HTTP response code */
174 throw new IOException(tr("Unexpected response from HTTP server. Got {0} response without ''Location'' header." +
175 " Can''t redirect. Aborting.", cr.getResponseCode()));
176 } else if (maxRedirects > 0) {
177 url = new URL(url, redirectLocation);
178 maxRedirects--;
179 logRequest(tr("Download redirected to ''{0}''", redirectLocation));
180 response = connect();
181 successfulConnection = true;
182 return response;
183 } else if (maxRedirects == 0) {
184 String msg = tr("Too many redirects to the download URL detected. Aborting.");
185 throw new IOException(msg);
186 }
187 }
188 response = buildResponse(progressMonitor);
189 successfulConnection = true;
190 return response;
191 } finally {
192 if (!successfulConnection) {
193 performDisconnection();
194 }
195 }
196 }
197
198 protected abstract void setupConnection(ProgressMonitor progressMonitor) throws IOException;
199
200 protected abstract ConnectionResponse performConnection() throws IOException;
201
202 protected abstract void performDisconnection() throws IOException;
203
204 protected abstract Response buildResponse(ProgressMonitor progressMonitor) throws IOException;
205
206 protected final void notifyConnect(ProgressMonitor progressMonitor) {
207 progressMonitor.beginTask(tr("Contacting Server..."), 1);
208 progressMonitor.indeterminateSubTask(null);
209 }
210
211 protected final void logRequest(String pattern, Object... args) {
212 if (debug) {
213 Logging.debug(pattern, args);
214 } else {
215 Logging.info(pattern, args);
216 }
217 }
218
219 protected final void logRequestBody() {
220 logRequest("{0} {1} ({2}) ...", requestMethod, url, Utils.getSizeString(requestBody.length, Locale.getDefault()));
221 if (Logging.isTraceEnabled() && hasRequestBody()) {
222 Logging.trace("BODY: {0}", new String(requestBody, StandardCharsets.UTF_8));
223 }
224 }
225
226 /**
227 * Returns the HTTP response which is set only after calling {@link #connect()}.
228 * Calling this method again, returns the identical object (unless another {@link #connect()} is performed).
229 *
230 * @return the HTTP response
231 * @since 9309
232 */
233 public final Response getResponse() {
234 return response;
235 }
236
237 /**
238 * A wrapper for the HTTP connection response.
239 * @since 15229
240 */
241 public interface ConnectionResponse {
242 /**
243 * Gets the HTTP version from the HTTP response.
244 * @return the HTTP version from the HTTP response
245 */
246 String getResponseVersion();
247
248 /**
249 * Gets the status code from an HTTP response message.
250 * For example, in the case of the following status lines:
251 * <PRE>
252 * HTTP/1.0 200 OK
253 * HTTP/1.0 401 Unauthorized
254 * </PRE>
255 * It will return 200 and 401 respectively.
256 * Returns -1 if no code can be discerned
257 * from the response (i.e., the response is not valid HTTP).
258 * @return the HTTP Status-Code, or -1
259 * @throws IOException if an error occurred connecting to the server.
260 */
261 int getResponseCode() throws IOException;
262
263 /**
264 * Returns the value of the {@code content-length} header field as a long.
265 *
266 * @return the content length of the resource that this connection's URL
267 * references, or {@code -1} if the content length is not known.
268 */
269 long getContentLengthLong();
270
271 /**
272 * Returns an unmodifiable Map of the header fields.
273 * The Map keys are Strings that represent the response-header field names.
274 * Each Map value is an unmodifiable List of Strings that represents
275 * the corresponding field values.
276 *
277 * @return a Map of header fields
278 */
279 Map<String, List<String>> getHeaderFields();
280
281 /**
282 * Returns the value of the named header field.
283 * @param name the name of a header field.
284 * @return the value of the named header field, or {@code null}
285 * if there is no such field in the header.
286 */
287 String getHeaderField(String name);
288 }
289
290 /**
291 * A wrapper for the HTTP response.
292 */
293 public abstract static class Response {
294 private final ProgressMonitor monitor;
295 private final int responseCode;
296 private final String responseMessage;
297 private boolean uncompress;
298 private boolean uncompressAccordingToContentDisposition;
299 private String responseData;
300
301 protected Response(ProgressMonitor monitor, int responseCode, String responseMessage) {
302 this.monitor = Objects.requireNonNull(monitor, "monitor");
303 this.responseCode = responseCode;
304 this.responseMessage = responseMessage;
305 }
306
307 protected final void debugRedirect() throws IOException {
308 if (responseCode >= 300) {
309 String contentType = getContentType();
310 if (contentType == null ||
311 contentType.contains("text") ||
312 contentType.contains("html") ||
313 contentType.contains("xml")
314 ) {
315 String content = fetchContent();
316 Logging.debug(content.isEmpty() ? "Server did not return any body" : "Response body: \n" + content);
317 } else {
318 Logging.debug("Server returned content: {0} of length: {1}. Not printing.", contentType, getContentLength());
319 }
320 }
321 }
322
323 /**
324 * Sets whether {@link #getContent()} should uncompress the input stream if necessary.
325 *
326 * @param uncompress whether the input stream should be uncompressed if necessary
327 * @return {@code this}
328 */
329 public final Response uncompress(boolean uncompress) {
330 this.uncompress = uncompress;
331 return this;
332 }
333
334 /**
335 * Sets whether {@link #getContent()} should uncompress the input stream according to {@code Content-Disposition}
336 * HTTP header.
337 * @param uncompressAccordingToContentDisposition whether the input stream should be uncompressed according to
338 * {@code Content-Disposition}
339 * @return {@code this}
340 * @since 9172
341 */
342 public final Response uncompressAccordingToContentDisposition(boolean uncompressAccordingToContentDisposition) {
343 this.uncompressAccordingToContentDisposition = uncompressAccordingToContentDisposition;
344 return this;
345 }
346
347 /**
348 * Returns the URL.
349 * @return the URL
350 * @see HttpURLConnection#getURL()
351 * @since 9172
352 */
353 public abstract URL getURL();
354
355 /**
356 * Returns the request method.
357 * @return the HTTP request method
358 * @see HttpURLConnection#getRequestMethod()
359 * @since 9172
360 */
361 public abstract String getRequestMethod();
362
363 /**
364 * Returns an input stream that reads from this HTTP connection, or,
365 * error stream if the connection failed but the server sent useful data.
366 * <p>
367 * Note: the return value can be null, if both the input and the error stream are null.
368 * Seems to be the case if the OSM server replies a 401 Unauthorized, see #3887
369 * @return input or error stream
370 * @throws IOException if any I/O error occurs
371 *
372 * @see HttpURLConnection#getInputStream()
373 * @see HttpURLConnection#getErrorStream()
374 */
375 @SuppressWarnings("resource")
376 public final InputStream getContent() throws IOException {
377 InputStream in = getInputStream();
378 in = new ProgressInputStream(in, getContentLength(), monitor);
379 in = "gzip".equalsIgnoreCase(getContentEncoding())
380 ? new GZIPInputStream(in)
381 : "deflate".equalsIgnoreCase(getContentEncoding())
382 ? new InflaterInputStream(in)
383 : in;
384 Compression compression = Compression.NONE;
385 if (uncompress) {
386 final String contentType = getContentType();
387 Logging.debug("Uncompressing input stream according to Content-Type header: {0}", contentType);
388 compression = Compression.forContentType(contentType);
389 }
390 if (uncompressAccordingToContentDisposition && Compression.NONE == compression) {
391 final String contentDisposition = getHeaderField("Content-Disposition");
392 final Matcher matcher = Pattern.compile("filename=\"([^\"]+)\"").matcher(
393 contentDisposition != null ? contentDisposition : "");
394 if (matcher.find()) {
395 Logging.debug("Uncompressing input stream according to Content-Disposition header: {0}", contentDisposition);
396 compression = Compression.byExtension(matcher.group(1));
397 }
398 }
399 in = compression.getUncompressedInputStream(in);
400 return in;
401 }
402
403 protected abstract InputStream getInputStream() throws IOException;
404
405 /**
406 * Returns {@link #getContent()} wrapped in a buffered reader.
407 *
408 * Detects Unicode charset in use utilizing {@link UTFInputStreamReader}.
409 * @return buffered reader
410 * @throws IOException if any I/O error occurs
411 */
412 public final BufferedReader getContentReader() throws IOException {
413 return new BufferedReader(
414 UTFInputStreamReader.create(getContent())
415 );
416 }
417
418 /**
419 * Fetches the HTTP response as String.
420 * @return the response
421 * @throws IOException if any I/O error occurs
422 */
423 public final synchronized String fetchContent() throws IOException {
424 if (responseData == null) {
425 try (Scanner scanner = new Scanner(getContentReader()).useDelimiter("\\A")) { // \A - beginning of input
426 responseData = scanner.hasNext() ? scanner.next() : "";
427 }
428 }
429 return responseData;
430 }
431
432 /**
433 * Gets the response code from this HTTP connection.
434 * @return HTTP response code
435 *
436 * @see HttpURLConnection#getResponseCode()
437 */
438 public final int getResponseCode() {
439 return responseCode;
440 }
441
442 /**
443 * Gets the response message from this HTTP connection.
444 * @return HTTP response message
445 *
446 * @see HttpURLConnection#getResponseMessage()
447 * @since 9172
448 */
449 public final String getResponseMessage() {
450 return responseMessage;
451 }
452
453 /**
454 * Returns the {@code Content-Encoding} header.
455 * @return {@code Content-Encoding} HTTP header
456 * @see HttpURLConnection#getContentEncoding()
457 */
458 public abstract String getContentEncoding();
459
460 /**
461 * Returns the {@code Content-Type} header.
462 * @return {@code Content-Type} HTTP header
463 * @see HttpURLConnection#getContentType()
464 */
465 public abstract String getContentType();
466
467 /**
468 * Returns the {@code Expire} header.
469 * @return {@code Expire} HTTP header
470 * @see HttpURLConnection#getExpiration()
471 * @since 9232
472 */
473 public abstract long getExpiration();
474
475 /**
476 * Returns the {@code Last-Modified} header.
477 * @return {@code Last-Modified} HTTP header
478 * @see HttpURLConnection#getLastModified()
479 * @since 9232
480 */
481 public abstract long getLastModified();
482
483 /**
484 * Returns the {@code Content-Length} header.
485 * @return {@code Content-Length} HTTP header
486 * @see HttpURLConnection#getContentLengthLong()
487 */
488 public abstract long getContentLength();
489
490 /**
491 * Returns the value of the named header field.
492 * @param name the name of a header field
493 * @return the value of the named header field, or {@code null} if there is no such field in the header
494 * @see HttpURLConnection#getHeaderField(String)
495 * @since 9172
496 */
497 public abstract String getHeaderField(String name);
498
499 /**
500 * Returns an unmodifiable Map mapping header keys to a List of header values.
501 * As per RFC 2616, section 4.2 header names are case insensitive, so returned map is also case insensitive
502 * @return unmodifiable Map mapping header keys to a List of header values
503 * @see HttpURLConnection#getHeaderFields()
504 * @since 9232
505 */
506 public abstract Map<String, List<String>> getHeaderFields();
507
508 /**
509 * @see HttpURLConnection#disconnect()
510 */
511 public abstract void disconnect();
512 }
513
514 /**
515 * Creates a new instance for the given URL and a {@code GET} request
516 *
517 * @param url the URL
518 * @return a new instance
519 */
520 public static HttpClient create(URL url) {
521 return create(url, "GET");
522 }
523
524 /**
525 * Creates a new instance for the given URL and a {@code GET} request
526 *
527 * @param url the URL
528 * @param requestMethod the HTTP request method to perform when calling
529 * @return a new instance
530 */
531 public static HttpClient create(URL url, String requestMethod) {
532 return factory.create(url, requestMethod);
533 }
534
535 /**
536 * Returns the URL set for this connection.
537 * @return the URL
538 * @see #create(URL)
539 * @see #create(URL, String)
540 * @since 9172
541 */
542 public final URL getURL() {
543 return url;
544 }
545
546 /**
547 * Returns the request body set for this connection.
548 * @return the HTTP request body, or null
549 * @since 15229
550 */
551 public final byte[] getRequestBody() {
552 return Utils.copyArray(requestBody);
553 }
554
555 /**
556 * Determines if a non-empty request body has been set for this connection.
557 * @return {@code true} if the request body is set and non-empty
558 * @since 15229
559 */
560 public final boolean hasRequestBody() {
561 return requestBody != null && requestBody.length > 0;
562 }
563
564 /**
565 * Determines if the underlying HTTP method requires a body.
566 * @return {@code true} if the underlying HTTP method requires a body
567 * @since 15229
568 */
569 public final boolean requiresBody() {
570 return "PUT".equals(requestMethod) || "POST".equals(requestMethod) || "DELETE".equals(requestMethod);
571 }
572
573 /**
574 * Returns the request method set for this connection.
575 * @return the HTTP request method
576 * @see #create(URL, String)
577 * @since 9172
578 */
579 public final String getRequestMethod() {
580 return requestMethod;
581 }
582
583 /**
584 * Returns the set value for the given {@code header}.
585 * @param header HTTP header name
586 * @return HTTP header value
587 * @since 9172
588 */
589 public final String getRequestHeader(String header) {
590 return headers.get(header);
591 }
592
593 /**
594 * Returns the connect timeout.
595 * @return the connect timeout, in milliseconds
596 * @since 15229
597 */
598 public final int getConnectTimeout() {
599 return connectTimeout;
600 }
601
602 /**
603 * Returns the read timeout.
604 * @return the read timeout, in milliseconds
605 * @since 15229
606 */
607 public final int getReadTimeout() {
608 return readTimeout;
609 }
610
611 /**
612 * Returns the {@code If-Modified-Since} header value.
613 * @return the {@code If-Modified-Since} header value
614 * @since 15229
615 */
616 public final long getIfModifiedSince() {
617 return ifModifiedSince;
618 }
619
620 /**
621 * Determines whether not to set header {@code Cache-Control=no-cache}.
622 * By default, {@code useCache} is true, i.e., the header {@code Cache-Control=no-cache} is not sent.
623 *
624 * @return whether not to set header {@code Cache-Control=no-cache}
625 * @since 15229
626 */
627 public final boolean isUseCache() {
628 return useCache;
629 }
630
631 /**
632 * Returns the headers.
633 * @return the headers
634 * @since 15229
635 */
636 public final Map<String, String> getHeaders() {
637 return headers;
638 }
639
640 /**
641 * Returns the reason for request.
642 * @return the reason for request
643 * @since 15229
644 */
645 public final String getReasonForRequest() {
646 return reasonForRequest;
647 }
648
649 /**
650 * Returns the output message.
651 * @return the output message
652 */
653 protected final String getOutputMessage() {
654 return outputMessage;
655 }
656
657 /**
658 * Determines whether the progress monitor task will be finished when the output stream is closed. {@code true} by default.
659 * @return the finishOnCloseOutput
660 */
661 protected final boolean isFinishOnCloseOutput() {
662 return finishOnCloseOutput;
663 }
664
665 /**
666 * Sets whether not to set header {@code Cache-Control=no-cache}.
667 * By default, {@code useCache} is true, i.e., the header {@code Cache-Control=no-cache} is not sent.
668 *
669 * @param useCache whether not to set header {@code Cache-Control=no-cache}
670 * @return {@code this}
671 * @see HttpURLConnection#setUseCaches(boolean)
672 */
673 public final HttpClient useCache(boolean useCache) {
674 this.useCache = useCache;
675 return this;
676 }
677
678 /**
679 * Sets whether not to set header {@code Connection=close}
680 * <p>
681 * This might fix #7640, see
682 * <a href='https://web.archive.org/web/20140118201501/http://www.tikalk.com/java/forums/httpurlconnection-disable-keep-alive'>here</a>.
683 *
684 * @param keepAlive whether not to set header {@code Connection=close}
685 * @return {@code this}
686 */
687 public final HttpClient keepAlive(boolean keepAlive) {
688 return setHeader("Connection", keepAlive ? null : "close");
689 }
690
691 /**
692 * Sets a specified timeout value, in milliseconds, to be used when opening a communications link to the resource referenced
693 * by this URLConnection. If the timeout expires before the connection can be established, a
694 * {@link java.net.SocketTimeoutException} is raised. A timeout of zero is interpreted as an infinite timeout.
695 * @param connectTimeout an {@code int} that specifies the connect timeout value in milliseconds
696 * @return {@code this}
697 * @see HttpURLConnection#setConnectTimeout(int)
698 */
699 public final HttpClient setConnectTimeout(int connectTimeout) {
700 this.connectTimeout = connectTimeout;
701 return this;
702 }
703
704 /**
705 * Sets the read timeout to a specified timeout, in milliseconds. A non-zero value specifies the timeout when reading from
706 * input stream when a connection is established to a resource. If the timeout expires before there is data available for
707 * read, a {@link java.net.SocketTimeoutException} is raised. A timeout of zero is interpreted as an infinite timeout.
708 * @param readTimeout an {@code int} that specifies the read timeout value in milliseconds
709 * @return {@code this}
710 * @see HttpURLConnection#setReadTimeout(int)
711 */
712 public final HttpClient setReadTimeout(int readTimeout) {
713 this.readTimeout = readTimeout;
714 return this;
715 }
716
717 /**
718 * Sets the {@code Accept} header.
719 * @param accept header value
720 *
721 * @return {@code this}
722 */
723 public final HttpClient setAccept(String accept) {
724 return setHeader("Accept", accept);
725 }
726
727 /**
728 * Sets the request body for {@code PUT}/{@code POST} requests.
729 * @param requestBody request body
730 *
731 * @return {@code this}
732 */
733 public final HttpClient setRequestBody(byte[] requestBody) {
734 this.requestBody = Utils.copyArray(requestBody);
735 return this;
736 }
737
738 /**
739 * Sets the {@code If-Modified-Since} header.
740 * @param ifModifiedSince header value
741 *
742 * @return {@code this}
743 */
744 public final HttpClient setIfModifiedSince(long ifModifiedSince) {
745 this.ifModifiedSince = ifModifiedSince;
746 return this;
747 }
748
749 /**
750 * Sets the maximum number of redirections to follow.
751 *
752 * Set {@code maxRedirects} to {@code -1} in order to ignore redirects, i.e.,
753 * to not throw an {@link IOException} in {@link #connect()}.
754 * @param maxRedirects header value
755 *
756 * @return {@code this}
757 */
758 public final HttpClient setMaxRedirects(int maxRedirects) {
759 this.maxRedirects = maxRedirects;
760 return this;
761 }
762
763 /**
764 * Sets an arbitrary HTTP header.
765 * @param key header name
766 * @param value header value
767 *
768 * @return {@code this}
769 */
770 public final HttpClient setHeader(String key, String value) {
771 this.headers.put(key, value);
772 return this;
773 }
774
775 /**
776 * Sets arbitrary HTTP headers.
777 * @param headers HTTP headers
778 *
779 * @return {@code this}
780 */
781 public final HttpClient setHeaders(Map<String, String> headers) {
782 this.headers.putAll(headers);
783 return this;
784 }
785
786 /**
787 * Sets a reason to show on console. Can be {@code null} if no reason is given.
788 * @param reasonForRequest Reason to show
789 * @return {@code this}
790 * @since 9172
791 */
792 public final HttpClient setReasonForRequest(String reasonForRequest) {
793 this.reasonForRequest = reasonForRequest;
794 return this;
795 }
796
797 /**
798 * Sets the output message to be displayed in progress monitor for {@code PUT}, {@code POST} and {@code DELETE} methods.
799 * Defaults to "Uploading data ..." (translated). Has no effect for {@code GET} or any other method.
800 * @param outputMessage message to be displayed in progress monitor
801 * @return {@code this}
802 * @since 12711
803 */
804 public final HttpClient setOutputMessage(String outputMessage) {
805 this.outputMessage = outputMessage;
806 return this;
807 }
808
809 /**
810 * Sets whether the progress monitor task will be finished when the output stream is closed. This is {@code true} by default.
811 * @param finishOnCloseOutput whether the progress monitor task will be finished when the output stream is closed
812 * @return {@code this}
813 * @since 10302
814 */
815 public final HttpClient setFinishOnCloseOutput(boolean finishOnCloseOutput) {
816 this.finishOnCloseOutput = finishOnCloseOutput;
817 return this;
818 }
819
820 /**
821 * Sets the connect log at DEBUG level instead of the default INFO level.
822 * @param debug {@code true} to set the connect log at DEBUG level
823 * @return {@code this}
824 * @since 15389
825 */
826 public final HttpClient setLogAtDebug(boolean debug) {
827 this.debug = debug;
828 return this;
829 }
830
831 /**
832 * Determines if the given status code is an HTTP redirection.
833 * @param statusCode HTTP status code
834 * @return {@code true} if the given status code is an HTTP redirection
835 * @since 15423
836 */
837 public static boolean isRedirect(final int statusCode) {
838 switch (statusCode) {
839 case HttpURLConnection.HTTP_MOVED_PERM: // 301
840 case HttpURLConnection.HTTP_MOVED_TEMP: // 302
841 case HttpURLConnection.HTTP_SEE_OTHER: // 303
842 case 307: // TEMPORARY_REDIRECT:
843 case 308: // PERMANENT_REDIRECT:
844 return true;
845 default:
846 return false;
847 }
848 }
849
850 /**
851 * Disconnect client.
852 * @see HttpURLConnection#disconnect()
853 * @since 9309
854 */
855 public abstract void disconnect();
856
857 /**
858 * Returns a {@link Matcher} against predefined Tomcat error messages.
859 * If it matches, error message can be extracted from {@code group(1)}.
860 * @param data HTML contents to check
861 * @return a {@link Matcher} against predefined Tomcat error messages
862 * @since 13358
863 */
864 public static Matcher getTomcatErrorMatcher(String data) {
865 return data != null ? TOMCAT_ERR_MESSAGE.matcher(data) : null;
866 }
867}
Note: See TracBrowser for help on using the repository browser.