Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .github/workflows/samples-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ on:
- samples/server/petstore/java-camel/**
- samples/server/petstore/java-helidon-server/v3/mp/**
- samples/server/petstore/java-helidon-server/v3/se/**
- samples/server/petstore/jaxrs-spec-sealed/**
pull_request:
paths:
# clients
Expand All @@ -44,6 +45,7 @@ on:
- samples/server/petstore/java-camel/**
- samples/server/petstore/java-helidon-server/v3/mp/**
- samples/server/petstore/java-helidon-server/v3/se/**
- samples/server/petstore/jaxrs-spec-sealed/**
jobs:
build:
name: Build with JDK17
Expand Down Expand Up @@ -72,6 +74,7 @@ jobs:
- samples/server/petstore/java-camel/
- samples/server/petstore/java-helidon-server/v3/mp/
- samples/server/petstore/java-helidon-server/v3/se
- samples/server/petstore/jaxrs-spec-sealed
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v5
Expand Down
10 changes: 10 additions & 0 deletions bin/configs/jaxrs-spec-sealed.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec-sealed
inputSpec: modules/openapi-generator/src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaJaxRS/spec
additionalProperties:
artifactId: jaxrs-spec-sealed-petstore-server
useOneOfInterfaces: "true"
useSealed: "true"
serializableModel: "true"
hideGenerationTimestamp: "true"
1 change: 1 addition & 0 deletions docs/generators/jaxrs-cxf-cdi.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useMicroProfileOpenAPIAnnotations|Whether to generate Microprofile OpenAPI annotations. Only valid when library is set to quarkus.| |false|
|useMutiny|Whether to use Smallrye Mutiny instead of CompletionStage for asynchronous computation. Only valid when library is set to quarkus.| |false|
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
|useSealed|Whether to generate sealed model interfaces and classes.| |false|
|useSwaggerAnnotations|Whether to generate Swagger annotations.| |true|
|useSwaggerV3Annotations|Whether to generate Swagger v3 (OpenAPI v3) annotations.| |false|
|useTags|use tags for creating interface and controller classnames| |false|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/jaxrs-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useMicroProfileOpenAPIAnnotations|Whether to generate Microprofile OpenAPI annotations. Only valid when library is set to quarkus.| |false|
|useMutiny|Whether to use Smallrye Mutiny instead of CompletionStage for asynchronous computation. Only valid when library is set to quarkus.| |false|
|useOneOfInterfaces|whether to use a java interface to describe a set of oneOf options, where each option is a class that implements the interface| |false|
|useSealed|Whether to generate sealed model interfaces and classes.| |false|
|useSwaggerAnnotations|Whether to generate Swagger annotations.| |true|
|useSwaggerV3Annotations|Whether to generate Swagger v3 (OpenAPI v3) annotations.| |false|
|useTags|use tags for creating interface and controller classnames| |false|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
public static final String GENERATE_JSON_CREATOR = "generateJsonCreator";
public static final String USE_JAKARTA_SECURITY_ANNOTATIONS = "useJakartaSecurityAnnotations";
public static final String USE_ENUM_CASE_INSENSITIVE = "useEnumCaseInsensitive";
public static final String USE_SEALED = "useSealed";

public static final String QUARKUS_LIBRARY = "quarkus";
public static final String THORNTAIL_LIBRARY = "thorntail";
Expand All @@ -79,6 +80,9 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
@Setter
private boolean useEnumCaseInsensitive = false;

@Setter
protected boolean useSealed = false;

private final JakartaSecurityAnnotationProcessor jakartaSecurityAnnotationProcessor = new JakartaSecurityAnnotationProcessor();

@Getter @Setter
Expand Down Expand Up @@ -162,6 +166,7 @@ public JavaJAXRSSpecServerCodegen() {
cliOptions.add(CliOption.newBoolean(USE_JAKARTA_SECURITY_ANNOTATIONS, "Whether to generate Jakarta security annotations (@RolesAllowed, @PermitAll). Requires useJakartaEe=true. Currently only supported when library is set to quarkus.", useJakartaSecurityAnnotations));
cliOptions.add(CliOption.newBoolean(GENERATE_JSON_CREATOR, "Whether to generate @JsonCreator constructor for required properties.", generateJsonCreator));
cliOptions.add(CliOption.newBoolean(USE_ENUM_CASE_INSENSITIVE, "Use `equalsIgnoreCase` when String for enum comparison", useEnumCaseInsensitive));
cliOptions.add(CliOption.newBoolean(USE_SEALED, "Whether to generate sealed model interfaces and classes.", useSealed));
}

@Override
Expand Down Expand Up @@ -205,6 +210,7 @@ public void processOpts() {

convertPropertyToBooleanAndWriteBack(GENERATE_JSON_CREATOR, this::setGenerateJsonCreator);
convertPropertyToBooleanAndWriteBack(USE_ENUM_CASE_INSENSITIVE, this::setUseEnumCaseInsensitive);
convertPropertyToBooleanAndWriteBack(USE_SEALED, this::setUseSealed);
Comment thread
Ignacio-Vidal marked this conversation as resolved.

if (additionalProperties.containsKey(OPEN_API_SPEC_FILE_LOCATION)) {
openApiSpecFileLocation = additionalProperties.get(OPEN_API_SPEC_FILE_LOCATION).toString();
Expand Down Expand Up @@ -424,6 +430,13 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
}
}
}
// A oneOf container rendered as a plain class (useOneOfInterfaces disabled or the model
// not selected for interface generation) must not be sealed over its oneOf members: they
// do not extend it, so a permits clause would not compile. Only child-derived permits
// (subclasses that actually extend the model) may remain.
if (useSealed && !Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface"))) {
model.permits.removeAll(model.oneOf);
}
}
}
return result;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
{{/discriminator}}
{{>generatedAnnotation}}

public interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
{{#discriminator}}
{{propertyType}} {{propertyGetter}}();
{{/discriminator}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#useSealed}}{{#permits}}{{#-first}}permits {{/-first}}{{{.}}}{{^-last}}, {{/-last}}{{#-last}} {{/-last}}{{/permits}}{{/useSealed}}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ import {{javaxPackage}}.xml.bind.annotation.XmlEnumValue;
{{#vendorExtensions.x-class-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-class-extra-annotation}}
public class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
public {{>sealed}}class {{classname}} {{#parent}}extends {{{.}}}{{/parent}} {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
{{#vars}}
{{#isEnum}}
{{^isContainer}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@
{{/openApiNullable}}
</dependencies>
<properties>
<java.version>1.8</java.version>
<java.version>{{#useSealed}}17{{/useSealed}}{{^useSealed}}1.8{{/useSealed}}</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#useSealed}}{{#permits.0}}sealed {{/permits.0}}{{^permits.0}}{{^vendorExtensions.x-is-one-of-interface}}final {{/vendorExtensions.x-is-one-of-interface}}{{/permits.0}}{{/useSealed}}
Original file line number Diff line number Diff line change
Expand Up @@ -1371,6 +1371,167 @@ public void testDiscriminatorJsonIgnorePropertiesPropagatesToChildren_whenLegacy
}
}

/**
* With {@code useOneOfInterfaces=true} a oneOf schema is generated as a Java interface, and the
* concrete subtypes implement it. With {@code useSealed=true} the interface is {@code sealed} and
* {@code permits} its subtypes, which become {@code final} and {@code implements} the interface.
* The discriminator property is declared only on a shared non-discriminator base (acyclic pattern),
* so the interface getter type must resolve to the enum model (PetType) from the mapped children.
* Assertions use {@code assertFileContains} rather than {@code JavaFileAssert} because the latter
* parses the source with a JavaParser language level that predates the sealed/permits keywords.
*/
@Test
public void testOneOfSealedInterfaceGeneration() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Map<String, Object> properties = new HashMap<>();
properties.put("useOneOfInterfaces", "true");
properties.put("useSealed", "true");

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("jaxrs-spec")
.setAdditionalProperties(properties)
.setInputSpec("src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.opts(configurator.toClientOptInput()).generate();

String modelDir = output.getAbsolutePath().replace("\\", "/") + "/src/gen/java/org/openapitools/model/";

assertFileContains(Paths.get(modelDir + "PetRequest.java"),
"public sealed interface PetRequest",
"permits CatRequest, DogRequest",
"PetType getPetType();");
assertFileNotContains(Paths.get(modelDir + "PetRequest.java"), "class PetRequest");

assertFileContains(Paths.get(modelDir + "CatRequest.java"),
"public final class CatRequest",
"implements PetRequest",
"public PetType getPetType()");
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "extends PetRequest");

assertFileContains(Paths.get(modelDir + "DogRequest.java"),
"public final class DogRequest",
"implements PetRequest",
"public PetType getPetType()");
assertFileNotContains(Paths.get(modelDir + "DogRequest.java"), "extends PetRequest");
}

/**
* With {@code useSealed=true} an allOf class hierarchy driven by a parent discriminator is
* generated as a sealed parent class that permits its subclasses; the subclasses extend the
* parent and become final. A middle tier with its own discriminator stays open downwards but
* closed to outsiders: it is sealed over its own subclasses while extending its parent. A
* standalone model with no subtypes becomes final, and the generated pom targets Java 17
* (sealed types need JDK 17+).
*/
@Test
public void testSealedClassHierarchyGeneration() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Map<String, Object> properties = new HashMap<>();
properties.put("useSealed", "true");

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("jaxrs-spec")
.setAdditionalProperties(properties)
.setInputSpec("src/test/resources/3_0/jaxrs-spec/sealed_hierarchy.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.opts(configurator.toClientOptInput()).generate();

String outputDir = output.getAbsolutePath().replace("\\", "/");
String modelDir = outputDir + "/src/gen/java/org/openapitools/model/";

assertFileContains(Paths.get(modelDir + "Pet.java"),
"public sealed class Pet",
"permits Cat, Dog");
assertFileContains(Paths.get(modelDir + "Cat.java"),
"public sealed class Cat",
"extends Pet",
"permits PersianCat");
assertFileNotContains(Paths.get(modelDir + "Cat.java"), "final class Cat");
assertFileContains(Paths.get(modelDir + "PersianCat.java"),
"public final class PersianCat",
"extends Cat");
assertFileContains(Paths.get(modelDir + "Dog.java"),
"public final class Dog",
"extends Pet");
assertFileContains(Paths.get(modelDir + "Toy.java"), "public final class Toy");
assertFileNotContains(Paths.get(modelDir + "Toy.java"), "sealed ", "permits ");

assertFileContains(Paths.get(outputDir + "/pom.xml"), "<java.version>17</java.version>");
}

/**
* {@code useSealed=true} without {@code useOneOfInterfaces}: the oneOf container is rendered as
* a plain class whose oneOf members do not extend it, so it must not carry a sealed/permits
* clause over them (the generated code would not compile). Like any other model without
* subclasses it becomes final.
*/
@Test
public void testUseSealedWithoutOneOfInterfaces() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Map<String, Object> properties = new HashMap<>();
properties.put("useSealed", "true");

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("jaxrs-spec")
.setAdditionalProperties(properties)
.setInputSpec("src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.opts(configurator.toClientOptInput()).generate();

String modelDir = output.getAbsolutePath().replace("\\", "/") + "/src/gen/java/org/openapitools/model/";

assertFileContains(Paths.get(modelDir + "PetRequest.java"), "public final class PetRequest");
assertFileNotContains(Paths.get(modelDir + "PetRequest.java"), "sealed ", "permits ");
assertFileContains(Paths.get(modelDir + "CatRequest.java"), "public final class CatRequest");
assertFileContains(Paths.get(modelDir + "DogRequest.java"), "public final class DogRequest");
assertFileContains(Paths.get(modelDir + "PetBase.java"), "public final class PetBase");
}

/**
* Without {@code useSealed} the output is unchanged: no sealed/final/permits modifiers are
* emitted (even though the permits list is populated on the models) and the generated pom
* still targets Java 1.8.
*/
@Test
public void testWithoutUseSealedOutputIsUnchanged() throws Exception {
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();

Map<String, Object> properties = new HashMap<>();
properties.put("useOneOfInterfaces", "true");

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("jaxrs-spec")
.setAdditionalProperties(properties)
.setInputSpec("src/test/resources/3_0/jaxrs-spec/oneof_interface.yaml")
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

DefaultGenerator generator = new DefaultGenerator();
generator.opts(configurator.toClientOptInput()).generate();

String outputDir = output.getAbsolutePath().replace("\\", "/");
String modelDir = outputDir + "/src/gen/java/org/openapitools/model/";

assertFileContains(Paths.get(modelDir + "PetRequest.java"), "public interface PetRequest");
assertFileNotContains(Paths.get(modelDir + "PetRequest.java"), "sealed ", "permits ");
assertFileContains(Paths.get(modelDir + "CatRequest.java"), "public class CatRequest");
assertFileNotContains(Paths.get(modelDir + "CatRequest.java"), "final class");

assertFileContains(Paths.get(outputDir + "/pom.xml"), "<java.version>1.8</java.version>");
}

@Test
public void testGenerateJsonNullableListFieldsHelperMethodReferences_issue23251() throws Exception {
Map<String, Object> properties = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
openapi: 3.0.3
info:
title: sealed class hierarchy
description: >
An allOf class hierarchy driven by a parent discriminator. Under useSealed the
parent class is sealed and permits its subclasses, which extend it and become final.
Cat is a middle tier with its own discriminator: it is both a permitted subclass of
Pet and a sealed parent of PersianCat.
version: 1.0.0
paths:
/pets:
post:
operationId: createPet
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/Pet'
responses:
'201':
description: created
components:
schemas:
Pet:
type: object
required:
- petType
- name
properties:
petType:
type: string
name:
type: string
discriminator:
propertyName: petType
mapping:
CAT: '#/components/schemas/Cat'
DOG: '#/components/schemas/Dog'

Cat:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
required:
- catType
properties:
catType:
type: string
indoor:
type: boolean
discriminator:
propertyName: catType
mapping:
PERSIAN: '#/components/schemas/PersianCat'

PersianCat:
allOf:
- $ref: '#/components/schemas/Cat'
- type: object
properties:
longHair:
type: boolean

Dog:
allOf:
- $ref: '#/components/schemas/Pet'
- type: object
properties:
trained:
type: boolean

Toy:
type: object
properties:
name:
type: string
Loading
Loading