1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit;
16
17 import java.io.ByteArrayInputStream;
18 import java.io.ByteArrayOutputStream;
19 import java.io.EOFException;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.OutputStream;
24 import java.net.InetAddress;
25 import java.net.URI;
26 import java.net.URISyntaxException;
27 import java.net.URL;
28 import java.nio.charset.Charset;
29 import java.nio.file.Files;
30 import java.util.ArrayList;
31 import java.util.HashMap;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.WeakHashMap;
35 import java.util.concurrent.TimeUnit;
36
37 import javax.net.ssl.HostnameVerifier;
38 import javax.net.ssl.SSLContext;
39 import javax.net.ssl.SSLPeerUnverifiedException;
40 import javax.net.ssl.SSLSocketFactory;
41
42 import org.apache.commons.io.IOUtils;
43 import org.apache.commons.lang3.StringUtils;
44 import org.apache.commons.lang3.reflect.FieldUtils;
45 import org.apache.commons.logging.Log;
46 import org.apache.commons.logging.LogFactory;
47 import org.apache.http.ConnectionClosedException;
48 import org.apache.http.Header;
49 import org.apache.http.HttpEntity;
50 import org.apache.http.HttpEntityEnclosingRequest;
51 import org.apache.http.HttpException;
52 import org.apache.http.HttpHost;
53 import org.apache.http.HttpRequest;
54 import org.apache.http.HttpRequestInterceptor;
55 import org.apache.http.HttpResponse;
56 import org.apache.http.auth.AuthScheme;
57 import org.apache.http.auth.AuthScope;
58 import org.apache.http.auth.Credentials;
59 import org.apache.http.client.AuthCache;
60 import org.apache.http.client.CredentialsProvider;
61 import org.apache.http.client.config.RequestConfig;
62 import org.apache.http.client.methods.CloseableHttpResponse;
63 import org.apache.http.client.methods.HttpGet;
64 import org.apache.http.client.methods.HttpHead;
65 import org.apache.http.client.methods.HttpPatch;
66 import org.apache.http.client.methods.HttpPost;
67 import org.apache.http.client.methods.HttpPut;
68 import org.apache.http.client.methods.HttpRequestBase;
69 import org.apache.http.client.methods.HttpTrace;
70 import org.apache.http.client.methods.HttpUriRequest;
71 import org.apache.http.client.protocol.HttpClientContext;
72 import org.apache.http.client.protocol.RequestAcceptEncoding;
73 import org.apache.http.client.protocol.RequestAddCookies;
74 import org.apache.http.client.protocol.RequestAuthCache;
75 import org.apache.http.client.protocol.RequestDefaultHeaders;
76 import org.apache.http.client.protocol.RequestExpectContinue;
77 import org.apache.http.client.protocol.ResponseProcessCookies;
78 import org.apache.http.client.utils.URLEncodedUtils;
79 import org.apache.http.config.ConnectionConfig;
80 import org.apache.http.config.RegistryBuilder;
81 import org.apache.http.config.SocketConfig;
82 import org.apache.http.conn.DnsResolver;
83 import org.apache.http.conn.routing.RouteInfo;
84 import org.apache.http.conn.socket.ConnectionSocketFactory;
85 import org.apache.http.conn.socket.LayeredConnectionSocketFactory;
86 import org.apache.http.conn.ssl.DefaultHostnameVerifier;
87 import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
88 import org.apache.http.conn.util.PublicSuffixMatcher;
89 import org.apache.http.conn.util.PublicSuffixMatcherLoader;
90 import org.apache.http.cookie.CookieSpecProvider;
91 import org.apache.http.entity.ContentType;
92 import org.apache.http.entity.StringEntity;
93 import org.apache.http.entity.mime.MultipartEntityBuilder;
94 import org.apache.http.entity.mime.content.InputStreamBody;
95 import org.apache.http.impl.client.BasicAuthCache;
96 import org.apache.http.impl.client.CloseableHttpClient;
97 import org.apache.http.impl.client.HttpClientBuilder;
98 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
99 import org.apache.http.protocol.HttpContext;
100 import org.apache.http.protocol.HttpProcessorBuilder;
101 import org.apache.http.protocol.RequestContent;
102 import org.apache.http.protocol.RequestTargetHost;
103 import org.apache.http.ssl.SSLContexts;
104 import org.apache.http.util.TextUtils;
105 import org.htmlunit.WebRequest.HttpHint;
106 import org.htmlunit.http.HttpUtils;
107 import org.htmlunit.httpclient.HtmlUnitCookieSpecProvider;
108 import org.htmlunit.httpclient.HtmlUnitCookieStore;
109 import org.htmlunit.httpclient.HtmlUnitRedirectStrategie;
110 import org.htmlunit.httpclient.HtmlUnitSSLConnectionSocketFactory;
111 import org.htmlunit.httpclient.SocksConnectionSocketFactory;
112 import org.htmlunit.util.KeyDataPair;
113 import org.htmlunit.util.MimeType;
114 import org.htmlunit.util.NameValuePair;
115 import org.htmlunit.util.UrlUtils;
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133 public class HttpWebConnection implements WebConnection {
134
135 private static final Log LOG = LogFactory.getLog(HttpWebConnection.class);
136
137 private static final String HACKED_COOKIE_POLICY = "mine";
138
139
140
141 private final Map<Thread, HttpClientBuilder> httpClientBuilder_ = new WeakHashMap<>();
142 private final WebClient webClient_;
143
144 private String virtualHost_;
145 private final HtmlUnitCookieSpecProvider htmlUnitCookieSpecProvider_;
146 private final WebClientOptions usedOptions_;
147 private PoolingHttpClientConnectionManager connectionManager_;
148
149
150 private final AuthCache sharedAuthCache_ = new SynchronizedAuthCache();
151
152
153 private final Map<Thread, HttpClientContext> httpClientContextByThread_ = new WeakHashMap<>();
154
155
156
157
158
159 public HttpWebConnection(final WebClient webClient) {
160 super();
161 webClient_ = webClient;
162 htmlUnitCookieSpecProvider_ = new HtmlUnitCookieSpecProvider(webClient.getBrowserVersion());
163 usedOptions_ = new WebClientOptions();
164 }
165
166
167
168
169 @Override
170 public WebResponse getResponse(final WebRequest webRequest) throws IOException {
171 final HttpClientBuilder builder = reconfigureHttpClientIfNeeded(getHttpClientBuilder(), webRequest);
172
173 HttpUriRequest httpMethod = null;
174 try {
175 try {
176 httpMethod = makeHttpMethod(webRequest, builder);
177 }
178 catch (final URISyntaxException e) {
179 throw new IOException("Unable to create URI from URL: " + webRequest.getUrl().toExternalForm()
180 + " (reason: " + e.getMessage() + ")", e);
181 }
182
183 final URL url = webRequest.getUrl();
184 final HttpHost httpHost = new HttpHost(url.getHost(), url.getPort(), url.getProtocol());
185 final long startTime = System.currentTimeMillis();
186
187 final HttpContext httpContext = getHttpContext();
188 try {
189 try (CloseableHttpClient closeableHttpClient = builder.build()) {
190 try (CloseableHttpResponse httpResponse =
191 closeableHttpClient.execute(httpHost, httpMethod, httpContext)) {
192 return downloadResponse(httpMethod, webRequest, httpResponse, startTime);
193 }
194 }
195 }
196 catch (final SSLPeerUnverifiedException ex) {
197
198 if (webClient_.getOptions().isUseInsecureSSL()) {
199 HtmlUnitSSLConnectionSocketFactory.setUseSSL3Only(httpContext, true);
200 try (CloseableHttpClient closeableHttpClient = builder.build()) {
201 try (CloseableHttpResponse httpResponse =
202 closeableHttpClient.execute(httpHost, httpMethod, httpContext)) {
203 return downloadResponse(httpMethod, webRequest, httpResponse, startTime);
204 }
205 }
206 }
207 throw ex;
208 }
209 catch (final Error e) {
210
211
212
213
214 synchronized (httpClientBuilder_) {
215 httpClientBuilder_.remove(Thread.currentThread());
216 }
217 throw e;
218 }
219 }
220 finally {
221 if (httpMethod != null) {
222 onResponseGenerated(httpMethod);
223 }
224 }
225 }
226
227
228
229
230
231
232 protected void onResponseGenerated(final HttpUriRequest httpMethod) {
233
234 }
235
236
237
238
239 private synchronized HttpContext getHttpContext() {
240 HttpClientContext httpClientContext = httpClientContextByThread_.get(Thread.currentThread());
241 if (httpClientContext == null) {
242 httpClientContext = new HttpClientContext();
243
244
245 httpClientContext.setAttribute(HttpClientContext.AUTH_CACHE, sharedAuthCache_);
246
247 httpClientContextByThread_.put(Thread.currentThread(), httpClientContext);
248 }
249 return httpClientContext;
250 }
251
252 private void setProxy(final HttpRequestBase httpRequest, final WebRequest webRequest) {
253 final InetAddress localAddress = webClient_.getOptions().getLocalAddress();
254 final RequestConfig.Builder requestBuilder = createRequestConfigBuilder(getTimeout(webRequest), localAddress);
255
256 if (webRequest.getProxyHost() == null) {
257 requestBuilder.setProxy(null);
258 httpRequest.setConfig(requestBuilder.build());
259 return;
260 }
261
262 final HttpHost proxy = new HttpHost(webRequest.getProxyHost(),
263 webRequest.getProxyPort(), webRequest.getProxyScheme());
264 if (webRequest.isSocksProxy()) {
265 SocksConnectionSocketFactory.setSocksProxy(getHttpContext(), proxy);
266 }
267 else {
268 requestBuilder.setProxy(proxy);
269 httpRequest.setConfig(requestBuilder.build());
270 }
271 }
272
273
274
275
276
277
278
279
280 private HttpUriRequest makeHttpMethod(final WebRequest webRequest, final HttpClientBuilder httpClientBuilder)
281 throws URISyntaxException {
282
283 final HttpContext httpContext = getHttpContext();
284 final Charset charset = webRequest.getCharset();
285
286
287
288
289 final URL url = UrlUtils.encodeUrl(webRequest.getUrl(), charset);
290
291 URI uri = UrlUtils.toURI(url, escapeQuery(url.getQuery()));
292 if (getVirtualHost() != null) {
293 uri = URI.create(getVirtualHost());
294 }
295 final HttpRequestBase httpMethod = buildHttpMethod(webRequest.getHttpMethod(), uri);
296 setProxy(httpMethod, webRequest);
297
298
299
300
301
302 if (httpMethod instanceof HttpPost
303 || httpMethod instanceof HttpPut
304 || httpMethod instanceof HttpPatch
305 || httpMethod instanceof org.htmlunit.httpclient.HttpDelete
306 || httpMethod instanceof org.htmlunit.httpclient.HttpOptions) {
307
308 final HttpEntityEnclosingRequest method = (HttpEntityEnclosingRequest) httpMethod;
309
310 if (FormEncodingType.URL_ENCODED == webRequest.getEncodingType()) {
311 if (webRequest.getRequestBody() == null) {
312 final List<NameValuePair> pairs = webRequest.getRequestParameters();
313 final String query = HttpUtils.toQueryFormFields(pairs, charset);
314
315 final StringEntity urlEncodedEntity;
316 if (webRequest.hasHint(HttpHint.IncludeCharsetInContentTypeHeader)) {
317 urlEncodedEntity = new StringEntity(query,
318 ContentType.create(URLEncodedUtils.CONTENT_TYPE, charset));
319
320 }
321 else {
322 urlEncodedEntity = new StringEntity(query, charset);
323 urlEncodedEntity.setContentType(URLEncodedUtils.CONTENT_TYPE);
324 }
325 method.setEntity(urlEncodedEntity);
326 }
327 else {
328 final String body = StringUtils.defaultString(webRequest.getRequestBody());
329 final StringEntity urlEncodedEntity = new StringEntity(body, charset);
330 urlEncodedEntity.setContentType(URLEncodedUtils.CONTENT_TYPE);
331 method.setEntity(urlEncodedEntity);
332 }
333 }
334 else if (FormEncodingType.TEXT_PLAIN == webRequest.getEncodingType()) {
335 if (webRequest.getRequestBody() == null) {
336 final StringBuilder body = new StringBuilder();
337 for (final NameValuePair pair : webRequest.getRequestParameters()) {
338 body.append(StringUtils.remove(StringUtils.remove(pair.getName(), '\r'), '\n'))
339 .append('=')
340 .append(StringUtils.remove(StringUtils.remove(pair.getValue(), '\r'), '\n'))
341 .append("\r\n");
342 }
343 final StringEntity bodyEntity = new StringEntity(body.toString(), charset);
344 bodyEntity.setContentType(MimeType.TEXT_PLAIN);
345 method.setEntity(bodyEntity);
346 }
347 else {
348 final String body = StringUtils.defaultString(webRequest.getRequestBody());
349 final StringEntity bodyEntity =
350 new StringEntity(body, ContentType.create(MimeType.TEXT_PLAIN, charset));
351 method.setEntity(bodyEntity);
352 }
353 }
354 else if (FormEncodingType.MULTIPART == webRequest.getEncodingType()) {
355 final Charset c = getCharset(charset, webRequest.getRequestParameters());
356 final MultipartEntityBuilder builder = MultipartEntityBuilder.create().setLaxMode();
357 builder.setCharset(c);
358
359 for (final NameValuePair pair : webRequest.getRequestParameters()) {
360 if (pair instanceof KeyDataPair dataPair) {
361 buildFilePart(dataPair, builder);
362 }
363 else {
364 builder.addTextBody(pair.getName(), pair.getValue(),
365 ContentType.create(MimeType.TEXT_PLAIN, charset));
366 }
367 }
368 method.setEntity(builder.build());
369 }
370 else {
371
372 final String body = webRequest.getRequestBody();
373 if (body != null) {
374 method.setEntity(new StringEntity(body, charset));
375 }
376 }
377 }
378 else {
379
380 final List<NameValuePair> pairs = webRequest.getRequestParameters();
381 if (!pairs.isEmpty()) {
382 final String query = HttpUtils.toQueryFormFields(pairs, charset);
383 uri = UrlUtils.toURI(url, query);
384 httpMethod.setURI(uri);
385 }
386 }
387
388 configureHttpProcessorBuilder(httpClientBuilder, webRequest);
389
390
391
392 final CredentialsProvider credentialsProvider = webClient_.getCredentialsProvider();
393
394
395 final Credentials requestUrlCredentials = webRequest.getUrlCredentials();
396 if (null != requestUrlCredentials) {
397 final URL requestUrl = webRequest.getUrl();
398 final AuthScope authScope = new AuthScope(requestUrl.getHost(), requestUrl.getPort());
399
400 credentialsProvider.setCredentials(authScope, requestUrlCredentials);
401 }
402
403
404 final Credentials requestCredentials = webRequest.getCredentials();
405 if (null != requestCredentials) {
406 final URL requestUrl = webRequest.getUrl();
407 final AuthScope authScope = new AuthScope(requestUrl.getHost(), requestUrl.getPort());
408
409 credentialsProvider.setCredentials(authScope, requestCredentials);
410 }
411 httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider);
412 httpContext.removeAttribute(HttpClientContext.CREDS_PROVIDER);
413 httpContext.removeAttribute(HttpClientContext.TARGET_AUTH_STATE);
414 return httpMethod;
415 }
416
417 private static String escapeQuery(final String query) {
418 if (query == null) {
419 return null;
420 }
421 return query.replace("%%", "%25%25");
422 }
423
424 private static Charset getCharset(final Charset charset, final List<NameValuePair> pairs) {
425 for (final NameValuePair pair : pairs) {
426 if (pair instanceof KeyDataPair pairWithFile) {
427 if (pairWithFile.getData() == null && pairWithFile.getFile() != null) {
428 final String fileName = pairWithFile.getFile().getName();
429 final int length = fileName.length();
430 for (int i = 0; i < length; i++) {
431 if (fileName.codePointAt(i) > 127) {
432 return charset;
433 }
434 }
435 }
436 }
437 }
438 return null;
439 }
440
441 void buildFilePart(final KeyDataPair pairWithFile, final MultipartEntityBuilder builder) {
442 String mimeType = pairWithFile.getMimeType();
443 if (mimeType == null) {
444 mimeType = MimeType.APPLICATION_OCTET_STREAM;
445 }
446
447 final ContentType contentType = ContentType.create(mimeType);
448
449 final File file = pairWithFile.getFile();
450 if (file != null) {
451 String filename = pairWithFile.getFileName();
452 if (filename == null) {
453 filename = pairWithFile.getFile().getName();
454 }
455 builder.addBinaryBody(pairWithFile.getName(), file, contentType, filename);
456 return;
457 }
458
459 final byte[] data = pairWithFile.getData();
460 if (data != null) {
461 String filename = pairWithFile.getFileName();
462 if (filename == null) {
463 filename = pairWithFile.getValue();
464 }
465
466 builder.addBinaryBody(pairWithFile.getName(), new ByteArrayInputStream(data),
467 contentType, filename);
468 return;
469 }
470
471 builder.addPart(pairWithFile.getName(),
472
473 new InputStreamBody(new ByteArrayInputStream(new byte[0]), contentType, pairWithFile.getValue()) {
474 @Override
475 public long getContentLength() {
476 return 0;
477 }
478 });
479 }
480
481
482
483
484
485
486
487 private static HttpRequestBase buildHttpMethod(final HttpMethod submitMethod, final URI uri) {
488 final HttpRequestBase method = switch (submitMethod) {
489 case GET -> new HttpGet(uri);
490 case POST -> new HttpPost(uri);
491 case PUT -> new HttpPut(uri);
492 case DELETE -> new org.htmlunit.httpclient.HttpDelete(uri);
493 case OPTIONS -> new org.htmlunit.httpclient.HttpOptions(uri);
494 case HEAD -> new HttpHead(uri);
495 case TRACE -> new HttpTrace(uri);
496 case PATCH -> new HttpPatch(uri);
497 };
498 return method;
499 }
500
501
502
503
504
505
506 protected HttpClientBuilder getHttpClientBuilder() {
507 final Thread currentThread = Thread.currentThread();
508
509 synchronized (httpClientBuilder_) {
510 HttpClientBuilder builder = httpClientBuilder_.get(currentThread);
511 if (builder == null) {
512 builder = createHttpClientBuilder();
513
514
515
516 final RegistryBuilder<CookieSpecProvider> registeryBuilder
517 = RegistryBuilder.<CookieSpecProvider>create()
518 .register(HACKED_COOKIE_POLICY, htmlUnitCookieSpecProvider_);
519 builder.setDefaultCookieSpecRegistry(registeryBuilder.build());
520
521 builder.setDefaultCookieStore(new HtmlUnitCookieStore(webClient_.getCookieManager()));
522 builder.setUserAgent(webClient_.getBrowserVersion().getUserAgent());
523 httpClientBuilder_.put(currentThread, builder);
524 }
525
526 return builder;
527 }
528 }
529
530
531
532
533
534
535
536
537 protected int getTimeout(final WebRequest webRequest) {
538 if (webRequest == null || webRequest.getTimeout() < 0) {
539 return webClient_.getOptions().getTimeout();
540 }
541
542 return webRequest.getTimeout();
543 }
544
545
546
547
548
549
550
551
552
553 protected HttpClientBuilder createHttpClientBuilder() {
554 final HttpClientBuilder builder = HttpClientBuilder.create();
555 builder.setRedirectStrategy(new HtmlUnitRedirectStrategie());
556 configureTimeout(builder, getTimeout(null));
557 configureHttpsScheme(builder);
558 builder.setMaxConnPerRoute(6);
559
560 builder.setConnectionManagerShared(true);
561 return builder;
562 }
563
564 private void configureTimeout(final HttpClientBuilder builder, final int timeout) {
565 final InetAddress localAddress = webClient_.getOptions().getLocalAddress();
566 final RequestConfig.Builder requestBuilder = createRequestConfigBuilder(timeout, localAddress);
567 builder.setDefaultRequestConfig(requestBuilder.build());
568
569 builder.setDefaultSocketConfig(createSocketConfigBuilder(timeout).build());
570
571 getHttpContext().removeAttribute(HttpClientContext.REQUEST_CONFIG);
572 usedOptions_.setTimeout(timeout);
573 }
574
575 private static RequestConfig.Builder createRequestConfigBuilder(final int timeout, final InetAddress localAddress) {
576 return RequestConfig.custom()
577 .setCookieSpec(HACKED_COOKIE_POLICY)
578 .setRedirectsEnabled(false)
579 .setLocalAddress(localAddress)
580
581
582 .setConnectTimeout(timeout)
583 .setConnectionRequestTimeout(timeout)
584 .setSocketTimeout(timeout);
585 }
586
587 private static SocketConfig.Builder createSocketConfigBuilder(final int timeout) {
588 return SocketConfig.custom()
589
590 .setSoTimeout(timeout);
591 }
592
593
594
595
596
597 private HttpClientBuilder reconfigureHttpClientIfNeeded(final HttpClientBuilder httpClientBuilder,
598 final WebRequest webRequest) {
599 final WebClientOptions options = webClient_.getOptions();
600
601
602 if (options.isUseInsecureSSL() != usedOptions_.isUseInsecureSSL()
603 || options.getSSLClientCertificateStore() != usedOptions_.getSSLClientCertificateStore()
604 || options.getSSLTrustStore() != usedOptions_.getSSLTrustStore()
605 || options.getSSLClientCipherSuites() != usedOptions_.getSSLClientCipherSuites()
606 || options.getSSLClientProtocols() != usedOptions_.getSSLClientProtocols()
607 || options.getProxyConfig() != usedOptions_.getProxyConfig()) {
608 configureHttpsScheme(httpClientBuilder);
609
610 if (connectionManager_ != null) {
611 connectionManager_.shutdown();
612 connectionManager_ = null;
613 }
614 }
615
616 final int timeout = getTimeout(webRequest);
617 if (timeout != usedOptions_.getTimeout()) {
618 configureTimeout(httpClientBuilder, timeout);
619 }
620
621 final long connectionTimeToLive = webClient_.getOptions().getConnectionTimeToLive();
622 if (connectionTimeToLive != usedOptions_.getConnectionTimeToLive()) {
623 httpClientBuilder.setConnectionTimeToLive(connectionTimeToLive, TimeUnit.MILLISECONDS);
624 usedOptions_.setConnectionTimeToLive(connectionTimeToLive);
625 }
626
627 if (connectionManager_ == null) {
628 connectionManager_ = createConnectionManager(httpClientBuilder);
629 }
630 httpClientBuilder.setConnectionManager(connectionManager_);
631
632 return httpClientBuilder;
633 }
634
635 private void configureHttpsScheme(final HttpClientBuilder builder) {
636 final WebClientOptions options = webClient_.getOptions();
637
638 final SSLConnectionSocketFactory socketFactory =
639 HtmlUnitSSLConnectionSocketFactory.buildSSLSocketFactory(options);
640
641 builder.setSSLSocketFactory(socketFactory);
642
643 usedOptions_.setUseInsecureSSL(options.isUseInsecureSSL());
644 usedOptions_.setSSLClientCertificateKeyStore(options.getSSLClientCertificateStore(),
645 options.getSSLClientCertificatePassword());
646 usedOptions_.setSSLTrustStore(options.getSSLTrustStore());
647 usedOptions_.setSSLClientCipherSuites(options.getSSLClientCipherSuites());
648 usedOptions_.setSSLClientProtocols(options.getSSLClientProtocols());
649 usedOptions_.setProxyConfig(options.getProxyConfig());
650 }
651
652 private void configureHttpProcessorBuilder(final HttpClientBuilder builder, final WebRequest webRequest) {
653 final HttpProcessorBuilder b = HttpProcessorBuilder.create();
654 for (final HttpRequestInterceptor i : getHttpRequestInterceptors(webRequest)) {
655 b.add(i);
656 }
657
658
659
660 b.addAll(new RequestDefaultHeaders(null),
661 new RequestContent(),
662 new RequestTargetHost(),
663 new RequestExpectContinue());
664 b.add(new RequestAcceptEncoding());
665 b.add(new RequestAuthCache());
666
667 if (!webRequest.hasHint(HttpHint.BlockCookies)) {
668 b.add(new ResponseProcessCookies());
669 }
670 builder.setHttpProcessor(b.build());
671 }
672
673
674
675
676
677 public void setVirtualHost(final String virtualHost) {
678 virtualHost_ = virtualHost;
679 }
680
681
682
683
684
685 public String getVirtualHost() {
686 return virtualHost_;
687 }
688
689
690
691
692
693
694
695
696
697 protected WebResponse makeWebResponse(final HttpResponse httpResponse,
698 final WebRequest webRequest, final DownloadedContent responseBody, final long loadTime) {
699
700 String statusMessage = httpResponse.getStatusLine().getReasonPhrase();
701 if (statusMessage == null) {
702 statusMessage = "Unknown status message";
703 }
704 final int statusCode = httpResponse.getStatusLine().getStatusCode();
705 final List<NameValuePair> headers = new ArrayList<>();
706 for (final Header header : httpResponse.getAllHeaders()) {
707 headers.add(new NameValuePair(header.getName(), header.getValue()));
708 }
709 final WebResponseData responseData = new WebResponseData(responseBody, statusCode, statusMessage, headers);
710 return newWebResponseInstance(responseData, loadTime, webRequest);
711 }
712
713
714
715
716
717
718
719
720
721
722
723 protected WebResponse downloadResponse(final HttpUriRequest httpMethod,
724 final WebRequest webRequest, final HttpResponse httpResponse,
725 final long startTime) throws IOException {
726
727 final DownloadedContent downloadedBody = downloadResponseBody(httpResponse);
728 final long endTime = System.currentTimeMillis();
729
730 return makeWebResponse(httpResponse, webRequest, downloadedBody, endTime - startTime);
731 }
732
733
734
735
736
737
738
739 protected DownloadedContent downloadResponseBody(final HttpResponse httpResponse) throws IOException {
740 final HttpEntity httpEntity = httpResponse.getEntity();
741 if (httpEntity == null) {
742 return new DownloadedContent.InMemory(null);
743 }
744
745 try (InputStream is = httpEntity.getContent()) {
746 return downloadContent(is, webClient_.getOptions().getMaxInMemory(),
747 webClient_.getOptions().getTempFileDirectory());
748 }
749 }
750
751
752
753
754
755
756
757
758
759 public static DownloadedContent downloadContent(final InputStream is, final int maxInMemory,
760 final File tempFileDirectory) throws IOException {
761 if (is == null) {
762 return new DownloadedContent.InMemory(null);
763 }
764
765 try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
766 final byte[] buffer = new byte[1024];
767 int nbRead;
768 try {
769 while ((nbRead = is.read(buffer)) != -1) {
770 bos.write(buffer, 0, nbRead);
771 if (maxInMemory > 0 && bos.size() > maxInMemory) {
772
773 final File file = File.createTempFile("htmlunit", ".tmp", tempFileDirectory);
774 file.deleteOnExit();
775 try (OutputStream fos = Files.newOutputStream(file.toPath())) {
776 bos.writeTo(fos);
777 IOUtils.copyLarge(is, fos);
778 }
779 return new DownloadedContent.OnFile(file, true);
780 }
781 }
782 }
783 catch (final ConnectionClosedException e) {
784 LOG.warn("Connection was closed while reading from stream.", e);
785 return new DownloadedContent.InMemory(bos.toByteArray());
786 }
787 catch (final EOFException e) {
788
789 LOG.warn("EOFException while reading from stream.", e);
790 return new DownloadedContent.InMemory(bos.toByteArray());
791 }
792
793 return new DownloadedContent.InMemory(bos.toByteArray());
794 }
795 }
796
797
798
799
800
801
802
803
804
805 protected WebResponse newWebResponseInstance(
806 final WebResponseData responseData,
807 final long loadTime,
808 final WebRequest webRequest) {
809 return new WebResponse(responseData, webRequest, loadTime);
810 }
811
812 private List<HttpRequestInterceptor> getHttpRequestInterceptors(final WebRequest webRequest) {
813 final List<HttpRequestInterceptor> list = new ArrayList<>();
814 final Map<String, String> requestHeaders = webRequest.getAdditionalHeaders();
815 final URL url = webRequest.getUrl();
816 final StringBuilder host = new StringBuilder(url.getHost());
817
818 final int port = url.getPort();
819 if (port > 0 && port != url.getDefaultPort()) {
820 host.append(':').append(port);
821 }
822
823
824 final String[] headerNames = webClient_.getBrowserVersion().getHeaderNamesOrdered();
825 for (final String header : headerNames) {
826 if (HttpHeader.HOST.equals(header)) {
827 list.add(new HostHeaderHttpRequestInterceptor(host.toString()));
828 }
829 else if (HttpHeader.USER_AGENT.equals(header)) {
830 String headerValue = webRequest.getAdditionalHeader(HttpHeader.USER_AGENT);
831 if (headerValue == null) {
832 headerValue = webClient_.getBrowserVersion().getUserAgent();
833 }
834 list.add(new UserAgentHeaderHttpRequestInterceptor(headerValue));
835 }
836 else if (HttpHeader.ACCEPT.equals(header)) {
837 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT);
838 if (headerValue != null) {
839 list.add(new AcceptHeaderHttpRequestInterceptor(headerValue));
840 }
841 }
842 else if (HttpHeader.ACCEPT_LANGUAGE.equals(header)) {
843 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT_LANGUAGE);
844 if (headerValue != null) {
845 list.add(new AcceptLanguageHeaderHttpRequestInterceptor(headerValue));
846 }
847 }
848 else if (HttpHeader.ACCEPT_ENCODING.equals(header)) {
849 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.ACCEPT_ENCODING);
850 if (headerValue != null) {
851 list.add(new AcceptEncodingHeaderHttpRequestInterceptor(headerValue));
852 }
853 }
854 else if (HttpHeader.SEC_FETCH_DEST.equals(header)) {
855 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_DEST);
856 if (headerValue != null) {
857 list.add(new SecFetchDestHeaderHttpRequestInterceptor(headerValue));
858 }
859 }
860 else if (HttpHeader.SEC_FETCH_MODE.equals(header)) {
861 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_MODE);
862 if (headerValue != null) {
863 list.add(new SecFetchModeHeaderHttpRequestInterceptor(headerValue));
864 }
865 }
866 else if (HttpHeader.SEC_FETCH_SITE.equals(header)) {
867 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_SITE);
868 if (headerValue != null) {
869 list.add(new SecFetchSiteHeaderHttpRequestInterceptor(headerValue));
870 }
871 }
872 else if (HttpHeader.SEC_FETCH_USER.equals(header)) {
873 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_FETCH_USER);
874 if (headerValue != null) {
875 list.add(new SecFetchUserHeaderHttpRequestInterceptor(headerValue));
876 }
877 }
878 else if (HttpHeader.SEC_CH_UA.equals(header)) {
879 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA);
880 if (headerValue != null) {
881 list.add(new SecClientHintUserAgentHeaderHttpRequestInterceptor(headerValue));
882 }
883 }
884 else if (HttpHeader.SEC_CH_UA_MOBILE.equals(header)) {
885 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA_MOBILE);
886 if (headerValue != null) {
887 list.add(new SecClientHintUserAgentMobileHeaderHttpRequestInterceptor(headerValue));
888 }
889 }
890 else if (HttpHeader.SEC_CH_UA_PLATFORM.equals(header)) {
891 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.SEC_CH_UA_PLATFORM);
892 if (headerValue != null) {
893 list.add(new SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor(headerValue));
894 }
895 }
896 else if (HttpHeader.PRIORITY.equals(header)) {
897 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.PRIORITY);
898 if (headerValue != null) {
899 list.add(new PriorityHeaderHttpRequestInterceptor(headerValue));
900 }
901 }
902 else if (HttpHeader.UPGRADE_INSECURE_REQUESTS.equals(header)) {
903 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.UPGRADE_INSECURE_REQUESTS);
904 if (headerValue != null) {
905 list.add(new UpgradeInsecureRequestHeaderHttpRequestInterceptor(headerValue));
906 }
907 }
908 else if (HttpHeader.REFERER.equals(header)) {
909 final String headerValue = webRequest.getAdditionalHeader(HttpHeader.REFERER);
910 if (headerValue != null) {
911 list.add(new RefererHeaderHttpRequestInterceptor(headerValue));
912 }
913 }
914 else if (HttpHeader.CONNECTION.equals(header)) {
915 list.add(new RequestClientConnControl());
916 }
917 else if (HttpHeader.COOKIE.equals(header)) {
918 if (!webRequest.hasHint(HttpHint.BlockCookies)) {
919 list.add(new RequestAddCookies());
920 }
921 }
922 else if (HttpHeader.DNT.equals(header) && webClient_.getOptions().isDoNotTrackEnabled()) {
923 list.add(new DntHeaderHttpRequestInterceptor("1"));
924 }
925 }
926
927
928
929 if (webClient_.getOptions().isDoNotTrackEnabled()) {
930 list.add(new DntHeaderHttpRequestInterceptor("1"));
931 }
932
933 synchronized (requestHeaders) {
934 list.add(new MultiHttpRequestInterceptor(new HashMap<>(requestHeaders)));
935 }
936 return list;
937 }
938
939
940 private static final class HostHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
941 private final String value_;
942
943 HostHeaderHttpRequestInterceptor(final String value) {
944 value_ = value;
945 }
946
947 @Override
948 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
949 request.setHeader(HttpHeader.HOST, value_);
950 }
951 }
952
953 private static final class UserAgentHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
954 private final String value_;
955
956 UserAgentHeaderHttpRequestInterceptor(final String value) {
957 value_ = value;
958 }
959
960 @Override
961 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
962 request.setHeader(HttpHeader.USER_AGENT, value_);
963 }
964 }
965
966 private static final class AcceptHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
967 private final String value_;
968
969 AcceptHeaderHttpRequestInterceptor(final String value) {
970 value_ = value;
971 }
972
973 @Override
974 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
975 request.setHeader(HttpHeader.ACCEPT, value_);
976 }
977 }
978
979 private static final class AcceptLanguageHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
980 private final String value_;
981
982 AcceptLanguageHeaderHttpRequestInterceptor(final String value) {
983 value_ = value;
984 }
985
986 @Override
987 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
988 request.setHeader(HttpHeader.ACCEPT_LANGUAGE, value_);
989 }
990 }
991
992 private static final class UpgradeInsecureRequestHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
993 private final String value_;
994
995 UpgradeInsecureRequestHeaderHttpRequestInterceptor(final String value) {
996 value_ = value;
997 }
998
999 @Override
1000 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1001 request.setHeader(HttpHeader.UPGRADE_INSECURE_REQUESTS, value_);
1002 }
1003 }
1004
1005 private static final class AcceptEncodingHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1006 private final String value_;
1007
1008 AcceptEncodingHeaderHttpRequestInterceptor(final String value) {
1009 value_ = value;
1010 }
1011
1012 @Override
1013 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1014 request.setHeader("Accept-Encoding", value_);
1015 }
1016 }
1017
1018 private static final class RefererHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1019 private final String value_;
1020
1021 RefererHeaderHttpRequestInterceptor(final String value) {
1022 value_ = value;
1023 }
1024
1025 @Override
1026 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1027 request.setHeader(HttpHeader.REFERER, value_);
1028 }
1029 }
1030
1031 private static final class DntHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1032 private final String value_;
1033
1034 DntHeaderHttpRequestInterceptor(final String value) {
1035 value_ = value;
1036 }
1037
1038 @Override
1039 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1040 request.setHeader(HttpHeader.DNT, value_);
1041 }
1042 }
1043
1044 private static final class SecFetchModeHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1045 private final String value_;
1046
1047 SecFetchModeHeaderHttpRequestInterceptor(final String value) {
1048 value_ = value;
1049 }
1050
1051 @Override
1052 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1053 request.setHeader(HttpHeader.SEC_FETCH_MODE, value_);
1054 }
1055 }
1056
1057 private static final class SecFetchSiteHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1058 private final String value_;
1059
1060 SecFetchSiteHeaderHttpRequestInterceptor(final String value) {
1061 value_ = value;
1062 }
1063
1064 @Override
1065 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1066 request.setHeader(HttpHeader.SEC_FETCH_SITE, value_);
1067 }
1068 }
1069
1070 private static final class SecFetchUserHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1071 private final String value_;
1072
1073 SecFetchUserHeaderHttpRequestInterceptor(final String value) {
1074 value_ = value;
1075 }
1076
1077 @Override
1078 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1079 request.setHeader(HttpHeader.SEC_FETCH_USER, value_);
1080 }
1081 }
1082
1083 private static final class SecFetchDestHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1084 private final String value_;
1085
1086 SecFetchDestHeaderHttpRequestInterceptor(final String value) {
1087 value_ = value;
1088 }
1089
1090 @Override
1091 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1092 request.setHeader(HttpHeader.SEC_FETCH_DEST, value_);
1093 }
1094 }
1095
1096 private static final class SecClientHintUserAgentHeaderHttpRequestInterceptor implements HttpRequestInterceptor {
1097 private final String value_;
1098
1099 SecClientHintUserAgentHeaderHttpRequestInterceptor(final String value) {
1100 value_ = value;
1101 }
1102
1103 @Override
1104 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1105 request.setHeader(HttpHeader.SEC_CH_UA, value_);
1106 }
1107 }
1108
1109 private static final class SecClientHintUserAgentMobileHeaderHttpRequestInterceptor
1110 implements HttpRequestInterceptor {
1111 private final String value_;
1112
1113 SecClientHintUserAgentMobileHeaderHttpRequestInterceptor(final String value) {
1114 value_ = value;
1115 }
1116
1117 @Override
1118 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1119 request.setHeader(HttpHeader.SEC_CH_UA_MOBILE, value_);
1120 }
1121 }
1122
1123 private static final class SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor
1124 implements HttpRequestInterceptor {
1125 private final String value_;
1126
1127 SecClientHintUserAgentPlatformHeaderHttpRequestInterceptor(final String value) {
1128 value_ = value;
1129 }
1130
1131 @Override
1132 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1133 request.setHeader(HttpHeader.SEC_CH_UA_PLATFORM, value_);
1134 }
1135 }
1136
1137 private static final class PriorityHeaderHttpRequestInterceptor
1138 implements HttpRequestInterceptor {
1139 private final String value_;
1140
1141 PriorityHeaderHttpRequestInterceptor(final String value) {
1142 value_ = value;
1143 }
1144
1145 @Override
1146 public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException {
1147 request.setHeader(HttpHeader.PRIORITY, value_);
1148 }
1149 }
1150
1151 private static class MultiHttpRequestInterceptor implements HttpRequestInterceptor {
1152 private final Map<String, String> map_;
1153
1154 MultiHttpRequestInterceptor(final Map<String, String> map) {
1155 map_ = map;
1156 }
1157
1158 @Override
1159 public void process(final HttpRequest request, final HttpContext context)
1160 throws HttpException, IOException {
1161 for (final Map.Entry<String, String> entry : map_.entrySet()) {
1162 request.setHeader(entry.getKey(), entry.getValue());
1163 }
1164 }
1165 }
1166
1167 private static class RequestClientConnControl implements HttpRequestInterceptor {
1168
1169 private static final String PROXY_CONN_DIRECTIVE = "Proxy-Connection";
1170 private static final String CONN_DIRECTIVE = "Connection";
1171 private static final String CONN_KEEP_ALIVE = "keep-alive";
1172
1173
1174
1175
1176 RequestClientConnControl() {
1177 super();
1178 }
1179
1180 @Override
1181 public void process(final HttpRequest request, final HttpContext context)
1182 throws HttpException, IOException {
1183 final String method = request.getRequestLine().getMethod();
1184 if ("CONNECT".equalsIgnoreCase(method)) {
1185 request.setHeader(PROXY_CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1186 return;
1187 }
1188
1189 final HttpClientContext clientContext = HttpClientContext.adapt(context);
1190
1191
1192 final RouteInfo route = clientContext.getHttpRoute();
1193 if (route == null) {
1194 return;
1195 }
1196
1197 if ((route.getHopCount() == 1 || route.isTunnelled())
1198 && !request.containsHeader(CONN_DIRECTIVE)) {
1199 request.addHeader(CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1200 }
1201 if (route.getHopCount() == 2
1202 && !route.isTunnelled()
1203 && !request.containsHeader(PROXY_CONN_DIRECTIVE)) {
1204 request.addHeader(PROXY_CONN_DIRECTIVE, CONN_KEEP_ALIVE);
1205 }
1206 }
1207 }
1208
1209
1210
1211
1212 private static final class SynchronizedAuthCache extends BasicAuthCache {
1213
1214
1215
1216
1217 SynchronizedAuthCache() {
1218 super();
1219 }
1220
1221
1222
1223
1224 @Override
1225 public synchronized void put(final HttpHost host, final AuthScheme authScheme) {
1226 super.put(host, authScheme);
1227 }
1228
1229
1230
1231
1232 @Override
1233 public synchronized AuthScheme get(final HttpHost host) {
1234 return super.get(host);
1235 }
1236
1237
1238
1239
1240 @Override
1241 public synchronized void remove(final HttpHost host) {
1242 super.remove(host);
1243 }
1244
1245
1246
1247
1248 @Override
1249 public synchronized void clear() {
1250 super.clear();
1251 }
1252
1253
1254
1255
1256 @Override
1257 public synchronized String toString() {
1258 return super.toString();
1259 }
1260 }
1261
1262
1263
1264
1265 @Override
1266 public void close() {
1267 synchronized (httpClientBuilder_) {
1268 httpClientBuilder_.clear();
1269 }
1270 sharedAuthCache_.clear();
1271 httpClientContextByThread_.clear();
1272
1273 if (connectionManager_ != null) {
1274 connectionManager_.shutdown();
1275 connectionManager_ = null;
1276 }
1277 }
1278
1279
1280
1281
1282
1283 private static PoolingHttpClientConnectionManager createConnectionManager(final HttpClientBuilder builder) {
1284 try {
1285 PublicSuffixMatcher publicSuffixMatcher = getField(builder, "publicSuffixMatcher");
1286 if (publicSuffixMatcher == null) {
1287 publicSuffixMatcher = PublicSuffixMatcherLoader.getDefault();
1288 }
1289
1290 LayeredConnectionSocketFactory sslSocketFactory = getField(builder, "sslSocketFactory");
1291 final SocketConfig defaultSocketConfig = getField(builder, "defaultSocketConfig");
1292 final ConnectionConfig defaultConnectionConfig = getField(builder, "defaultConnectionConfig");
1293 final boolean systemProperties = getField(builder, "systemProperties");
1294 final int maxConnTotal = getField(builder, "maxConnTotal");
1295 final int maxConnPerRoute = getField(builder, "maxConnPerRoute");
1296 HostnameVerifier hostnameVerifier = getField(builder, "hostnameVerifier");
1297 final SSLContext sslcontext = getField(builder, "sslContext");
1298 final DnsResolver dnsResolver = getField(builder, "dnsResolver");
1299 final long connTimeToLive = getField(builder, "connTimeToLive");
1300 final TimeUnit connTimeToLiveTimeUnit = getField(builder, "connTimeToLiveTimeUnit");
1301
1302 if (sslSocketFactory == null) {
1303 final String[] supportedProtocols = systemProperties
1304 ? split(System.getProperty("https.protocols")) : null;
1305 final String[] supportedCipherSuites = systemProperties
1306 ? split(System.getProperty("https.cipherSuites")) : null;
1307 if (hostnameVerifier == null) {
1308 hostnameVerifier = new DefaultHostnameVerifier(publicSuffixMatcher);
1309 }
1310 if (sslcontext == null) {
1311 if (systemProperties) {
1312 sslSocketFactory = new SSLConnectionSocketFactory(
1313 (SSLSocketFactory) SSLSocketFactory.getDefault(),
1314 supportedProtocols, supportedCipherSuites, hostnameVerifier);
1315 }
1316 else {
1317 sslSocketFactory = new SSLConnectionSocketFactory(
1318 SSLContexts.createDefault(),
1319 hostnameVerifier);
1320 }
1321 }
1322 else {
1323 sslSocketFactory = new SSLConnectionSocketFactory(
1324 sslcontext, supportedProtocols, supportedCipherSuites, hostnameVerifier);
1325 }
1326 }
1327
1328 final PoolingHttpClientConnectionManager poolingmgr = new PoolingHttpClientConnectionManager(
1329 RegistryBuilder.<ConnectionSocketFactory>create()
1330 .register("http", new SocksConnectionSocketFactory())
1331 .register("https", sslSocketFactory)
1332 .build(),
1333 null,
1334 null,
1335 dnsResolver,
1336 connTimeToLive,
1337 connTimeToLiveTimeUnit != null ? connTimeToLiveTimeUnit : TimeUnit.MILLISECONDS);
1338 if (defaultSocketConfig != null) {
1339 poolingmgr.setDefaultSocketConfig(defaultSocketConfig);
1340 }
1341 if (defaultConnectionConfig != null) {
1342 poolingmgr.setDefaultConnectionConfig(defaultConnectionConfig);
1343 }
1344 if (systemProperties) {
1345 String s = System.getProperty("http.keepAlive", "true");
1346 if ("true".equalsIgnoreCase(s)) {
1347 s = System.getProperty("http.maxConnections", "5");
1348 final int max = Integer.parseInt(s);
1349 poolingmgr.setDefaultMaxPerRoute(max);
1350 poolingmgr.setMaxTotal(2 * max);
1351 }
1352 }
1353 if (maxConnTotal > 0) {
1354 poolingmgr.setMaxTotal(maxConnTotal);
1355 }
1356 if (maxConnPerRoute > 0) {
1357 poolingmgr.setDefaultMaxPerRoute(maxConnPerRoute);
1358 }
1359 return poolingmgr;
1360 }
1361 catch (final IllegalAccessException e) {
1362 throw new RuntimeException(e);
1363 }
1364 }
1365
1366 private static String[] split(final String s) {
1367 if (TextUtils.isBlank(s)) {
1368 return null;
1369 }
1370 return s.split(" *, *");
1371 }
1372
1373 @SuppressWarnings("unchecked")
1374 private static <T> T getField(final Object target, final String fieldName) throws IllegalAccessException {
1375 return (T) FieldUtils.readDeclaredField(target, fieldName, true);
1376 }
1377 }