diff --git a/datamodel/openapi/openapi-core-apache/pom.xml b/datamodel/openapi/openapi-core-apache/pom.xml
index da363410d..47287dea5 100644
--- a/datamodel/openapi/openapi-core-apache/pom.xml
+++ b/datamodel/openapi/openapi-core-apache/pom.xml
@@ -94,5 +94,15 @@
junit-jupiter-params
test
+
+ org.mockito
+ mockito-core
+ test
+
+
+ io.vavr
+ vavr
+ test
+
-
\ No newline at end of file
+
diff --git a/datamodel/openapi/openapi-core-apache/src/main/java/com/sap/cloud/sdk/services/openapi/apache/apiclient/ApiClient.java b/datamodel/openapi/openapi-core-apache/src/main/java/com/sap/cloud/sdk/services/openapi/apache/apiclient/ApiClient.java
index 9c41962c0..ac2b4fad9 100644
--- a/datamodel/openapi/openapi-core-apache/src/main/java/com/sap/cloud/sdk/services/openapi/apache/apiclient/ApiClient.java
+++ b/datamodel/openapi/openapi-core-apache/src/main/java/com/sap/cloud/sdk/services/openapi/apache/apiclient/ApiClient.java
@@ -14,7 +14,9 @@
import static com.sap.cloud.sdk.services.openapi.apache.apiclient.DefaultApiResponseHandler.isJsonMime;
import static lombok.AccessLevel.PRIVATE;
+import static org.apache.hc.core5.http.HttpHeaders.CONTENT_ENCODING;
+import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.net.URLEncoder;
@@ -28,6 +30,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
+import java.util.zip.GZIPOutputStream;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@@ -68,6 +71,7 @@
import lombok.Getter;
import lombok.ToString;
import lombok.With;
+import lombok.val;
/**
* API client for executing HTTP requests using Apache HttpClient 5.
@@ -348,77 +352,6 @@ private ContentType getContentType( @Nonnull final String headerValue )
}
}
- /**
- * Serialize the given Java object into string according the given Content-Type (only JSON is supported for now).
- *
- * @param obj
- * Object
- * @param contentType
- * Content type
- * @param formParams
- * Form parameters
- * @return Object
- * @throws OpenApiRequestException
- * API exception
- */
- @Nonnull
- private HttpEntity serialize(
- @Nullable final Object obj,
- @Nonnull final Map formParams,
- @Nonnull final ContentType contentType )
- throws OpenApiRequestException
- {
- final String mimeType = contentType.getMimeType();
- if( isJsonMime(mimeType) ) {
- try {
- return new StringEntity(
- objectMapper.writeValueAsString(obj),
- contentType.withCharset(StandardCharsets.UTF_8));
- }
- catch( JsonProcessingException e ) {
- throw new OpenApiRequestException(e);
- }
- } else if( mimeType.equals(ContentType.MULTIPART_FORM_DATA.getMimeType()) ) {
- final MultipartEntityBuilder multiPartBuilder = MultipartEntityBuilder.create();
- for( final Entry paramEntry : formParams.entrySet() ) {
- final Object value = paramEntry.getValue();
- if( value instanceof File file ) {
- multiPartBuilder.addBinaryBody(paramEntry.getKey(), file);
- } else if( value instanceof byte[] byteArray ) {
- multiPartBuilder.addBinaryBody(paramEntry.getKey(), byteArray);
- } else {
- final Charset charset = contentType.getCharset();
- if( charset != null ) {
- final ContentType customContentType =
- ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), charset);
- multiPartBuilder
- .addTextBody(
- paramEntry.getKey(),
- parameterToString(paramEntry.getValue()),
- customContentType);
- } else {
- multiPartBuilder.addTextBody(paramEntry.getKey(), parameterToString(paramEntry.getValue()));
- }
- }
- }
- return multiPartBuilder.build();
- } else if( mimeType.equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) ) {
- final List formValues = new ArrayList<>();
- for( final Entry paramEntry : formParams.entrySet() ) {
- formValues.add(new BasicNameValuePair(paramEntry.getKey(), parameterToString(paramEntry.getValue())));
- }
- return new UrlEncodedFormEntity(formValues, contentType.getCharset());
- } else {
- // Handle files with unknown content type
- if( obj instanceof File file ) {
- return new FileEntity(file, contentType);
- } else if( obj instanceof byte[] byteArray ) {
- return new ByteArrayEntity(byteArray, contentType);
- }
- throw new OpenApiRequestException("Serialization for content type '" + contentType + "' not supported");
- }
- }
-
/**
* Build full URL by concatenating base URL, the given sub path and query parameters.
*
@@ -560,7 +493,7 @@ public T invokeAPI(
if( body != null || !formParams.isEmpty() ) {
if( isBodyAllowed(Method.valueOf(method)) ) {
// Add entity if we have content and a valid method
- builder.setEntity(serialize(body, formParams, contentTypeObj));
+ builder.setEntity(serialize(body, formParams, contentTypeObj, headerParams));
} else {
throw new OpenApiRequestException("method " + method + " does not support a request body");
}
@@ -578,4 +511,118 @@ public T invokeAPI(
throw new OpenApiRequestException(e);
}
}
+
+ /**
+ * Serialize the given Java object into string according the given Content-Type (only JSON is supported for now).
+ *
+ * @param body
+ * Object
+ * @param contentType
+ * Content type
+ * @param formParams
+ * Form parameters
+ * @param headerParams
+ * Header parameters, used to check content encoding for JSON serialization
+ * @return Object
+ * @throws OpenApiRequestException
+ * API exception
+ */
+ @Nonnull
+ private HttpEntity serialize(
+ @Nullable final Object body,
+ @Nonnull final Map formParams,
+ @Nonnull final ContentType contentType,
+ @Nonnull final Map headerParams )
+ throws OpenApiRequestException
+ {
+ final String mimeType = contentType.getMimeType();
+ if( isJsonMime(mimeType) ) {
+ return serializeJson(body, contentType, headerParams);
+ } else if( mimeType.equals(ContentType.MULTIPART_FORM_DATA.getMimeType()) ) {
+ return serializeMultipart(formParams, contentType);
+ } else if( mimeType.equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType()) ) {
+ return serializeFormUrlEncoded(formParams, contentType);
+ } else if( body instanceof File file ) {
+ return new FileEntity(file, contentType);
+ } else if( body instanceof byte[] byteArray ) {
+ return new ByteArrayEntity(byteArray, contentType);
+ }
+ throw new OpenApiRequestException("Serialization for content type '" + contentType + "' not supported");
+ }
+
+ @Nonnull
+ private HttpEntity serializeJson(
+ @Nullable final Object body,
+ @Nonnull final ContentType contentType,
+ @Nonnull final Map headerParams )
+ throws OpenApiRequestException
+ {
+ if( "gzip".equalsIgnoreCase(headerParams.get(CONTENT_ENCODING))
+ || "gzip".equalsIgnoreCase(headerParams.get(CONTENT_ENCODING.toLowerCase())) ) {
+ val outputStream = new ByteArrayOutputStream();
+ try( val gzip = new GZIPOutputStream(outputStream) ) {
+ gzip.write(objectMapper.writeValueAsBytes(body));
+ }
+ catch( IOException e ) {
+ throw new OpenApiRequestException("Failed to GZIP compress request body", e);
+ }
+ return new ByteArrayEntity(
+ outputStream.toByteArray(),
+ contentType.withCharset(StandardCharsets.UTF_8),
+ "gzip");
+ }
+ try {
+ return new StringEntity(
+ objectMapper.writeValueAsString(body),
+ contentType.withCharset(StandardCharsets.UTF_8));
+ }
+ catch( JsonProcessingException e ) {
+ throw new OpenApiRequestException(e);
+ }
+ }
+
+ @Nonnull
+ private
+ HttpEntity
+ serializeMultipart( @Nonnull final Map formParams, @Nonnull final ContentType contentType )
+ {
+ final MultipartEntityBuilder builder = MultipartEntityBuilder.create();
+ for( final Entry entry : formParams.entrySet() ) {
+ final Object value = entry.getValue();
+ if( value instanceof File file ) {
+ builder.addBinaryBody(entry.getKey(), file);
+ } else if( value instanceof byte[] byteArray ) {
+ builder.addBinaryBody(entry.getKey(), byteArray);
+ } else {
+ addMultipartTextField(builder, entry, contentType);
+ }
+ }
+ return builder.build();
+ }
+
+ private void addMultipartTextField(
+ @Nonnull final MultipartEntityBuilder builder,
+ @Nonnull final Entry entry,
+ @Nonnull final ContentType contentType )
+ {
+ final Charset charset = contentType.getCharset();
+ if( charset != null ) {
+ final ContentType textContentType = ContentType.create(ContentType.TEXT_PLAIN.getMimeType(), charset);
+ builder.addTextBody(entry.getKey(), parameterToString(entry.getValue()), textContentType);
+ } else {
+ builder.addTextBody(entry.getKey(), parameterToString(entry.getValue()));
+ }
+ }
+
+ @Nonnull
+ private
+ HttpEntity
+ serializeFormUrlEncoded( @Nonnull final Map formParams, @Nonnull final ContentType contentType )
+ {
+ final List formValues = new ArrayList<>();
+ for( final Entry entry : formParams.entrySet() ) {
+ formValues.add(new BasicNameValuePair(entry.getKey(), parameterToString(entry.getValue())));
+ }
+ return new UrlEncodedFormEntity(formValues, contentType.getCharset());
+ }
}
diff --git a/datamodel/openapi/openapi-core-apache/src/test/java/com/sap/cloud/sdk/services/openapi/apache/apiclient/ApacheApiClientResponseHandlingTest.java b/datamodel/openapi/openapi-core-apache/src/test/java/com/sap/cloud/sdk/services/openapi/apache/apiclient/ApacheApiClientResponseHandlingTest.java
index 5c64fdce1..0a3e39464 100644
--- a/datamodel/openapi/openapi-core-apache/src/test/java/com/sap/cloud/sdk/services/openapi/apache/apiclient/ApacheApiClientResponseHandlingTest.java
+++ b/datamodel/openapi/openapi-core-apache/src/test/java/com/sap/cloud/sdk/services/openapi/apache/apiclient/ApacheApiClientResponseHandlingTest.java
@@ -3,33 +3,44 @@
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
import static org.assertj.core.api.Assertions.assertThat;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.zip.GZIPInputStream;
+import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
+import org.apache.hc.core5.http.protocol.HttpContext;
import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.type.TypeReference;
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
+import com.sap.cloud.sdk.cloudplatform.connectivity.ApacheHttpClient5Accessor;
import com.sap.cloud.sdk.services.openapi.apache.core.OpenApiRequestException;
import com.sap.cloud.sdk.services.openapi.apache.core.OpenApiResponse;
+import io.vavr.control.Try;
import lombok.Data;
+import lombok.SneakyThrows;
@WireMockTest
class ApacheApiClientResponseHandlingTest
{
private static final String TEST_PATH = "/test";
private static final String TEST_RESPONSE_BODY = "{\"message\": \"success\"}";
+ private static final String TEST_POST_PATH = "/test-post";
@Test
void testResponseMetadataListener( final WireMockRuntimeInfo wmInfo )
@@ -84,6 +95,27 @@ void testCaseInsensitiveHeaderLookup( final WireMockRuntimeInfo wmInfo )
assertThat(headers.get("X-CUSTOM-HEADER")).contains("some-value");
}
+ @Test
+ @SneakyThrows
+ void testGzipEncodedPayload( final WireMockRuntimeInfo wmInfo )
+ {
+ stubFor(post(urlEqualTo(TEST_POST_PATH)).willReturn(aResponse().withStatus(200).withBody(TEST_RESPONSE_BODY)));
+
+ final CloseableHttpClient client = Mockito.spy((CloseableHttpClient) ApacheHttpClient5Accessor.getHttpClient());
+ final ApiClient apiClient = ApiClient.fromHttpClient(client).withBasePath(wmInfo.getHttpBaseUrl());
+ final TestPostApi api = new TestPostApi(apiClient);
+ final TestResponse result = api.executeGzipRequest();
+ Mockito.verify(client, Mockito.times(1)).execute(Mockito.argThat(request -> {
+ final byte[] c = Try.of(() -> new GZIPInputStream(request.getEntity().getContent()).readAllBytes()).get();
+ return new String(c, StandardCharsets.UTF_8).contains("test payload");
+ }), Mockito.any(HttpContext.class), Mockito.any());
+
+ assertThat(result).isNotNull();
+ assertThat(result.getMessage()).isEqualTo("success");
+
+ verify(1, postRequestedFor(urlEqualTo(TEST_POST_PATH)));
+ }
+
private static class TestApi extends BaseApi
{
private final String path;
@@ -113,7 +145,7 @@ TestResponse executeRequest()
final String[] localVarContentTypes = {};
final String localVarContentType = ApiClient.selectHeaderContentType(localVarContentTypes);
- final TypeReference localVarReturnType = new TypeReference()
+ final TypeReference localVarReturnType = new TypeReference<>()
{
};
@@ -133,6 +165,59 @@ TestResponse executeRequest()
}
}
+ private static class TestPostApi extends BaseApi
+ {
+ private final String path;
+
+ TestPostApi( final ApiClient apiClient )
+ {
+ this(apiClient, TEST_POST_PATH);
+ }
+
+ TestPostApi( final ApiClient apiClient, final String path )
+ {
+ super(apiClient);
+ this.path = path;
+ }
+
+ TestResponse executeGzipRequest()
+ throws OpenApiRequestException
+ {
+ final TestResponse requestBody = new TestResponse();
+ requestBody.setMessage("test payload");
+
+ final List localVarQueryParams = new ArrayList<>();
+ final List localVarCollectionQueryParams = new ArrayList<>();
+ final Map localVarHeaderParams = new HashMap<>();
+ localVarHeaderParams.put("Content-Encoding", "gzip");
+ final Map localVarFormParams = new HashMap<>();
+
+ final String[] localVarAccepts = { "application/json" };
+ final String localVarAccept = ApiClient.selectHeaderAccept(localVarAccepts);
+
+ final String[] localVarContentTypes = { "application/json" };
+ final String localVarContentType = ApiClient.selectHeaderContentType(localVarContentTypes);
+
+ final TypeReference localVarReturnType = new TypeReference<>()
+ {
+ };
+
+ return apiClient
+ .invokeAPI(
+ path,
+ "POST",
+ localVarQueryParams,
+ localVarCollectionQueryParams,
+ null,
+ requestBody,
+ localVarHeaderParams,
+ localVarFormParams,
+ localVarAccept,
+ localVarContentType,
+ localVarReturnType);
+ }
+ }
+
@Data
private static class TestResponse
{