diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/ApiClient.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/ApiClient.mustache index 8a1350c35431..896e8b922445 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/ApiClient.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/ApiClient.mustache @@ -1,5 +1,6 @@ package {{invokerPackage}}; +import {{javaxPackage}}.ws.rs.client.AsyncInvoker; import {{javaxPackage}}.ws.rs.client.Client; import {{javaxPackage}}.ws.rs.client.ClientBuilder; import {{javaxPackage}}.ws.rs.client.Entity; @@ -39,6 +40,7 @@ import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import org.glassfish.jersey.logging.LoggingFeature; import java.util.AbstractMap.SimpleEntry; +import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import java.util.Collection; @@ -1160,7 +1162,46 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { } /** - * Invoke API by sending HTTP request with the given options. + * Asynchronously invoke API by sending HTTP request with the given options. + * + * @param Type + * @param operation The qualified name of the operation + * @param path The sub-path of the HTTP URL + * @param method The request method, one of "GET", "POST", "PUT", "HEAD" and "DELETE" + * @param queryParams The query parameters + * @param body The request body object + * @param headerParams The header parameters + * @param cookieParams The cookie parameters + * @param formParams The form parameters + * @param accept The request's Accept header + * @param contentType The request's Content-Type header + * @param authNames The authentications to apply + * @param returnType The return type into which to deserialize the response + * @param isBodyNullable True if the body is nullable + * @return The future response body in type of string + * @throws ApiException API exception + */ + public Future invokeAPIAsync( + String operation, + String path, + String method, + List queryParams, + Object body, + Map headerParams, + Map cookieParams, + Map formParams, + String accept, + String contentType, + String[] authNames, + GenericType returnType, + boolean isBodyNullable) + throws ApiException { + BuilderAndEntity builderAndEntity = prepareInvoker(operation, path, method, queryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames, isBodyNullable); + return sendRequestAsync(method, builderAndEntity.invocationBuilder.async(), builderAndEntity.entity); + } + + /** + * Invoke API synchronously by sending HTTP request with the given options. * * @param Type * @param operation The qualified name of the operation @@ -1180,38 +1221,117 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { * @throws ApiException API exception */ public ApiResponse invokeAPI( - String operation, - String path, - String method, - List queryParams, - Object body, - Map headerParams, - Map cookieParams, - Map formParams, - String accept, - String contentType, - String[] authNames, - GenericType returnType, - boolean isBodyNullable) - throws ApiException { + String operation, + String path, + String method, + List queryParams, + Object body, + Map headerParams, + Map cookieParams, + Map formParams, + String accept, + String contentType, + String[] authNames, + GenericType returnType, + boolean isBodyNullable) + throws ApiException { + BuilderAndEntity builderAndEntity = prepareInvoker(operation, path, method, queryParams, body, headerParams, cookieParams, formParams, accept, contentType, authNames, isBodyNullable); + + Response response = null; + + try { + response = sendRequest(method, builderAndEntity.invocationBuilder, builderAndEntity.entity); + {{#hasOAuthMethods}} + response = renewOauthIfNeeded(method, authNames, response.getStatus(), builderAndEntity.invocationBuilder, builderAndEntity.entity); + {{/hasOAuthMethods}} + return toApiResponse(returnType, response); + } finally { + try { + response.close(); + } catch (Exception e) { + // it's not critical, since the response object is local in method invokeAPI; that's fine, + // just continue + } + } + } + {{#hasOAuthMethods}} + private Response renewOauthIfNeeded(String method, String[] authNames, int statusCode, Invocation.Builder invocationBuilder, Entity entity) { + // If OAuth is used and a status 401 is received, renew the access token and retry the request + Response response = null; + if (authNames != null && statusCode == Status.UNAUTHORIZED.getStatusCode()) { + for (String authName : authNames) { + Authentication authentication = authentications.get(authName); + if (authentication instanceof OAuth) { + OAuth2AccessToken accessToken = ((OAuth) authentication).renewAccessToken(); + if (accessToken != null) { + invocationBuilder.header("Authorization", null); + invocationBuilder.header("Authorization", "Bearer " + accessToken.getAccessToken()); + response = sendRequest(method, invocationBuilder, entity); + } + break; + } + } + } + return response; + } + {{/hasOAuthMethods}} + + /** + * Convert the Jax RS Response to ApiResponse. + * @param returnType The return type into which to deserialize the response + * @param response The response from the jax-rs invocation. + * @return The ApiResponse that respresents this Jax RS response. + * @param The type into which to deserialize the response + * @throws ApiException API Exceptions are thrown in case of failing status codes. + */ + public ApiResponse toApiResponse(GenericType returnType, Response response) throws ApiException { + int statusCode = response.getStatusInfo().getStatusCode(); + Map> responseHeaders = buildResponseHeaders(response); + + if (response.getStatusInfo() == Status.NO_CONTENT) { + return new ApiResponse(statusCode, responseHeaders); + } else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) { + if (returnType == null) { + return new ApiResponse(statusCode, responseHeaders); + } else { + return new ApiResponse(statusCode, responseHeaders, deserialize(response, returnType)); + } + } else { + String message = "error"; + String respBody = null; + if (response.hasEntity()) { + try { + respBody = String.valueOf(response.readEntity(String.class)); + message = respBody; + } catch (RuntimeException e) { + // e.printStackTrace(); + } + } + throw new ApiException( + response.getStatus(), message, buildResponseHeaders(response), respBody); + } + } + + private BuilderAndEntity prepareInvoker(String operation, String path, String method, List queryParams, Object body, Map headerParams, Map cookieParams, Map formParams, String accept, String contentType, String[] authNames, boolean isBodyNullable) throws ApiException { + // Not using `.target(targetURL).path(path)` below, + // to support (constant) query string in `path`, e.g. "/posts?draft=1" String targetURL; - List serverConfigurations; - if (serverIndex != null && (serverConfigurations = operationServers.get(operation)) != null) { - int index = operationServerIndex.getOrDefault(operation, serverIndex).intValue(); - Map variables = operationServerVariables.getOrDefault(operation, serverVariables); + if (serverIndex != null && operationServers.containsKey(operation)) { + Integer index = operationServerIndex.containsKey(operation) ? operationServerIndex.get(operation) : serverIndex; + Map variables = operationServerVariables.containsKey(operation) ? + operationServerVariables.get(operation) : serverVariables; + List serverConfigurations = operationServers.get(operation); if (index < 0 || index >= serverConfigurations.size()) { throw new ArrayIndexOutOfBoundsException( - String.format( - "Invalid index %d when selecting the host settings. Must be less than %d", - index, serverConfigurations.size())); + String.format( + "Invalid index %d when selecting the host settings. Must be less than %d", + index, serverConfigurations.size())); } targetURL = serverConfigurations.get(index).URL(variables) + path; } else { targetURL = this.basePath + path; } - // Not using `.target(targetURL).path(path)` below, - // to support (constant) query string in `path`, e.g. "/posts?draft=1" WebTarget target = httpClient.target(targetURL); if (queryParams != null) { @@ -1222,10 +1342,11 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { } } - Invocation.Builder invocationBuilder = target.request(); - + Invocation.Builder invocationBuilder; if (accept != null) { - invocationBuilder = invocationBuilder.accept(accept); + invocationBuilder = target.request().accept(accept); + } else { + invocationBuilder = target.request(); } for (Entry entry : cookieParams.entrySet()) { @@ -1248,22 +1369,15 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { Map allHeaderParams = new HashMap<>(defaultHeaderMap); allHeaderParams.putAll(headerParams); - if (authNames != null) { - // update different parameters (e.g. headers) for authentication - updateParamsForAuth( - authNames, - queryParams, - allHeaderParams, - cookieParams, - {{#hasHttpSignatureMethods}} - serializeToString(body, formParams, contentType, isBodyNullable), - {{/hasHttpSignatureMethods}} - {{^hasHttpSignatureMethods}} - null, - {{/hasHttpSignatureMethods}} - method, - target.getUri()); - } + // update different parameters (e.g. headers) for authentication + updateParamsForAuth( + authNames, + queryParams, + allHeaderParams, + cookieParams, + null, + method, + target.getUri()); for (Entry entry : allHeaderParams.entrySet()) { String value = entry.getValue(); @@ -1271,63 +1385,16 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { invocationBuilder = invocationBuilder.header(entry.getKey(), value); } } + return new BuilderAndEntity(invocationBuilder, entity); + } - Response response = null; - - try { - response = sendRequest(method, invocationBuilder, entity); - - final int statusCode = response.getStatusInfo().getStatusCode(); - - {{#hasOAuthMethods}} - // If OAuth is used and a status 401 is received, renew the access token and retry the request - if (authNames != null && statusCode == Status.UNAUTHORIZED.getStatusCode()) { - for (String authName : authNames) { - Authentication authentication = authentications.get(authName); - if (authentication instanceof OAuth) { - OAuth2AccessToken accessToken = ((OAuth) authentication).renewAccessToken(); - if (accessToken != null) { - invocationBuilder.header("Authorization", null); - invocationBuilder.header("Authorization", "Bearer " + accessToken.getAccessToken()); - response = sendRequest(method, invocationBuilder, entity); - } - break; - } - } - } - - {{/hasOAuthMethods}} - Map> responseHeaders = buildResponseHeaders(response); + private static class BuilderAndEntity { + public final Invocation.Builder invocationBuilder; + public final Entity entity; - if (statusCode == Status.NO_CONTENT.getStatusCode()) { - return new ApiResponse(statusCode, responseHeaders); - } else if (response.getStatusInfo().getFamily() == Status.Family.SUCCESSFUL) { - if (returnType == null) { - return new ApiResponse(statusCode, responseHeaders); - } else { - return new ApiResponse(statusCode, responseHeaders, deserialize(response, returnType)); - } - } else { - String message = "error"; - String respBody = null; - if (response.hasEntity()) { - try { - respBody = String.valueOf(response.readEntity(String.class)); - message = respBody; - } catch (RuntimeException e) { - // e.printStackTrace(); - } - } - throw new ApiException( - response.getStatus(), message, buildResponseHeaders(response), respBody); - } - } finally { - try { - response.close(); - } catch (Exception e) { - // it's not critical, since the response object is local in method invokeAPI; that's fine, - // just continue - } + public BuilderAndEntity(Invocation.Builder invocationBuilder, Entity entity) { + this.invocationBuilder = invocationBuilder; + this.entity = entity; } } @@ -1347,6 +1414,22 @@ public class ApiClient{{#jsr310}} extends JavaTimeFormatter{{/jsr310}} { return response; } + private Future sendRequestAsync(String method, AsyncInvoker asyncInvoker, Entity entity) { + Future response; + if ("POST".equals(method)) { + response = asyncInvoker.post(entity); + } else if ("PUT".equals(method)) { + response = asyncInvoker.put(entity); + } else if ("DELETE".equals(method)) { + response = asyncInvoker.method("DELETE", entity); + } else if ("PATCH".equals(method)) { + response = asyncInvoker.method("PATCH", entity); + } else { + response = asyncInvoker.method(method); + } + return response; + } + /** * @deprecated Add qualified name of the operation as a first parameter. */ diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/api.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/api.mustache index 5f8019da4f40..265f076c327b 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/api.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/jersey2/api.mustache @@ -6,6 +6,7 @@ import {{invokerPackage}}.ApiResponse; import {{invokerPackage}}.Configuration; import {{invokerPackage}}.Pair; +import {{javaxPackage}}.ws.rs.core.Response; import {{javaxPackage}}.ws.rs.core.GenericType; {{#imports}}import {{import}}; @@ -16,6 +17,7 @@ import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Future; {{>generatedAnnotation}} {{#operations}} @@ -85,6 +87,42 @@ public class {{classname}} { } {{/vendorExtensions.x-group-parameters}} + {{^vendorExtensions.x-group-parameters}} + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + {{#returnType}} + * @return {{.}} + {{/returnType}} + * @throws ApiException if fails to make API call + {{#responses.0}} + * @http.response.details + + + {{#responses}} + + {{/responses}} +
Status Code Description Response Headers
{{code}} {{message}} {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}}
+ {{/responses.0}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public Future {{operationId}}Async({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { + return {{operationId}}WithHttpInfoAsync({{#allParams}}{{paramName}}{{^-last}}, {{/-last}}{{/allParams}}); + } + {{/vendorExtensions.x-group-parameters}} + {{^vendorExtensions.x-group-parameters}} /** * {{summary}} @@ -114,7 +152,7 @@ public class {{classname}} { {{#isDeprecated}} @Deprecated {{/isDeprecated}} - public{{/vendorExtensions.x-group-parameters}}{{#vendorExtensions.x-group-parameters}}private{{/vendorExtensions.x-group-parameters}} ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}> {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { + public ApiResponse<{{{returnType}}}{{^returnType}}Void{{/returnType}}> {{operationId}}WithHttpInfo({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { {{#hasRequiredParams}} // Check required parameters {{#allParams}} @@ -194,8 +232,120 @@ public class {{classname}} { {{#headerParams}}{{#-first}}localVarHeaderParams{{/-first}}{{/headerParams}}{{^headerParams}}new LinkedHashMap<>(){{/headerParams}}, {{#cookieParams}}{{#-first}}localVarCookieParams{{/-first}}{{/cookieParams}}{{^cookieParams}}new LinkedHashMap<>(){{/cookieParams}}, {{#formParams}}{{#-first}}localVarFormParams{{/-first}}{{/formParams}}{{^formParams}}new LinkedHashMap<>(){{/formParams}}, localVarAccept, localVarContentType, {{#hasAuthMethods}}localVarAuthNames{{/hasAuthMethods}}{{^hasAuthMethods}}null{{/hasAuthMethods}}, {{#returnType}}localVarReturnType{{/returnType}}{{^returnType}}null{{/returnType}}, {{#bodyParam}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/bodyParam}}{{^bodyParam}}false{{/bodyParam}}); } - {{#vendorExtensions.x-group-parameters}} + {{/vendorExtensions.x-group-parameters}} + + {{^vendorExtensions.x-group-parameters}} + /** + * {{summary}} + * {{notes}} + {{#allParams}} + * @param {{paramName}} {{description}}{{#required}} (required){{/required}}{{^required}} (optional{{#defaultValue}}, default to {{.}}{{/defaultValue}}){{/required}} + {{/allParams}} + * @return ApiResponse<{{returnType}}{{^returnType}}Void{{/returnType}}> + * @throws ApiException if fails to make API call + {{#responses.0}} + * @http.response.details + + + {{#responses}} + + {{/responses}} +
Status Code Description Response Headers
{{code}} {{message}} {{#headers}} * {{baseName}} - {{description}}
{{/headers}}{{^headers.0}} - {{/headers.0}}
+ {{/responses.0}} + {{#isDeprecated}} + * @deprecated + {{/isDeprecated}} + {{#externalDocs}} + * {{description}} + * @see {{summary}} Documentation + {{/externalDocs}} + */ + {{#isDeprecated}} + @Deprecated + {{/isDeprecated}} + public Future {{operationId}}WithHttpInfoAsync({{#allParams}}{{{dataType}}} {{paramName}}{{^-last}}, {{/-last}}{{/allParams}}) throws ApiException { + {{#hasRequiredParams}} + // Check required parameters + {{#allParams}} + {{#required}} + if ({{paramName}} == null) { + throw new ApiException(400, "Missing the required parameter '{{paramName}}' when calling {{operationId}}"); + } + {{/required}} + {{/allParams}} + + {{/hasRequiredParams}} + {{#hasPathParams}} + // Path parameters + String localVarPath = "{{{path}}}"{{#pathParams}} + .replaceAll({{=% %=}}"\\{%baseName%}"%={{ }}=%, apiClient.escapeString({{{paramName}}}{{#isUuid}}.toString(){{/isUuid}}{{^isString}}.toString(){{/isString}})){{/pathParams}}; + {{/hasPathParams}} + {{#queryParams}} + {{#-first}} + // Query parameters + List localVarQueryParams = new ArrayList<>( + apiClient.parameterToPairs("{{{collectionFormat}}}", "{{baseName}}", {{paramName}}) + ); + {{/-first}} + {{^-first}} + localVarQueryParams.addAll(apiClient.parameterToPairs("{{{collectionFormat}}}", "{{baseName}}", {{paramName}})); + {{/-first}} + {{#-last}} + + {{/-last}} + {{/queryParams}} + {{#headerParams}} + {{#-first}} + // Header parameters + Map localVarHeaderParams = new LinkedHashMap<>(); + {{/-first}} + {{^required}}if ({{paramName}} != null) { + {{/required}}localVarHeaderParams.put("{{baseName}}", apiClient.parameterToString({{paramName}}));{{^required}} + }{{/required}} + {{#-last}} + + {{/-last}} + {{/headerParams}} + {{#cookieParams}} + {{#-first}} + // Cookie parameters + Map localVarCookieParams = new LinkedHashMap<>(); + {{/-first}} + {{^required}}if ({{paramName}} != null) { + {{/required}}localVarCookieParams.put("{{baseName}}", apiClient.parameterToString({{paramName}}));{{^required}} + }{{/required}} + {{#-last}} + + {{/-last}} + {{/cookieParams}} + {{#formParams}} + {{#-first}} + // Form parameters + Map localVarFormParams = new LinkedHashMap<>(); + {{/-first}} + {{^required}}if ({{paramName}} != null) { + {{/required}}localVarFormParams.put("{{baseName}}", {{paramName}});{{^required}} + }{{/required}} + {{#-last}} + + {{/-last}} + {{/formParams}} + String localVarAccept = apiClient.selectHeaderAccept({{#produces}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/produces}}); + String localVarContentType = apiClient.selectHeaderContentType({{#consumes}}"{{{mediaType}}}"{{^-last}}, {{/-last}}{{/consumes}}); + {{#hasAuthMethods}} + String[] localVarAuthNames = {{=% %=}}new String[] {%#authMethods%"%name%"%^-last%, %/-last%%/authMethods%};%={{ }}=% + {{/hasAuthMethods}} + {{#returnType}} + GenericType<{{{returnType}}}> localVarReturnType = new GenericType<{{{returnType}}}>() {}; + {{/returnType}} + return apiClient.invokeAPIAsync("{{classname}}.{{operationId}}", {{#hasPathParams}}localVarPath{{/hasPathParams}}{{^hasPathParams}}"{{path}}"{{/hasPathParams}}, "{{httpMethod}}", {{#queryParams}}{{#-first}}localVarQueryParams{{/-first}}{{/queryParams}}{{^queryParams}}new ArrayList<>(){{/queryParams}}, {{#bodyParam}}{{paramName}}{{/bodyParam}}{{^bodyParam}}null{{/bodyParam}}, + {{#headerParams}}{{#-first}}localVarHeaderParams{{/-first}}{{/headerParams}}{{^headerParams}}new LinkedHashMap<>(){{/headerParams}}, {{#cookieParams}}{{#-first}}localVarCookieParams{{/-first}}{{/cookieParams}}{{^cookieParams}}new LinkedHashMap<>(){{/cookieParams}}, {{#formParams}}{{#-first}}localVarFormParams{{/-first}}{{/formParams}}{{^formParams}}new LinkedHashMap<>(){{/formParams}}, localVarAccept, localVarContentType, + {{#hasAuthMethods}}localVarAuthNames{{/hasAuthMethods}}{{^hasAuthMethods}}null{{/hasAuthMethods}}, {{#returnType}}localVarReturnType{{/returnType}}{{^returnType}}null{{/returnType}}, {{#bodyParam}}{{#isNullable}}true{{/isNullable}}{{^isNullable}}false{{/isNullable}}{{/bodyParam}}{{^bodyParam}}false{{/bodyParam}}); + } + {{/vendorExtensions.x-group-parameters}} + + {{#vendorExtensions.x-group-parameters}} public class API{{operationId}}Request { {{#allParams}} private {{{dataType}}} {{paramName}}; diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java index aef203469363..a37df4fa4dbb 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaClientCodegenTest.java @@ -2699,4 +2699,32 @@ public void testHandleConstantParams() throws IOException { .bodyContainsLines( "localVarHeaderParams.put(\"X-CUSTOM_CONSTANT_HEADER\", \"CONSTANT_VALUE\")"); } + + @Test + public void testIssue16841Jersey2Async() throws Exception { + File output = Files.createTempDirectory("test").toFile(); + output.deleteOnExit(); + + final CodegenConfigurator configurator = + new CodegenConfigurator() + .setGeneratorName("java") + .setLibrary("jersey2") + .setInputSpec("src/test/resources/3_1/java/petstore.yaml") + .setOutputDir(output.getAbsolutePath().replace("\\", "/")); + + final ClientOptInput clientOptInput = configurator.toClientOptInput(); + DefaultGenerator generator = new DefaultGenerator(); + Map files = generator.opts(clientOptInput).generate().stream() + .collect(Collectors.toMap(File::getName, Function.identity())); + + validateJavaSourceFiles(new ArrayList<>(files.values())); + + JavaFileAssert javaFileAssert = JavaFileAssert.assertThat(files.get("ApiClient.java")); + javaFileAssert + .assertMethod("invokeAPIAsync") + .bodyContainsLines("return sendRequestAsync(method, builderAndEntity.invocationBuilder.async(), builderAndEntity.entity);"); + JavaFileAssert.assertThat(files.get("UserApi.java")) + .assertMethod("createUserAsync") + .bodyContainsLines("return createUserWithHttpInfoAsync(user);"); + } }