Skip to content

Commit

Permalink
feat: Introduce hapi-fhir-client-apache-http5 module for Apache HttpC…
Browse files Browse the repository at this point in the history
…lient 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."
  • Loading branch information
iyt-trifork committed Nov 27, 2024
1 parent e182694 commit 66622c6
Show file tree
Hide file tree
Showing 23 changed files with 1,340 additions and 0 deletions.
2 changes: 2 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions hapi-fhir-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
86 changes: 86 additions & 0 deletions hapi-fhir-client-apache-http5/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<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-fhir</artifactId>
<version>7.7.5-SNAPSHOT</version>
</parent>

<artifactId>hapi-fhir-client-apache-http5</artifactId>
<packaging>bundle</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>
</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>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>@{argLine} ${surefire_jvm_args}</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.felix</groupId>
<artifactId>maven-bundle-plugin</artifactId>
<extensions>true</extensions>
<configuration>
<instructions>
<_nouses>true</_nouses>
<_removeheaders>Built-By, Include-Resource, Private-Package, Require-Capability</_removeheaders>
<!-- No need to disable normal OSGi class loading
<Fragment-Host>
ca.uhn.hapi.fhir.hapi-fhir-base
</Fragment-Host>
-->
</instructions>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>CI</id>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>animal-sniffer-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* #%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
}
}
Original file line number Diff line number Diff line change
@@ -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(2447) + 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());
}
}
}
Loading

0 comments on commit 66622c6

Please sign in to comment.