Skip to content

Instantly share code, notes, and snippets.

@robtimus
Created September 7, 2025 12:13
Show Gist options
  • Select an option

  • Save robtimus/cff0557249123b12fae61ebbfcaf5a6b to your computer and use it in GitHub Desktop.

Select an option

Save robtimus/cff0557249123b12fae61ebbfcaf5a6b to your computer and use it in GitHub Desktop.
A Worldline Connect `Connection` implementation based on Apache HttpClient 5
/*
* 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