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