From f3991dd43078a69d65f03a38c951efdcb5a4b157 Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Thu, 11 Sep 2025 18:18:09 +0200 Subject: [PATCH 1/2] feat(java-sdk): allow passing OkHttpClient objects relates to STACKITSDK-238 --- openapi-generator-config-java.yml | 3 + .../.openapi-generator-ignore-java | 2 +- scripts/generate-sdk/languages/java.sh | 6 +- templates/java/ApiClient.mustache | 90 +++++++++++++------ templates/java/api.mustache | 53 +++++++++-- templates/java/api_test.mustache | 56 ++++++++++++ templates/java/custom/ApiClientTest.mustache | 57 ++++++++++++ templates/java/custom/serviceApi.mustache | 41 ++++++++- templates/java/custom/serviceApiTest.mustache | 57 ++++++++++++ 9 files changed, 325 insertions(+), 40 deletions(-) create mode 100644 templates/java/api_test.mustache create mode 100644 templates/java/custom/ApiClientTest.mustache create mode 100644 templates/java/custom/serviceApiTest.mustache diff --git a/openapi-generator-config-java.yml b/openapi-generator-config-java.yml index 7daf919..fb4b055 100644 --- a/openapi-generator-config-java.yml +++ b/openapi-generator-config-java.yml @@ -18,3 +18,6 @@ files: custom/serviceApi.mustache: templateType: API destinationFilename: ServiceApi.java + custom/serviceApiTest.mustache: + templateType: APITests + destinationFilename: ServiceApiTest.java diff --git a/scripts/generate-sdk/.openapi-generator-ignore-java b/scripts/generate-sdk/.openapi-generator-ignore-java index 274c522..6323e06 100644 --- a/scripts/generate-sdk/.openapi-generator-ignore-java +++ b/scripts/generate-sdk/.openapi-generator-ignore-java @@ -26,7 +26,7 @@ gradle/** # ApiException from core library should be used (avoid multiple ApiException imports in case different services are used at the same time) **/ApiException.java -# Authentication classes are not required because the KeyFlowAuth is attached as interceptor +# Authentication classes are not required because the KeyFlowAuth is attached as authenticator **/auth/** # Service Configuration is not required. It only stores a defaultApiClient diff --git a/scripts/generate-sdk/languages/java.sh b/scripts/generate-sdk/languages/java.sh index 875380a..19364b0 100644 --- a/scripts/generate-sdk/languages/java.sh +++ b/scripts/generate-sdk/languages/java.sh @@ -118,7 +118,7 @@ generate_java_sdk() { --git-host "${GIT_HOST}" \ --git-user-id "${GIT_USER_ID}" \ --git-repo-id "${GIT_REPO_ID}" \ - --global-property apis,models,modelTests=false,modelDocs=false,apiDocs=false,apiTests=false,supportingFiles \ + --global-property apis,models,modelTests=false,modelDocs=false,apiDocs=false,apiTests=true,supportingFiles \ --additional-properties="artifactId=${service},artifactDescription=${SERVICE_DESCRIPTION},invokerPackage=cloud.stackit.sdk.${service},modelPackage=cloud.stackit.sdk.${service}.model,apiPackage=cloud.stackit.sdk.${service}.api,serviceName=${service_pascal_case}" >/dev/null \ --http-user-agent stackit-sdk-java/"${service}" \ --config openapi-generator-config-java.yml @@ -129,6 +129,10 @@ generate_java_sdk() { if [ -f "$api_file" ]; then mv "$api_file" "${SERVICES_FOLDER}/${service}/src/main/java/cloud/stackit/sdk/${service}/api/${service_pascal_case}Api.java" fi + api_test_file="${SERVICES_FOLDER}/${service}/src/test/java/cloud/stackit/sdk/${service}/api/DefaultApiTestServiceApiTest.java" + if [ -f "$api_test_file" ]; then + mv "$api_test_file" "${SERVICES_FOLDER}/${service}/src/test/java/cloud/stackit/sdk/${service}/api/${service_pascal_case}ApiTest.java" + fi # Remove unnecessary files rm "${SERVICES_FOLDER}/${service}/.openapi-generator-ignore" diff --git a/templates/java/ApiClient.mustache b/templates/java/ApiClient.mustache index 247d062..9f8a693 100644 --- a/templates/java/ApiClient.mustache +++ b/templates/java/ApiClient.mustache @@ -78,6 +78,7 @@ import {{invokerPackage}}.auth.AWS4Auth; import cloud.stackit.sdk.core.auth.SetupAuth; import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.core.exception.ApiException; +import cloud.stackit.sdk.core.KeyFlowAuthenticator; /** *

ApiClient class.

@@ -130,7 +131,6 @@ public class ApiClient { protected JSON json; protected HttpLoggingInterceptor loggingInterceptor; - protected SetupAuth authenticationInterceptor; protected CoreConfiguration configuration; @@ -138,42 +138,68 @@ public class ApiClient { protected Map operationLookupMap = new HashMap<>(); {{/dynamicOperations}} - /** - * Basic constructor for ApiClient + {{! BEGIN - Removed ApiClient constructor and replaced it with a custom constructors which create the ApiClient with the CoreConfiguration }} + /** + * Basic constructor for ApiClient. + * + * Not recommended for production use, use the one with the OkHttpClient parameter instead. + * + * @throws IOException thrown when a file can not be found */ public ApiClient() throws IOException { - {{! BEGIN - replace basic constructur }} - this(new CoreConfiguration()); - {{! END - replace basic constructur }} + this(null, new CoreConfiguration()); } - {{! BEGIN - Removed ApiClient constructor with OkHttpClient as param and replaced it with a custom constructor which creates the ApiClient with the CoreConfiguration }} /** - * Basic constructor with custom CoreConfiguration - * - * @param config a {@link cloud.stackit.sdk.core.config} object - * @throws IOException thrown when a file can not be found - */ + * Basic constructor for ApiClient + * + * Not recommended for production use, use the one with the OkHttpClient parameter instead. + * + * @param config a {@link cloud.stackit.sdk.core.config.CoreConfiguration} object + * @throws IOException thrown when a file can not be found + */ public ApiClient(CoreConfiguration config) throws IOException { + this(null, config); + } + + /** + * Constructor for ApiClient with OkHttpClient parameter. Recommended for production use. + * + * @param httpClient a OkHttpClient object + * @throws IOException thrown when a file can not be found + */ + public ApiClient(OkHttpClient httpClient) throws IOException { + this(httpClient, new CoreConfiguration()); + } + + /** + * Constructor for ApiClient with OkHttpClient parameter. Recommended for production use. + * + * @param httpClient a OkHttpClient object + * @param config a {@link cloud.stackit.sdk.core.config.CoreConfiguration} object + * @throws IOException thrown when a file can not be found + */ + public ApiClient(OkHttpClient httpClient, CoreConfiguration config) throws IOException { init(); - if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { - basePath = config.getCustomEndpoint(); - } - if (config.getDefaultHeader() != null) { - defaultHeaderMap = config.getDefaultHeader(); - } + if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { + basePath = config.getCustomEndpoint(); + } + if (config.getDefaultHeader() != null) { + defaultHeaderMap = config.getDefaultHeader(); + } this.configuration = config; - // Setup AuthHandler - SetupAuth auth; - auth = new SetupAuth(config); - auth.init(); - authenticationInterceptor = auth; - - initHttpClient(); + if (httpClient == null) { + initHttpClient(); + KeyFlowAuthenticator authenticator = new KeyFlowAuthenticator(this.httpClient, config); + this.httpClient = this.httpClient.newBuilder().authenticator(authenticator).build(); + } else { + // Authorization has to be configured manually in case a custom http client object is passed + this.httpClient = httpClient; + } } - {{! END - Removed ApiClient constructor with OkHttpClient as param and replaced it with a custom constructor which creates the ApiClient with the CoreConfiguration }} + {{! END - Removed ApiClient constructor and replaced it with a custom constructors which create the ApiClient with the CoreConfiguration }} {{#hasOAuthMethods}} {{#oauthMethods}} @@ -261,8 +287,6 @@ public class ApiClient { for (Interceptor interceptor: interceptors) { builder.addInterceptor(interceptor); } - // Adds the Authorization header to requests - builder.addInterceptor(authenticationInterceptor.getAuthHandler()); {{#useGzipFeature}} // Enable gzip request compression builder.addInterceptor(new GzipRequestInterceptor()); @@ -334,7 +358,15 @@ public class ApiClient { return this; } - {{! remove getter and setter for httpClient because this would configure a client without the authentication interceptor }} + /** + * Get HTTP client + * + * @return An instance of OkHttpClient + */ + public OkHttpClient getHttpClient() { + return httpClient; + } + {{! remove setter for httpClient because this would configure a client without the authentication interceptor }} {{! if this will be requested in a feature-request, it should be implemented with the core configuration module like in the Go SDK }} /** diff --git a/templates/java/api.mustache b/templates/java/api.mustache index eba41c8..6f30a0b 100644 --- a/templates/java/api.mustache +++ b/templates/java/api.mustache @@ -54,6 +54,7 @@ import java.io.InputStream; {{/supportStreaming}} import cloud.stackit.sdk.core.config.CoreConfiguration; import cloud.stackit.sdk.core.exception.ApiException; +import okhttp3.OkHttpClient; {{#operations}} // Package-private access to enforce service-specific API usage (DefaultApi => Api) @@ -63,16 +64,52 @@ class {{classname}} { private String localCustomBaseUrl; {{! BEGIN - Remove default constructor and replaced with constructor which uses CoreConfiguration }} - public {{classname}}() throws IOException { - this(new CoreConfiguration()); - } - - public {{classname}}(CoreConfiguration config) throws IOException { - if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { + /** + * Basic constructor for DefaultApi + * + * For production use consider using the constructor with the OkHttpClient parameter. + * + * @throws IOException + */ + public DefaultApi() throws IOException { + this(null, new CoreConfiguration()); + } + + /** + * Basic Constructor for DefaultApi + * + * For production use consider using the constructor with the OkHttpClient parameter. + * + * @param config your STACKIT SDK CoreConfiguration + * @throws IOException + */ + public DefaultApi(CoreConfiguration config) throws IOException { + this(null, config); + } + + /** + * Constructor for DefaultApi + * + * @param httpClient OkHttpClient object + * @throws IOException + */ + public DefaultApi(OkHttpClient httpClient) throws IOException { + this(httpClient, new CoreConfiguration()); + } + + /** + * Constructor for DefaultApi + * + * @param httpClient OkHttpClient object + * @param config your STACKIT SDK CoreConfiguration + * @throws IOException + */ + public DefaultApi(OkHttpClient httpClient, CoreConfiguration config) throws IOException { + if (config.getCustomEndpoint() != null && !config.getCustomEndpoint().trim().isEmpty()) { localCustomBaseUrl = config.getCustomEndpoint(); } - this.localVarApiClient = new ApiClient(config); - } + this.localVarApiClient = new ApiClient(httpClient, config); + } {{! END - Remove default constructor and replaced with constructor which uses CoreConfiguration }} public ApiClient getApiClient() { diff --git a/templates/java/api_test.mustache b/templates/java/api_test.mustache new file mode 100644 index 0000000..822bf16 --- /dev/null +++ b/templates/java/api_test.mustache @@ -0,0 +1,56 @@ +{{>licenseInfo}} + +package {{invokerPackage}}; + +import cloud.stackit.sdk.core.KeyFlowAuthenticator; +import cloud.stackit.sdk.core.auth.SetupAuth; +import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.core.utils.TestUtils; +import java.io.IOException; +import okhttp3.Authenticator; +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class DefaultApiTest { + @Test + public void TestCustomHttpClient() throws IOException { + // before + CoreConfiguration conf = + new CoreConfiguration().serviceAccountKey(TestUtils.MOCK_SERVICE_ACCOUNT_KEY); + + // when + OkHttpClient httpClient = new OkHttpClient(); + ApiClient apiClient = new ApiClient(httpClient, conf); + + // then + Assertions.assertEquals(httpClient, apiClient.getHttpClient()); + // make sure the http client object is exactly the same object + Assertions.assertSame(httpClient, apiClient.getHttpClient()); + } + + @Test + public void TestNoCustomHttpClient() throws IOException { + // before + CoreConfiguration conf = + new CoreConfiguration().serviceAccountKey(TestUtils.MOCK_SERVICE_ACCOUNT_KEY); + + // when + ApiClient apiClient = new ApiClient(conf); + + // then + /* + * verify a fresh OkHttpClient got created which will have the auth header set + * by the {@link cloud.stackit.sdk.core.KeyFlowAuthenticator} + */ + OkHttpClient httpClient = new OkHttpClient(); + Authenticator authenticator = + new KeyFlowAuthenticator(httpClient, conf, SetupAuth.setupKeyFlow(conf)); + httpClient = httpClient.newBuilder().authenticator(authenticator).build(); + + Assertions.assertNotNull(apiClient.getHttpClient()); + Assertions.assertEquals( + httpClient.authenticator().getClass(), + apiClient.getHttpClient().authenticator().getClass()); + } +} diff --git a/templates/java/custom/ApiClientTest.mustache b/templates/java/custom/ApiClientTest.mustache new file mode 100644 index 0000000..f090566 --- /dev/null +++ b/templates/java/custom/ApiClientTest.mustache @@ -0,0 +1,57 @@ +{{>licenseInfo}} + +package {{invokerPackage}}; + +import cloud.stackit.sdk.core.KeyFlowAuthenticator; +import cloud.stackit.sdk.core.auth.SetupAuth; +import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.core.utils.TestUtils; +import java.io.IOException; +import okhttp3.Authenticator; +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** Tests for ApiClient */ +public class ApiClientTest { + @Test + public void TestCustomHttpClient() throws IOException { + // before + CoreConfiguration conf = + new CoreConfiguration().serviceAccountKey(TestUtils.MOCK_SERVICE_ACCOUNT_KEY); + + // when + OkHttpClient httpClient = new OkHttpClient(); + ApiClient apiClient = new ApiClient(httpClient, conf); + + // then + Assertions.assertEquals(httpClient, apiClient.getHttpClient()); + // make sure the http client object is exactly the same object + Assertions.assertSame(httpClient, apiClient.getHttpClient()); + } + + @Test + public void TestNoCustomHttpClient() throws IOException { + // before + CoreConfiguration conf = + new CoreConfiguration().serviceAccountKey(TestUtils.MOCK_SERVICE_ACCOUNT_KEY); + + // when + ApiClient apiClient = new ApiClient(conf); + + // then + /* + * verify a fresh OkHttpClient got created which will have the auth header set + * by the {@link cloud.stackit.sdk.core.KeyFlowAuthenticator} + */ + OkHttpClient httpClient = new OkHttpClient(); + Authenticator authenticator = + new KeyFlowAuthenticator(httpClient, conf, SetupAuth.setupKeyFlow(conf)); + httpClient = httpClient.newBuilder().authenticator(authenticator).build(); + + Assertions.assertNotNull(apiClient.getHttpClient()); + Assertions.assertEquals( + httpClient.authenticator().getClass(), + apiClient.getHttpClient().authenticator().getClass()); + } +} diff --git a/templates/java/custom/serviceApi.mustache b/templates/java/custom/serviceApi.mustache index 7f90aa9..a5037d1 100644 --- a/templates/java/custom/serviceApi.mustache +++ b/templates/java/custom/serviceApi.mustache @@ -1,15 +1,54 @@ +{{>licenseInfo}} + package {{invokerPackage}}.api; import cloud.stackit.sdk.core.config.CoreConfiguration; import java.io.IOException; +import okhttp3.OkHttpClient; public class {{serviceName}}Api extends DefaultApi { + /** + * Basic constructor for {{serviceName}}Api + * + * For production use consider using the constructor with the OkHttpClient parameter. + * + * @throws IOException + */ public {{serviceName}}Api() throws IOException { super(); } + /** + * Basic Constructor for {{serviceName}}Api + * + * For production use consider using the constructor with the OkHttpClient parameter. + * + * @param config your STACKIT SDK CoreConfiguration + * @throws IOException + */ public {{serviceName}}Api(CoreConfiguration configuration) throws IOException { super(configuration); } -} \ No newline at end of file + + /** + * Constructor for {{serviceName}}Api + * + * @param httpClient OkHttpClient object + * @throws IOException + */ + public {{serviceName}}Api(OkHttpClient httpClient) throws IOException { + super(httpClient); + } + + /** + * Constructor for {{serviceName}}Api + * + * @param httpClient OkHttpClient object + * @param configuraction your STACKIT SDK CoreConfiguration + * @throws IOException + */ + public {{serviceName}}Api(OkHttpClient httpClient, CoreConfiguration configuration) throws IOException { + super(httpClient, configuration); + } +} diff --git a/templates/java/custom/serviceApiTest.mustache b/templates/java/custom/serviceApiTest.mustache new file mode 100644 index 0000000..3d5e79f --- /dev/null +++ b/templates/java/custom/serviceApiTest.mustache @@ -0,0 +1,57 @@ +{{>licenseInfo}} + +package {{invokerPackage}}.api; + +import cloud.stackit.sdk.core.KeyFlowAuthenticator; +import cloud.stackit.sdk.core.auth.SetupAuth; +import cloud.stackit.sdk.core.config.CoreConfiguration; +import cloud.stackit.sdk.core.utils.TestUtils; +import java.io.IOException; +import okhttp3.Authenticator; +import okhttp3.OkHttpClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** API tests for {{serviceName}}Api */ +public class {{serviceName}}ApiTest { + @Test + public void TestCustomHttpClient() throws IOException { + // before + CoreConfiguration conf = + new CoreConfiguration().serviceAccountKey(TestUtils.MOCK_SERVICE_ACCOUNT_KEY); + + // when + OkHttpClient httpClient = new OkHttpClient(); + {{serviceName}}Api api = new {{serviceName}}Api(httpClient); + + // then + Assertions.assertEquals(httpClient, api.getApiClient().getHttpClient()); + // make sure the http client object is exactly the same object + Assertions.assertSame(httpClient, api.getApiClient().getHttpClient()); + } + + @Test + public void TestNoCustomHttpClient() throws IOException { + // before + CoreConfiguration conf = + new CoreConfiguration().serviceAccountKey(TestUtils.MOCK_SERVICE_ACCOUNT_KEY); + + // when + {{serviceName}}Api api = new {{serviceName}}Api(conf); + + // then + /* + * verify a fresh OkHttpClient got created which will have the auth header set + * by the {@link cloud.stackit.sdk.core.KeyFlowAuthenticator} + */ + OkHttpClient httpClient = new OkHttpClient(); + Authenticator authenticator = + new KeyFlowAuthenticator(httpClient, conf, SetupAuth.setupKeyFlow(conf)); + httpClient = httpClient.newBuilder().authenticator(authenticator).build(); + + Assertions.assertNotNull(api.getApiClient().getHttpClient()); + Assertions.assertEquals( + httpClient.authenticator().getClass(), + api.getApiClient().getHttpClient().authenticator().getClass()); + } +} From 6dd193dbf1c6eb560dff6883c97835d0f689baae Mon Sep 17 00:00:00 2001 From: Ruben Hoenle Date: Tue, 23 Sep 2025 11:25:10 +0200 Subject: [PATCH 2/2] review findings --- templates/java/api_test.mustache | 3 +++ templates/java/custom/serviceApi.mustache | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/templates/java/api_test.mustache b/templates/java/api_test.mustache index 822bf16..466db0c 100644 --- a/templates/java/api_test.mustache +++ b/templates/java/api_test.mustache @@ -1,3 +1,6 @@ +{{! This template had to be customized because of our changes to the DefaultApi and ApiClient classes }} +{{! Original template: https://github.com/OpenAPITools/openapi-generator/blob/v7.15.0/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/api_test.mustache }} + {{>licenseInfo}} package {{invokerPackage}}; diff --git a/templates/java/custom/serviceApi.mustache b/templates/java/custom/serviceApi.mustache index a5037d1..9fcae87 100644 --- a/templates/java/custom/serviceApi.mustache +++ b/templates/java/custom/serviceApi.mustache @@ -45,7 +45,7 @@ public class {{serviceName}}Api extends DefaultApi { * Constructor for {{serviceName}}Api * * @param httpClient OkHttpClient object - * @param configuraction your STACKIT SDK CoreConfiguration + * @param configuration your STACKIT SDK CoreConfiguration * @throws IOException */ public {{serviceName}}Api(OkHttpClient httpClient, CoreConfiguration configuration) throws IOException {