Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
888bc4d
Move resttemplate moustache into dedicated folder
rpanackal Dec 10, 2025
3473fff
Revert "Move resttemplate moustache into dedicated folder"
rpanackal Dec 10, 2025
7991ce9
Add apache-httpclient/api.moustache
rpanackal Dec 10, 2025
6638e7a
openapi apache sample
rpanackal Dec 11, 2025
c08ceb6
remove ``@Generated`
rpanackal Dec 11, 2025
a697b6e
Add apache-client common generated classes and dependency
rpanackal Dec 11, 2025
28e8135
dependency and formatting
rpanackal Dec 11, 2025
c3e438f
Add all required file (pruned later)
rpanackal Dec 11, 2025
c934b1f
Fix dependencies
rpanackal Dec 11, 2025
ea0eeb6
Simply api-apache-sample
rpanackal Dec 11, 2025
b81f4c2
Merge branch 'main' into feat/openapi/optional-spring-base
rpanackal Dec 29, 2025
50a57bb
Fix version issue
rpanackal Dec 29, 2025
6a33cf6
Pom config and exclusions
rpanackal Dec 29, 2025
cde369f
Set format skip false for sample module
rpanackal Dec 30, 2025
7aed42d
regenerate with prompt-registry.yaml
rpanackal Dec 16, 2025
d4747ad
regenerate with prompt-registry.yaml
rpanackal Dec 16, 2025
03933a0
Remove ServerVariable.java and ServerConfiguration.java files
rpanackal Dec 16, 2025
27cc015
NoArgs ctor preserved but remove Configuration
rpanackal Dec 16, 2025
112360d
Remove apache specific date classes addition. Reuse existing class in…
rpanackal Dec 16, 2025
a9e609b
Pair - Simplify and improve code quality
rpanackal Dec 16, 2025
2f684c6
Remove `Authentication` classes and `authNames` from `ApiClient.invok…
rpanackal Dec 17, 2025
aa60080
make unimported methods private and use ApacheHttpClient5Accessor
rpanackal Dec 17, 2025
73ffecc
Remove invokeAPI methods in BaseApi and *Api.java
rpanackal Dec 18, 2025
f4358bc
Refactoring ApiClient
rpanackal Dec 22, 2025
3ce5400
Refactoring ApiClient
rpanackal Dec 22, 2025
ad8a738
Fix dependency and warning
rpanackal Dec 22, 2025
d8add74
Remove method overloading per operation with additionalHeaders
rpanackal Dec 22, 2025
0c0176c
Extract response handling out of ApiClient into ApiClientResponseHandler
rpanackal Dec 22, 2025
db04109
Make static ApiClient ctors
rpanackal Dec 23, 2025
ffada46
Remove ApiException and use enhanced OpenApiRequestException
rpanackal Dec 23, 2025
cd26cf8
Borrow sample module handling
rpanackal Dec 29, 2025
8357f5f
Apache mustache parity
rpanackal Dec 29, 2025
4d5b97b
Apache mustache parity
rpanackal Dec 29, 2025
e1390af
Apache mustache parity and object mapper config
rpanackal Dec 29, 2025
97c5e03
Revert to sodastore generation in sample
rpanackal Dec 30, 2025
59fedb1
Add apache/OpenApiResponse instead of void returns
rpanackal Dec 30, 2025
c5cba53
Remove guava dependency
rpanackal Dec 30, 2025
e6ec8b9
Update apache pom for general and generator configuration
rpanackal Jan 2, 2026
f16b2c9
Generate and test sodastore and petstore
rpanackal Jan 5, 2026
1b4ffcb
Merge remote-tracking branch 'origin/main' into feat/openapi/refactor…
rpanackal Jan 5, 2026
c56376c
Merge branch 'feat/openapi/refactor-apache-templates' into feat/opena…
rpanackal Jan 5, 2026
0773674
Extending existing integration test
rpanackal Jan 5, 2026
943de3c
Complete and cleanup integration test
rpanackal Jan 6, 2026
cc08bfb
Make url building more forgiving
rpanackal Jan 7, 2026
bdc8c72
Almost @Value class but not final
rpanackal Jan 7, 2026
3387432
Remove as much unnecessary integration test files
rpanackal Jan 7, 2026
64c7bdd
Add unit testing for Apache components
rpanackal Jan 7, 2026
8257a9a
Add unit testing for Apache components
rpanackal Jan 7, 2026
45e4dbc
Merge remote-tracking branch 'origin/feat/openapi/test-apache-client-…
rpanackal Jan 7, 2026
41ef22f
Add unit test for query params, client factory config and objectmappe…
rpanackal Jan 8, 2026
50a0e0f
Merge branch 'main' into feat/openapi/optional-spring-base
rpanackal Jan 8, 2026
eb21453
Borrow downstream PR changes
rpanackal Jan 8, 2026
acff1d1
Merge branch 'feat/openapi/optional-spring-base' into feat/openapi/re…
rpanackal Jan 8, 2026
9f00a54
Merge branch 'feat/openapi/refactor-apache-templates' into feat/opena…
rpanackal Jan 8, 2026
1c0bef8
refactor: [OpenAPI] Part 1: Refactor Apache related files and templat…
rpanackal Jan 12, 2026
84b4e9e
Merge branch 'main' into feat/openapi/optional-spring-base
rpanackal Jan 12, 2026
f02fd1d
Fix CI/CD
rpanackal Jan 12, 2026
cefb244
checkstyle
rpanackal Jan 12, 2026
1632d2c
pmd
rpanackal Jan 12, 2026
1e00604
pmd
rpanackal Jan 12, 2026
31d8620
Merge branch 'feat/openapi/optional-spring-base' into feat/openapi/re…
rpanackal Jan 12, 2026
0a71d8e
Merging
rpanackal Jan 12, 2026
119dd09
Merge branch 'feat/openapi/refactor-apache-templates' into feat/opena…
rpanackal Jan 12, 2026
a84b2ca
Merging
rpanackal Jan 12, 2026
476fef7
Fix serialization of null and
rpanackal Jan 12, 2026
5884d6e
fix byte[] test and pmd
rpanackal Jan 12, 2026
5f61ebf
fix oneOf serialization test
rpanackal Jan 13, 2026
4835a34
Update datamodel/openapi/openapi-generator/src/main/resources/openapi…
rpanackal Jan 13, 2026
de76c72
Review: Apache Method enum and ctor formatting
rpanackal Jan 13, 2026
067ddc8
Merge remote-tracking branch 'origin/feat/openapi/optional-spring-bas…
rpanackal Jan 13, 2026
de01665
Update datamodel/openapi/openapi-core/src/main/java/com/sap/cloud/sdk…
rpanackal Jan 13, 2026
b7d623e
Fix compliance issue and formatting
rpanackal Jan 13, 2026
16f26ab
Merge branch 'feat/openapi/optional-spring-base' into feat/openapi/te…
rpanackal Jan 14, 2026
1226e6f
Make return statement always present
rpanackal Jan 14, 2026
4cf87b9
Merge branch 'feat/openapi/optional-spring-base' into feat/openapi/te…
rpanackal Jan 14, 2026
d8bc954
Reeview changes
rpanackal Jan 14, 2026
f1216a9
Case-insensitive header key match
rpanackal Jan 14, 2026
d8a945e
minor changes in mustache files
rpanackal Jan 15, 2026
859a8f1
feat: [OpenAPI] Part 2: Testing Apache Client Support and some fixes.…
rpanackal Jan 15, 2026
f05242f
Fix missed line on merge
rpanackal Jan 15, 2026
c6483a2
Merge branch 'feat/openapi/optional-spring-base' into feat/openapi/re…
rpanackal Jan 16, 2026
27c7843
Add response metadata listener
rpanackal Jan 16, 2026
587e038
Test metadata listener and header lookup
rpanackal Jan 16, 2026
49a1081
Merge branch 'main' into feat/openapi/response-status-and-header
rpanackal Jan 16, 2026
3c7ee80
Add missing javadoc
rpanackal Jan 16, 2026
b75ed48
Add missing javadoc
rpanackal Jan 16, 2026
b94ace5
Rename to OpenApiResponseListener
rpanackal Jan 19, 2026
aa1f126
Rename to OpenApiResponseListener
rpanackal Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ public class ApiClient
@Nullable
private final String tempFolderPath;

@With
private final OpenApiResponseListener openApiResponseListener;
Copy link
Contributor

@newtork newtork Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Minor)

Could we add the @Beta annotation here?

While I'm okay with current state, I feel like having options in the future would be nice:

  • rename it, e.g. to withResponseListener
  • extend interface API, e.g. with HttpResponse reference, settings for error handling


// Methods that can have a request body
private static final Set<Method> BODY_METHODS = Set.of(Method.POST, Method.PUT, Method.PATCH, Method.DELETE);
private static final String DEFAULT_BASE_PATH = "http://localhost";
Expand All @@ -107,7 +110,7 @@ public class ApiClient
@Nonnull
public static ApiClient fromHttpClient( @Nonnull final CloseableHttpClient httpClient )
{
return new ApiClient(httpClient, DEFAULT_BASE_PATH, createDefaultObjectMapper(), null);
return new ApiClient(httpClient, DEFAULT_BASE_PATH, createDefaultObjectMapper(), null, null);
}

/**
Expand Down Expand Up @@ -567,7 +570,7 @@ public <T> T invokeAPI(

try {
final HttpClientResponseHandler<T> responseHandler =
new DefaultApiResponseHandler<>(objectMapper, tempFolderPath, returnType);
new DefaultApiResponseHandler<>(objectMapper, tempFolderPath, returnType, openApiResponseListener);
return httpClient.execute(builder.build(), context, responseHandler);
}
catch( IOException e ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Expand All @@ -44,38 +45,31 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException;

import lombok.RequiredArgsConstructor;

/**
* Handles HTTP response processing for API client operations. This class encapsulates response deserialization, error
* handling, and file download functionality.
*
* @param <T>
* The type of object to deserialize the response into
*/
@RequiredArgsConstructor
class DefaultApiResponseHandler<T> implements HttpClientResponseHandler<T>
{
/** Jackson ObjectMapper for JSON deserialization */
@Nonnull
private final ObjectMapper objectMapper;
/** Temporary folder path for file downloads (null for system default) */
@Nullable
private final String tempFolderPath;
/** Type reference for response deserialization */
@Nonnull
private final TypeReference<T> returnType;

/**
* Creates a new response handler with the specified configuration.
*
* @param objectMapper
* The Jackson ObjectMapper for JSON deserialization
* @param tempFolderPath
* The temporary folder path for file downloads (null for system default)
* @param returnType
* The type reference for response deserialization
*/
DefaultApiResponseHandler(
@Nonnull final ObjectMapper objectMapper,
@Nullable final String tempFolderPath,
@Nonnull final TypeReference<T> returnType )
{
this.objectMapper = objectMapper;
this.tempFolderPath = tempFolderPath;
this.returnType = returnType;
}
/** Optional listener for OpenAPI response including status code and headers */
@Nullable
private final OpenApiResponseListener openApiResponseListener;

@Nullable
@Override
Expand Down Expand Up @@ -112,21 +106,25 @@ private T processResponse( @Nonnull final ClassicHttpResponse response )
ParseException
{
final int statusCode = response.getCode();
final Map<String, List<String>> headers = transformResponseHeaders(response.getHeaders());
if( openApiResponseListener != null ) {
openApiResponseListener.onResponse(new OpenApiResponse(statusCode, headers));
}

if( statusCode == HttpStatus.SC_NO_CONTENT ) {
if( returnType.getType().equals(OpenApiResponse.class) ) {
return (T) new OpenApiResponse(statusCode, transformResponseHeaders(response.getHeaders()));
return (T) new OpenApiResponse(statusCode, headers);
}
return null;
}

if( isSuccessfulStatus(statusCode) ) {
return deserialize(response);
} else {
final Map<String, List<String>> responseHeaders = transformResponseHeaders(response.getHeaders());
final String message = new StatusLine(response).toString();
throw new OpenApiRequestException(message)
.statusCode(statusCode)
.responseHeaders(responseHeaders)
.responseHeaders(headers)
.responseBody(EntityUtils.toString(response.getEntity()));
}
}
Expand Down Expand Up @@ -308,18 +306,11 @@ private static ContentType parseContentType( @Nonnull final String headerValue )
@Nonnull
private static Map<String, List<String>> transformResponseHeaders( @Nonnull final Header[] headers )
{
final Map<String, List<String>> headersMap = new HashMap<>();
final var headersMap = new TreeMap<String, List<String>>(String.CASE_INSENSITIVE_ORDER);
for( final Header header : headers ) {
List<String> valuesList = headersMap.get(header.getName());
if( valuesList != null ) {
valuesList.add(header.getValue());
} else {
valuesList = new ArrayList<>();
valuesList.add(header.getValue());
headersMap.put(header.getName(), valuesList);
}
headersMap.computeIfAbsent(header.getName(), k -> new ArrayList<>()).add(header.getValue());
}
return headersMap;
return Collections.unmodifiableMap(headersMap);
}

private static boolean isSuccessfulStatus( final int statusCode )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,18 @@
import javax.annotation.Nonnull;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
* Response object for Apache HTTP client OpenAPI calls containing status code and headers
*/
@Getter
@RequiredArgsConstructor
public class OpenApiResponse
{

private final int statusCode;

@Nonnull
private final Map<String, List<String>> headers;

/**
* Create a new OpenApiResponse with status code and headers.
*/
OpenApiResponse( final int statusCode, @Nonnull final Map<String, List<String>> headers )
{
this.statusCode = statusCode;
this.headers = Map.copyOf(headers);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Map.copy() doesn't return a TreeMap and therefore removes the nice case insensitive matching of headers. Instead I opted to make the treeMap unmodifiable at creation site with Collections.unmodifiableMap()

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.sap.cloud.sdk.services.openapi.apache;

import javax.annotation.Nonnull;

/**
* Listener for receiving metadata about HTTP responses.
*
* @since 5.25.0
*/
@FunctionalInterface
public interface OpenApiResponseListener
{
/**
* Called when an HTTP response is received.
*
* @param response
* The response metadata.
*/
void onResponse( @Nonnull final OpenApiResponse response );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package com.sap.cloud.sdk.services.openapi.apiclient;

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.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.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;

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.services.openapi.apache.ApiClient;
import com.sap.cloud.sdk.services.openapi.apache.BaseApi;
import com.sap.cloud.sdk.services.openapi.apache.OpenApiResponse;
import com.sap.cloud.sdk.services.openapi.apache.Pair;
import com.sap.cloud.sdk.services.openapi.core.OpenApiRequestException;

import lombok.Data;

@WireMockTest
class ApacheApiClientResponseHandlingTest
{
private static final String TEST_PATH = "/test";
private static final String TEST_RESPONSE_BODY = "{\"message\": \"success\"}";

@Test
void testResponseMetadataListener( final WireMockRuntimeInfo wmInfo )
{
stubFor(
get(urlEqualTo(TEST_PATH))
.willReturn(
aResponse()
.withStatus(200)
.withHeader("x-custom-header", "some-value")
.withBody(TEST_RESPONSE_BODY)));

final AtomicReference<OpenApiResponse> metadata = new AtomicReference<>();
final ApiClient apiClient =
ApiClient.create().withBasePath(wmInfo.getHttpBaseUrl()).withOpenApiResponseListener(metadata::set);

final TestApi api = new TestApi(apiClient);
final TestResponse result = api.executeRequest();

assertThat(result).isNotNull();
assertThat(result.getMessage()).isEqualTo("success");
assertThat(metadata.get()).isNotNull();
assertThat(metadata.get().getStatusCode()).isEqualTo(200);
assertThat(metadata.get().getHeaders()).isNotEmpty();
assertThat(metadata.get().getHeaders()).containsKey("x-custom-header");

verify(1, getRequestedFor(urlEqualTo(TEST_PATH)));
}

@Test
void testCaseInsensitiveHeaderLookup( final WireMockRuntimeInfo wmInfo )
{
stubFor(
get(urlEqualTo(TEST_PATH))
.willReturn(
aResponse()
.withStatus(200)
.withBody(TEST_RESPONSE_BODY)
.withHeader("x-custom-header", "some-value")));

final AtomicReference<OpenApiResponse> capturedResponse = new AtomicReference<>();
final ApiClient apiClient =
ApiClient.create().withBasePath(wmInfo.getHttpBaseUrl()).withOpenApiResponseListener(capturedResponse::set);

final TestApi api = new TestApi(apiClient);
api.executeRequest();

// Verify case-insensitive access works
final Map<String, List<String>> headers = capturedResponse.get().getHeaders();
assertThat(headers.get("x-custom-header")).contains("some-value");
assertThat(headers.get("X-Custom-Header")).contains("some-value");
assertThat(headers.get("X-CUSTOM-HEADER")).contains("some-value");
}

private static class TestApi extends BaseApi
{
private final String path;

TestApi( final ApiClient apiClient )
{
this(apiClient, TEST_PATH);
}

TestApi( final ApiClient apiClient, final String path )
{
super(apiClient);
this.path = path;
}

TestResponse executeRequest()
throws OpenApiRequestException
{
final List<Pair> localVarQueryParams = new ArrayList<>();
final List<Pair> localVarCollectionQueryParams = new ArrayList<>();
final Map<String, String> localVarHeaderParams = new HashMap<>();
final Map<String, Object> localVarFormParams = new HashMap<>();

final String[] localVarAccepts = { "application/json" };
final String localVarAccept = ApiClient.selectHeaderAccept(localVarAccepts);

final String[] localVarContentTypes = {};
final String localVarContentType = ApiClient.selectHeaderContentType(localVarContentTypes);

final TypeReference<TestResponse> localVarReturnType = new TypeReference<TestResponse>()
{
};

return apiClient
.invokeAPI(
path,
"GET",
localVarQueryParams,
localVarCollectionQueryParams,
null,
null,
localVarHeaderParams,
localVarFormParams,
localVarAccept,
localVarContentType,
localVarReturnType);
}
}

@Data
private static class TestResponse
{
@JsonProperty( "message" )
private String message;
}
}