Skip to content
Open
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
17 changes: 17 additions & 0 deletions bin/configs/java-native-jackson3-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generatorName: java
outputDir: samples/client/petstore/java/native-jackson3-jspecify
library: native
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
validateSpec: false
additionalProperties:
artifactId: petstore-native-jackson3
hideGenerationTimestamp: "true"
generateBuilders: true
useReflectionEqualsHashCode: "true"
useJackson3: "true"
openApiNullable: "false"
useJspecify: true
typeMappings:
OffsetDateTime: java.time.Instant
BigDecimal: java.math.BigDecimal
17 changes: 17 additions & 0 deletions bin/configs/java-restclient-springBoot4-jackson3-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generatorName: java
outputDir: samples/client/petstore/java/restclient-springBoot4-jackson3-jspecify
library: restclient
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
validateSpec: false
additionalProperties:
artifactId: petstore-restclient
hideGenerationTimestamp: "true"
containerDefaultToNull: "true"
useSpringBoot4: true
useJackson3: true
openApiNullable: false
useJspecify: true
typeMappings:
OffsetDateTime: java.time.Instant
BigDecimal: java.math.BigDecimal
18 changes: 18 additions & 0 deletions bin/configs/java-resttemplate-springBoot4-jackson3-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
generatorName: java
outputDir: samples/client/petstore/java/resttemplate-springBoot4-jackson3-jspecify
library: resttemplate
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
validateSpec: false
additionalProperties:
artifactId: petstore-resttemplate
hideGenerationTimestamp: "true"
containerDefaultToNull: "true"
useJakartaEe: true
useSpringBoot4: true
useJackson3: true
openApiNullable: false
useJspecify: true
typeMappings:
OffsetDateTime: java.time.Instant
BigDecimal: java.math.BigDecimal
17 changes: 17 additions & 0 deletions bin/configs/java-webclient-springBoot4-jackson3-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
generatorName: java
outputDir: samples/client/petstore/java/webclient-springBoot4-jackson3-jspecify
library: webclient
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/Java
validateSpec: false
additionalProperties:
artifactId: petstore-webclient
hideGenerationTimestamp: "true"
containerDefaultToNull: "true"
useSpringBoot4: true
useJackson3: true
openApiNullable: false
useJspecify: true
typeMappings:
OffsetDateTime: java.time.Instant
BigDecimal: java.math.BigDecimal
21 changes: 21 additions & 0 deletions bin/configs/spring-boot-4-jspecify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
generatorName: spring
library: spring-boot
outputDir: samples/openapi3/server/petstore/springboot-4-jspecify
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
validateSpec: false
additionalProperties:
groupId: org.openapitools.openapi3
documentationProvider: springdoc
interfaceOnly: true
artifactId: springboot
snapshotVersion: "true"
useSpringBoot4: true
useJackson3: true
useBeanValidation: true
withXml: true
hideGenerationTimestamp: "true"
generateConstructorWithAllArgs: true
generateBuilders: true
openApiNullable: false
useJspecify: true
1 change: 1 addition & 0 deletions docs/generators/java-camel.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useHttpServiceProxyFactoryInterfacesConfigurator|Generate HttpInterfacesAbstractConfigurator based on an HttpServiceProxyFactory instance (as opposed to a WebClient instance, when disabled) for generating Spring HTTP interfaces.| |false|
|useJackson3|Set it in order to use jackson 3 dependencies (only allowed when `useSpringBoot4` is set and incompatible with `openApiNullable`).| |false|
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
|useJspecify|Use Jspecify for null checks| |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| |true|
|useOptional|Use Optional container for optional parameters| |false|
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/java-microprofile.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useGzipFeature|Send gzip-encoded requests| |false|
|useJackson3|Use Jackson 3 instead of Jackson 2. Supported for 'native' library (requires Java 17+) and for Spring 'resttemplate', 'webclient', and 'restclient' libraries (require useSpringBoot4=true). Incompatible with 'openApiNullable'.| |false|
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
|useJspecify|Use Jspecify for null checks. Ony supported for [native, restclient, resttemplate, webclient]| |false|
|useOneOfDiscriminatorLookup|Use the discriminator's mapping in oneOf to speed up the model lookup. IMPORTANT: Validation (e.g. one and only one match in oneOf's schemas) will be skipped. Only jersey2, jersey3, native, okhttp-gson support this option.| |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|
|usePlayWS|Use Play! Async HTTP client (Play WS API)| |false|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/java.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useGzipFeature|Send gzip-encoded requests| |false|
|useJackson3|Use Jackson 3 instead of Jackson 2. Supported for 'native' library (requires Java 17+) and for Spring 'resttemplate', 'webclient', and 'restclient' libraries (require useSpringBoot4=true). Incompatible with 'openApiNullable'.| |false|
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
|useJspecify|Use Jspecify for null checks. Ony supported for [native, restclient, resttemplate, webclient]| |false|
|useOneOfDiscriminatorLookup|Use the discriminator's mapping in oneOf to speed up the model lookup. IMPORTANT: Validation (e.g. one and only one match in oneOf's schemas) will be skipped. Only jersey2, jersey3, native, okhttp-gson support this option.| |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|
|usePlayWS|Use Play! Async HTTP client (Play WS API)| |false|
Expand Down
1 change: 1 addition & 0 deletions docs/generators/spring.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|useHttpServiceProxyFactoryInterfacesConfigurator|Generate HttpInterfacesAbstractConfigurator based on an HttpServiceProxyFactory instance (as opposed to a WebClient instance, when disabled) for generating Spring HTTP interfaces.| |false|
|useJackson3|Set it in order to use jackson 3 dependencies (only allowed when `useSpringBoot4` is set and incompatible with `openApiNullable`).| |false|
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
|useJspecify|Use Jspecify for null checks| |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| |true|
|useOptional|Use Optional container for optional parameters| |false|
|useResponseEntity|Use the `ResponseEntity` type to wrap return values of generated API methods. If disabled, method are annotated using a `@ResponseStatus` annotation, which has the status of the first response declared in the Api definition| |true|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
Expand Down Expand Up @@ -58,6 +60,8 @@
import javax.lang.model.SourceVersion;

import java.io.File;
import java.io.IOException;
import java.io.Writer;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
Expand Down Expand Up @@ -104,6 +108,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
public static final String IMPLICIT_HEADERS_REGEX = "implicitHeadersRegex";
public static final String JAVAX_PACKAGE = "javaxPackage";
public static final String USE_JAKARTA_EE = "useJakartaEe";
public static final String USE_JSPECIFY = "useJspecify";
public static final String CONTAINER_DEFAULT_TO_NULL = "containerDefaultToNull";
public static final String DISABLE_DISCRIMINATOR_JSON_IGNORE_PROPERTIES = "disableDiscriminatorJsonIgnoreProperties";

Expand Down Expand Up @@ -216,6 +221,11 @@ protected enum ENUM_PROPERTY_NAMING_TYPE {MACRO_CASE, legacy, original}
*/
@Getter @Setter
protected boolean useBeanValidation = false;
@Getter
@Setter
protected boolean useJspecify;
protected JSpecifyNullableLambda jSpecifyNullableLambda;

private Map<String, String> schemaKeyToModelNameCache = new HashMap<>();

public AbstractJavaCodegen() {
Expand Down Expand Up @@ -597,6 +607,7 @@ public void processOpts() {
convertPropertyToBooleanAndWriteBack(CAMEL_CASE_DOLLAR_SIGN, this::setCamelCaseDollarSign);
convertPropertyToBooleanAndWriteBack(USE_ONE_OF_INTERFACES, this::setUseOneOfInterfaces);
convertPropertyToStringAndWriteBack(CodegenConstants.ENUM_PROPERTY_NAMING, this::setEnumPropertyNaming);
convertPropertyToBooleanAndWriteBack(USE_JSPECIFY, this::setUseJspecify);

if (!StringUtils.isEmpty(parentGroupId) && !StringUtils.isEmpty(parentArtifactId) && !StringUtils.isEmpty(parentVersion)) {
additionalProperties.put("parentOverridden", true);
Expand Down Expand Up @@ -847,6 +858,20 @@ protected void applyJakartaPackage() {
writePropertyBack(JAVAX_PACKAGE, "jakarta");
}

protected void applyJspecify() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to set the JAVAX_PACKAGE to jspecify?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, obsolete commit, I've removed it

@Chrimle thanks for the review

importMapping.put("Nullable", "org.jspecify.annotations.Nullable");
if (Boolean.TRUE.equals(additionalProperties.get(CodegenConstants.GENERATE_MODELS))) {
supportingFiles.add(new SupportingFile("modelPackageInfo.mustache",
(sourceFolder + File.separator + modelPackage).replace(".", java.io.File.separator),
"package-info.java"));
}
if (Boolean.TRUE.equals(additionalProperties.get(CodegenConstants.GENERATE_APIS))) {
supportingFiles.add(new SupportingFile("apiPackageInfo.mustache",
(sourceFolder + File.separator + apiPackage).replace(".", java.io.File.separator),
"package-info.java"));
}
}

@Override
public String escapeReservedWord(String name) {
if (this.reservedWordsMappings().containsKey(name)) {
Expand Down Expand Up @@ -2653,4 +2678,73 @@ public void setEnumPropertyNaming(final String enumPropertyNamingType) {
throw new RuntimeException(sb.toString());
}
}

@Override
protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
this.jSpecifyNullableLambda = new JSpecifyNullableLambda();
// Add jSpecify nullable annotation in the correct location before or inside a declartion
Mustache.Lambda jSpecifyDatatypeLambda = (fragment, writer) -> {
String dataType = fragment.execute();
if (jSpecifyNullableLambda.keptNullable) {
jSpecifyNullableLambda.keptNullable = false;
int idx = dataType.lastIndexOf('.');
if (idx > 0) {
// generate declaration like java.time.@Nullable Timestamp
writer.write(dataType.substring(0, idx + 1));
writer.write("@Nullable ");
writer.write(dataType.substring(idx + 1));
} else {
writer.write("@Nullable ");
writer.write(dataType);
}
} else {
writer.write(dataType);
}
};
return super.addMustacheLambdas()
.put("jSpecifyDatatype", jSpecifyDatatypeLambda)
.put("jSpecifyNullable", jSpecifyNullableLambda);

}

/**
* for Jspecify, remove @Nullable before the datatype and set keptNullable to true if done.
*/
class JSpecifyNullableLambda implements Mustache.Lambda {
private String nullableAnnotation = "@Nullable";
// remember if @Nullable is needed
boolean keptNullable = false;

public void setNullableAnnotation(String nullableAnnotation) {
this.nullableAnnotation = nullableAnnotation;
}

@Override
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
keptNullable = false;
String value = fragment.execute();
if (useJspecify) {
if (value.startsWith(nullableAnnotation)) {
keptNullable = true;
int idx = nullableAnnotation.length();
// trim left
while (idx < value.length() && value.charAt(idx) == ' ') {
idx ++;
}
value = value.substring(idx);
}
}
writer.write(value);
}
}

/**
* Adds Nullable import if any parameter is nullable or optional.
*/
protected void addNullableImportForOperation(CodegenOperation codegenOperation) {
codegenOperation.allParams.stream()
.filter(CodegenParameter::notRequiredOrIsNullable)
.findAny()
.ifPresent(param -> codegenOperation.imports.add("Nullable"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.servers.Server;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
Expand Down Expand Up @@ -47,7 +48,6 @@
import static com.google.common.base.CaseFormat.LOWER_CAMEL;
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
import static java.util.Collections.sort;
import static org.openapitools.codegen.CodegenConstants.SERIALIZATION_LIBRARY;
import static org.openapitools.codegen.CodegenConstants.X_IMPLEMENTS;
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.StringUtils.camelize;
Expand Down Expand Up @@ -176,6 +176,7 @@ public class JavaClientCodegen extends AbstractJavaCodegen

@Setter protected int maxAttemptsForRetry = 1;
@Setter protected long waitTimeMillis = 10l;
private final Set<String> JSPECIFY_SUPPORTED_LIBRARIES = new TreeSet<>(Arrays.asList(RESTCLIENT, WEBCLIENT, NATIVE, RESTTEMPLATE));

private static class MpRestClientVersion {
public final String rootPackage;
Expand Down Expand Up @@ -274,6 +275,7 @@ public JavaClientCodegen() {
cliOptions.add(CliOption.newBoolean(SUPPORT_VERTX_FUTURE, "Also generate api methods that return a vertx Future instead of taking a callback. Only `vertx` supports this option. Requires vertx 4 or greater.", this.supportVertxFuture));
cliOptions.add(CliOption.newBoolean(USE_SEALED_ONE_OF_INTERFACES, "Generate the oneOf interfaces as sealed interfaces. Only supported for WebClient and RestClient.", this.useSealedOneOfInterfaces));
cliOptions.add(CliOption.newBoolean(USE_UNARY_INTERCEPTOR, "If true it will generate ResponseInterceptors using a UnaryOperator. This can be usefull for manipulating the request before it gets passed, for example doing your own decryption", this.useUnaryInterceptor));
cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks. Ony supported for " + JSPECIFY_SUPPORTED_LIBRARIES, useJspecify));

supportedLibraries.put(JERSEY2, "HTTP client: Jersey client 2.25.1. JSON processing: Jackson 2.17.1");
supportedLibraries.put(JERSEY3, "HTTP client: Jersey client 3.1.1. JSON processing: Jackson 2.17.1");
Expand Down Expand Up @@ -322,6 +324,15 @@ private void initMpRestClientVersionToRootPackage() {
mpRestClientVersions.put("3.0", new MpRestClientVersion("jakarta", "pom_3.0.mustache"));
}

@Override
public void setUseJspecify(boolean useJspecify) {
// currently only available for a limited set of libraries
if (useJspecify && !JSPECIFY_SUPPORTED_LIBRARIES.contains(library)) {
throw new IllegalArgumentException("useJspecify is only suppored in these libraries: " + JSPECIFY_SUPPORTED_LIBRARIES);
}
super.setUseJspecify(useJspecify);
}

@Override
public CodegenType getTag() {
return CodegenType.CLIENT;
Expand Down Expand Up @@ -788,6 +799,9 @@ public void processOpts() {
} else {
LOGGER.error("Unknown library option (-l/--library): {}", getLibrary());
}
if (useJspecify) {
applyJspecify();
}

if (usePlayWS) {
// remove unsupported auth
Expand Down Expand Up @@ -1006,6 +1020,15 @@ public int compare(CodegenParameter one, CodegenParameter another) {
return objs;
}

@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
if (useJspecify) {
addNullableImportForOperation(op);
}
return op;
}

@Override
public String apiFilename(String templateName, String tag) {
if (isLibrary(VERTX)) {
Expand Down Expand Up @@ -1136,6 +1159,9 @@ public CodegenModel fromModel(String name, Schema model) {
if (!AnnotationLibrary.SWAGGER2.equals(getAnnotationLibrary())) {
codegenModel.imports.remove("Schema");
}
if (useJspecify) {
codegenModel.imports.add("Nullable");
}

return codegenModel;
}
Expand Down Expand Up @@ -1345,4 +1371,17 @@ public List<VendorExtension> getSupportedVendorExtensions() {
extensions.add(VendorExtension.X_WEBCLIENT_BLOCKING);
return extensions;
}

@Override
protected void applyJspecify() {
super.applyJspecify();
if (Boolean.TRUE.equals(additionalProperties.get(CodegenConstants.GENERATE_APIS))) {
supportingFiles.add(new SupportingFile("invokerPackageInfo.mustache",
(sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator),
"package-info.java"));
}
// nullable_var_annotations.mustache generates nullable annotations as @{{javaxPackage}}.annotation.Nullable
// override the default pattern for the "find and replace"
jSpecifyNullableLambda.setNullableAnnotation("@" + additionalProperties.get(JAVAX_PACKAGE) + ".annotation.Nullable");
}
}
Loading
Loading