Created
September 7, 2025 12:13
-
-
Save robtimus/cff0557249123b12fae61ebbfcaf5a6b to your computer and use it in GitHub Desktop.
A Worldline Connect `Connection` implementation based on Apache HttpClient 5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * HttpClient5Connection.java | |
| * Copyright 2025 Rob Spoor | |
| * | |
| * Licensed under the Apache License, Version 2.0 (the "License"); | |
| * you may not use this file except in compliance with the License. | |
| * You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, software | |
| * distributed under the License is distributed on an "AS IS" BASIS, | |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| * See the License for the specific language governing permissions and | |
| * limitations under the License. | |
| */ | |
| package com.github.robtimus.connect.sdk.httpclient5; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.io.UncheckedIOException; | |
| import java.net.ProxySelector; | |
| import java.net.URI; | |
| import java.nio.charset.Charset; | |
| import java.nio.charset.StandardCharsets; | |
| import java.util.ArrayList; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Set; | |
| import java.util.UUID; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import java.util.function.Supplier; | |
| import javax.net.ssl.HostnameVerifier; | |
| import javax.net.ssl.SSLContext; | |
| import org.apache.hc.client5.http.auth.AuthScope; | |
| import org.apache.hc.client5.http.auth.Credentials; | |
| import org.apache.hc.client5.http.auth.CredentialsProvider; | |
| import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; | |
| import org.apache.hc.client5.http.classic.HttpClient; | |
| import org.apache.hc.client5.http.classic.methods.HttpDelete; | |
| import org.apache.hc.client5.http.classic.methods.HttpGet; | |
| import org.apache.hc.client5.http.classic.methods.HttpPost; | |
| import org.apache.hc.client5.http.classic.methods.HttpPut; | |
| import org.apache.hc.client5.http.classic.methods.HttpUriRequest; | |
| import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; | |
| import org.apache.hc.client5.http.config.ConnectionConfig; | |
| import org.apache.hc.client5.http.config.RequestConfig; | |
| import org.apache.hc.client5.http.entity.mime.ContentBody; | |
| import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; | |
| import org.apache.hc.client5.http.entity.mime.InputStreamBody; | |
| import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; | |
| import org.apache.hc.client5.http.impl.DefaultSchemePortResolver; | |
| import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; | |
| import org.apache.hc.client5.http.impl.auth.BasicScheme; | |
| import org.apache.hc.client5.http.impl.auth.SystemDefaultCredentialsProvider; | |
| import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; | |
| import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; | |
| import org.apache.hc.client5.http.impl.classic.HttpClients; | |
| import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; | |
| import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; | |
| import org.apache.hc.client5.http.impl.routing.DefaultProxyRoutePlanner; | |
| import org.apache.hc.client5.http.impl.routing.SystemDefaultRoutePlanner; | |
| import org.apache.hc.client5.http.protocol.HttpClientContext; | |
| import org.apache.hc.client5.http.routing.HttpRoutePlanner; | |
| import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; | |
| import org.apache.hc.client5.http.ssl.HttpsSupport; | |
| import org.apache.hc.client5.http.ssl.TlsSocketStrategy; | |
| import org.apache.hc.core5.http.ContentType; | |
| import org.apache.hc.core5.http.EntityDetails; | |
| import org.apache.hc.core5.http.Header; | |
| import org.apache.hc.core5.http.HttpEntity; | |
| import org.apache.hc.core5.http.HttpEntityContainer; | |
| import org.apache.hc.core5.http.HttpException; | |
| import org.apache.hc.core5.http.HttpHeaders; | |
| import org.apache.hc.core5.http.HttpHost; | |
| import org.apache.hc.core5.http.HttpRequest; | |
| import org.apache.hc.core5.http.HttpRequestInterceptor; | |
| import org.apache.hc.core5.http.HttpResponse; | |
| import org.apache.hc.core5.http.HttpResponseInterceptor; | |
| import org.apache.hc.core5.http.NoHttpResponseException; | |
| import org.apache.hc.core5.http.io.entity.BufferedHttpEntity; | |
| import org.apache.hc.core5.http.io.entity.EmptyInputStream; | |
| import org.apache.hc.core5.http.io.entity.StringEntity; | |
| import org.apache.hc.core5.http.message.BasicHeader; | |
| import org.apache.hc.core5.http.protocol.HttpContext; | |
| import org.apache.hc.core5.ssl.SSLContexts; | |
| import org.apache.hc.core5.util.Timeout; | |
| import com.worldline.connect.sdk.java.CommunicatorConfiguration; | |
| import com.worldline.connect.sdk.java.ProxyConfiguration; | |
| import com.worldline.connect.sdk.java.communication.CommunicationException; | |
| import com.worldline.connect.sdk.java.communication.Connection; | |
| import com.worldline.connect.sdk.java.communication.MultipartFormDataObject; | |
| import com.worldline.connect.sdk.java.communication.PooledConnection; | |
| import com.worldline.connect.sdk.java.communication.RequestHeader; | |
| import com.worldline.connect.sdk.java.communication.ResponseHandler; | |
| import com.worldline.connect.sdk.java.communication.ResponseHeader; | |
| import com.worldline.connect.sdk.java.domain.UploadableFile; | |
| import com.worldline.connect.sdk.java.logging.BodyObfuscator; | |
| import com.worldline.connect.sdk.java.logging.CommunicatorLogger; | |
| import com.worldline.connect.sdk.java.logging.HeaderObfuscator; | |
| import com.worldline.connect.sdk.java.logging.LogMessageBuilder; | |
| import com.worldline.connect.sdk.java.logging.RequestLogMessageBuilder; | |
| import com.worldline.connect.sdk.java.logging.ResponseLogMessageBuilder; | |
| /** | |
| * A {@link Connection} implementation based on {@link HttpClient}. | |
| * | |
| * @author Rob Spoor | |
| */ | |
| @SuppressWarnings("nls") | |
| public final class HttpClient5Connection implements PooledConnection { | |
| private static final Charset CHARSET = StandardCharsets.UTF_8; | |
| private static final String REQUEST_ID_ATTRIBUTE = HttpClient5Connection.class.getName() + ".requestId"; | |
| private static final String START_TIMME_ATTRIBUTE = HttpClient5Connection.class.getName() + ".startTme"; | |
| private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; | |
| // CloseableHttpClient is marked to be thread safe | |
| private final CloseableHttpClient httpClient; | |
| // PoolingHttpClientConnectionManager is marked to be thread safe | |
| private final PoolingHttpClientConnectionManager connectionManager; | |
| // RequestConfig is marked to be immutable | |
| private final RequestConfig requestConfig; | |
| private final AtomicReference<BodyObfuscator> bodyObfuscator = new AtomicReference<>(BodyObfuscator.defaultObfuscator()); | |
| private final AtomicReference<HeaderObfuscator> headerObfuscator = new AtomicReference<>(HeaderObfuscator.defaultObfuscator()); | |
| private final AtomicReference<CommunicatorLogger> communicatorLogger = new AtomicReference<>(); | |
| private HttpClient5Connection(Builder builder) { | |
| requestConfig = RequestConfig.custom() | |
| .setConnectionRequestTimeout(builder.connectionRequestTimeout, TimeUnit.MILLISECONDS) | |
| .setResponseTimeout(builder.socketTimeout, TimeUnit.MILLISECONDS) | |
| .build(); | |
| connectionManager = createHttpClientConnectionManager(builder); | |
| httpClient = createHttpClient(builder); | |
| } | |
| private PoolingHttpClientConnectionManager createHttpClientConnectionManager(Builder builder) { | |
| ConnectionConfig connectionConfig = ConnectionConfig.custom() | |
| .setConnectTimeout(builder.connectTimeout, TimeUnit.MILLISECONDS) | |
| .setSocketTimeout(builder.socketTimeout, TimeUnit.MILLISECONDS) | |
| .build(); | |
| TlsSocketStrategy tlsSocketStrategy = builder.tlsSocketStrategy; | |
| if (tlsSocketStrategy == null) { | |
| tlsSocketStrategy = Builder.createTlsSocketStrategy(CommunicatorConfiguration.DEFAULT_HTTPS_PROTOCOLS); | |
| } | |
| return PoolingHttpClientConnectionManagerBuilder.create() | |
| .setDefaultConnectionConfig(connectionConfig) | |
| .setTlsSocketStrategy(tlsSocketStrategy) | |
| .setMaxConnPerRoute(builder.maxConnections) | |
| .setMaxConnTotal(builder.maxConnections + 20) | |
| .build(); | |
| } | |
| private CloseableHttpClient createHttpClient(Builder builder) { | |
| HttpClientBuilder httpClientBuilder = HttpClients.custom() | |
| .setDefaultRequestConfig(requestConfig) | |
| .setConnectionManager(connectionManager) | |
| .evictExpiredConnections() | |
| ; | |
| HttpRoutePlanner routePlanner; | |
| CredentialsProvider credentialsProvider; | |
| ProxyConfiguration proxyConfiguration = builder.proxyConfiguration; | |
| if (proxyConfiguration != null) { | |
| HttpHost proxy = new HttpHost(proxyConfiguration.getScheme(), proxyConfiguration.getHost(), proxyConfiguration.getPort()); | |
| routePlanner = new DefaultProxyRoutePlanner(proxy, DefaultSchemePortResolver.INSTANCE); | |
| BasicCredentialsProvider basicCredentialsProvider = new BasicCredentialsProvider(); | |
| credentialsProvider = basicCredentialsProvider; | |
| if (proxyConfiguration.getUsername() != null) { | |
| AuthScope authscope = new AuthScope(proxyConfiguration.getHost(), proxyConfiguration.getPort()); | |
| char[] proxyPassword = proxyConfiguration.getPassword() != null ? proxyConfiguration.getPassword().toCharArray() : null; | |
| Credentials credentials = new UsernamePasswordCredentials(proxyConfiguration.getUsername(), proxyPassword); | |
| basicCredentialsProvider.setCredentials(authscope, credentials); | |
| // enable preemptive authentication | |
| HttpRequestInterceptor proxyAuthenticationInterceptor = (request, entity, context) -> { | |
| Header header = request.getFirstHeader(PROXY_AUTHORIZATION_HEADER); | |
| if (header == null) { | |
| BasicScheme basicScheme = new BasicScheme(); | |
| basicScheme.initPreemptive(credentials); | |
| String proxyAuthorization = basicScheme.generateAuthResponse(proxy, request, context); | |
| header = new BasicHeader(PROXY_AUTHORIZATION_HEADER, proxyAuthorization); | |
| request.setHeader(header); | |
| } | |
| }; | |
| httpClientBuilder = httpClientBuilder.addRequestInterceptorLast(proxyAuthenticationInterceptor); | |
| } | |
| } else { | |
| // add support for system properties | |
| routePlanner = new SystemDefaultRoutePlanner(DefaultSchemePortResolver.INSTANCE, ProxySelector.getDefault()); | |
| credentialsProvider = new SystemDefaultCredentialsProvider(); | |
| } | |
| // add logging - last for requests, first for responses | |
| LoggingInterceptor loggingInterceptor = new LoggingInterceptor(); | |
| httpClientBuilder = httpClientBuilder.addRequestInterceptorLast(loggingInterceptor); | |
| httpClientBuilder = httpClientBuilder.addResponseInterceptorFirst(loggingInterceptor); | |
| if (!builder.connectionReuse) { | |
| httpClientBuilder = httpClientBuilder.setConnectionReuseStrategy((request, response, context) -> false); | |
| } | |
| return httpClientBuilder | |
| .setRoutePlanner(routePlanner) | |
| .setDefaultCredentialsProvider(credentialsProvider) | |
| .build(); | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| httpClient.close(); | |
| } | |
| @Override | |
| public <R> R get(URI uri, List<RequestHeader> requestHeaders, ResponseHandler<R> responseHandler) { | |
| HttpGet httpGet = new HttpGet(uri); | |
| httpGet.setConfig(requestConfig); | |
| addHeaders(httpGet, requestHeaders); | |
| return executeRequest(httpGet, responseHandler); | |
| } | |
| @Override | |
| public <R> R delete(URI uri, List<RequestHeader> requestHeaders, ResponseHandler<R> responseHandler) { | |
| HttpDelete httpDelete = new HttpDelete(uri); | |
| httpDelete.setConfig(requestConfig); | |
| addHeaders(httpDelete, requestHeaders); | |
| return executeRequest(httpDelete, responseHandler); | |
| } | |
| @Override | |
| public <R> R post(URI uri, List<RequestHeader> requestHeaders, String body, ResponseHandler<R> responseHandler) { | |
| try (HttpEntity requestEntity = createRequestEntity(body)) { | |
| return post(uri, requestHeaders, requestEntity, responseHandler); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException(e); | |
| } | |
| } | |
| @Override | |
| public <R> R post(URI uri, List<RequestHeader> requestHeaders, MultipartFormDataObject multipart, ResponseHandler<R> responseHandler) { | |
| try (HttpEntity requestEntity = createRequestEntity(multipart)) { | |
| return post(uri, requestHeaders, requestEntity, responseHandler); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException(e); | |
| } | |
| } | |
| private <R> R post(URI uri, List<RequestHeader> requestHeaders, HttpEntity requestEntity, ResponseHandler<R> responseHandler) { | |
| HttpPost httpPost = new HttpPost(uri); | |
| httpPost.setConfig(requestConfig); | |
| addHeaders(httpPost, requestHeaders); | |
| if (requestEntity != null) { | |
| httpPost.setEntity(requestEntity); | |
| } | |
| return executeRequest(httpPost, responseHandler); | |
| } | |
| @Override | |
| public <R> R put(URI uri, List<RequestHeader> requestHeaders, String body, ResponseHandler<R> responseHandler) { | |
| try (HttpEntity requestEntity = createRequestEntity(body)) { | |
| return put(uri, requestHeaders, requestEntity, responseHandler); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException(e); | |
| } | |
| } | |
| @Override | |
| public <R> R put(URI uri, List<RequestHeader> requestHeaders, MultipartFormDataObject multipart, ResponseHandler<R> responseHandler) { | |
| try (HttpEntity requestEntity = createRequestEntity(multipart)) { | |
| return put(uri, requestHeaders, requestEntity, responseHandler); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException(e); | |
| } | |
| } | |
| private <R> R put(URI uri, List<RequestHeader> requestHeaders, HttpEntity requestEntity, ResponseHandler<R> responseHandler) { | |
| HttpPut httpPut = new HttpPut(uri); | |
| httpPut.setConfig(requestConfig); | |
| addHeaders(httpPut, requestHeaders); | |
| if (requestEntity != null) { | |
| httpPut.setEntity(requestEntity); | |
| } | |
| return executeRequest(httpPut, responseHandler); | |
| } | |
| private static HttpEntity createRequestEntity(String body) { | |
| return body != null ? new JsonEntity(body, CHARSET) : null; | |
| } | |
| private static HttpEntity createRequestEntity(MultipartFormDataObject multipart) { | |
| return new MultipartFormDataEntity(multipart); | |
| } | |
| @SuppressWarnings("resource") | |
| private <R> R executeRequest(HttpUriRequest request, ResponseHandler<R> responseHandler) { | |
| String requestId = UUID.randomUUID().toString(); | |
| long startTime = System.currentTimeMillis(); | |
| HttpContext context = new HttpClientContext(); | |
| context.setAttribute(REQUEST_ID_ATTRIBUTE, requestId); | |
| context.setAttribute(START_TIMME_ATTRIBUTE, startTime); | |
| boolean[] logRuntimeExceptions = { true }; | |
| try { | |
| return httpClient.execute(request, context, httpResponse -> { | |
| HttpEntity entity = httpResponse.getEntity(); | |
| InputStream bodyStream = EmptyInputStream.INSTANCE; | |
| try { | |
| int statusCode = httpResponse.getCode(); | |
| List<ResponseHeader> headers = getHeaders(httpResponse); | |
| bodyStream = entity == null ? null : entity.getContent(); | |
| if (bodyStream == null) { | |
| bodyStream = EmptyInputStream.INSTANCE; | |
| } | |
| // do not log runtime exceptions that originate from the response handler, as those are not communication errors | |
| logRuntimeExceptions[0] = false; | |
| return responseHandler.handleResponse(statusCode, bodyStream, headers); | |
| } finally { | |
| /* | |
| * Ensure that the content stream is closed so the connection can be reused. | |
| * Do not close the httpResponse because that will prevent the connection from being reused. | |
| * Note that the body stream will always be set; closing the EmptyInputStream instance will do nothing. | |
| */ | |
| if (bodyStream != null) { | |
| bodyStream.close(); | |
| } | |
| } | |
| }); | |
| } catch (IOException e) { | |
| logError(requestId, e, startTime, communicatorLogger.get()); | |
| throw new CommunicationException(e); | |
| } catch (CommunicationException e) { | |
| logError(requestId, e, startTime, communicatorLogger.get()); | |
| throw e; | |
| } catch (RuntimeException e) { | |
| if (logRuntimeExceptions[0]) { | |
| logError(requestId, e, startTime, communicatorLogger.get()); | |
| } | |
| throw e; | |
| } | |
| } | |
| private void addHeaders(HttpUriRequestBase httpRequest, List<RequestHeader> requestHeaders) { | |
| if (requestHeaders != null) { | |
| for (RequestHeader requestHeader : requestHeaders) { | |
| httpRequest.addHeader(new BasicHeader(requestHeader.getName(), requestHeader.getValue())); | |
| } | |
| } | |
| } | |
| private List<ResponseHeader> getHeaders(HttpResponse httpResponse) { | |
| Header[] headers = httpResponse.getHeaders(); | |
| List<ResponseHeader> result = new ArrayList<>(headers.length); | |
| for (Header header : headers) { | |
| result.add(new ResponseHeader(header.getName(), header.getValue())); | |
| } | |
| return result; | |
| } | |
| @Override | |
| public void closeIdleConnections(long idleTime, TimeUnit timeUnit) { | |
| connectionManager.closeIdle(Timeout.of(idleTime, timeUnit)); | |
| } | |
| @Override | |
| public void closeExpiredConnections() { | |
| connectionManager.closeExpired(); | |
| } | |
| @Override | |
| public void setBodyObfuscator(BodyObfuscator bodyObfuscator) { | |
| if (bodyObfuscator == null) { | |
| throw new IllegalArgumentException("bodyObfuscator is required"); | |
| } | |
| this.bodyObfuscator.set(bodyObfuscator); | |
| } | |
| @Override | |
| public void setHeaderObfuscator(HeaderObfuscator headerObfuscator) { | |
| if (headerObfuscator == null) { | |
| throw new IllegalArgumentException("headerObfuscator is required"); | |
| } | |
| this.headerObfuscator.set(headerObfuscator); | |
| } | |
| @Override | |
| public void enableLogging(CommunicatorLogger communicatorLogger) { | |
| if (communicatorLogger == null) { | |
| throw new IllegalArgumentException("communicatorLogger is required"); | |
| } | |
| this.communicatorLogger.set(communicatorLogger); | |
| } | |
| @Override | |
| public void disableLogging() { | |
| this.communicatorLogger.set(null); | |
| } | |
| /** | |
| * Creates a builder for {@code HttpClient5Connection} instances using the given configuration. | |
| * | |
| * @param configuration The configuration to use to create the builder. | |
| * @return The created builder. | |
| */ | |
| public static Builder fromConfiguration(CommunicatorConfiguration configuration) { | |
| return new Builder(configuration.getConnectTimeout(), configuration.getSocketTimeout()) | |
| .withMaxConnections(configuration.getMaxConnections()) | |
| .withConnectionReuse(configuration.isConnectionReuse()) | |
| .withProxyConfiguration(configuration.getProxyConfiguration()) | |
| .withHttpsProtocols(configuration.getHttpsProtocols()); | |
| } | |
| /** | |
| * A builder for {@link HttpClient5Connection} objects. | |
| * | |
| * @author Rob Spoor | |
| */ | |
| public static final class Builder { | |
| private final int connectTimeout; | |
| private final int socketTimeout; | |
| private int connectionRequestTimeout; | |
| private int maxConnections; | |
| private boolean connectionReuse; | |
| private ProxyConfiguration proxyConfiguration; | |
| private TlsSocketStrategy tlsSocketStrategy; | |
| /** | |
| * Creates a new builder. | |
| * | |
| * @param connectTimeout The connect timeout in milliseconds. | |
| * @param socketTimeout The socket timeout in milliseconds. | |
| */ | |
| public Builder(int connectTimeout, int socketTimeout) { | |
| this.connectTimeout = connectTimeout; | |
| this.socketTimeout = socketTimeout; | |
| connectionRequestTimeout = connectTimeout; | |
| maxConnections = CommunicatorConfiguration.DEFAULT_MAX_CONNECTIONS; | |
| connectionReuse = true; | |
| } | |
| /** | |
| * Sets the connection request timeout. Defaults to the connect timeout. | |
| * | |
| * @param connectionRequestTimeout The connection request timeout in milliseconds. | |
| * @return This object. | |
| */ | |
| public Builder withConnectionRequestTimeout(int connectionRequestTimeout) { | |
| this.connectionRequestTimeout = connectionRequestTimeout; | |
| return this; | |
| } | |
| /** | |
| * Sets the maximum number of connections. Defaults to {@link CommunicatorConfiguration#DEFAULT_MAX_CONNECTIONS}. | |
| * | |
| * @param maxConnections The maximum number of connections. | |
| * @return This object. | |
| */ | |
| public Builder withMaxConnections(int maxConnections) { | |
| this.maxConnections = maxConnections; | |
| return this; | |
| } | |
| /** | |
| * Sets whether or not connections should be reused. Defaults to on ({@code true}). | |
| * <p> | |
| * This method can be used to turn off connection reuse. This may be necessary in case (proxy) servers do not handle reused connections well. | |
| * This may lead to instances of {@link NoHttpResponseException} to be thrown. | |
| * | |
| * @param connectionReuse {@code true} if connections should be reused, or {@code false} otherwise. | |
| * @return This object. | |
| */ | |
| public Builder withConnectionReuse(boolean connectionReuse) { | |
| this.connectionReuse = connectionReuse; | |
| return this; | |
| } | |
| /** | |
| * Sets the proxy configuration to use. Defaults to no proxy configuration. | |
| * | |
| * @param proxyConfiguration The proxy configuration to use. | |
| * @return This object. | |
| */ | |
| public Builder withProxyConfiguration(ProxyConfiguration proxyConfiguration) { | |
| this.proxyConfiguration = proxyConfiguration; | |
| return this; | |
| } | |
| /** | |
| * Sets the HTTPS protocols to support. Defaults to {@link CommunicatorConfiguration#DEFAULT_HTTPS_PROTOCOLS}. | |
| * <p> | |
| * This method is mutually exclusive with {@link #withTlsSocketStrategy(TlsSocketStrategy)}. | |
| * | |
| * @param httpsProtocols The HTTPS protocols to support. | |
| * @return This object. | |
| */ | |
| public Builder withHttpsProtocols(Set<String> httpsProtocols) { | |
| return withTlsSocketStrategy(createTlsSocketStrategy(httpsProtocols)); | |
| } | |
| /** | |
| * Sets a custom TLS protocol upgrade strategy to use. | |
| * <p> | |
| * This method can be used to provide a fully customizable TLS protocol upgrade strategy, in case the SSL connection socket factory that is | |
| * created by default cannot be used due to SSL issues. | |
| * <p> | |
| * This method is mutually exclusive with {@link #withHttpsProtocols(Set)}. | |
| * | |
| * @param tlsSocketStrategy The TLS protocol upgrade strategy to use. | |
| * @return This object. | |
| */ | |
| public Builder withTlsSocketStrategy(TlsSocketStrategy tlsSocketStrategy) { | |
| this.tlsSocketStrategy = tlsSocketStrategy; | |
| return this; | |
| } | |
| private static TlsSocketStrategy createTlsSocketStrategy(Set<String> httpsProtocols) { | |
| SSLContext sslContext = SSLContexts.createDefault(); | |
| HostnameVerifier hostnameVerifier = HttpsSupport.getDefaultHostnameVerifier(); | |
| Set<String> supportedProtocols = httpsProtocols != null && !httpsProtocols.isEmpty() | |
| ? httpsProtocols | |
| : CommunicatorConfiguration.DEFAULT_HTTPS_PROTOCOLS; | |
| return new DefaultClientTlsStrategy(sslContext, supportedProtocols.toArray(String[]::new), null, null, hostnameVerifier); | |
| } | |
| /** | |
| * Creates a fully initialized {@link HttpClient5Connection} object. | |
| * | |
| * @return The created {@link HttpClient5Connection} object. | |
| */ | |
| public HttpClient5Connection build() { | |
| return new HttpClient5Connection(this); | |
| } | |
| } | |
| private static void logError(String requestId, Exception error, long startTime, CommunicatorLogger logger) { | |
| if (logger != null) { | |
| final String messageTemplate = "Error occurred for outgoing request (requestId='%s', %d ms)"; | |
| long endTime = System.currentTimeMillis(); | |
| long duration = endTime - startTime; | |
| String message = String.format(messageTemplate, requestId, duration); | |
| logger.log(message, error); | |
| } | |
| } | |
| private final class LoggingInterceptor implements HttpRequestInterceptor, HttpResponseInterceptor { | |
| @Override | |
| public void process(HttpRequest request, EntityDetails entity, HttpContext context) throws HttpException, IOException { | |
| CommunicatorLogger logger = communicatorLogger.get(); | |
| if (logger != null) { | |
| String requestId = (String) context.getAttribute(REQUEST_ID_ATTRIBUTE); | |
| if (requestId != null) { | |
| logRequest(request, requestId, logger); | |
| } | |
| // else the context was not sent through executeRequest | |
| } | |
| } | |
| @Override | |
| public void process(HttpResponse response, EntityDetails entity, HttpContext context) throws HttpException, IOException { | |
| CommunicatorLogger logger = communicatorLogger.get(); | |
| if (logger != null) { | |
| String requestId = (String) context.getAttribute(REQUEST_ID_ATTRIBUTE); | |
| Long startTime = (Long) context.getAttribute(START_TIMME_ATTRIBUTE); | |
| if (requestId != null && startTime != null) { | |
| logResponse(response, requestId, startTime, logger); | |
| } | |
| // else the context was not sent through executeRequest | |
| } | |
| } | |
| @SuppressWarnings("resource") | |
| private void logRequest(HttpRequest request, String requestId, CommunicatorLogger logger) { | |
| try { | |
| String method = request.getMethod(); | |
| String uri = request.getRequestUri(); | |
| RequestLogMessageBuilder logMessageBuilder = new RequestLogMessageBuilder(requestId, method, uri, | |
| bodyObfuscator.get(), headerObfuscator.get()); | |
| addHeaders(logMessageBuilder, request.getHeaders()); | |
| if (request instanceof HttpEntityContainer) { | |
| HttpEntityContainer httpEntityContainer = (HttpEntityContainer) request; | |
| HttpEntity entity = httpEntityContainer.getEntity(); | |
| String contentType = getContentType(entity, () -> request.getFirstHeader(HttpHeaders.CONTENT_TYPE)); | |
| boolean isBinaryContent = isBinaryContent(contentType); | |
| if (entity != null && !entity.isRepeatable() && !isBinaryContent) { | |
| entity = new BufferedHttpEntity(entity); | |
| httpEntityContainer.setEntity(entity); | |
| } | |
| setBody(logMessageBuilder, entity, contentType, isBinaryContent); | |
| } | |
| logger.log(logMessageBuilder.getMessage()); | |
| } catch (Exception e) { | |
| logger.log(String.format("An error occurred trying to log request '%s'", requestId), e); | |
| } | |
| } | |
| @SuppressWarnings("resource") | |
| private void logResponse(HttpResponse response, String requestId, long startTime, CommunicatorLogger logger) { | |
| long endTime = System.currentTimeMillis(); | |
| long duration = endTime - startTime; | |
| try { | |
| int statusCode = response.getCode(); | |
| ResponseLogMessageBuilder logMessageBuilder = new ResponseLogMessageBuilder(requestId, statusCode, duration, | |
| bodyObfuscator.get(), headerObfuscator.get()); | |
| addHeaders(logMessageBuilder, response.getHeaders()); | |
| if (response instanceof HttpEntityContainer) { | |
| HttpEntityContainer httpEntityContainer = (HttpEntityContainer) response; | |
| HttpEntity entity = httpEntityContainer.getEntity(); | |
| String contentType = getContentType(entity, () -> response.getFirstHeader(HttpHeaders.CONTENT_TYPE)); | |
| boolean isBinaryContent = isBinaryContent(contentType); | |
| if (entity != null && !entity.isRepeatable() && !isBinaryContent) { | |
| entity = new BufferedHttpEntity(entity); | |
| httpEntityContainer.setEntity(entity); | |
| } | |
| setBody(logMessageBuilder, entity, contentType, isBinaryContent); | |
| } | |
| logger.log(logMessageBuilder.getMessage()); | |
| } catch (Exception e) { | |
| logger.log(String.format("An error occurred trying to log response '%s'", requestId), e); | |
| } | |
| } | |
| private void addHeaders(LogMessageBuilder logMessageBuilder, Header[] headers) { | |
| if (headers != null) { | |
| for (Header header : headers) { | |
| logMessageBuilder.addHeader(header.getName(), header.getValue()); | |
| } | |
| } | |
| } | |
| private String getContentType(HttpEntity entity, Supplier<Header> defaultHeaderSupplier) { | |
| String contentType = entity != null ? entity.getContentType() : null; | |
| if (contentType == null) { | |
| Header contentTypeHeader = defaultHeaderSupplier.get(); | |
| if (contentTypeHeader != null) { | |
| contentType = contentTypeHeader.getValue(); | |
| } | |
| } | |
| return contentType; | |
| } | |
| private void setBody(LogMessageBuilder logMessageBuilder, HttpEntity entity, String contentType, boolean isBinaryContent) throws IOException { | |
| if (entity == null) { | |
| logMessageBuilder.setBody("", contentType); | |
| } else if (entity instanceof JsonEntity) { | |
| String body = ((JsonEntity) entity).json; | |
| logMessageBuilder.setBody(body, contentType); | |
| } else if (isBinaryContent) { | |
| logMessageBuilder.setBinaryContentBody(contentType); | |
| } else { | |
| @SuppressWarnings("resource") | |
| InputStream body = entity.getContent(); | |
| logMessageBuilder.setBody(body, CHARSET, contentType); | |
| } | |
| } | |
| private boolean isBinaryContent(String contentType) { | |
| return contentType != null | |
| && !contentType.startsWith("text/") | |
| && !contentType.contains("json") | |
| && !contentType.contains("xml"); | |
| } | |
| } | |
| private static final class JsonEntity extends StringEntity { | |
| private final String json; | |
| JsonEntity(String json, Charset charset) { | |
| super(json, ContentType.create(ContentType.APPLICATION_JSON.getMimeType(), charset)); | |
| this.json = json; | |
| } | |
| } | |
| private static final class MultipartFormDataEntity implements HttpEntity { | |
| private static final ContentType TEXT_PLAIN_UTF8 = ContentType.create("text/plain", CHARSET); | |
| private final HttpEntity delegate; | |
| private final boolean isChunked; | |
| private MultipartFormDataEntity(MultipartFormDataObject multipart) { | |
| boolean hasNegativeContentLength = false; | |
| MultipartEntityBuilder builder = MultipartEntityBuilder.create() | |
| .setBoundary(multipart.getBoundary()) | |
| .setContentType(ContentType.parse(multipart.getContentType())) | |
| .setMode(HttpMultipartMode.EXTENDED); | |
| for (Map.Entry<String, String> entry : multipart.getValues().entrySet()) { | |
| builder = builder.addTextBody(entry.getKey(), entry.getValue(), TEXT_PLAIN_UTF8); | |
| } | |
| for (Map.Entry<String, UploadableFile> entry : multipart.getFiles().entrySet()) { | |
| builder = builder.addPart(entry.getKey(), new UploadableFileBody(entry.getValue())); | |
| hasNegativeContentLength |= entry.getValue().getContentLength() < 0; | |
| } | |
| delegate = builder.build(); | |
| isChunked = hasNegativeContentLength; | |
| String contentType = delegate.getContentType(); | |
| if (contentType == null || !(multipart.getContentType()).equals(contentType)) { | |
| throw new IllegalStateException("MultipartEntityBuilder did not create the expected content type"); | |
| } | |
| } | |
| @Override | |
| public long getContentLength() { | |
| return delegate.getContentLength(); | |
| } | |
| @Override | |
| public String getContentType() { | |
| return delegate.getContentType(); | |
| } | |
| @Override | |
| public String getContentEncoding() { | |
| return delegate.getContentEncoding(); | |
| } | |
| @Override | |
| public boolean isChunked() { | |
| return isChunked; | |
| } | |
| @Override | |
| public Set<String> getTrailerNames() { | |
| return delegate.getTrailerNames(); | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| delegate.close(); | |
| } | |
| @Override | |
| public boolean isRepeatable() { | |
| return false; | |
| } | |
| @Override | |
| public InputStream getContent() throws IOException, UnsupportedOperationException { | |
| return delegate.getContent(); | |
| } | |
| @Override | |
| public void writeTo(OutputStream outStream) throws IOException { | |
| delegate.writeTo(outStream); | |
| } | |
| @Override | |
| public boolean isStreaming() { | |
| return true; | |
| } | |
| @Override | |
| public org.apache.hc.core5.function.Supplier<List<? extends Header>> getTrailers() { | |
| return delegate.getTrailers(); | |
| } | |
| } | |
| private static final class UploadableFileBody implements ContentBody { | |
| private final ContentBody delegate; | |
| private final long contentLength; | |
| private UploadableFileBody(UploadableFile file) { | |
| @SuppressWarnings("resource") | |
| InputStream content = file.getContent(); | |
| delegate = new InputStreamBody(content, ContentType.create(file.getContentType()), file.getFileName()); | |
| contentLength = Math.max(file.getContentLength(), -1); | |
| } | |
| @Override | |
| public String getMimeType() { | |
| return delegate.getMimeType(); | |
| } | |
| @Override | |
| public String getMediaType() { | |
| return delegate.getMediaType(); | |
| } | |
| @Override | |
| public String getSubType() { | |
| return delegate.getSubType(); | |
| } | |
| @Override | |
| public String getCharset() { | |
| return delegate.getCharset(); | |
| } | |
| @Override | |
| public long getContentLength() { | |
| return contentLength; | |
| } | |
| @Override | |
| public String getFilename() { | |
| return delegate.getFilename(); | |
| } | |
| @Override | |
| public void writeTo(OutputStream out) throws IOException { | |
| delegate.writeTo(out); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment