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.
This commit is contained in:
Ibrahim Tallouzi 2024-12-06 19:46:18 +01:00 committed by GitHub
parent b7e94ece95
commit 43ce89cda6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1314 additions and 0 deletions

View File

@ -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

View File

@ -44,6 +44,11 @@
<artifactId>hapi-fhir-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-client-apache-http5</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>hapi-fhir-client-okhttp</artifactId>

View File

@ -0,0 +1,60 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-deployable-pom</artifactId>
<version>7.7.7-SNAPSHOT</version>
<relativePath>../hapi-deployable-pom/pom.xml</relativePath>
</parent>
<artifactId>hapi-fhir-client-apache-http5</artifactId>
<packaging>jar</packaging>
<name>HAPI FHIR - Client Framework using Apache HttpClient 5</name>
<dependencies>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-base</artifactId>
<version>${project.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Apache HTTP Client 5 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
<groupId>commons-logging</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -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
}
}

View File

@ -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.
* <p/>
* 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<String, List<String>> getAllHeaders() {
return myOrigHttpResponse.getAllHeaders();
}
@Override
public List<String> 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());
}
}
}

View File

@ -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<String, List<String>> getAllHeaders() {
Map<String, List<String>> 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();
}
}

View File

@ -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));
}
}

View File

@ -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<String, List<String>> getAllHeaders() {
Map<String, List<String>> headers = new HashMap<>();
Header[] allHeaders = myResponse.getHeaders();
if (allHeaders != null) {
for (Header next : allHeaders) {
String name = next.getName().toLowerCase();
List<String> list = headers.computeIfAbsent(name, k -> new ArrayList<>());
list.add(next.getValue());
}
}
return headers;
}
@Override
public List<String> getHeaders(String theName) {
Header[] headers = myResponse.getHeaders(theName);
if (headers == null) {
headers = new Header[0];
}
List<String> 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;
}
}
}

View File

@ -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<String, List<String>> theIfNoneExistParams,
String theIfNoneExistString,
RequestTypeEnum theRequestType,
List<Header> 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<NameValuePair> 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<String, List<String>> theParams) {
List<NameValuePair> parameters = new ArrayList<>();
for (Entry<String, List<String>> nextParam : theParams.entrySet()) {
List<String> 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);
}
}

View File

@ -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<String, List<String>> theIfNoneExistParams,
String theIfNoneExistString,
RequestTypeEnum theRequestType,
List<Header> 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;
}
}
}

View File

@ -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<TrustStoreInfo> 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<TrustStoreInfo> 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);
}
}
}

View File

@ -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);
}
}

View File

@ -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> trustStoreInfo = Optional.empty();
HostnameVerifier result = ApacheHttp5TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo);
assertEquals(NoopHostnameVerifier.class, result.getClass());
}
@Test
public void testCreateHostnameVerifierPresentTrustStoreInfo(){
Optional<TrustStoreInfo> trustStoreInfo = Optional.of(myServerTrustStoreInfo);
HostnameVerifier result = ApacheHttp5TlsAuthenticationSvc.createHostnameVerifier(trustStoreInfo);
assertEquals(DefaultHostnameVerifier.class, result.getClass());
}
}

View File

@ -41,6 +41,11 @@
<artifactId>hapi-fhir-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client-apache-http5</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client-okhttp</artifactId>

View File

@ -162,6 +162,11 @@
<version>${project.version}</version>
</dependency>
-->
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client-apache-http5</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client-okhttp</artifactId>

View File

@ -53,6 +53,12 @@
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client-apache-http5</artifactId>
<version>${project.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>ca.uhn.hapi.fhir</groupId>
<artifactId>hapi-fhir-client-okhttp</artifactId>

14
pom.xml
View File

@ -115,6 +115,7 @@
<module>hapi-fhir-jpaserver-mdm</module>
<module>hapi-fhir-testpage-overlay</module>
<module>hapi-fhir-jpaserver-uhnfhirtest</module>
<module>hapi-fhir-client-apache-http5</module>
<module>hapi-fhir-client-okhttp</module>
<module>hapi-fhir-android</module>
<module>hapi-fhir-cli</module>
@ -125,6 +126,7 @@
<module>hapi-fhir-spring-boot</module>
<module>hapi-fhir-jacoco</module>
<module>hapi-fhir-server-cds-hooks</module>
</modules>
<dependencies>
@ -1029,6 +1031,8 @@
<hibernate_validator_version>8.0.0.Final</hibernate_validator_version>
<httpcore_version>4.4.13</httpcore_version>
<httpclient_version>4.5.13</httpclient_version>
<httpclient5_version>5.4.1</httpclient5_version>
<httpcore5_version>5.3.1</httpcore5_version>
<jackson_version>2.17.1</jackson_version>
<jackson_databind_version>2.17.1</jackson_databind_version>
<maven_assembly_plugin_version>3.3.0</maven_assembly_plugin_version>
@ -1560,6 +1564,16 @@
<artifactId>commons-text</artifactId>
<version>${commons_text_version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>${httpcore5_version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpclient5_version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>