From 43ce89cda614a682200e5c5c48b39b7dbf717385 Mon Sep 17 00:00:00 2001
From: Ibrahim Tallouzi <110388796+iyt-trifork@users.noreply.github.com>
Date: Fri, 6 Dec 2024 19:46:18 +0100
Subject: [PATCH] Introduce hapi-fhir-client-apache-http5 module for Apache
HttpClient 5 support (#6520)
* feat: Introduce hapi-fhir-client-apache-http5 module for Apache HttpClient 5 support
- Added a new module `hapi-fhir-client-apache-http5` to provide HAPI FHIR Client functionality using Apache HttpClient 5.
- Supports gradual migration from HttpClient 4 to HttpClient 5.
- Aligns with Spring Boot 3.0's adoption of HttpClient 5, enabling consistent HTTP client configuration for users of both libraries.
Key Changes:
- Integrated Apache HttpClient 5 for modern, high-performance HTTP requests.
- Ensured compatibility with existing `hapi-fhir-client` and `hapi-fhir-client-okhttp` modules.
- Added basic tests to validate functionality and coexistence of HttpClient 4 and 5.
Impact:
- Non-breaking change; the new module can be adopted independently.
- Facilitates eventual migration of HAPI FHIR to HttpClient 5 across the codebase."
* Add new error codes to the apache-httpclient5 client module.
---
azure-pipelines.yml | 2 +
hapi-fhir-bom/pom.xml | 5 +
hapi-fhir-client-apache-http5/pom.xml | 60 ++++++
.../ApacheHttp5GZipContentInterceptor.java | 74 ++++++++
.../ApacheHttp5ModifiedStringResponse.java | 135 +++++++++++++
.../client/apache/ApacheHttp5Request.java | 131 +++++++++++++
.../apache/ApacheHttp5ResourceEntity.java | 42 +++++
.../client/apache/ApacheHttp5Response.java | 177 ++++++++++++++++++
.../apache/ApacheHttp5RestfulClient.java | 142 ++++++++++++++
.../ApacheHttp5RestfulClientFactory.java | 170 +++++++++++++++++
.../tls/ApacheHttp5TlsAuthenticationSvc.java | 148 +++++++++++++++
.../client/apache/ApacheHttp5RequestTest.java | 40 ++++
.../ApacheHttp5TlsAuthenticationSvcTest.java | 158 ++++++++++++++++
.../src/test/resources/client-keystore.p12 | Bin 0 -> 4424 bytes
.../src/test/resources/client-truststore.p12 | Bin 0 -> 1782 bytes
.../src/test/resources/keystore.jks | Bin 0 -> 3945 bytes
.../src/test/resources/server-keystore.p12 | Bin 0 -> 4408 bytes
.../src/test/resources/server-truststore.p12 | Bin 0 -> 1814 bytes
.../src/test/resources/truststore.jks | Bin 0 -> 1472 bytes
hapi-fhir-docs/pom.xml | 5 +
hapi-fhir-jacoco/pom.xml | 5 +
.../pom.xml | 6 +
pom.xml | 14 ++
23 files changed, 1314 insertions(+)
create mode 100644 hapi-fhir-client-apache-http5/pom.xml
create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5GZipContentInterceptor.java
create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ModifiedStringResponse.java
create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Request.java
create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ResourceEntity.java
create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Response.java
create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClient.java
create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClientFactory.java
create mode 100644 hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvc.java
create mode 100644 hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RequestTest.java
create mode 100644 hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvcTest.java
create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/client-keystore.p12
create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/client-truststore.p12
create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/keystore.jks
create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/server-keystore.p12
create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/server-truststore.p12
create mode 100644 hapi-fhir-client-apache-http5/src/test/resources/truststore.jks
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 5756e093c49..7e89540de50 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -44,6 +44,8 @@ stages:
module: hapi-fhir-cli/hapi-fhir-cli-api
- name: hapi_fhir_client
module: hapi-fhir-client
+ - name: hapi_fhir_client_apache_http5
+ module: hapi-fhir-client-apache-http5
- name: hapi_fhir_client_okhttp
module: hapi-fhir-client-okhttp
- name: hapi_fhir_converter
diff --git a/hapi-fhir-bom/pom.xml b/hapi-fhir-bom/pom.xml
index 08f577f3dff..9269f6860ef 100644
--- a/hapi-fhir-bom/pom.xml
+++ b/hapi-fhir-bom/pom.xml
@@ -44,6 +44,11 @@
hapi-fhir-client
${project.version}
+
+ ${project.groupId}
+ hapi-fhir-client-apache-http5
+ ${project.version}
+
${project.groupId}
hapi-fhir-client-okhttp
diff --git a/hapi-fhir-client-apache-http5/pom.xml b/hapi-fhir-client-apache-http5/pom.xml
new file mode 100644
index 00000000000..00c91cb4f2b
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/pom.xml
@@ -0,0 +1,60 @@
+
+ 4.0.0
+
+ ca.uhn.hapi.fhir
+ hapi-deployable-pom
+ 7.7.7-SNAPSHOT
+
+ ../hapi-deployable-pom/pom.xml
+
+
+ hapi-fhir-client-apache-http5
+ jar
+
+ HAPI FHIR - Client Framework using Apache HttpClient 5
+
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-base
+ ${project.version}
+
+
+ commons-logging
+ commons-logging
+
+
+ org.apache.httpcomponents
+ httpclient
+
+
+ org.apache.httpcomponents
+ httpcore
+
+
+
+
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+
+
+ commons-logging
+ commons-logging
+
+
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+
+
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-client
+ ${project.version}
+
+
+
diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5GZipContentInterceptor.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5GZipContentInterceptor.java
new file mode 100644
index 00000000000..8a5e8ed9a4f
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5GZipContentInterceptor.java
@@ -0,0 +1,74 @@
+/*
+ * #%L
+ * HAPI FHIR - Client Framework
+ * %%
+ * Copyright (C) 2014 - 2024 Smile CDR, Inc.
+ * %%
+ * 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.
+ * #L%
+ */
+package ca.uhn.fhir.rest.client.apache;
+
+import ca.uhn.fhir.rest.api.Constants;
+import ca.uhn.fhir.rest.client.api.IClientInterceptor;
+import ca.uhn.fhir.rest.client.api.IHttpRequest;
+import ca.uhn.fhir.rest.client.api.IHttpResponse;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Client interceptor which GZip compresses outgoing (POST/PUT) contents being uploaded
+ * from the client to the server. This can improve performance by reducing network
+ * load time.
+ */
+public class ApacheHttp5GZipContentInterceptor implements IClientInterceptor {
+ private static final org.slf4j.Logger ourLog =
+ org.slf4j.LoggerFactory.getLogger(ApacheHttp5GZipContentInterceptor.class);
+
+ @Override
+ public void interceptRequest(IHttpRequest theRequestInterface) {
+ HttpUriRequest theRequest = ((ApacheHttp5Request) theRequestInterface).getApacheRequest();
+ if (theRequest != null) {
+ Header[] encodingHeaders = theRequest.getHeaders(Constants.HEADER_CONTENT_ENCODING);
+ if (encodingHeaders == null || encodingHeaders.length == 0) {
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ GZIPOutputStream gos;
+ try {
+ gos = new GZIPOutputStream(bos);
+ theRequest.getEntity().writeTo(gos);
+ gos.finish();
+ } catch (IOException e) {
+ ourLog.warn("Failed to GZip outgoing content", e);
+ return;
+ }
+
+ byte[] byteArray = bos.toByteArray();
+ ByteArrayEntity newEntity = new ByteArrayEntity(byteArray, ContentType.APPLICATION_OCTET_STREAM);
+ theRequest.setEntity(newEntity);
+ theRequest.addHeader(Constants.HEADER_CONTENT_ENCODING, "gzip");
+ }
+ }
+ }
+
+ @Override
+ public void interceptResponse(IHttpResponse theResponse) throws IOException {
+ // nothing
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ModifiedStringResponse.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ModifiedStringResponse.java
new file mode 100644
index 00000000000..cb11c80ca12
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ModifiedStringResponse.java
@@ -0,0 +1,135 @@
+/*
+ * #%L
+ * HAPI FHIR - Client Framework
+ * %%
+ * Copyright (C) 2014 - 2024 Smile CDR, Inc.
+ * %%
+ * 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.
+ * #L%
+ */
+package ca.uhn.fhir.rest.client.apache;
+
+import ca.uhn.fhir.i18n.Msg;
+import ca.uhn.fhir.rest.client.api.IHttpResponse;
+import ca.uhn.fhir.rest.client.impl.BaseHttpResponse;
+import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
+import ca.uhn.fhir.util.StopWatch;
+import org.apache.commons.io.IOUtils;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Process a modified copy of an existing {@link IHttpResponse} with a String containing new content.
+ *
+ * Meant to be used with custom interceptors that need to hijack an existing IHttpResponse with new content.
+ */
+public class ApacheHttp5ModifiedStringResponse extends BaseHttpResponse implements IHttpResponse {
+ private static final org.slf4j.Logger ourLog =
+ org.slf4j.LoggerFactory.getLogger(ApacheHttp5ModifiedStringResponse.class);
+ private boolean myEntityBuffered = false;
+ private final String myNewContent;
+ private final IHttpResponse myOrigHttpResponse;
+ private byte[] myEntityBytes = null;
+
+ public ApacheHttp5ModifiedStringResponse(
+ IHttpResponse theOrigHttpResponse, String theNewContent, StopWatch theResponseStopWatch) {
+ super(theResponseStopWatch);
+ myOrigHttpResponse = theOrigHttpResponse;
+ myNewContent = theNewContent;
+ }
+
+ @Override
+ public void bufferEntity() throws IOException {
+ if (myEntityBuffered) {
+ return;
+ }
+ try (InputStream respEntity = readEntity()) {
+ if (respEntity != null) {
+ try {
+ myEntityBytes = IOUtils.toByteArray(respEntity);
+ } catch (IllegalStateException exception) {
+ throw new InternalErrorException(Msg.code(2580) + exception);
+ }
+ myEntityBuffered = true;
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ if (myOrigHttpResponse instanceof Closeable) {
+ try {
+ ((Closeable) myOrigHttpResponse).close();
+ } catch (IOException exception) {
+ ourLog.debug("Failed to close response", exception);
+ }
+ }
+ }
+
+ @Override
+ public Reader createReader() {
+ return new InputStreamReader(readEntity(), StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Map> getAllHeaders() {
+ return myOrigHttpResponse.getAllHeaders();
+ }
+
+ @Override
+ public List getHeaders(String theName) {
+ return myOrigHttpResponse.getHeaders(theName);
+ }
+
+ @Override
+ public String getMimeType() {
+ return myOrigHttpResponse.getMimeType();
+ }
+
+ @Override
+ public StopWatch getRequestStopWatch() {
+ return myOrigHttpResponse.getRequestStopWatch();
+ }
+
+ @Override
+ public Object getResponse() {
+ return null;
+ }
+
+ @Override
+ public int getStatus() {
+ return myOrigHttpResponse.getStatus();
+ }
+
+ @Override
+ public String getStatusInfo() {
+ return myOrigHttpResponse.getStatusInfo();
+ }
+
+ @Override
+ public InputStream readEntity() {
+ if (myEntityBuffered) {
+ return new ByteArrayInputStream(myEntityBytes);
+ } else {
+ return new ByteArrayInputStream(myNewContent.getBytes());
+ }
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Request.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Request.java
new file mode 100644
index 00000000000..a70be6c4c21
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Request.java
@@ -0,0 +1,131 @@
+/*
+ * #%L
+ * HAPI FHIR - Client Framework
+ * %%
+ * Copyright (C) 2014 - 2024 Smile CDR, Inc.
+ * %%
+ * 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.
+ * #L%
+ */
+package ca.uhn.fhir.rest.client.apache;
+
+import ca.uhn.fhir.rest.client.api.BaseHttpRequest;
+import ca.uhn.fhir.rest.client.api.IHttpRequest;
+import ca.uhn.fhir.rest.client.api.IHttpResponse;
+import ca.uhn.fhir.util.StopWatch;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.Validate;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.HttpUriRequest;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A Http Request based on Apache. This is an adapter around the class
+ * {@link HttpUriRequest HttpRequestBase }
+ *
+ * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
+ */
+public class ApacheHttp5Request extends BaseHttpRequest implements IHttpRequest {
+
+ private HttpClient myClient;
+ private HttpUriRequest myRequest;
+
+ public ApacheHttp5Request(HttpClient theClient, HttpUriRequest theApacheRequest) {
+ this.myClient = theClient;
+ this.myRequest = theApacheRequest;
+ }
+
+ @Override
+ public void addHeader(String theName, String theValue) {
+ myRequest.addHeader(theName, theValue);
+ }
+
+ @Override
+ public IHttpResponse execute() throws IOException {
+ StopWatch responseStopWatch = new StopWatch();
+ return myClient.execute(myRequest, httpResponse -> new ApacheHttp5Response(httpResponse, responseStopWatch));
+ }
+
+ @Override
+ public Map> getAllHeaders() {
+ Map> result = new HashMap<>();
+ for (Header header : myRequest.getHeaders()) {
+ if (!result.containsKey(header.getName())) {
+ result.put(header.getName(), new LinkedList<>());
+ }
+ result.get(header.getName()).add(header.getValue());
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ /**
+ * Get the ApacheRequest
+ *
+ * @return the ApacheRequest
+ */
+ public HttpUriRequest getApacheRequest() {
+ return myRequest;
+ }
+
+ @Override
+ public String getHttpVerbName() {
+ return myRequest.getMethod();
+ }
+
+ @Override
+ public void removeHeaders(String theHeaderName) {
+ Validate.notBlank(theHeaderName, "theHeaderName must not be null or blank");
+ myRequest.removeHeaders(theHeaderName);
+ }
+
+ @Override
+ public String getRequestBodyFromStream() throws IOException {
+ if (myRequest != null) {
+ HttpEntity entity = myRequest.getEntity();
+ if (entity.isRepeatable()) {
+ final Header contentTypeHeader = myRequest.getFirstHeader("Content-Type");
+ Charset charset = contentTypeHeader == null
+ ? null
+ : ContentType.parse(contentTypeHeader.getValue()).getCharset();
+ return IOUtils.toString(entity.getContent(), charset);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String getUri() {
+ return myRequest.getRequestUri().toString();
+ }
+
+ @Override
+ public void setUri(String theUrl) {
+ myRequest.setUri(URI.create(theUrl));
+ }
+
+ @Override
+ public String toString() {
+ return myRequest.toString();
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ResourceEntity.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ResourceEntity.java
new file mode 100644
index 00000000000..a0e97607654
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5ResourceEntity.java
@@ -0,0 +1,42 @@
+/*-
+ * #%L
+ * HAPI FHIR - Client Framework
+ * %%
+ * Copyright (C) 2014 - 2024 Smile CDR, Inc.
+ * %%
+ * 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.
+ * #L%
+ */
+package ca.uhn.fhir.rest.client.apache;
+
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.rest.api.Constants;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.hl7.fhir.instance.model.api.IBaseResource;
+
+import java.nio.charset.UnsupportedCharsetException;
+
+/**
+ * Apache HttpClient request content entity where the body is a FHIR resource, that will
+ * be encoded as JSON by default
+ */
+public class ApacheHttp5ResourceEntity extends StringEntity {
+
+ public ApacheHttp5ResourceEntity(FhirContext theContext, IBaseResource theResource)
+ throws UnsupportedCharsetException {
+ super(
+ theContext.newJsonParser().encodeResourceToString(theResource),
+ ContentType.parse(Constants.CT_FHIR_JSON_NEW));
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Response.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Response.java
new file mode 100644
index 00000000000..a9b7aefabf1
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5Response.java
@@ -0,0 +1,177 @@
+/*
+ * #%L
+ * HAPI FHIR - Client Framework
+ * %%
+ * Copyright (C) 2014 - 2024 Smile CDR, Inc.
+ * %%
+ * 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.
+ * #L%
+ */
+package ca.uhn.fhir.rest.client.apache;
+
+import ca.uhn.fhir.i18n.Msg;
+import ca.uhn.fhir.rest.api.Constants;
+import ca.uhn.fhir.rest.client.api.IHttpResponse;
+import ca.uhn.fhir.rest.client.impl.BaseHttpResponse;
+import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
+import ca.uhn.fhir.util.StopWatch;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpResponse;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A Http Response based on Apache. This is an adapter around the class
+ * {@link org.apache.hc.core5.http.ClassicHttpResponse HttpResponse}
+ *
+ * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
+ */
+public class ApacheHttp5Response extends BaseHttpResponse implements IHttpResponse {
+
+ private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(ApacheHttp5Response.class);
+
+ private boolean myEntityBuffered = false;
+ private byte[] myEntityBytes;
+ private final ClassicHttpResponse myResponse;
+
+ public ApacheHttp5Response(ClassicHttpResponse theResponse, StopWatch theResponseStopWatch) {
+ super(theResponseStopWatch);
+ this.myResponse = theResponse;
+ }
+
+ @Override
+ public void bufferEntity() throws IOException {
+ if (myEntityBuffered) {
+ return;
+ }
+ try (InputStream respEntity = readEntity()) {
+ if (respEntity != null) {
+ this.myEntityBuffered = true;
+ try {
+ this.myEntityBytes = IOUtils.toByteArray(respEntity);
+ } catch (IllegalStateException e) {
+ throw new InternalErrorException(Msg.code(2581) + e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ if (myResponse instanceof CloseableHttpResponse) {
+ try {
+ myResponse.close();
+ } catch (IOException e) {
+ ourLog.debug("Failed to close response", e);
+ }
+ }
+ }
+
+ @Override
+ public Reader createReader() throws IOException {
+ HttpEntity entity = myResponse.getEntity();
+ if (entity == null) {
+ return new StringReader("");
+ }
+ Charset charset = null;
+ String contentType = entity.getContentType();
+ if (StringUtils.isNotBlank(contentType)) {
+ ContentType ct = ContentType.parse(contentType);
+ charset = ct.getCharset();
+ }
+ if (charset == null) {
+ if (Constants.STATUS_HTTP_204_NO_CONTENT != myResponse.getCode()) {
+ ourLog.debug("Response did not specify a charset, defaulting to utf-8");
+ }
+ charset = StandardCharsets.UTF_8;
+ }
+
+ return new InputStreamReader(readEntity(), charset);
+ }
+
+ @Override
+ public Map> getAllHeaders() {
+ Map> headers = new HashMap<>();
+ Header[] allHeaders = myResponse.getHeaders();
+ if (allHeaders != null) {
+ for (Header next : allHeaders) {
+ String name = next.getName().toLowerCase();
+ List list = headers.computeIfAbsent(name, k -> new ArrayList<>());
+ list.add(next.getValue());
+ }
+ }
+ return headers;
+ }
+
+ @Override
+ public List getHeaders(String theName) {
+ Header[] headers = myResponse.getHeaders(theName);
+ if (headers == null) {
+ headers = new Header[0];
+ }
+ List retVal = new ArrayList<>();
+ for (Header next : headers) {
+ retVal.add(next.getValue());
+ }
+ return retVal;
+ }
+
+ @Override
+ public String getMimeType() {
+ ContentType ct = ContentType.parse(myResponse.getEntity().getContentType());
+ return ct != null ? ct.getMimeType() : null;
+ }
+
+ @Override
+ public HttpResponse getResponse() {
+ return myResponse;
+ }
+
+ @Override
+ public int getStatus() {
+ return myResponse.getCode();
+ }
+
+ @Override
+ public String getStatusInfo() {
+ return myResponse.getReasonPhrase();
+ }
+
+ @Override
+ public InputStream readEntity() throws IOException {
+ if (this.myEntityBuffered) {
+ return new ByteArrayInputStream(myEntityBytes);
+ } else if (myResponse.getEntity() != null) {
+ return myResponse.getEntity().getContent();
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClient.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClient.java
new file mode 100644
index 00000000000..d7ecd6f761c
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClient.java
@@ -0,0 +1,142 @@
+/*
+ * #%L
+ * HAPI FHIR - Client Framework
+ * %%
+ * Copyright (C) 2014 - 2024 Smile CDR, Inc.
+ * %%
+ * 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.
+ * #L%
+ */
+package ca.uhn.fhir.rest.client.apache;
+
+import ca.uhn.fhir.rest.api.RequestTypeEnum;
+import ca.uhn.fhir.rest.client.api.Header;
+import ca.uhn.fhir.rest.client.api.IHttpClient;
+import ca.uhn.fhir.rest.client.api.IHttpRequest;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.classic.methods.*;
+import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
+import org.apache.hc.core5.http.HttpEntity;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.NameValuePair;
+import org.apache.hc.core5.http.io.entity.ByteArrayEntity;
+import org.apache.hc.core5.http.message.BasicNameValuePair;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import static org.apache.hc.core5.http.ContentType.APPLICATION_OCTET_STREAM;
+
+/**
+ * A Http Client based on Apache. This is an adapter around the class
+ * {@link org.apache.hc.client5.http.classic HttpClient}
+ *
+ * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
+ */
+public class ApacheHttp5RestfulClient extends BaseHttpClient implements IHttpClient {
+
+ private final HttpClient myClient;
+ private final HttpHost host;
+
+ public ApacheHttp5RestfulClient(
+ HttpClient theClient,
+ StringBuilder theUrl,
+ Map> theIfNoneExistParams,
+ String theIfNoneExistString,
+ RequestTypeEnum theRequestType,
+ List theHeaders) {
+ super(theUrl, theIfNoneExistParams, theIfNoneExistString, theRequestType, theHeaders);
+ this.myClient = theClient;
+ this.host = new HttpHost(theUrl.toString());
+ }
+
+ private HttpUriRequestBase constructRequestBase(HttpEntity theEntity) {
+ String url = myUrl.toString();
+ switch (myRequestType) {
+ case DELETE:
+ return new HttpDelete(url);
+ case PATCH:
+ HttpPatch httpPatch = new HttpPatch(url);
+ httpPatch.setEntity(theEntity);
+ return httpPatch;
+ case OPTIONS:
+ return new HttpOptions(url);
+ case POST:
+ HttpPost httpPost = new HttpPost(url);
+ httpPost.setEntity(theEntity);
+ return httpPost;
+ case PUT:
+ HttpPut httpPut = new HttpPut(url);
+ httpPut.setEntity(theEntity);
+ return httpPut;
+ case GET:
+ default:
+ return new HttpGet(url);
+ }
+ }
+
+ private UrlEncodedFormEntity createFormEntity(List parameters) {
+ return new UrlEncodedFormEntity(parameters, StandardCharsets.UTF_8);
+ }
+
+ @Override
+ protected IHttpRequest createHttpRequest() {
+ return createHttpRequest((HttpEntity) null);
+ }
+
+ @Override
+ protected IHttpRequest createHttpRequest(byte[] content) {
+ /*
+ * Note: Be careful about changing which constructor we use for
+ * ByteArrayEntity, as Android's version of HTTPClient doesn't support
+ * the newer ones for whatever reason.
+ */
+ ByteArrayEntity entity = new ByteArrayEntity(content, APPLICATION_OCTET_STREAM);
+ return createHttpRequest(entity);
+ }
+
+ private ApacheHttp5Request createHttpRequest(HttpEntity theEntity) {
+ HttpUriRequest request = constructRequestBase(theEntity);
+ return new ApacheHttp5Request(myClient, request);
+ }
+
+ @Override
+ protected IHttpRequest createHttpRequest(Map> theParams) {
+ List parameters = new ArrayList<>();
+ for (Entry> nextParam : theParams.entrySet()) {
+ List value = nextParam.getValue();
+ for (String s : value) {
+ parameters.add(new BasicNameValuePair(nextParam.getKey(), s));
+ }
+ }
+
+ UrlEncodedFormEntity entity = createFormEntity(parameters);
+ return createHttpRequest(entity);
+ }
+
+ @Override
+ protected IHttpRequest createHttpRequest(String theContents) {
+ /*
+ * We aren't using a StringEntity here because the constructors
+ * supported by Android aren't available in non-Android, and vice versa.
+ * Since we add the content type header manually, it makes no difference
+ * which one we use anyhow.
+ */
+ ByteArrayEntity entity =
+ new ByteArrayEntity(theContents.getBytes(StandardCharsets.UTF_8), APPLICATION_OCTET_STREAM);
+ return createHttpRequest(entity);
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClientFactory.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClientFactory.java
new file mode 100644
index 00000000000..979d0910169
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RestfulClientFactory.java
@@ -0,0 +1,170 @@
+/*
+ * #%L
+ * HAPI FHIR - Client Framework
+ * %%
+ * Copyright (C) 2014 - 2024 Smile CDR, Inc.
+ * %%
+ * 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.
+ * #L%
+ */
+package ca.uhn.fhir.rest.client.apache;
+
+import ca.uhn.fhir.context.FhirContext;
+import ca.uhn.fhir.rest.api.RequestTypeEnum;
+import ca.uhn.fhir.rest.client.api.Header;
+import ca.uhn.fhir.rest.client.api.IHttpClient;
+import ca.uhn.fhir.rest.client.impl.RestfulClientFactory;
+import org.apache.hc.client5.http.auth.AuthScope;
+import org.apache.hc.client5.http.auth.UsernamePasswordCredentials;
+import org.apache.hc.client5.http.classic.HttpClient;
+import org.apache.hc.client5.http.config.ConnectionConfig;
+import org.apache.hc.client5.http.config.RequestConfig;
+import org.apache.hc.client5.http.impl.DefaultAuthenticationStrategy;
+import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider;
+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.core5.http.HttpHost;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.util.TimeValue;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+/**
+ * A Restful Factory to create clients, requests and responses based on the Apache httpclient library.
+ *
+ * @author Peter Van Houte | peter.vanhoute@agfa.com | Agfa Healthcare
+ */
+public class ApacheHttp5RestfulClientFactory extends RestfulClientFactory {
+
+ private HttpClient myHttpClient;
+ private HttpHost myProxy;
+
+ /**
+ * Constructor
+ */
+ public ApacheHttp5RestfulClientFactory() {
+ super();
+ }
+
+ /**
+ * Constructor
+ *
+ * @param theContext
+ * The context
+ */
+ public ApacheHttp5RestfulClientFactory(FhirContext theContext) {
+ super(theContext);
+ }
+
+ @Override
+ protected synchronized IHttpClient getHttpClient(String theServerBase) {
+ return getHttpClient(new StringBuilder(theServerBase), null, null, null, null);
+ }
+
+ @Override
+ public synchronized IHttpClient getHttpClient(
+ StringBuilder theUrl,
+ Map> theIfNoneExistParams,
+ String theIfNoneExistString,
+ RequestTypeEnum theRequestType,
+ List theHeaders) {
+ return new ApacheHttp5RestfulClient(
+ getNativeHttpClient(), theUrl, theIfNoneExistParams, theIfNoneExistString, theRequestType, theHeaders);
+ }
+
+ public HttpClient getNativeHttpClient() {
+ if (myHttpClient == null) {
+ ConnectionConfig connectionConfig = ConnectionConfig.custom()
+ .setConnectTimeout(getConnectTimeout(), TimeUnit.MILLISECONDS)
+ .build();
+
+ RequestConfig defaultRequestConfig = RequestConfig.custom()
+ .setResponseTimeout(getSocketTimeout(), TimeUnit.MILLISECONDS)
+ .setConnectionRequestTimeout(getConnectionRequestTimeout(), TimeUnit.MILLISECONDS)
+ .build();
+
+ SocketConfig socketConfig = SocketConfig.custom()
+ .setSoTimeout(getSocketTimeout(), TimeUnit.MILLISECONDS)
+ .build();
+
+ HttpClientBuilder builder = getHttpClientBuilder()
+ .useSystemProperties()
+ .setDefaultRequestConfig(defaultRequestConfig)
+ .disableCookieManagement();
+
+ PoolingHttpClientConnectionManager connectionManager =
+ createPoolingHttpClientConnectionManager(socketConfig, connectionConfig);
+ builder.setConnectionManager(connectionManager);
+
+ if (myProxy != null && isNotBlank(getProxyUsername()) && isNotBlank(getProxyPassword())) {
+ BasicCredentialsProvider credsProvider = new BasicCredentialsProvider();
+ credsProvider.setCredentials(
+ new AuthScope(myProxy.getHostName(), myProxy.getPort()),
+ new UsernamePasswordCredentials(
+ getProxyUsername(), getProxyPassword().toCharArray()));
+ builder.setProxyAuthenticationStrategy(new DefaultAuthenticationStrategy());
+ builder.setDefaultCredentialsProvider(credsProvider);
+ }
+
+ myHttpClient = builder.build();
+ }
+
+ return myHttpClient;
+ }
+
+ private PoolingHttpClientConnectionManager createPoolingHttpClientConnectionManager(
+ SocketConfig socketConfig, ConnectionConfig connectionConfig) {
+ PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
+ connectionManager.setMaxTotal(getPoolMaxTotal());
+ connectionManager.setDefaultMaxPerRoute(getPoolMaxPerRoute());
+ connectionManager.setDefaultSocketConfig(socketConfig);
+ connectionManager.setDefaultConnectionConfig(connectionConfig);
+ connectionManager.setConnectionConfigResolver(route -> ConnectionConfig.custom()
+ .setValidateAfterInactivity(
+ TimeValue.ofSeconds(5)) // Validate connections after 5 seconds of inactivity
+ .build());
+ return connectionManager;
+ }
+
+ protected HttpClientBuilder getHttpClientBuilder() {
+ return HttpClients.custom();
+ }
+
+ @Override
+ protected void resetHttpClient() {
+ this.myHttpClient = null;
+ }
+
+ /**
+ * Only allows to set an instance of type org.apache.hc.client5.http.classic.HttpClient
+ * @see ca.uhn.fhir.rest.client.api.IRestfulClientFactory#setHttpClient(Object)
+ */
+ @Override
+ public synchronized void setHttpClient(Object theHttpClient) {
+ this.myHttpClient = (HttpClient) theHttpClient;
+ }
+
+ @Override
+ public void setProxy(String theHost, Integer thePort) {
+ if (theHost != null) {
+ myProxy = new HttpHost("http", theHost, thePort);
+ } else {
+ myProxy = null;
+ }
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvc.java b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvc.java
new file mode 100644
index 00000000000..b791f8dd122
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/main/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvc.java
@@ -0,0 +1,148 @@
+/*-
+ * #%L
+ * HAPI FHIR - Client Framework
+ * %%
+ * Copyright (C) 2014 - 2024 Smile CDR, Inc.
+ * %%
+ * 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.
+ * #L%
+ */
+package ca.uhn.fhir.rest.client.tls;
+
+import ca.uhn.fhir.i18n.Msg;
+import ca.uhn.fhir.tls.BaseStoreInfo;
+import ca.uhn.fhir.tls.KeyStoreInfo;
+import ca.uhn.fhir.tls.PathType;
+import ca.uhn.fhir.tls.TlsAuthentication;
+import ca.uhn.fhir.tls.TrustStoreInfo;
+import jakarta.annotation.Nonnull;
+import org.apache.commons.lang3.Validate;
+import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
+import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy;
+import org.apache.hc.core5.ssl.PrivateKeyStrategy;
+import org.apache.hc.core5.ssl.SSLContextBuilder;
+import org.apache.hc.core5.ssl.SSLContexts;
+
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.util.Optional;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509TrustManager;
+
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+public class ApacheHttp5TlsAuthenticationSvc {
+
+ private ApacheHttp5TlsAuthenticationSvc() {}
+
+ public static SSLContext createSslContext(@Nonnull TlsAuthentication theTlsAuthentication) {
+ Validate.notNull(theTlsAuthentication, "theTlsAuthentication cannot be null");
+
+ try {
+ SSLContextBuilder contextBuilder = SSLContexts.custom();
+
+ if (theTlsAuthentication.getKeyStoreInfo().isPresent()) {
+ KeyStoreInfo keyStoreInfo =
+ theTlsAuthentication.getKeyStoreInfo().get();
+ PrivateKeyStrategy privateKeyStrategy = null;
+ if (isNotBlank(keyStoreInfo.getAlias())) {
+ privateKeyStrategy = (aliases, socket) -> keyStoreInfo.getAlias();
+ }
+ KeyStore keyStore = createKeyStore(keyStoreInfo);
+ contextBuilder.loadKeyMaterial(keyStore, keyStoreInfo.getKeyPass(), privateKeyStrategy);
+ }
+
+ if (theTlsAuthentication.getTrustStoreInfo().isPresent()) {
+ TrustStoreInfo trustStoreInfo =
+ theTlsAuthentication.getTrustStoreInfo().get();
+ KeyStore trustStore = createKeyStore(trustStoreInfo);
+ contextBuilder.loadTrustMaterial(trustStore, TrustSelfSignedStrategy.INSTANCE);
+ }
+
+ return contextBuilder.build();
+ } catch (Exception e) {
+ throw new TlsAuthenticationException(Msg.code(2575) + "Failed to create SSLContext", e);
+ }
+ }
+
+ public static KeyStore createKeyStore(BaseStoreInfo theStoreInfo) {
+ try {
+ KeyStore keyStore = KeyStore.getInstance(theStoreInfo.getType().toString());
+
+ if (PathType.RESOURCE.equals(theStoreInfo.getPathType())) {
+ try (InputStream inputStream =
+ ApacheHttp5TlsAuthenticationSvc.class.getResourceAsStream(theStoreInfo.getFilePath())) {
+ validateKeyStoreExists(inputStream);
+ keyStore.load(inputStream, theStoreInfo.getStorePass());
+ }
+ } else if (PathType.FILE.equals(theStoreInfo.getPathType())) {
+ try (InputStream inputStream = new FileInputStream(theStoreInfo.getFilePath())) {
+ validateKeyStoreExists(inputStream);
+ keyStore.load(inputStream, theStoreInfo.getStorePass());
+ }
+ }
+ return keyStore;
+ } catch (Exception e) {
+ throw new TlsAuthenticationException(Msg.code(2576) + "Failed to create KeyStore", e);
+ }
+ }
+
+ public static void validateKeyStoreExists(InputStream theInputStream) {
+ if (theInputStream == null) {
+ throw new TlsAuthenticationException(Msg.code(2577) + "Keystore does not exists");
+ }
+ }
+
+ public static X509TrustManager createTrustManager(Optional theTrustStoreInfo) {
+ try {
+ TrustManagerFactory trustManagerFactory =
+ TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ if (!theTrustStoreInfo.isPresent()) {
+ trustManagerFactory.init((KeyStore) null); // Load Trust Manager Factory with default Java truststore
+ } else {
+ TrustStoreInfo trustStoreInfo = theTrustStoreInfo.get();
+ KeyStore trustStore = createKeyStore(trustStoreInfo);
+ trustManagerFactory.init(trustStore);
+ }
+ for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
+ if (trustManager instanceof X509TrustManager) {
+ return (X509TrustManager) trustManager;
+ }
+ }
+ throw new TlsAuthenticationException(Msg.code(2578) + "Could not find X509TrustManager");
+ } catch (Exception e) {
+ throw new TlsAuthenticationException(Msg.code(2579) + "Failed to create X509TrustManager");
+ }
+ }
+
+ public static HostnameVerifier createHostnameVerifier(Optional theTrustStoreInfo) {
+ return theTrustStoreInfo.isPresent() ? new DefaultHostnameVerifier() : new NoopHostnameVerifier();
+ }
+
+ public static class TlsAuthenticationException extends RuntimeException {
+ private static final long serialVersionUID = 1l;
+
+ public TlsAuthenticationException(String theMessage, Throwable theCause) {
+ super(theMessage, theCause);
+ }
+
+ public TlsAuthenticationException(String theMessage) {
+ super(theMessage);
+ }
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RequestTest.java b/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RequestTest.java
new file mode 100644
index 00000000000..0ea69159bad
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/apache/ApacheHttp5RequestTest.java
@@ -0,0 +1,40 @@
+package ca.uhn.fhir.rest.client.apache;
+
+import org.apache.hc.client5.http.classic.methods.HttpPost;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ApacheHttp5RequestTest {
+
+ private final String ENTITY_CONTENT = "Some entity with special characters: é";
+ private StringEntity entity;
+ private final HttpPost request = new HttpPost("");
+
+ @Test
+ public void testGetRequestBodyFromStream() throws IOException {
+ entity = new StringEntity(ENTITY_CONTENT, StandardCharsets.ISO_8859_1);
+ request.setHeader("Content-type", "text/plain; charset=ISO-8859-1");
+ request.setEntity(entity);
+
+ String result = new ApacheHttp5Request(null, request).getRequestBodyFromStream();
+
+ assertEquals(ENTITY_CONTENT, result);
+ }
+
+ @Test
+ public void testGetRequestBodyFromStreamWithDefaultCharset() throws IOException {
+ entity = new StringEntity(ENTITY_CONTENT, Charset.defaultCharset());
+ request.setHeader("Content-type", "text/plain");
+ request.setEntity(entity);
+
+ String result = new ApacheHttp5Request(null, request).getRequestBodyFromStream();
+
+ assertEquals(ENTITY_CONTENT, result);
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvcTest.java b/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvcTest.java
new file mode 100644
index 00000000000..42caea25cbf
--- /dev/null
+++ b/hapi-fhir-client-apache-http5/src/test/java/ca/uhn/fhir/rest/client/tls/ApacheHttp5TlsAuthenticationSvcTest.java
@@ -0,0 +1,158 @@
+package ca.uhn.fhir.rest.client.tls;
+
+import ca.uhn.fhir.i18n.Msg;
+import ca.uhn.fhir.tls.KeyStoreInfo;
+import ca.uhn.fhir.tls.TlsAuthentication;
+import ca.uhn.fhir.tls.TrustStoreInfo;
+import org.apache.hc.client5.http.ssl.DefaultHostnameVerifier;
+import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.X509TrustManager;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+
+public class ApacheHttp5TlsAuthenticationSvcTest {
+
+ private KeyStoreInfo myServerKeyStoreInfo;
+ private TrustStoreInfo myServerTrustStoreInfo;
+ private TlsAuthentication myServerTlsAuthentication;
+
+ private KeyStoreInfo myClientKeyStoreInfo;
+ private TrustStoreInfo myClientTrustStoreInfo;
+ private TlsAuthentication myClientTlsAuthentication;
+
+ @BeforeEach
+ public void beforeEach(){
+ myServerKeyStoreInfo = new KeyStoreInfo("classpath:/server-keystore.p12", "changeit", "changeit", "server");
+ myServerTrustStoreInfo = new TrustStoreInfo("classpath:/server-truststore.p12", "changeit", "client");
+ myServerTlsAuthentication = new TlsAuthentication(Optional.of(myServerKeyStoreInfo), Optional.of(myServerTrustStoreInfo));
+
+ myClientKeyStoreInfo = new KeyStoreInfo("classpath:/client-keystore.p12", "changeit", "changeit", "client");
+ myClientTrustStoreInfo = new TrustStoreInfo("classpath:/client-truststore.p12", "changeit", "server");
+ myClientTlsAuthentication = new TlsAuthentication(Optional.of(myClientKeyStoreInfo), Optional.of(myClientTrustStoreInfo));
+ }
+
+ @Test
+ public void testCreateSslContextEmpty(){
+ TlsAuthentication emptyAuthentication = null;
+ try {
+ ApacheHttp5TlsAuthenticationSvc.createSslContext(emptyAuthentication);
+ fail(); } catch (Exception e) {
+ assertEquals("theTlsAuthentication cannot be null", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCreateSslContextPresent(){
+ SSLContext result = ApacheHttp5TlsAuthenticationSvc.createSslContext(myServerTlsAuthentication);
+ assertEquals("TLS", result.getProtocol());
+ }
+
+ @Test
+ public void testCreateSslContextPresentInvalid(){
+ KeyStoreInfo invalidKeyStoreInfo = new KeyStoreInfo("file:///INVALID.p12", "changeit", "changeit", "server");
+ TlsAuthentication invalidTlsAuthentication = new TlsAuthentication(Optional.of(invalidKeyStoreInfo), Optional.of(myServerTrustStoreInfo));
+ try {
+ ApacheHttp5TlsAuthenticationSvc.createSslContext(invalidTlsAuthentication);
+ fail(); } catch (Exception e) {
+ assertEquals(Msg.code(2575) + "Failed to create SSLContext", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCreateKeyStoreP12() throws Exception {
+ KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(myServerKeyStoreInfo);
+ assertNotNull(keyStore.getKey(myServerKeyStoreInfo.getAlias(), myServerKeyStoreInfo.getKeyPass()));
+ }
+
+ @Test
+ public void testCreateKeyStoreJKS() throws Exception {
+ KeyStoreInfo keyStoreInfo = new KeyStoreInfo("classpath:/keystore.jks", "changeit", "changeit", "client");
+ KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(keyStoreInfo);
+ assertNotNull(keyStore.getKey(keyStoreInfo.getAlias(), keyStoreInfo.getKeyPass()));
+ }
+
+ @Test
+ public void testCreateKeyStoreNonExistentFile() {
+ KeyStoreInfo keyStoreInfo = new KeyStoreInfo("classpath:/non-existent.p12", "changeit", "changeit", "server");
+ try {
+ ApacheHttp5TlsAuthenticationSvc.createKeyStore(keyStoreInfo);
+ fail(); }
+ catch (Exception e) {
+ assertEquals(Msg.code(2576) + "Failed to create KeyStore", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCreateTrustStoreJks() throws Exception {
+ TrustStoreInfo trustStoreInfo = new TrustStoreInfo("classpath:/truststore.jks", "changeit", "client");
+ KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(trustStoreInfo);
+ assertNotNull(keyStore.getCertificate(trustStoreInfo.getAlias()));
+ }
+
+ @Test
+ public void testCreateTrustStoreP12() throws Exception {
+ KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(myServerTrustStoreInfo);
+ assertNotNull(keyStore.getCertificate(myServerTrustStoreInfo.getAlias()));
+ }
+
+ @Test
+ public void testCreateTrustStoreNonExistentFile() throws Exception {
+ TrustStoreInfo trustStoreInfo = new TrustStoreInfo("classpath:/non-existent.p12", "changeit", "server");
+ try {
+ ApacheHttp5TlsAuthenticationSvc.createKeyStore(trustStoreInfo);
+ fail(); } catch (Exception e) {
+ assertEquals(Msg.code(2576) + "Failed to create KeyStore", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCreateTrustManager() throws Exception {
+ X509TrustManager trustManager = ApacheHttp5TlsAuthenticationSvc.createTrustManager(Optional.of(myClientTrustStoreInfo));
+ KeyStore keyStore = ApacheHttp5TlsAuthenticationSvc.createKeyStore(myServerKeyStoreInfo);
+ Certificate serverCertificate = keyStore.getCertificate(myServerKeyStoreInfo.getAlias());
+
+ assertEquals(1, trustManager.getAcceptedIssuers().length);
+ assertEquals(serverCertificate, trustManager.getAcceptedIssuers()[0]);
+ }
+
+ @Test
+ public void testCreateTrustManagerNoTrustStore() {
+ // trust manager should contain common certifications if no trust store information is used
+ X509TrustManager trustManager = ApacheHttp5TlsAuthenticationSvc.createTrustManager(Optional.empty());
+ assertThat(trustManager.getAcceptedIssuers().length).isNotEqualTo(0);
+ }
+
+ @Test
+ public void testCreateTrustManagerInvalid() {
+ TrustStoreInfo invalidKeyStoreInfo = new TrustStoreInfo("file:///INVALID.p12", "changeit", "client");
+ try {
+ ApacheHttp5TlsAuthenticationSvc.createTrustManager(Optional.of(invalidKeyStoreInfo));
+ fail(); } catch (Exception e) {
+ assertEquals(Msg.code(2579) + "Failed to create X509TrustManager", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testCreateHostnameVerifierEmptyTrustStoreInfo(){
+ Optional trustStoreInfo = Optional.empty();
+ HostnameVerifier result = ApacheHttp5TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo);
+ assertEquals(NoopHostnameVerifier.class, result.getClass());
+ }
+
+ @Test
+ public void testCreateHostnameVerifierPresentTrustStoreInfo(){
+ Optional trustStoreInfo = Optional.of(myServerTrustStoreInfo);
+ HostnameVerifier result = ApacheHttp5TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo);
+ assertEquals(DefaultHostnameVerifier.class, result.getClass());
+ }
+}
diff --git a/hapi-fhir-client-apache-http5/src/test/resources/client-keystore.p12 b/hapi-fhir-client-apache-http5/src/test/resources/client-keystore.p12
new file mode 100644
index 0000000000000000000000000000000000000000..c169ab6827861698f9e14d981f02396ef8fc886a
GIT binary patch
literal 4424
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
literal 0
HcmV?d00001
diff --git a/hapi-fhir-client-apache-http5/src/test/resources/client-truststore.p12 b/hapi-fhir-client-apache-http5/src/test/resources/client-truststore.p12
new file mode 100644
index 0000000000000000000000000000000000000000..d12c71997081e84eb7c77cb38c09dba50c5b368c
GIT binary patch
literal 1782
zcmV7Duzgg_YDCD0ic2gjRb-Qi72`Yw2hW8Bt2LUi<1_>&LNQUZFWRng#81`;k7TD8Xxi8sx1NnClCSwATSID2r7n1hW8Bu2?YQ!
z9R>+thDZTr0|Wso1Q2gUT%)B6<&<*E{DV-sW+Q-t1@I1SdjZl^feORkdVb)XH|}vs
zkrvif2K0tLng^@hcztwxT9v^TXG&ZxtJ6@l=$t0tT
z<#eE8nJrwU3T|!GU)a`H0#%bBI{`qxMr=71F0iMv!2)(igtI^1kdS&1CW3g)Ridzr
z#FP|k%MJPNu@@kNU`Gq^_nX8ZD+?sY{2<=K!p2Wn6L+3+*_P2_eO0SBus-6qFJUK*
z#8FPsYz<5sEXAd9=oLM#L0W;O4^=ex(;^WFfa-jD&_#r>$mHK^9Z#3%NGFIKYB%1w
zfHj9alf4C!9fi87SgSPfbl(Z?I}=@nV@Gt$K37d(XdD}ylvDu((P*FONWnzWrIZP;cdXZ+j=esNkPK^HLdRQ8WR
z+ocJyRl>!6TzLbZ=zdePwQyy!QhQ=t6}U-F?(AmL9Z2%ns-mi}H0q}(8`x~s>bt#d
zEcJb{24_?QPj)1Q5=YiEtpXw(L~U^A-U^r5H#cJ%GNV8}n=%{1V=BnQ+Ptb)I_UJ_
zEDi)>wAExeyhOv^7ScWL+cDjXLJ(ZeEwfY&{t$GlN@y~$PkQaH{;aoxRRF4+{+K;bDQrO
zEMfv!pR@jM6>UKle+JG|jJk9AHBYPem3K={({jTsGq*^TS2P2&ta{&|?S9vh!J3tv|uAmoze
zaary(QpW1b*pd!>TdPQpVyj_-vg%zLS3R3^s
z6W53cLyT>f#1oC8hS4QuCF0_UIx+PybWhc
z;pB&FLZ%rYguqx^rKoinA}98nmTEb&ks_dXMS$fOFD|#dhx}5B53>~AhD8w*K$xqb
zO)={7!A)XLm_>hgiI4k)y4XZ@6!PgT7H3Sv8h@JdzoT3$^MFU{rs3Y088)b=MK
z`eP(PF=%7HT~6?eBPhu;rLSb5Zn<&6csmmb#r`=|m5&imAW1!qB1PExvw6r+e=+PT_q)}uJT(~y{W*mK
z(6_@ZAu43<
z>fbi0d*qZNONC9O71OfpC00bbzO{+$R-JkQjd;;qHMZ%g^Dvxjijp>&N)e+pbjh8G06z#3y
Ylixyj7^TM)2`lKFV|9Ym<^lpI5JOi(6951J
literal 0
HcmV?d00001
diff --git a/hapi-fhir-client-apache-http5/src/test/resources/keystore.jks b/hapi-fhir-client-apache-http5/src/test/resources/keystore.jks
new file mode 100644
index 0000000000000000000000000000000000000000..ccce89d0aba8bca41d13ef811a710943334c920d
GIT binary patch
literal 3945
zcma);XHXMr7KW1mq4$o6bOc30OcA6SL{Pf)jzNmF(7_7|L3%GzLKCD2gr*b`u!Is^
zgiw?&2nYeGp$bx#>)qYCv$H?;$NBNT-#PO=bLRVI-jlVHH2?qrIxFDs0;Ans?|b^4
z$?*N%kTZ`y3WBA_LRi2Im%u=3pbUr|2&4f}W9dByP|Xv(h75Y$c0HHYfeu+w&$fal
zkw~b84l^YJS*)KkYjrovpheze@>)**IbEpi4NU2}wm`}_Ktn3H-_#`FuCI0iHErZN
z_1H~Rk!yWqlE;{3?qRhvx%1G}g;YdDCs-gc%;RiI#W;qY;hVnZrVmn_mkV{V=;~hz
zKUvWdF@8yPrUkyuz~nq%qk+C52q_O!ZF^b=0e2uOA8N%-c~-D6)?+>ek@=#aQNr
z>}sM$9znUDbMe~`mI0I3d~2&WCiHz=!6(U*h5F3{0Dxyx2<0pHb@?X~cHul(NbMi*
z4X5;8u4BS2Yqt{N##3KQo}k$}F4}yca#SJ7$Es?O^4j#%H`-rNpL8UQYgQ}r@`IAb
z7hO*iw_PZ$A%x?%1D%ZQ0=(lUU;Lap-|@t*W~2u5{&X^%pW%|Cp#{Wwm68PQbI1pS
zQ7elMn|UFJKa%M{YyeLa8%Ot2YcJZio!R54pnjG65q?oy!I}Ya
z8LAbYQ!LV<c+nW}4V3jqFs*@_5mD{ZO&I^&uA81xpFc#3-)1XZfr%
zuAqq67-+%WhP?0h<`~>YZ`-I&NY&7~^&*-5=A3sQjmM8gXLoM*hCSCQd;$CTQ}a->
zj-R-Gr)aS0HL5SF;;@ULq9MLo;PeP3=XFKZr)5pjgtg4azWlX`jW(s8&Iidw?KtLK
z!xYnz;u+v!FP<;EzkR=!X5-5)24I<^;D3cNmrRo8o?kY%tD<8wfU>DN7j)UgsJ$HT
zdl~W2?}nEuEET`Xb-yaEOF@gM?$vN>Gy)Mv(|IDMm+Z)~CDx+T#WzHXX0vj-Jygm@
z{6@AAw*RahcBv}VU3^?HNKz}O^%i$>CC}C=-zIAh@bAc&SZYM1}^PBd}=q)=@rrr}=9zA-%g-F^Tf#(SySDTaH6k
zg?$9+*{)Lsfi?;C-LTM+q+BS%T2B^U8uKcW?Cm`?iCWRk3i|T$`iK-{pJWw>ioRQ4
zso^$g<3n}r&qlX#n`T@VO1=LI7pe7G>%%FE5F`h
zy
zsxlOb1f+Z}6TWRQH5vFiMO-~nC>{cPal(0f
zH~zUyAI~zofqDDR%y}lw8H7`;m7zy$!~pG+ie#*0KaQKqX~un-J*{=}rdz6u)i8N*
z&Oc2|gf~0w)@TC*`J~8aTF*QAQr;$w^nqdV0g)LzeVq3u!s+YEdnKkuuLL=srUOWs
z>6rKBS8E4P->??$W0Hj>=HgX-z2rQ1Cu3tSlw9O72<&&$rtBxQHg$Uz*B=@&=wcc&
zV-u=n_+I}E3=eXcg60ZAevH58<4)oa%HY)MHH9s)e(PgY4#n%ighT5Jxdsc{s5_P|
z6O{Q{F6^Q6oy+04-6WH(p3dVjNt5cBZf$emD9OVuu-^_nTm6pwmCXMnKX6zZ=QM53
zBYb}4+1wJr8c8&U)f%6c;1CbDqT^~Hs^&5_aX?|4D4wIQ{0NVVS}BtXOnLCS&HHo(
z>F3NP>AkmPRh$JLdkblp8-MC9(!xu=W;n?)3#=o>RCP<{FL1770`K4x*L(tW*8zh(
zggLL3^Kx~9*XPl2hw0#X;}!zkn~N~J;Pi`tsst89nz1uhUg=u#9_q8BnbPK7*6-9!
zmryC-e&?a1%tAy+-CXz4L*RELqeBH9$j-SXQYwsM)LELKb
zEEt~il+KSt<4yQgU2TK^@g~bV
zCgN(F>cZY=k>lnz2tIUK_?0JZX+s52+1P1ECnJ+oM8Q=xMOCQB*YhU3sy=heIBMz(
ze%{_g{MO1H^>P$b8OY6G<3Svwq0pEC=I*fd==
zUF9gq8Cd;#{|&xJMb>ss4Q$hDXf&BZ#^Zz|OimFI%9sb%?H~6BO{TNQi_&(daIg*U
z6`g3&z9v2mgWb`-4EX(}tn5`lxl8DWDw4pg44(>4o#YZdY8QCkD-KtbpHP#`O6ISM
zaQ*W*T(IjDV#BT14N2zHgFYP4D<-O$FjzHDzt^9x@Yo
zLE>_c>P}j2e?RbH?=lVjeB?w^dXy<)9$L%jhtLeid3UKLXL1t|;iU
zGaB5W)E(ph8#EgFt|KC)uMg4o=
zzsRv5(Z8?uEcH@hL974>7Q_I;f`EWVpZCq8Is2E4s2!MP#esclE=a^Vh{{FSW}&wzSJk~xa~o=3xPU2N?LvmJS#uillorHaaRKSidno4
zgz<+piK9q!EA5ko-NUMtXr^IYTxZm#!ydUFSu$;x6lxG9*iYwC4uKN-BC;M=oA0wb
zI<0K>`VL&U=3|3-TbSK2VODBDE%6v7`)+zUFWwz|{8X8T*_S9`siB4m9@?LMN|au|
z1O2u7*Z}nkY=r*UvN4{J$hbR5#tp|4;!^MNr~+Zh70wio*K{#MW@K-k_iWd9?yk_zg7
zJ;Wo}vURb{_31{-_BuvswaRj~67QNdPDS7*Ok??Kw7*GwA6vY54ncuxdJ=fp_yFW%%&~g41o2_)L
z^D`HN&%OwTJAa^&+aM|+;J-iL*$V*zK-4)Z9jq$$BW^BlT#4;Xmkj&fMC8qw?*&_N
z?mrSTmQ&xiQ3i($#LnLZ4AOJ7U^rEfU;NUjlr-%UOU!s1=)@|_XM*3I3~2_K0lo`+
zYMR&=;l-|JxpzEXKOGQXnr3eF;!l1Df0x(oX~>Zpl`qg+y09rC9F^AQS$l0|xQOXk
zL}jKyr0%BjQXq8o9^>AluT%&hZ8lmy
zj-&;lT-Q@5AJUz_5)c@d;cA<<1Jz$;Vh-KNaWbyK@VGAlw9SG@PPta8yKh5!kpUyoMRwsQk3kv!qV;30|$M&
z?19<(m%8lwdaGH&F@O=)@SaQoWlQ1KNkIY2eAQ7PsQ_)ckVouuDi|8Rkp053X5x!RMBc3-y+r9
pbWs0TJT{E17;SJeKCH0u9NJ+c#Zu2BjtO9OnYGA71Z;G0@t@iiFtPvu
literal 0
HcmV?d00001
diff --git a/hapi-fhir-client-apache-http5/src/test/resources/server-keystore.p12 b/hapi-fhir-client-apache-http5/src/test/resources/server-keystore.p12
new file mode 100644
index 0000000000000000000000000000000000000000..00ae45348ef293d40db10cc642ca9796abf0d700
GIT binary patch
literal 4408
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
V`iZW$UsV;_m}amDFRzrm0RS^bS#JOU
literal 0
HcmV?d00001
diff --git a/hapi-fhir-docs/pom.xml b/hapi-fhir-docs/pom.xml
index 79b067aafd4..0a55640077f 100644
--- a/hapi-fhir-docs/pom.xml
+++ b/hapi-fhir-docs/pom.xml
@@ -41,6 +41,11 @@
hapi-fhir-client
${project.version}
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-client-apache-http5
+ ${project.version}
+
ca.uhn.hapi.fhir
hapi-fhir-client-okhttp
diff --git a/hapi-fhir-jacoco/pom.xml b/hapi-fhir-jacoco/pom.xml
index 86e3fd4383b..dac274bae96 100644
--- a/hapi-fhir-jacoco/pom.xml
+++ b/hapi-fhir-jacoco/pom.xml
@@ -162,6 +162,11 @@
${project.version}
-->
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-client-apache-http5
+ ${project.version}
+
ca.uhn.hapi.fhir
hapi-fhir-client-okhttp
diff --git a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml
index 5f2a71be410..49c7e0eea55 100644
--- a/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml
+++ b/hapi-fhir-spring-boot/hapi-fhir-spring-boot-autoconfigure/pom.xml
@@ -53,6 +53,12 @@
${project.version}
true
+
+ ca.uhn.hapi.fhir
+ hapi-fhir-client-apache-http5
+ ${project.version}
+ true
+
ca.uhn.hapi.fhir
hapi-fhir-client-okhttp
diff --git a/pom.xml b/pom.xml
index 1cba1096550..82e20acaf66 100644
--- a/pom.xml
+++ b/pom.xml
@@ -115,6 +115,7 @@
hapi-fhir-jpaserver-mdm
hapi-fhir-testpage-overlay
hapi-fhir-jpaserver-uhnfhirtest
+ hapi-fhir-client-apache-http5
hapi-fhir-client-okhttp
hapi-fhir-android
hapi-fhir-cli
@@ -125,6 +126,7 @@
hapi-fhir-spring-boot
hapi-fhir-jacoco
hapi-fhir-server-cds-hooks
+
@@ -1029,6 +1031,8 @@
8.0.0.Final
4.4.13
4.5.13
+ 5.4.1
+ 5.3.1
2.17.1
2.17.1
3.3.0
@@ -1560,6 +1564,16 @@
commons-text
${commons_text_version}
+
+ org.apache.httpcomponents.core5
+ httpcore5
+ ${httpcore5_version}
+
+
+ org.apache.httpcomponents.client5
+ httpclient5
+ ${httpclient5_version}
+
org.apache.httpcomponents
httpclient