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

Last change on this file since 14017 was 13647, checked in by Don-vip, 6 years ago

see #16204 - Allow to start and close JOSM in WebStart sandbox mode (where every external access is denied). This was very useful to reproduce some very tricky bugs that occured in real life but were almost impossible to diagnose.

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