Last active
September 7, 2025 12:14
-
-
Save robtimus/9395a42596390f4efca8e936996016d6 to your computer and use it in GitHub Desktop.
A Worldline Connect `Connection` implementation based on java.net.http.HttpClient
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
| /* | |
| * HttpClientConnection.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.httpclient; | |
| import java.io.ByteArrayOutputStream; | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.UncheckedIOException; | |
| import java.lang.invoke.MethodHandle; | |
| import java.lang.invoke.MethodHandles; | |
| import java.lang.invoke.MethodType; | |
| import java.net.InetSocketAddress; | |
| import java.net.ProxySelector; | |
| import java.net.URI; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpHeaders; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpResponse; | |
| import java.nio.ByteBuffer; | |
| import java.nio.channels.Channels; | |
| import java.nio.channels.WritableByteChannel; | |
| import java.nio.charset.Charset; | |
| import java.nio.charset.StandardCharsets; | |
| import java.time.Duration; | |
| import java.util.ArrayList; | |
| import java.util.Base64; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.Objects; | |
| import java.util.Set; | |
| import java.util.UUID; | |
| import java.util.concurrent.CompletionStage; | |
| import java.util.concurrent.Flow.Subscriber; | |
| import java.util.concurrent.Flow.Subscription; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import javax.net.ssl.SSLContext; | |
| import javax.net.ssl.SSLParameters; | |
| 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 HttpClientConnection implements PooledConnection { | |
| private static final Charset CHARSET = StandardCharsets.UTF_8; | |
| private static final String PROXY_AUTHORIZATION_HEADER = "Proxy-Authorization"; | |
| // HttpClient only has close and shutdown methods since Java 21, so let's use the first if it's available | |
| private static final MethodHandle HTTP_CLIENT_CLOSE_HANDLE; | |
| static { | |
| MethodHandle closeHandle; | |
| try { | |
| closeHandle = MethodHandles.lookup().findVirtual(HttpClient.class, "close", MethodType.methodType(void.class)); | |
| } catch (@SuppressWarnings("unused") NoSuchMethodException e) { | |
| closeHandle = null; | |
| } catch (IllegalAccessException e) { | |
| throw new IllegalStateException(e); | |
| } | |
| HTTP_CLIENT_CLOSE_HANDLE = closeHandle; | |
| } | |
| private final HttpClient httpClient; | |
| private final Duration requestTimeout; | |
| private final String proxyAuthorization; | |
| 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 HttpClientConnection(Builder builder) { | |
| httpClient = createHttpClient(builder); | |
| requestTimeout = Duration.ofMillis(builder.socketTimeout); | |
| proxyAuthorization = createProxyAuthorization(builder); | |
| } | |
| private HttpClient createHttpClient(Builder builder) { | |
| HttpClient.Builder httpClientBuilder = HttpClient.newBuilder() | |
| .connectTimeout(Duration.ofMillis(builder.connectTimeout)); | |
| ProxyConfiguration proxyConfiguration = builder.proxyConfiguration; | |
| if (proxyConfiguration != null) { | |
| InetSocketAddress proxyAddress = InetSocketAddress.createUnresolved(proxyConfiguration.getHost(), proxyConfiguration.getPort()); | |
| ProxySelector proxySelector = ProxySelector.of(proxyAddress); | |
| httpClientBuilder.proxy(proxySelector); | |
| } | |
| if (builder.sslContext != null) { | |
| httpClientBuilder.sslContext(builder.sslContext); | |
| } | |
| if (builder.sslParameters != null) { | |
| httpClientBuilder.sslParameters(builder.sslParameters); | |
| } | |
| return httpClientBuilder.build(); | |
| } | |
| private String createProxyAuthorization(Builder builder) { | |
| ProxyConfiguration proxyConfiguration = builder.proxyConfiguration; | |
| if (proxyConfiguration == null || proxyConfiguration.getUsername() == null) { | |
| return null; | |
| } | |
| String credentials = proxyConfiguration.getUsername() + ":" + Objects.toString(proxyConfiguration.getPassword(), ""); | |
| return "BASIC " + Base64.getEncoder().encodeToString(credentials.getBytes(CHARSET)); | |
| } | |
| @Override | |
| public void close() throws IOException { | |
| if (HTTP_CLIENT_CLOSE_HANDLE != null) { | |
| try { | |
| HTTP_CLIENT_CLOSE_HANDLE.invokeExact(httpClient); | |
| } catch (IOException | RuntimeException e) { | |
| throw e; | |
| } catch (Throwable e) { | |
| throw new IllegalStateException(e); | |
| } | |
| } | |
| } | |
| @Override | |
| public <R> R get(URI uri, List<RequestHeader> requestHeaders, ResponseHandler<R> responseHandler) { | |
| HttpRequest.Builder getBuilder = HttpRequest.newBuilder(uri) | |
| .GET() | |
| .timeout(requestTimeout); | |
| addHeaders(getBuilder, requestHeaders); | |
| return executeRequest(getBuilder.build(), responseHandler); | |
| } | |
| @Override | |
| public <R> R delete(URI uri, List<RequestHeader> requestHeaders, ResponseHandler<R> responseHandler) { | |
| HttpRequest.Builder deleteBuilder = HttpRequest.newBuilder(uri) | |
| .DELETE() | |
| .timeout(requestTimeout); | |
| addHeaders(deleteBuilder, requestHeaders); | |
| return executeRequest(deleteBuilder.build(), responseHandler); | |
| } | |
| @Override | |
| public <R> R post(URI uri, List<RequestHeader> requestHeaders, String body, ResponseHandler<R> responseHandler) { | |
| HttpRequest.BodyPublisher bodyPublisher = createBodyPublisher(body); | |
| return post(uri, requestHeaders, bodyPublisher, responseHandler); | |
| } | |
| @Override | |
| public <R> R post(URI uri, List<RequestHeader> requestHeaders, MultipartFormDataObject multipart, ResponseHandler<R> responseHandler) { | |
| HttpRequest.BodyPublisher bodyPublisher = createBodyPublisher(multipart); | |
| return post(uri, requestHeaders, bodyPublisher, responseHandler); | |
| } | |
| private <R> R post(URI uri, List<RequestHeader> requestHeaders, HttpRequest.BodyPublisher bodyPublisher, ResponseHandler<R> responseHandler) { | |
| HttpRequest.Builder postBuilder = HttpRequest.newBuilder(uri) | |
| .POST(bodyPublisher) | |
| .timeout(requestTimeout); | |
| addHeaders(postBuilder, requestHeaders); | |
| return executeRequest(postBuilder.build(), responseHandler); | |
| } | |
| @Override | |
| public <R> R put(URI uri, List<RequestHeader> requestHeaders, String body, ResponseHandler<R> responseHandler) { | |
| HttpRequest.BodyPublisher bodyPublisher = createBodyPublisher(body); | |
| return put(uri, requestHeaders, bodyPublisher, responseHandler); | |
| } | |
| @Override | |
| public <R> R put(URI uri, List<RequestHeader> requestHeaders, MultipartFormDataObject multipart, ResponseHandler<R> responseHandler) { | |
| HttpRequest.BodyPublisher bodyPublisher = createBodyPublisher(multipart); | |
| return put(uri, requestHeaders, bodyPublisher, responseHandler); | |
| } | |
| private <R> R put(URI uri, List<RequestHeader> requestHeaders, HttpRequest.BodyPublisher bodyPublisher, ResponseHandler<R> responseHandler) { | |
| HttpRequest.Builder putBuilder = HttpRequest.newBuilder(uri) | |
| .PUT(bodyPublisher) | |
| .timeout(requestTimeout); | |
| addHeaders(putBuilder, requestHeaders); | |
| return executeRequest(putBuilder.build(), responseHandler); | |
| } | |
| private static HttpRequest.BodyPublisher createBodyPublisher(String body) { | |
| return body != null ? new JsonRequestBodyPublisher(body, CHARSET) : HttpRequest.BodyPublishers.noBody(); | |
| } | |
| private static HttpRequest.BodyPublisher createBodyPublisher(MultipartFormDataObject multipart) { | |
| return new MultipartBodyPublisher(multipart); | |
| } | |
| private <R> R executeRequest(HttpRequest request, ResponseHandler<R> responseHandler) { | |
| String requestId = UUID.randomUUID().toString(); | |
| logRequest(request, requestId); | |
| long startTime = System.currentTimeMillis(); | |
| boolean[] logRuntimeExceptions = { true }; | |
| try { | |
| HttpResponse<R> response = httpClient.send(request, responseInfo -> { | |
| int statusCode = responseInfo.statusCode(); | |
| List<ResponseHeader> headers = getHeaders(responseInfo); | |
| HttpResponse.BodySubscriber<InputStream> inputStreamSubscriber = HttpResponse.BodySubscribers.ofInputStream(); | |
| HttpResponse.BodySubscriber<R> subscriber = HttpResponse.BodySubscribers.mapping(inputStreamSubscriber, inputStream -> { | |
| // do not log runtime exceptions that originate from the response handler, as those are not communication errors | |
| logRuntimeExceptions[0] = false; | |
| return responseHandler.handleResponse(statusCode, inputStream, headers); | |
| }); | |
| CommunicatorLogger logger = communicatorLogger.get(); | |
| if (logger != null) { | |
| subscriber = new LoggingResponseBodySubscriber<>(subscriber, requestId, startTime, responseInfo, logger); | |
| } | |
| return subscriber; | |
| }); | |
| return response.body(); | |
| } catch (InterruptedException e) { | |
| // Restore interrupt status | |
| Thread.currentThread().interrupt(); | |
| logError(requestId, e, startTime, communicatorLogger.get()); | |
| throw new CommunicationException(e); | |
| } 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(HttpRequest.Builder httpRequestBuilder, List<RequestHeader> requestHeaders) { | |
| if (requestHeaders != null) { | |
| for (RequestHeader requestHeader : requestHeaders) { | |
| httpRequestBuilder.header(requestHeader.getName(), requestHeader.getValue()); | |
| } | |
| } | |
| if (proxyAuthorization != null) { | |
| httpRequestBuilder.header(PROXY_AUTHORIZATION_HEADER, proxyAuthorization); | |
| } | |
| } | |
| private List<ResponseHeader> getHeaders(HttpResponse.ResponseInfo httpResponseInfo) { | |
| Map<String, List<String>> headerMap = httpResponseInfo.headers().map(); | |
| // Under normal circumstances, each header only occurs once | |
| List<ResponseHeader> result = new ArrayList<>(headerMap.size()); | |
| for (Map.Entry<String, List<String>> entry : headerMap.entrySet()) { | |
| String headerName = entry.getKey(); | |
| for (String headerValue : entry.getValue()) { | |
| result.add(new ResponseHeader(headerName, headerValue)); | |
| } | |
| } | |
| return result; | |
| } | |
| @Override | |
| public void closeIdleConnections(long idleTime, TimeUnit timeUnit) { | |
| // HttpClient does not support closing idle connections explicitly | |
| } | |
| @Override | |
| public void closeExpiredConnections() { | |
| // HttpClient does not support closing expired connections explicitly | |
| } | |
| @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); | |
| } | |
| // logging code | |
| private void logRequest(HttpRequest request, String requestId) { | |
| CommunicatorLogger logger = communicatorLogger.get(); | |
| if (logger != null) { | |
| logRequest(request, requestId, logger); | |
| } | |
| } | |
| private void logRequest(HttpRequest request, String requestId, CommunicatorLogger logger) { | |
| try { | |
| String method = request.method(); | |
| String uri = request.uri().toString(); | |
| RequestLogMessageBuilder logMessageBuilder = new RequestLogMessageBuilder(requestId, method, uri, | |
| bodyObfuscator.get(), headerObfuscator.get()); | |
| HttpHeaders requestHeaders = request.headers(); | |
| addHeaders(logMessageBuilder, requestHeaders); | |
| request.bodyPublisher().ifPresent(bodyPublisher -> { | |
| String contentType = getContentType(requestHeaders); | |
| // There are only two options: JSON or multipart; treat the latter as binary | |
| if (bodyPublisher instanceof JsonRequestBodyPublisher jsonBodyPublisher) { | |
| logMessageBuilder.setBody(jsonBodyPublisher.json, contentType); | |
| } else { | |
| logMessageBuilder.setBinaryContentBody(contentType); | |
| } | |
| }); | |
| logger.log(logMessageBuilder.getMessage()); | |
| } catch (Exception e) { | |
| logger.log(String.format("An error occurred trying to log request '%s'", requestId), e); | |
| } | |
| } | |
| private static void addHeaders(LogMessageBuilder logMessageBuilder, HttpHeaders headers) { | |
| if (headers != null) { | |
| Map<String, List<String>> headerMap = headers.map(); | |
| for (Map.Entry<String, List<String>> entry : headerMap.entrySet()) { | |
| String headerName = entry.getKey(); | |
| for (String headerValue : entry.getValue()) { | |
| logMessageBuilder.addHeader(headerName, headerValue); | |
| } | |
| } | |
| } | |
| } | |
| private static String getContentType(HttpHeaders headers) { | |
| return headers.firstValue("Content-Type").orElse(null); | |
| } | |
| private static void setBody(LogMessageBuilder logMessageBuilder, String body, String contentType, boolean isBinaryContent) { | |
| if (body == null) { | |
| logMessageBuilder.setBody("", contentType); | |
| } else if (isBinaryContent) { | |
| logMessageBuilder.setBinaryContentBody(contentType); | |
| } else { | |
| logMessageBuilder.setBody(body, contentType); | |
| } | |
| } | |
| private static boolean isBinaryContent(String contentType) { | |
| return contentType != null | |
| && !contentType.startsWith("text/") | |
| && !contentType.contains("json") | |
| && !contentType.contains("xml"); | |
| } | |
| 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); | |
| } | |
| } | |
| /** | |
| * A builder for {@link HttpClientConnection} objects. | |
| * | |
| * @author Rob Spoor | |
| */ | |
| public static final class Builder { | |
| private final int connectTimeout; | |
| private final int socketTimeout; | |
| private ProxyConfiguration proxyConfiguration; | |
| private SSLContext sslContext; | |
| private SSLParameters sslParameters; | |
| /** | |
| * 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; | |
| } | |
| /** | |
| * 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 SSL context to use. | |
| * | |
| * @param sslContext The SSL context to use. | |
| * @return This object. | |
| */ | |
| public Builder withSslContext(SSLContext sslContext) { | |
| this.sslContext = sslContext; | |
| return this; | |
| } | |
| /** | |
| * Sets the HTTPS protocols to support. Defaults to {@link CommunicatorConfiguration#DEFAULT_HTTPS_PROTOCOLS}. | |
| * <p> | |
| * This method is mutually exclusive with {@link #withSslParameters(SSLParameters)}. | |
| * | |
| * @param httpsProtocols The HTTPS protocols to support. | |
| * @return This object. | |
| */ | |
| public Builder withHttpsProtocols(Set<String> httpsProtocols) { | |
| return withSslParameters(createSslParameters(httpsProtocols)); | |
| } | |
| /** | |
| * Sets the SSL parameters to use. | |
| * <p> | |
| * This method is mutually exclusive with {@link #withHttpsProtocols(Set)}. | |
| * | |
| * @param sslParameters The SSL parameters to use. | |
| * @return This object. | |
| */ | |
| public Builder withSslParameters(SSLParameters sslParameters) { | |
| this.sslParameters = sslParameters; | |
| return this; | |
| } | |
| private static SSLParameters createSslParameters(Set<String> httpsProtocols) { | |
| Set<String> supportedProtocols = httpsProtocols != null && !httpsProtocols.isEmpty() | |
| ? httpsProtocols | |
| : CommunicatorConfiguration.DEFAULT_HTTPS_PROTOCOLS; | |
| return new SSLParameters(null, supportedProtocols.toArray(String[]::new)); | |
| } | |
| /** | |
| * Creates a fully initialized {@link HttpClientConnection} object. | |
| * | |
| * @return The created {@link HttpClientConnection} object. | |
| */ | |
| public HttpClientConnection build() { | |
| return new HttpClientConnection(this); | |
| } | |
| } | |
| private final class LoggingResponseBodySubscriber<T> implements HttpResponse.BodySubscriber<T> { | |
| private final HttpResponse.BodySubscriber<T> delegate; | |
| private final String requestId; | |
| private final long startTime; | |
| private final HttpResponse.ResponseInfo responseInfo; | |
| private final CommunicatorLogger logger; | |
| private final boolean captureBody; | |
| private ByteArrayOutputStream outputStream; | |
| private WritableByteChannel byteChannel; | |
| private LoggingResponseBodySubscriber(HttpResponse.BodySubscriber<T> delegate, String requestId, long startTime, | |
| HttpResponse.ResponseInfo responseInfo, CommunicatorLogger logger) { | |
| this.delegate = delegate; | |
| this.requestId = requestId; | |
| this.startTime = startTime; | |
| this.responseInfo = responseInfo; | |
| this.logger = logger; | |
| String contentType = getContentType(responseInfo.headers()); | |
| this.captureBody = !isBinaryContent(contentType); | |
| } | |
| @Override | |
| public void onSubscribe(Subscription subscription) { | |
| if (captureBody) { | |
| outputStream = new ByteArrayOutputStream(); | |
| byteChannel = Channels.newChannel(outputStream); | |
| } | |
| subscription.request(Long.MAX_VALUE); | |
| delegate.onSubscribe(subscription); | |
| } | |
| @Override | |
| public void onNext(List<ByteBuffer> item) { | |
| if (captureBody) { | |
| item.forEach(this::onNext); | |
| } | |
| delegate.onNext(item); | |
| } | |
| private void onNext(ByteBuffer item) { | |
| int position = item.position(); | |
| int limit = item.limit(); | |
| try { | |
| byteChannel.write(item); | |
| } catch (IOException e) { | |
| throw new UncheckedIOException(e); | |
| } finally { | |
| item.position(position).limit(limit); | |
| } | |
| } | |
| @Override | |
| public void onError(Throwable throwable) { | |
| log(throwable); | |
| delegate.onError(throwable); | |
| } | |
| @Override | |
| public void onComplete() { | |
| log(null); | |
| delegate.onComplete(); | |
| } | |
| @Override | |
| public CompletionStage<T> getBody() { | |
| return delegate.getBody(); | |
| } | |
| private void log(Throwable throwable) { | |
| String body = outputStream != null ? outputStream.toString(StandardCharsets.UTF_8) : null; | |
| logResponse(responseInfo, requestId, body, startTime, throwable, logger); | |
| } | |
| private void logResponse(HttpResponse.ResponseInfo responseInfo, String requestId, String body, long startTime, Throwable throwable, | |
| CommunicatorLogger logger) { | |
| long endTime = System.currentTimeMillis(); | |
| long duration = endTime - startTime; | |
| try { | |
| int statusCode = responseInfo.statusCode(); | |
| ResponseLogMessageBuilder logMessageBuilder = new ResponseLogMessageBuilder(requestId, statusCode, duration, | |
| bodyObfuscator.get(), headerObfuscator.get()); | |
| HttpHeaders responseHeaders = responseInfo.headers(); | |
| addHeaders(logMessageBuilder, responseHeaders); | |
| String contentType = getContentType(responseHeaders); | |
| boolean isBinaryContent = isBinaryContent(contentType); | |
| setBody(logMessageBuilder, body, contentType, isBinaryContent); | |
| if (throwable == null) { | |
| logger.log(logMessageBuilder.getMessage()); | |
| } else { | |
| logger.log(logMessageBuilder.getMessage(), throwable); | |
| } | |
| } catch (Exception e) { | |
| logger.log(String.format("An error occurred trying to log response '%s'", requestId), e); | |
| } | |
| } | |
| } | |
| private static final class JsonRequestBodyPublisher implements HttpRequest.BodyPublisher { | |
| private final HttpRequest.BodyPublisher delegate; | |
| private final String json; | |
| JsonRequestBodyPublisher(String json, Charset charset) { | |
| this.delegate = HttpRequest.BodyPublishers.ofString(json, charset); | |
| this.json = json; | |
| } | |
| @Override | |
| public void subscribe(Subscriber<? super ByteBuffer> subscriber) { | |
| delegate.subscribe(subscriber); | |
| } | |
| @Override | |
| public long contentLength() { | |
| return delegate.contentLength(); | |
| } | |
| } | |
| private static final class MultipartBodyPublisher implements HttpRequest.BodyPublisher { | |
| private static final String LINE_BREAK = "\r\n"; | |
| private final HttpRequest.BodyPublisher delegate; | |
| private MultipartBodyPublisher(MultipartFormDataObject multipart) { | |
| String boundary = multipart.getBoundary(); | |
| List<HttpRequest.BodyPublisher> parts = new ArrayList<>(); | |
| for (Map.Entry<String, String> entry : multipart.getValues().entrySet()) { | |
| addPart(entry.getKey(), entry.getValue(), boundary, parts); | |
| } | |
| for (Map.Entry<String, UploadableFile> entry : multipart.getFiles().entrySet()) { | |
| addPart(entry.getKey(), entry.getValue(), boundary, parts); | |
| } | |
| parts.add(HttpRequest.BodyPublishers.ofString("--" + boundary + "--" + LINE_BREAK, CHARSET)); | |
| this.delegate = HttpRequest.BodyPublishers.concat(parts.toArray(HttpRequest.BodyPublisher[]::new)); | |
| } | |
| private void addPart(String name, String value, String boundary, List<HttpRequest.BodyPublisher> parts) { | |
| StringBuilder part = new StringBuilder(); | |
| addBoundary(boundary, part); | |
| addContentDisposition(name, null, part); | |
| part.append(LINE_BREAK); | |
| part.append(value).append(LINE_BREAK); | |
| parts.add(HttpRequest.BodyPublishers.ofString(part.toString(), CHARSET)); | |
| } | |
| private void addPart(String name, UploadableFile file, String boundary, List<HttpRequest.BodyPublisher> parts) { | |
| StringBuilder part = new StringBuilder(); | |
| addBoundary(boundary, part); | |
| addContentDisposition(name, file.getFileName(), part); | |
| addContentType(file.getContentType(), part); | |
| part.append(LINE_BREAK); | |
| parts.add(HttpRequest.BodyPublishers.ofString(part.toString(), CHARSET)); | |
| parts.add(new UploadableFileContentBodyPublisher(file)); | |
| parts.add(HttpRequest.BodyPublishers.ofString(LINE_BREAK, CHARSET)); | |
| } | |
| private void addBoundary(String boundary, StringBuilder part) { | |
| part.append("--").append(boundary).append(LINE_BREAK); | |
| } | |
| private void addContentDisposition(String name, String fileName, StringBuilder part) { | |
| part.append("Content-Disposition: form-data; name=\"").append(name).append('"'); | |
| if (fileName != null) { | |
| part.append("; filename=\"").append(fileName).append('"'); | |
| } | |
| part.append(LINE_BREAK); | |
| } | |
| private void addContentType(String contentType, StringBuilder part) { | |
| part.append("Content-Type: ").append(contentType).append(LINE_BREAK); | |
| } | |
| @Override | |
| public void subscribe(Subscriber<? super ByteBuffer> subscriber) { | |
| delegate.subscribe(subscriber); | |
| } | |
| @Override | |
| public long contentLength() { | |
| return delegate.contentLength(); | |
| } | |
| } | |
| private static final class UploadableFileContentBodyPublisher implements HttpRequest.BodyPublisher { | |
| private final HttpRequest.BodyPublisher delegate; | |
| private final long contentLength; | |
| private UploadableFileContentBodyPublisher(UploadableFile file) { | |
| this.delegate = HttpRequest.BodyPublishers.ofInputStream(file::getContent); | |
| this.contentLength = file.getContentLength(); | |
| } | |
| @Override | |
| public void subscribe(Subscriber<? super ByteBuffer> subscriber) { | |
| delegate.subscribe(subscriber); | |
| } | |
| @Override | |
| public long contentLength() { | |
| return contentLength; | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment