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}