Skip to content
Draft
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
1 change: 1 addition & 0 deletions .github/workflows/samples-jaxrs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ jobs:
- samples/server/petstore/jaxrs-spec-withxml
- samples/server/petstore/jaxrs-spec-interface
- samples/server/petstore/jaxrs-spec-interface-response
- samples/server/petstore/jaxrs-spec-oneof-interface
- samples/server/petstore/jaxrs-datelib-j8
- samples/server/petstore/jaxrs-resteasy/default
- samples/server/petstore/jaxrs-resteasy/eap
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/samples-jdk17.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ 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/**
- samples/server/petstore/jaxrs-spec-records/**
pull_request:
paths:
# clients
Expand All @@ -44,6 +46,8 @@ 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/**
- samples/server/petstore/jaxrs-spec-records/**
jobs:
build:
name: Build with JDK17
Expand Down Expand Up @@ -72,6 +76,8 @@ 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
- samples/server/petstore/jaxrs-spec-records
steps:
- uses: actions/checkout@v7
- uses: actions/setup-java@v5
Expand Down
9 changes: 9 additions & 0 deletions bin/configs/jaxrs-spec-oneof-interface.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec-oneof-interface
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-oneof-interface-petstore-server
useOneOfInterfaces: "true"
serializableModel: "true"
hideGenerationTimestamp: "true"
11 changes: 11 additions & 0 deletions bin/configs/jaxrs-spec-records.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
generatorName: jaxrs-spec
outputDir: samples/server/petstore/jaxrs-spec-records
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-records-petstore-server
useOneOfInterfaces: "true"
useSealed: "true"
useRecords: "true"
serializableModel: "true"
hideGenerationTimestamp: "true"
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"
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@
import org.openapitools.codegen.model.OperationsMap;

import java.io.File;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.openapitools.codegen.languages.features.GzipFeatures.USE_GZIP_FEATURE;

Expand All @@ -56,6 +60,8 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
public static final String OPEN_API_SPEC_FILE_LOCATION = "openApiSpecFileLocation";
public static final String GENERATE_JSON_CREATOR = "generateJsonCreator";
public static final String USE_JAKARTA_SECURITY_ANNOTATIONS = "useJakartaSecurityAnnotations";
public static final String USE_SEALED = "useSealed";
public static final String USE_RECORDS = "useRecords";

public static final String QUARKUS_LIBRARY = "quarkus";
public static final String THORNTAIL_LIBRARY = "thorntail";
Expand All @@ -73,6 +79,12 @@ public class JavaJAXRSSpecServerCodegen extends AbstractJavaJAXRSServerCodegen {
private boolean useMutiny = false;
private boolean useJakartaSecurityAnnotations = false;

@Setter
protected boolean useSealed = false;

@Setter
protected boolean useRecords = false;

private final JakartaSecurityAnnotationProcessor jakartaSecurityAnnotationProcessor = new JakartaSecurityAnnotationProcessor();

@Getter @Setter
Expand Down Expand Up @@ -155,6 +167,8 @@ public JavaJAXRSSpecServerCodegen() {
cliOptions.add(CliOption.newBoolean(USE_MUTINY, "Whether to use Smallrye Mutiny instead of CompletionStage for asynchronous computation. Only valid when library is set to quarkus.", useMutiny));
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_SEALED, "Whether to generate sealed model interfaces and classes.", useSealed));
cliOptions.add(CliOption.newBoolean(USE_RECORDS, "Whether to render the implementations of a generated oneOf interface (useOneOfInterfaces=true) as Java records instead of classes.", useRecords));
}

@Override
Expand Down Expand Up @@ -197,6 +211,8 @@ public void processOpts() {
}

convertPropertyToBooleanAndWriteBack(GENERATE_JSON_CREATOR, this::setGenerateJsonCreator);
convertPropertyToBooleanAndWriteBack(USE_SEALED, this::setUseSealed);
convertPropertyToBooleanAndWriteBack(USE_RECORDS, this::setUseRecords);

if (additionalProperties.containsKey(OPEN_API_SPEC_FILE_LOCATION)) {
openApiSpecFileLocation = additionalProperties.get(OPEN_API_SPEC_FILE_LOCATION).toString();
Expand Down Expand Up @@ -385,25 +401,96 @@ public OperationsMap postProcessOperationsWithModels(OperationsMap objs, List<Mo
@Override
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
Map<String, ModelsMap> result = super.postProcessAllModels(objs);

// Collect, for the interfaces generated by the useOneOfInterfaces feature: their classnames (to
// identify which concrete models implement one of them and are therefore record candidates) and
// their discriminators indexed by classname (a child of a oneOf interface inherits a shared base
// via allOf and has no Java parentModel carrying the discriminator - it lives on the interface).
Set<String> oneOfInterfaceNames = new HashSet<>();
Map<String, CodegenDiscriminator> oneOfInterfaceDiscriminators = new HashMap<>();
for (ModelsMap modelsMap : result.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();
if (model.parentModel != null) {
CodegenDiscriminator discriminator = model.parentModel.getDiscriminator();
if (discriminator != null) {
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
if (mappedModel.getSchemaName().equals(model.schemaName)) {
model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName());
break;
}
if (Boolean.TRUE.equals(model.getVendorExtensions().get("x-is-one-of-interface"))) {
oneOfInterfaceNames.add(model.classname);
if (model.getDiscriminator() != null) {
oneOfInterfaceDiscriminators.put(model.classname, model.getDiscriminator());
}
}
}
}

for (ModelsMap modelsMap : result.values()) {
for (ModelMap modelMap : modelsMap.getModels()) {
CodegenModel model = modelMap.getModel();
// Resolve the @JsonTypeName mapping value from the Java parent's discriminator, if any,
// otherwise from the discriminator of the oneOf interface the model implements.
CodegenDiscriminator discriminator = model.parentModel != null
? model.parentModel.getDiscriminator()
: discriminatorOfImplementedOneOfInterface(model, oneOfInterfaceDiscriminators);
if (discriminator != null) {
for (CodegenDiscriminator.MappedModel mappedModel : discriminator.getMappedModels()) {
if (mappedModel.getSchemaName().equals(model.schemaName)) {
model.getVendorExtensions().put("x-discriminator-value", mappedModel.getMappingName());
break;
}
}
}
if (useRecords && isOneOfInterfaceRecordCandidate(model, oneOfInterfaceNames)) {
model.getVendorExtensions().put("x-jaxrs-record", true);
}
}
}
return result;
}

/**
* Returns the discriminator of the oneOf interface (produced by useOneOfInterfaces) that the given
* model implements, or {@code null} if the model does not implement such an interface. The interface
* name is read from the model's {@code x-implements} vendor extension.
*/
@SuppressWarnings("unchecked")
private CodegenDiscriminator discriminatorOfImplementedOneOfInterface(
CodegenModel model, Map<String, CodegenDiscriminator> oneOfInterfaceDiscriminators) {
Object implementsExtension = model.getVendorExtensions().get("x-implements");
if (!(implementsExtension instanceof Collection)) {
return null;
}
for (Object intf : (Collection<Object>) implementsExtension) {
CodegenDiscriminator discriminator = oneOfInterfaceDiscriminators.get(String.valueOf(intf));
if (discriminator != null) {
return discriminator;
}
}
return null;
}

/**
* A concrete model is rendered as a record only when it implements a oneOf interface produced by the
* useOneOfInterfaces feature, and it is structurally record-compatible: it has no Java parent (records
* cannot extend) and no additional (undeclared) properties (those require a mutable map-backed pojo).
*
* @param model the concrete model that may implement a oneOf interface
* @param oneOfInterfaceNames the classnames of the generated oneOf interfaces
* @return {@code true} when the model should be rendered as a record
*/
@SuppressWarnings("unchecked")
private boolean isOneOfInterfaceRecordCandidate(CodegenModel model, Set<String> oneOfInterfaceNames) {
if (model.getParent() != null || model.getAdditionalPropertiesType() != null) {
return false;
}
Object implementsExtension = model.getVendorExtensions().get("x-implements");
if (!(implementsExtension instanceof Collection)) {
return false;
}
for (Object intf : (Collection<Object>) implementsExtension) {
if (oneOfInterfaceNames.contains(String.valueOf(intf))) {
return true;
}
}
return false;
}

@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
{{#additionalOneOfTypeAnnotations}}{{{.}}}
{{/additionalOneOfTypeAnnotations}}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ import {{javaxPackage}}.validation.Valid;
{{>enumOuterClass}}

{{/isEnum}}
{{^isEnum}}{{>pojo}}{{/isEnum}}
{{^isEnum}}{{#vendorExtensions.x-is-one-of-interface}}{{>oneof_interface}}{{/vendorExtensions.x-is-one-of-interface}}{{^vendorExtensions.x-is-one-of-interface}}{{#vendorExtensions.x-jaxrs-record}}{{>record}}{{/vendorExtensions.x-jaxrs-record}}{{^vendorExtensions.x-jaxrs-record}}{{>pojo}}{{/vendorExtensions.x-jaxrs-record}}{{/vendorExtensions.x-is-one-of-interface}}{{/isEnum}}
{{/model}}
{{/models}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{{>additionalOneOfTypeAnnotations}}
{{#discriminator}}
{{>typeInfoAnnotation}}

{{/discriminator}}{{^discriminator}}{{#useDeductionForOneOfInterfaces}}
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes({
{{#interfaceModels}}
@JsonSubTypes.Type(value = {{classname}}.class){{^-last}}, {{/-last}}
{{/interfaceModels}}
})
{{/useDeductionForOneOfInterfaces}}{{#vendorExtensions.x-class-extra-annotation}}{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
{{/discriminator}}
{{>generatedAnnotation}}

public {{>sealed}}interface {{classname}}{{#vendorExtensions.x-implements}}{{#-first}} extends {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {{>permits}}{
{{#discriminator}}
{{propertyType}} {{#useRecords}}{{propertyName}}{{/useRecords}}{{^useRecords}}{{propertyGetter}}{{/useRecords}}();
{{/discriminator}}
}
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}}}
{{/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 @@ -180,7 +180,7 @@
{{/openApiNullable}}
</dependencies>
<properties>
<java.version>1.8</java.version>
<java.version>{{#useSealed}}17{{/useSealed}}{{^useSealed}}{{#useRecords}}17{{/useRecords}}{{^useRecords}}1.8{{/useRecords}}{{/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,64 @@
{{#useSwaggerAnnotations}}
import io.swagger.annotations.*;
{{/useSwaggerAnnotations}}
{{#useSwaggerV3Annotations}}
import io.swagger.v3.oas.annotations.media.Schema;
{{/useSwaggerV3Annotations}}
{{#jackson}}
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.annotation.JsonTypeName;
{{/jackson}}
{{#withXml}}
import {{javaxPackage}}.xml.bind.annotation.XmlElement;
import {{javaxPackage}}.xml.bind.annotation.XmlRootElement;
import {{javaxPackage}}.xml.bind.annotation.XmlAccessType;
import {{javaxPackage}}.xml.bind.annotation.XmlAccessorType;
import {{javaxPackage}}.xml.bind.annotation.XmlType;
import {{javaxPackage}}.xml.bind.annotation.XmlEnum;
import {{javaxPackage}}.xml.bind.annotation.XmlEnumValue;
{{/withXml}}

{{#discriminator}}{{>typeInfoAnnotation}}{{/discriminator}}{{#description}}/**
* {{.}}
{{#isDeprecated}}
* @deprecated
{{/isDeprecated}}
**/{{/description}}
{{#isDeprecated}}
@Deprecated

{{/isDeprecated}}{{#useSwaggerAnnotations}}{{#description}}@ApiModel(description = "{{{.}}}"){{/description}}{{/useSwaggerAnnotations}}{{#useSwaggerV3Annotations}}
@Schema({{#title}}title="{{{.}}}", {{/title}}{{#description}}description="{{{.}}}"{{/description}}{{^description}}description=""{{/description}}{{#isDeprecated}}, deprecated = true{{/isDeprecated}}){{/useSwaggerV3Annotations}}{{#useMicroProfileOpenAPIAnnotations}}
@org.eclipse.microprofile.openapi.annotations.media.Schema({{#title}}title="{{{.}}}", {{/title}}{{#description}}description="{{{.}}}"{{/description}}{{^description}}description=""{{/description}}{{#isDeprecated}}, deprecated = true{{/isDeprecated}}){{/useMicroProfileOpenAPIAnnotations}}
{{#jackson}}
@JsonTypeName("{{#vendorExtensions.x-discriminator-value}}{{{vendorExtensions.x-discriminator-value}}}{{/vendorExtensions.x-discriminator-value}}{{^vendorExtensions.x-discriminator-value}}{{name}}{{/vendorExtensions.x-discriminator-value}}")
{{/jackson}}
{{>generatedAnnotation}}{{>additionalModelTypeAnnotations}}
{{#vendorExtensions.x-class-extra-annotation}}
{{{vendorExtensions.x-class-extra-annotation}}}
{{/vendorExtensions.x-class-extra-annotation}}
public {{>sealed}}record {{classname}}(
{{#vars}}
{{#vendorExtensions.x-field-extra-annotation}}
{{{.}}}
{{/vendorExtensions.x-field-extra-annotation}}
{{#deprecated}}
@Deprecated
{{/deprecated}}
{{#jackson}}@JsonProperty({{#required}}required = {{required}}, value = {{/required}}"{{baseName}}") {{/jackson}}{{#useBeanValidation}}{{>beanValidation}}{{/useBeanValidation}}{{{datatypeWithEnum}}} {{name}}{{^-last}},{{/-last}}
{{/vars}}
) {{#vendorExtensions.x-implements}}{{#-first}}implements {{{.}}}{{/-first}}{{^-first}}, {{{.}}}{{/-first}}{{/vendorExtensions.x-implements}} {
{{#vars}}
{{#isEnum}}
{{^isContainer}}
{{>enumClass}}{{! prevent indent}}
{{/isContainer}}
{{#isContainer}}
{{#mostInnerItems}}
{{>enumClass}}{{! prevent indent}}
{{/mostInnerItems}}
{{/isContainer}}
{{/isEnum}}
{{/vars}}
}
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}}{{^vendorExtensions.x-jaxrs-record}}final {{/vendorExtensions.x-jaxrs-record}}{{/vendorExtensions.x-is-one-of-interface}}{{/permits.0}}{{/useSealed}}
Loading
Loading