Add support for asynchronous API for JavaJaxRsSpec (#10919)

* Add support for asynchronous API for JavaJaxRsSpec

https://github.com/OpenAPITools/openapi-generator/issues/4832

* Ran the requested command for a PR

Run the following to build the project and update samples:
./mvnw clean package
./bin/generate-samples.sh
./bin/utils/export_docs_generators.sh

* Set java 8 mode when using supportAsync=true

`CompletionStage` are only introduced since 1.8, enabling async should for use java8

Co-authored-by: Thomas Bredzinski <thomas.bredzinski@gemalto.com>
This commit is contained in:
Thomas Bredzinski 2022-01-02 20:55:01 -05:00 committed by GitHub
parent e71ee1bf43
commit 508da12ca7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 230 additions and 3 deletions

View File

@ -58,6 +58,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|sourceFolder|source folder for generated code| |src/gen/java|
|supportAsync|Wrap responses in CompletionStage type, allowing asynchronous computation (requires JAX-RS 2.1).| |false|
|title|a title describing the application| |OpenAPI Server|
|useBeanValidation|Use BeanValidation API annotations| |true|
|useSwaggerAnnotations|Whether to generate Swagger annotations.| |true|

View File

@ -58,6 +58,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|sourceFolder|source folder for generated code| |src/main/java|
|supportAsync|Wrap responses in CompletionStage type, allowing asynchronous computation (requires JAX-RS 2.1).| |false|
|title|a title describing the application| |OpenAPI Server|
|useBeanValidation|Use BeanValidation API annotations| |true|
|useSwaggerAnnotations|Whether to generate Swagger annotations.| |true|

View File

@ -102,6 +102,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
cliOptions.add(CliOption.newBoolean(RETURN_RESPONSE, "Whether generate API interface should return javax.ws.rs.core.Response instead of a deserialized entity. Only useful if interfaceOnly is true.").defaultValue(String.valueOf(returnResponse)));
cliOptions.add(CliOption.newBoolean(USE_SWAGGER_ANNOTATIONS, "Whether to generate Swagger annotations.", useSwaggerAnnotations));
cliOptions.add(CliOption.newString(OPEN_API_SPEC_FILE_LOCATION, "Location where the file containing the spec will be generated in the output folder. No file generated when set to null or empty string."));
cliOptions.add(CliOption.newBoolean(SUPPORT_ASYNC, "Wrap responses in CompletionStage type, allowing asynchronous computation (requires JAX-RS 2.1).", supportAsync));
}
@Override
@ -121,6 +122,14 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
additionalProperties.remove(RETURN_RESPONSE);
}
}
if (additionalProperties.containsKey(SUPPORT_ASYNC)) {
supportAsync = Boolean.parseBoolean(additionalProperties.get(SUPPORT_ASYNC).toString());
if (!supportAsync) {
additionalProperties.remove(SUPPORT_ASYNC);
} else {
setJava8ModeAndAdditionalProperties(true);
}
}
if (QUARKUS_LIBRARY.equals(library) || THORNTAIL_LIBRARY.equals(library) || HELIDON_LIBRARY.equals(library) || OPEN_LIBERTY_LIBRARY.equals(library) || KUMULUZEE_LIBRARY.equals(library)) {
useSwaggerAnnotations = false;
} else {

View File

@ -9,6 +9,10 @@ import javax.ws.rs.core.Response;
{{#useSwaggerAnnotations}}
import io.swagger.annotations.*;
{{/useSwaggerAnnotations}}
{{#supportAsync}}
import java.util.concurrent.CompletionStage;
import java.util.concurrent.CompletableFuture;
{{/supportAsync}}
import java.io.InputStream;
import java.util.Map;

View File

@ -10,4 +10,4 @@
{{/isOAuth}}{{/authMethods}} }{{/hasAuthMethods}}, tags={ {{#vendorExtensions.x-tags}}"{{tag}}"{{^-last}}, {{/-last}}{{/vendorExtensions.x-tags}} })
@ApiResponses(value = { {{#responses}}
@ApiResponse(code = {{{code}}}, message = "{{{message}}}", response = {{{baseType}}}.class{{#returnContainer}}, responseContainer = "{{{.}}}"{{/returnContainer}}){{^-last}},{{/-last}}{{/responses}} }){{/useSwaggerAnnotations}}
{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnTypeInterface}}{{/returnResponse}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}});
{{#supportAsync}}{{>returnAsyncTypeInterface}}{{/supportAsync}}{{^supportAsync}}{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnTypeInterface}}{{/returnResponse}}{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}});

View File

@ -11,6 +11,6 @@
@ApiResponses(value = { {{#responses}}
@ApiResponse(code = {{{code}}}, message = "{{{message}}}", response = {{{baseType}}}.class{{#containerType}}, responseContainer = "{{{.}}}"{{/containerType}}){{^-last}},{{/-last}}{{/responses}}
}){{/useSwaggerAnnotations}}
public Response {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}) {
return Response.ok().entity("magic!").build();
public {{#supportAsync}}CompletionStage<{{/supportAsync}}Response{{#supportAsync}}>{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}) {
return {{#supportAsync}}CompletableFuture.supplyAsync(() -> {{/supportAsync}}Response.ok().entity("magic!").build(){{#supportAsync}}){{/supportAsync}};
}

View File

@ -140,6 +140,11 @@
{{/useBeanValidation}}
</dependencies>
<properties>
{{#java8}}
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
{{/java8}}
<jackson-version>2.9.9</jackson-version>
<junit-version>4.13.1</junit-version>
{{#useBeanValidation}}

View File

@ -0,0 +1 @@
CompletionStage<{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{#returnContainer}}{{#isMap}}Map<String, {{{returnBaseType}}}>{{/isMap}}{{#isArray}}{{{returnContainer}}}<{{{returnBaseType}}}>{{/isArray}}{{/returnContainer}}{{^returnContainer}}{{{returnBaseType}}}{{/returnContainer}}{{/returnResponse}}>

View File

@ -28,6 +28,11 @@ import java.util.Map;
import static org.openapitools.codegen.TestUtils.assertFileContains;
import static org.openapitools.codegen.TestUtils.validateJavaSourceFiles;
import static org.openapitools.codegen.languages.AbstractJavaCodegen.JAVA8_MODE;
import static org.openapitools.codegen.languages.AbstractJavaJAXRSServerCodegen.USE_TAGS;
import static org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen.INTERFACE_ONLY;
import static org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen.SUPPORT_ASYNC;
import static org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen.RETURN_RESPONSE;
import static org.testng.Assert.assertTrue;
/**
@ -95,6 +100,8 @@ public class JavaJAXRSSpecServerCodegenTest extends JavaJaxrsBaseTest {
codegen.additionalProperties().put(CodegenConstants.INVOKER_PACKAGE, "xyz.yyyyy.iiii.invoker");
codegen.additionalProperties().put("serverPort", "8088");
codegen.additionalProperties().put(JavaJAXRSSpecServerCodegen.OPEN_API_SPEC_FILE_LOCATION, "openapi.yml");
codegen.additionalProperties().put(SUPPORT_ASYNC, true);
codegen.additionalProperties().put(JAVA8_MODE, false);
codegen.processOpts();
OpenAPI openAPI = new OpenAPI();
@ -112,6 +119,8 @@ public class JavaJAXRSSpecServerCodegenTest extends JavaJaxrsBaseTest {
Assert.assertEquals(codegen.additionalProperties().get(AbstractJavaJAXRSServerCodegen.SERVER_PORT), "8088");
Assert.assertEquals(codegen.getOpenApiSpecFileLocation(), "openapi.yml");
Assert.assertEquals(codegen.additionalProperties().get(JavaJAXRSSpecServerCodegen.OPEN_API_SPEC_FILE_LOCATION), "openapi.yml");
Assert.assertEquals(codegen.additionalProperties().get(SUPPORT_ASYNC), "true");
Assert.assertEquals(codegen.additionalProperties().get(JAVA8_MODE), true); //overridden by supportAsync=true
}
/**
@ -419,4 +428,170 @@ public class JavaJAXRSSpecServerCodegenTest extends JavaJaxrsBaseTest {
assertFileContains(path, "\nimport java.util.Set;\n");
}
@Test
public void generateApiWithAsyncSupport() throws Exception {
final File output = Files.createTempDirectory("test").toFile();
output.deleteOnExit();
final OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(SUPPORT_ASYNC, true); //Given support async is enabled
final ClientOptInput input = new ClientOptInput()
.openAPI(openAPI)
.config(codegen); //Using JavaJAXRSSpecServerCodegen
final DefaultGenerator generator = new DefaultGenerator();
final List<File> files = generator.opts(input).generate(); //When generating files
//Then the java files are compilable
validateJavaSourceFiles(files);
//And the generated class contains CompletionStage<Response>
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PingApi.java"),
"\nimport java.util.concurrent.CompletionStage;\n",
"\nimport java.util.concurrent.CompletableFuture;\n",
"\npublic CompletionStage<Response> pingGet() {\n",
"\nCompletableFuture.supplyAsync(() -> Response.ok().entity(\"magic!\").build())\n"
);
}
@Test
public void generateApiWithAsyncSupportAndInterfaceOnly() throws Exception {
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(SUPPORT_ASYNC, true); //Given support async is enabled
codegen.additionalProperties().put(INTERFACE_ONLY, true); //And only interfaces are generated
final ClientOptInput input = new ClientOptInput()
.openAPI(openAPI)
.config(codegen); //Using JavaJAXRSSpecServerCodegen
final DefaultGenerator generator = new DefaultGenerator();
final List<File> files = generator.opts(input).generate(); //When generating files
//Then the java files are compilable
validateJavaSourceFiles(files);
//And the generated interface contains CompletionStage<Void>
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PingApi.java"),
"\nimport java.util.concurrent.CompletionStage;\n",
"\nCompletionStage<Void> pingGet();\n");
}
@Test
public void generateApiWithAsyncSupportAndInterfaceOnlyAndResponse() throws Exception {
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/ping.yaml", null, new ParseOptions()).getOpenAPI();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(SUPPORT_ASYNC, true); //Given support async is enabled
codegen.additionalProperties().put(INTERFACE_ONLY, true); //And only interfaces are generated
codegen.additionalProperties().put(RETURN_RESPONSE, true); //And return type is Response
final ClientOptInput input = new ClientOptInput()
.openAPI(openAPI)
.config(codegen); //Using JavaJAXRSSpecServerCodegen
final DefaultGenerator generator = new DefaultGenerator();
final List<File> files = generator.opts(input).generate(); //When generating files
//Then the java files are compilable
validateJavaSourceFiles(files);
//And the generated interface contains CompletionStage<Response>
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
assertFileContains(output.toPath().resolve( "src/gen/java/org/openapitools/api/PingApi.java"),
"\nimport java.util.concurrent.CompletionStage;\n",
"\nCompletionStage<Response> pingGet();\n");
}
@Test
public void generatePetstoreAPIWithAsyncSupport() throws Exception {
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/petstore.yaml", null, new ParseOptions()).getOpenAPI();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(SUPPORT_ASYNC, true); //Given support async is enabled
codegen.additionalProperties().put(INTERFACE_ONLY, true); //And only interfaces are generated
final ClientOptInput input = new ClientOptInput()
.openAPI(openAPI)
.config(codegen); //using JavaJAXRSSpecServerCodegen
final DefaultGenerator generator = new DefaultGenerator();
final List<File> files = generator.opts(input).generate(); //When generating files
//Then the java files are compilable
validateJavaSourceFiles(files);
//And the generated interfaces contains CompletionStage
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PetApi.java");
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PetApi.java"),
"\nimport java.util.concurrent.CompletionStage;\n",
"CompletionStage<Void> deletePet", //Support empty response
"CompletionStage<List<Pet>> findPetsByStatus", //Support type of arrays response
"CompletionStage<Pet> getPetById" //Support single type response
);
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/StoreApi.java");
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/StoreApi.java"),
"\nimport java.util.concurrent.CompletionStage;\n",
"CompletionStage<Map<String, Integer>>" //Support map response
);
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/UserApi.java");
assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/UserApi.java"),
"\nimport java.util.concurrent.CompletionStage;\n",
"CompletionStage<String>" //Support simple types
);
}
@Test
public void generatePingWithAsyncSupportPrimitiveType() throws Exception {
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = new OpenAPIParser()
.readLocation("src/test/resources/3_0/issue_4832.yaml", null, new ParseOptions()).getOpenAPI();
codegen.setOutputDir(output.getAbsolutePath());
codegen.additionalProperties().put(SUPPORT_ASYNC, true); //Given support async is enabled
codegen.additionalProperties().put(INTERFACE_ONLY, true); //And only interfaces are generated
codegen.additionalProperties().put(USE_TAGS, true); //And use tags to generate everything in PingApi.java
final ClientOptInput input = new ClientOptInput()
.openAPI(openAPI)
.config(codegen); //using JavaJAXRSSpecServerCodegen
final DefaultGenerator generator = new DefaultGenerator();
final List<File> files = generator.opts(input).generate(); //When generating files
//Then the java files are compilable
validateJavaSourceFiles(files);
//And the generated interfaces contains CompletionStage with proper classes instead of primitive types
TestUtils.ensureContainsFile(files, output, "src/gen/java/org/openapitools/api/PingApi.java");
TestUtils.assertFileContains(output.toPath().resolve("src/gen/java/org/openapitools/api/PingApi.java"),
"CompletionStage<Boolean> pingGetBoolean", //Support primitive types response
"CompletionStage<Integer> pingGetInteger" //Support primitive types response
);
}
}

View File

@ -0,0 +1,31 @@
openapi: 3.0.1
info:
title: ping that return primitive types
version: '1.0'
servers:
- url: 'http://localhost:8082/'
paths:
/pingBoolean:
get:
operationId: pingGetBoolean
tags: [Ping]
responses:
'200':
description: OK
content:
'application/json':
schema:
type: boolean
/pingInteger:
get:
operationId: pingGetInteger
tags: [Ping]
responses:
'200':
description: OK
content:
'application/json':
schema:
type: integer
format: int32