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..00131da0d 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 @@ -15,6 +15,7 @@ import static com.sap.cloud.sdk.services.openapi.apache.apiclient.DefaultApiResponseHandler.isJsonMime; import static lombok.AccessLevel.PRIVATE; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.net.URLEncoder; @@ -28,6 +29,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 +70,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 +351,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 +492,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.get("Content-Encoding"))); } else { throw new OpenApiRequestException("method " + method + " does not support a request body"); } @@ -578,4 +510,112 @@ 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 + * @return Object + * @throws OpenApiRequestException + * API exception + */ + @Nonnull + private HttpEntity serialize( + @Nullable final Object body, + @Nonnull final Map formParams, + @Nonnull final ContentType contentType, + @Nonnull final String contentEncoding ) + throws OpenApiRequestException + { + final String mimeType = contentType.getMimeType(); + if( isJsonMime(mimeType) ) { + return serializeJson(body, contentType, contentEncoding); + } 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 String contentEncoding ) + throws OpenApiRequestException + { + if( "gzip".equals(contentEncoding) ) { + val outputStream = new ByteArrayOutputStream(); + try( val gzip = new GZIPOutputStream(outputStream) ) { + gzip.write(objectMapper.writeValueAsBytes(body)); + } + catch( IOException e ) { + throw new OpenApiRequestException(e); + } + return new ByteArrayEntity(outputStream.toByteArray(), contentType.withCharset(StandardCharsets.UTF_8)); + } + 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..5f3259d57 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,6 +3,8 @@ 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; @@ -30,6 +32,7 @@ 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 +87,22 @@ void testCaseInsensitiveHeaderLookup( final WireMockRuntimeInfo wmInfo ) assertThat(headers.get("X-CUSTOM-HEADER")).contains("some-value"); } + @Test + void testGzipEncodedPayload( final WireMockRuntimeInfo wmInfo ) + { + stubFor(post(urlEqualTo(TEST_POST_PATH)).willReturn(aResponse().withStatus(200).withBody(TEST_RESPONSE_BODY))); + + final ApiClient apiClient = ApiClient.create().withBasePath(wmInfo.getHttpBaseUrl()); + + final TestPostApi api = new TestPostApi(apiClient); + final TestResponse result = api.executeGzipRequest(); + + 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 +132,7 @@ TestResponse executeRequest() final String[] localVarContentTypes = {}; final String localVarContentType = ApiClient.selectHeaderContentType(localVarContentTypes); - final TypeReference localVarReturnType = new TypeReference() + final TypeReference localVarReturnType = new TypeReference<>() { }; @@ -133,6 +152,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 {