[jaxrs-spec][quarkus] Feat: Add an option to use "org.jboss.resteasy.reactive.RestResponse" (#21877)

* replace Response by RestResponse

* fix rest response for Async

* update documentation

* Add option "returnJBossResponse"

* Add tests + fixes

* Polish

* Add dependency "io.quarkus.resteasy.reactive:resteasy-reactive" when returnJBossResponse=true

* fix JavaJAXRSSpecServerCodegenTest

* generate samples & docs
This commit is contained in:
Christophe Moine 2025-09-14 11:11:06 +02:00 committed by GitHub
parent ec28d6261c
commit ee4cb9ac22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 148 additions and 3 deletions

View File

@ -64,6 +64,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|parentGroupId|parent groupId in generated pom N.B. parentGroupId, parentArtifactId and parentVersion must all be specified for any of them to take effect| |null|
|parentVersion|parent version in generated pom N.B. parentGroupId, parentArtifactId and parentVersion must all be specified for any of them to take effect| |null|
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
|returnJBossResponse|Whether generate API interface should return `org.jboss.resteasy.reactive.RestResponse` instead of a deserialized entity. This flag cannot be combined with `returnResponse` flag. It requires the flag `interfaceOnly` and `useJakartaEE` set to true, because `org.jboss.resteasy.reactive.RestResponse` was introduced in Quarkus 2.x| |false|
|returnResponse|Whether generate API interface should return javax.ws.rs.core.Response instead of a deserialized entity. Only useful if interfaceOnly is true.| |false|
|scmConnection|SCM connection in generated pom.xml| |scm:git:git@github.com:openapitools/openapi-generator.git|
|scmDeveloperConnection|SCM developer connection in generated pom.xml| |scm:git:git@github.com:openapitools/openapi-generator.git|

View File

@ -65,6 +65,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|parentGroupId|parent groupId in generated pom N.B. parentGroupId, parentArtifactId and parentVersion must all be specified for any of them to take effect| |null|
|parentVersion|parent version in generated pom N.B. parentGroupId, parentArtifactId and parentVersion must all be specified for any of them to take effect| |null|
|prependFormOrBodyParameters|Add form or body parameters to the beginning of the parameter list.| |false|
|returnJBossResponse|Whether generate API interface should return `org.jboss.resteasy.reactive.RestResponse` instead of a deserialized entity. This flag cannot be combined with `returnResponse` flag. It requires the flag `interfaceOnly` and `useJakartaEE` set to true, because `org.jboss.resteasy.reactive.RestResponse` was introduced in Quarkus 2.x| |false|
|returnResponse|Whether generate API interface should return javax.ws.rs.core.Response instead of a deserialized entity. Only useful if interfaceOnly is true.| |false|
|scmConnection|SCM connection in generated pom.xml| |scm:git:git@github.com:openapitools/openapi-generator.git|
|scmDeveloperConnection|SCM developer connection in generated pom.xml| |scm:git:git@github.com:openapitools/openapi-generator.git|

View File

@ -18,6 +18,7 @@
package org.openapitools.codegen.languages;
import io.swagger.v3.oas.models.media.Schema;
import java.util.Locale;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
@ -37,6 +38,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
public static final String INTERFACE_ONLY = "interfaceOnly";
public static final String RETURN_RESPONSE = "returnResponse";
public static final String RETURN_JBOSS_RESPONSE = "returnJBossResponse";
public static final String GENERATE_POM = "generatePom";
public static final String USE_SWAGGER_ANNOTATIONS = "useSwaggerAnnotations";
public static final String USE_MICROPROFILE_OPENAPI_ANNOTATIONS = "useMicroProfileOpenAPIAnnotations";
@ -52,6 +54,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
private boolean interfaceOnly = false;
private boolean returnResponse = false;
private boolean returnJbossResponse = false;
private boolean generatePom = true;
private boolean useSwaggerAnnotations = true;
private boolean useMicroProfileOpenAPIAnnotations = false;
@ -128,6 +131,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
cliOptions.add(CliOption.newBoolean(GENERATE_POM, "Whether to generate pom.xml if the file does not already exist.").defaultValue(String.valueOf(generatePom)));
cliOptions.add(CliOption.newBoolean(INTERFACE_ONLY, "Whether to generate only API interface stubs without the server files.").defaultValue(String.valueOf(interfaceOnly)));
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(RETURN_JBOSS_RESPONSE, "Whether generate API interface should return `org.jboss.resteasy.reactive.RestResponse` instead of a deserialized entity. This flag cannot be combined with `returnResponse` flag. It requires the flag `interfaceOnly` and `useJakartaEE` set to true, because `org.jboss.resteasy.reactive.RestResponse` was introduced in Quarkus 2.x").defaultValue(String.valueOf(returnJbossResponse)));
cliOptions.add(CliOption.newBoolean(USE_SWAGGER_ANNOTATIONS, "Whether to generate Swagger annotations.", useSwaggerAnnotations));
cliOptions.add(CliOption.newBoolean(USE_MICROPROFILE_OPENAPI_ANNOTATIONS, "Whether to generate Microprofile OpenAPI annotations. Only valid when library is set to quarkus.", useMicroProfileOpenAPIAnnotations));
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."));
@ -142,6 +146,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
convertPropertyToBooleanAndWriteBack(INTERFACE_ONLY, value -> interfaceOnly = value);
convertPropertyToBooleanAndWriteBack(RETURN_RESPONSE, value -> returnResponse = value);
convertPropertyToBooleanAndWriteBack(RETURN_JBOSS_RESPONSE, value -> returnJbossResponse = value);
convertPropertyToBooleanAndWriteBack(SUPPORT_ASYNC, this::setSupportAsync);
if (QUARKUS_LIBRARY.equals(library) || THORNTAIL_LIBRARY.equals(library) || HELIDON_LIBRARY.equals(library) || OPEN_LIBERTY_LIBRARY.equals(library) || KUMULUZEE_LIBRARY.equals(library)) {
useSwaggerAnnotations = false;
@ -218,6 +223,18 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
.doNotOverwrite());
supportingFiles.add(new SupportingFile("dockerignore.mustache", "", ".dockerignore")
.doNotOverwrite());
if(returnResponse && returnJbossResponse) {
String msg = String.format(Locale.ROOT,
"You cannot combine [%s] and [%s] since they are mutually exclusive",
RETURN_RESPONSE, RETURN_JBOSS_RESPONSE);
throw new IllegalArgumentException(msg);
}
if(returnJbossResponse && !useJakartaEe) {
String msg = String.format(Locale.ROOT,
"The [%s] requires [%s] to be true, because org.jboss.resteasy.reactive.RestResponse was introduced in Quarkus 2.x",
RETURN_JBOSS_RESPONSE, USE_JAKARTA_EE);
throw new IllegalArgumentException(msg);
}
} else if (OPEN_LIBERTY_LIBRARY.equals(library)) {
supportingFiles.add(new SupportingFile("server.xml.mustache", "src/main/liberty/config", "server.xml")
.doNotOverwrite());

View File

@ -5,6 +5,7 @@ package {{package}};
import {{javaxPackage}}.ws.rs.*;
import {{javaxPackage}}.ws.rs.core.Response;
{{#returnJBossResponse}}import org.jboss.resteasy.reactive.RestResponse;{{/returnJBossResponse}}
{{#useGzipFeature}}
import org.jboss.resteasy.annotations.GZIP;

View File

@ -41,4 +41,4 @@
{{^vendorExtensions.x-java-is-response-void}}@org.eclipse.microprofile.openapi.annotations.media.Content(schema = @org.eclipse.microprofile.openapi.annotations.media.Schema(implementation = {{{baseType}}}.class{{#vendorExtensions.x-microprofile-open-api-return-schema-container}}, type = {{{.}}} {{/vendorExtensions.x-microprofile-open-api-return-schema-container}}{{#vendorExtensions.x-microprofile-open-api-return-unique-items}}, uniqueItems = true {{/vendorExtensions.x-microprofile-open-api-return-unique-items}})){{/vendorExtensions.x-java-is-response-void}}
}){{^-last}},{{/-last}}{{/responses}}
}){{/hasProduces}}{{/useMicroProfileOpenAPIAnnotations}}
{{#supportAsync}}{{>returnAsyncTypeInterface}}{{/supportAsync}}{{^supportAsync}}{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnTypeInterface}}{{/returnResponse}}{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}});
{{#supportAsync}}{{>returnAsyncTypeInterface}}{{/supportAsync}}{{^supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnTypeInterface}}{{/returnResponse}}{{/returnJBossResponse}}{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}});

View File

@ -40,6 +40,6 @@
{{^vendorExtensions.x-java-is-response-void}}@org.eclipse.microprofile.openapi.annotations.media.Content(schema = @org.eclipse.microprofile.openapi.annotations.media.Schema(implementation = {{{baseType}}}.class{{#vendorExtensions.x-microprofile-open-api-return-schema-container}}, type = {{{.}}} {{/vendorExtensions.x-microprofile-open-api-return-schema-container}}{{#vendorExtensions.x-microprofile-open-api-return-unique-items}}, uniqueItems = true {{/vendorExtensions.x-microprofile-open-api-return-unique-items}})){{/vendorExtensions.x-java-is-response-void}}
}){{^-last}},{{/-last}}{{/responses}}
}){{/hasProduces}}{{/useMicroProfileOpenAPIAnnotations}}
public {{#supportAsync}}{{#useMutiny}}Uni{{/useMutiny}}{{^useMutiny}}CompletionStage{{/useMutiny}}<{{/supportAsync}}Response{{#supportAsync}}>{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}) {
public {{#supportAsync}}{{#useMutiny}}Uni{{/useMutiny}}{{^useMutiny}}CompletionStage{{/useMutiny}}<{{/supportAsync}}{{#returnJBossResponse}}{{>returnResponseTypeInterface}}{{/returnJBossResponse}}{{^returnJBossResponse}}Response{{/returnJBossResponse}}{{#supportAsync}}>{{/supportAsync}} {{nickname}}({{#allParams}}{{>queryParams}}{{>pathParams}}{{>cookieParams}}{{>headerParams}}{{>bodyParams}}{{>formParams}}{{^-last}},{{/-last}}{{/allParams}}) {
return {{#supportAsync}}{{#useMutiny}}Uni.createFrom().item({{/useMutiny}}{{^useMutiny}}CompletableFuture.supplyAsync(() -> {{/useMutiny}}{{/supportAsync}}Response.ok().entity("magic!").build(){{#supportAsync}}){{/supportAsync}};
}

View File

@ -79,6 +79,12 @@
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
{{#useJakartaEe}}
{{#returnJBossResponse}}
<dependency>
<groupId>io.quarkus.resteasy.reactive</groupId>
<artifactId>resteasy-reactive</artifactId>
</dependency>
{{/returnJBossResponse}}
<dependency>
<groupId>jakarta.ws.rs</groupId>
<artifactId>jakarta.ws.rs-api</artifactId>

View File

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

View File

@ -0,0 +1 @@
{{#returnJBossResponse}}RestResponse<{{>returnType}}>{{/returnJBossResponse}}{{^returnJBossResponse}}{{#returnResponse}}Response{{/returnResponse}}{{^returnResponse}}{{>returnType}}{{/returnResponse}}{{/returnJBossResponse}}

View File

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

View File

@ -524,6 +524,39 @@ public class JavaJAXRSSpecServerCodegenTest extends JavaJaxrsBaseTest {
"\nCompletionStage<Response> pingGet();\n");
}
@Test
public void generateApiWithAsyncSupportAndInterfaceOnlyAndJBossResponse() 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.setLibrary(QUARKUS_LIBRARY);
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_JBOSS_RESPONSE, true); //And return type is RestResponse
codegen.additionalProperties().put(USE_JAKARTA_EE, true); //And return type is RestResponse
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<RestResponse<Pet>>
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 org.jboss.resteasy.reactive.RestResponse;\n",
"\nimport java.util.concurrent.CompletionStage;\n",
"CompletionStage<RestResponse<Pet>> addPet", "CompletionStage<RestResponse<Void>> deletePet");
}
@Test
public void generatePetstoreAPIWithAsyncSupport() throws Exception {
@ -975,6 +1008,77 @@ public class JavaJAXRSSpecServerCodegenTest extends JavaJaxrsBaseTest {
" title = \"user\", version=\"1.0.0\", description=\"Operations about user\",");
}
@Test
public void generateSpecInterfaceWithJBossResponse() 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(INTERFACE_ONLY, true); //And only interfaces are generated
codegen.additionalProperties().put(USE_TAGS, true); //And use tags to generate everything in several API files
codegen.additionalProperties().put(RETURN_JBOSS_RESPONSE, true); // Use JBoss Response type
codegen.additionalProperties().put(USE_JAKARTA_EE, true); // Use Jakarta
codegen.setLibrary(QUARKUS_LIBRARY); // Set Quarkus
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 RestResponse
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 org.jboss.resteasy.reactive.RestResponse;\n",
"RestResponse<Pet> addPet", "RestResponse<Void> deletePet", "RestResponse<List<Pet>> findPetsByStatus",
"RestResponse<Void> updatePetWithForm", "RestResponse<ModelApiResponse> uploadFile");
assertFileContains(output.toPath().resolve("pom.xml"),
"<groupId>io.quarkus.resteasy.reactive</groupId>",
"<artifactId>resteasy-reactive</artifactId>");
}
@Test
public void generateSpecInterfaceWithMutinyAndJBossResponse() 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(INTERFACE_ONLY, true); //And only interfaces are generated
codegen.additionalProperties().put(USE_TAGS, true); //And use tags to generate everything in several API files
codegen.additionalProperties().put(RETURN_JBOSS_RESPONSE, true); // Use JBoss Response type
codegen.additionalProperties().put(USE_JAKARTA_EE, true); // Use JBoss Response type
codegen.additionalProperties().put(SUPPORT_ASYNC, true);
codegen.additionalProperties().put(USE_MUTINY, true); // Use Mutiny
codegen.setLibrary(QUARKUS_LIBRARY); // Set Quarkus
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 RestResponse
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 org.jboss.resteasy.reactive.RestResponse;\n", "Uni<RestResponse<Pet>> addPet",
"Uni<RestResponse<Void>> deletePet", "Uni<RestResponse<List<Pet>>> findPetsByStatus",
"Uni<RestResponse<ModelApiResponse>> uploadFile");
}
@Test
public void generateSpecNonInterfaceWithMicroprofileOpenApiAnnotations() throws Exception {
final File output = Files.createTempDirectory("test").toFile().getCanonicalFile();

View File

@ -9,6 +9,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -18,6 +18,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -8,6 +8,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -11,6 +11,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -9,6 +9,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -9,6 +9,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -8,6 +8,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -24,6 +24,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -8,6 +8,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -8,6 +8,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -11,6 +11,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -9,6 +9,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;

View File

@ -9,6 +9,7 @@ import javax.ws.rs.core.Response;
import java.io.InputStream;
import java.util.Map;
import java.util.List;