Skip to content

Commit 7bd73f8

Browse files
author
jpfinne
committed
useJspecify for java clients and spring generator
1 parent 0c31459 commit 7bd73f8

219 files changed

Lines changed: 15680 additions & 63 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
generatorName: java
2+
outputDir: samples/client/petstore/java/native-jackson3-jspecify
3+
library: native
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/Java
6+
validateSpec: false
7+
additionalProperties:
8+
artifactId: petstore-native-jackson3
9+
hideGenerationTimestamp: "true"
10+
generateBuilders: true
11+
useReflectionEqualsHashCode: "true"
12+
useJackson3: "true"
13+
openApiNullable: "false"
14+
useJspecify: true
15+
typeMappings:
16+
OffsetDateTime: java.time.Instant
17+
BigDecimal: java.math.BigDecimal
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
generatorName: java
2+
outputDir: samples/client/petstore/java/restclient-springBoot4-jackson3-jspecify
3+
library: restclient
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/Java
6+
validateSpec: false
7+
additionalProperties:
8+
artifactId: petstore-restclient
9+
hideGenerationTimestamp: "true"
10+
containerDefaultToNull: "true"
11+
useSpringBoot4: true
12+
useJackson3: true
13+
openApiNullable: false
14+
useJspecify: true
15+
typeMappings:
16+
OffsetDateTime: java.time.Instant
17+
BigDecimal: java.math.BigDecimal
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
generatorName: java
2+
outputDir: samples/client/petstore/java/resttemplate-springBoot4-jackson3-jspecify
3+
library: resttemplate
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/Java
6+
validateSpec: false
7+
additionalProperties:
8+
artifactId: petstore-resttemplate
9+
hideGenerationTimestamp: "true"
10+
containerDefaultToNull: "true"
11+
useJakartaEe: true
12+
useSpringBoot4: true
13+
useJackson3: true
14+
openApiNullable: false
15+
useJspecify: true
16+
typeMappings:
17+
OffsetDateTime: java.time.Instant
18+
BigDecimal: java.math.BigDecimal
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
generatorName: java
2+
outputDir: samples/client/petstore/java/webclient-springBoot4-jackson3-jspecify
3+
library: webclient
4+
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
5+
templateDir: modules/openapi-generator/src/main/resources/Java
6+
validateSpec: false
7+
additionalProperties:
8+
artifactId: petstore-webclient
9+
hideGenerationTimestamp: "true"
10+
containerDefaultToNull: "true"
11+
useSpringBoot4: true
12+
useJackson3: true
13+
openApiNullable: false
14+
useJspecify: true
15+
typeMappings:
16+
OffsetDateTime: java.time.Instant
17+
BigDecimal: java.math.BigDecimal
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
generatorName: spring
2+
outputDir: samples/openapi3/server/petstore/springboot-4-jspecify
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/java/jspecify.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/JavaSpring
5+
validateSpec: false
6+
additionalProperties:
7+
groupId: org.openapitools.openapi3
8+
documentationProvider: springdoc
9+
artifactId: springboot
10+
snapshotVersion: "true"
11+
useSpringBoot4: true
12+
useJackson3: true
13+
useBeanValidation: true
14+
withXml: true
15+
hideGenerationTimestamp: "true"
16+
generateConstructorWithAllArgs: true
17+
generateBuilders: true
18+
openApiNullable: false
19+
useJspecify: true

docs/generators/java-camel.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
111111
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
112112
|useJackson3|Set it in order to use jackson 3 dependencies (only allowed when `useSpringBoot4` is set and incompatible with `openApiNullable`).| |false|
113113
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
114+
|useJspecify|Use Jspecify for null checks. Ony available for Spring, RestClient, WebClient| |false|
114115
|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|
115116
|useOptional|Use Optional container for optional parameters| |false|
116117
|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|

docs/generators/java.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
9999
|useGzipFeature|Send gzip-encoded requests| |false|
100100
|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|
101101
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
102+
|useJspecify|Use Jspecify for null checks. Ony available for Spring, RestClient, WebClient| |false|
102103
|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|
103104
|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|
104105
|usePlayWS|Use Play! Async HTTP client (Play WS API)| |false|

docs/generators/spring.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
104104
|useFeignClientUrl|Whether to generate Feign client with url parameter.| |true|
105105
|useJackson3|Set it in order to use jackson 3 dependencies (only allowed when `useSpringBoot4` is set and incompatible with `openApiNullable`).| |false|
106106
|useJakartaEe|whether to use Jakarta EE namespace instead of javax| |false|
107+
|useJspecify|Use Jspecify for null checks. Ony available for Spring, RestClient, WebClient| |false|
107108
|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|
108109
|useOptional|Use Optional container for optional parameters| |false|
109110
|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|

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractJavaCodegen.java

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
import com.fasterxml.jackson.databind.node.ArrayNode;
2323
import com.fasterxml.jackson.databind.node.ObjectNode;
2424
import com.google.common.base.Strings;
25+
import com.google.common.collect.ImmutableMap;
2526
import com.google.common.collect.Sets;
2627
import com.samskivert.mustache.Mustache;
28+
import com.samskivert.mustache.Template;
2729
import io.swagger.v3.oas.models.OpenAPI;
2830
import io.swagger.v3.oas.models.Operation;
2931
import io.swagger.v3.oas.models.PathItem;
@@ -58,6 +60,8 @@
5860
import javax.lang.model.SourceVersion;
5961

6062
import java.io.File;
63+
import java.io.IOException;
64+
import java.io.Writer;
6165
import java.time.LocalDate;
6266
import java.time.ZoneId;
6367
import java.time.ZonedDateTime;
@@ -104,6 +108,7 @@ public abstract class AbstractJavaCodegen extends DefaultCodegen implements Code
104108
public static final String IMPLICIT_HEADERS_REGEX = "implicitHeadersRegex";
105109
public static final String JAVAX_PACKAGE = "javaxPackage";
106110
public static final String USE_JAKARTA_EE = "useJakartaEe";
111+
public static final String USE_JSPECIFY = "useJspecify";
107112
public static final String CONTAINER_DEFAULT_TO_NULL = "containerDefaultToNull";
108113
public static final String DISABLE_DISCRIMINATOR_JSON_IGNORE_PROPERTIES = "disableDiscriminatorJsonIgnoreProperties";
109114

@@ -216,6 +221,11 @@ protected enum ENUM_PROPERTY_NAMING_TYPE {MACRO_CASE, legacy, original}
216221
*/
217222
@Getter @Setter
218223
protected boolean useBeanValidation = false;
224+
@Getter
225+
@Setter
226+
protected boolean useJspecify;
227+
protected JSpecifyNullableLambda jSpecifyNullableLambda;
228+
219229
private Map<String, String> schemaKeyToModelNameCache = new HashMap<>();
220230

221231
public AbstractJavaCodegen() {
@@ -390,6 +400,7 @@ public AbstractJavaCodegen() {
390400
cliOptions.add(enumPropertyNamingOpt.defaultValue(enumPropertyNaming.name()));
391401

392402
cliOptions.add(CliOption.newString(CodegenConstants.DEFAULT_TO_EMPTY_CONTAINER, CodegenConstants.DEFAULT_TO_EMPTY_CONTAINER_DESC));
403+
cliOptions.add(CliOption.newBoolean(USE_JSPECIFY, "Use Jspecify for null checks. Ony available for Spring, RestClient, WebClient", useJspecify));
393404
}
394405

395406
@Override
@@ -597,6 +608,7 @@ public void processOpts() {
597608
convertPropertyToBooleanAndWriteBack(CAMEL_CASE_DOLLAR_SIGN, this::setCamelCaseDollarSign);
598609
convertPropertyToBooleanAndWriteBack(USE_ONE_OF_INTERFACES, this::setUseOneOfInterfaces);
599610
convertPropertyToStringAndWriteBack(CodegenConstants.ENUM_PROPERTY_NAMING, this::setEnumPropertyNaming);
611+
convertPropertyToBooleanAndWriteBack(USE_JSPECIFY, this::setUseJspecify);
600612

601613
if (!StringUtils.isEmpty(parentGroupId) && !StringUtils.isEmpty(parentArtifactId) && !StringUtils.isEmpty(parentVersion)) {
602614
additionalProperties.put("parentOverridden", true);
@@ -841,10 +853,19 @@ private void sanitizeConfig() {
841853

842854
protected void applyJavaxPackage() {
843855
writePropertyBack(JAVAX_PACKAGE, "javax");
856+
writePropertyBack("nullableAnnotation", "@javax.annotation.Nullable");
857+
writePropertyBack("nonNullAnnotation", "@javax.annotation.Nonnull");
844858
}
845859

846860
protected void applyJakartaPackage() {
847861
writePropertyBack(JAVAX_PACKAGE, "jakarta");
862+
writePropertyBack("nullableAnnotation", "@jakarta.annotation.Nullable");
863+
writePropertyBack("nonNullAnnotation", "@jakarta.annotation.Nonnull");
864+
}
865+
866+
protected void applyJspecify() {
867+
writePropertyBack("nullableAnnotation", "@org.jspecify.annotations.Nullable");
868+
writePropertyBack("nonNullAnnotation", "@NonNull");
848869
}
849870

850871
@Override
@@ -2653,4 +2674,82 @@ public void setEnumPropertyNaming(final String enumPropertyNamingType) {
26532674
throw new RuntimeException(sb.toString());
26542675
}
26552676
}
2677+
2678+
@Override
2679+
protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
2680+
this.jSpecifyNullableLambda = new JSpecifyNullableLambda();
2681+
return super.addMustacheLambdas()
2682+
.put("jSpecifyDatatype",(fragment, writer) -> {
2683+
String dataType = fragment.execute();
2684+
if (jSpecifyNullableLambda.keptNullable) {
2685+
jSpecifyNullableLambda.keptNullable = false;
2686+
int idx = dataType.lastIndexOf('.');
2687+
if (idx > 0) {
2688+
// generate declareation like java.time.@Nullable Timestamp
2689+
writer.write(dataType.substring(0, idx + 1));
2690+
writer.write("@Nullable ");
2691+
writer.write(dataType.substring(idx + 1));
2692+
} else {
2693+
writer.write("@Nullable ");
2694+
writer.write(dataType);
2695+
}
2696+
} else {
2697+
writer.write(dataType);
2698+
}
2699+
})
2700+
.put("jSpecifyNullable", jSpecifyNullableLambda);
2701+
2702+
}
2703+
2704+
/**
2705+
* for Jspecify, remove @Nullable before the datatype.
2706+
*/
2707+
class JSpecifyNullableLambda implements Mustache.Lambda {
2708+
private String nullableAnnotation = "@Nullable";
2709+
// remember if @Nullable is needed
2710+
boolean keptNullable = false;
2711+
2712+
public void setNullableAnnotation(String nullableAnnotation) {
2713+
this.nullableAnnotation = nullableAnnotation;
2714+
}
2715+
2716+
@Override
2717+
public void execute(Template.Fragment fragment, Writer writer) throws IOException {
2718+
keptNullable = false;
2719+
String value = fragment.execute();
2720+
if (useJspecify) {
2721+
if (value.startsWith(nullableAnnotation)) {
2722+
keptNullable = true;
2723+
int idx = nullableAnnotation.length();
2724+
// trim left
2725+
while (idx < value.length() && value.charAt(idx) == ' ') {
2726+
idx ++;
2727+
}
2728+
value = value.substring(idx);
2729+
}
2730+
}
2731+
writer.write(value);
2732+
}
2733+
}
2734+
2735+
protected void addPackagInfoSupportingFiles() {
2736+
if (useJspecify) {
2737+
supportingFiles.add(new SupportingFile("modelPackageInfo.mustache",
2738+
(sourceFolder + File.separator + modelPackage).replace(".", java.io.File.separator),
2739+
"package-info.java"));
2740+
supportingFiles.add(new SupportingFile("apiPackageInfo.mustache",
2741+
(sourceFolder + File.separator + apiPackage).replace(".", java.io.File.separator),
2742+
"package-info.java"));
2743+
}
2744+
}
2745+
2746+
/**
2747+
* Adds Nullable import if any parameter is nullable or optional.
2748+
*/
2749+
protected void addNullableImportForOperation(CodegenOperation codegenOperation) {
2750+
codegenOperation.allParams.stream()
2751+
.filter(CodegenParameter::notRequiredOrIsNullable)
2752+
.findAny()
2753+
.ifPresent(param -> codegenOperation.imports.add("Nullable"));
2754+
}
26562755
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/JavaClientCodegen.java

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import io.swagger.v3.oas.models.Operation;
2121
import io.swagger.v3.oas.models.media.Schema;
22+
import io.swagger.v3.oas.models.servers.Server;
2223
import lombok.AccessLevel;
2324
import lombok.Getter;
2425
import lombok.Setter;
@@ -47,7 +48,6 @@
4748
import static com.google.common.base.CaseFormat.LOWER_CAMEL;
4849
import static com.google.common.base.CaseFormat.UPPER_UNDERSCORE;
4950
import static java.util.Collections.sort;
50-
import static org.openapitools.codegen.CodegenConstants.SERIALIZATION_LIBRARY;
5151
import static org.openapitools.codegen.CodegenConstants.X_IMPLEMENTS;
5252
import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
5353
import static org.openapitools.codegen.utils.StringUtils.camelize;
@@ -176,6 +176,7 @@ public class JavaClientCodegen extends AbstractJavaCodegen
176176

177177
@Setter protected int maxAttemptsForRetry = 1;
178178
@Setter protected long waitTimeMillis = 10l;
179+
private final Set<String> JSPECIFY_SUPPORTED_LIBRARIES = Set.of(RESTCLIENT, WEBCLIENT, NATIVE, RESTTEMPLATE);
179180

180181
private static class MpRestClientVersion {
181182
public final String rootPackage;
@@ -322,6 +323,15 @@ private void initMpRestClientVersionToRootPackage() {
322323
mpRestClientVersions.put("3.0", new MpRestClientVersion("jakarta", "pom_3.0.mustache"));
323324
}
324325

326+
@Override
327+
public void setUseJspecify(boolean useJspecify) {
328+
// currently only available for a limited set of libraries
329+
if (useJspecify && !JSPECIFY_SUPPORTED_LIBRARIES.contains(library)) {
330+
throw new IllegalArgumentException("useJspecify is only suppored in these libraries: " + JSPECIFY_SUPPORTED_LIBRARIES);
331+
}
332+
super.setUseJspecify(useJspecify);
333+
}
334+
325335
@Override
326336
public CodegenType getTag() {
327337
return CodegenType.CLIENT;
@@ -788,6 +798,9 @@ public void processOpts() {
788798
} else {
789799
LOGGER.error("Unknown library option (-l/--library): {}", getLibrary());
790800
}
801+
if (useJspecify) {
802+
applyJspecify();
803+
}
791804

792805
if (usePlayWS) {
793806
// remove unsupported auth
@@ -1006,6 +1019,15 @@ public int compare(CodegenParameter one, CodegenParameter another) {
10061019
return objs;
10071020
}
10081021

1022+
@Override
1023+
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
1024+
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
1025+
if (useJspecify) {
1026+
addNullableImportForOperation(op);
1027+
}
1028+
return op;
1029+
}
1030+
10091031
@Override
10101032
public String apiFilename(String templateName, String tag) {
10111033
if (isLibrary(VERTX)) {
@@ -1136,6 +1158,9 @@ public CodegenModel fromModel(String name, Schema model) {
11361158
if (!AnnotationLibrary.SWAGGER2.equals(getAnnotationLibrary())) {
11371159
codegenModel.imports.remove("Schema");
11381160
}
1161+
if (useJspecify) {
1162+
codegenModel.imports.add("Nullable");
1163+
}
11391164

11401165
return codegenModel;
11411166
}
@@ -1345,4 +1370,22 @@ public List<VendorExtension> getSupportedVendorExtensions() {
13451370
extensions.add(VendorExtension.X_WEBCLIENT_BLOCKING);
13461371
return extensions;
13471372
}
1373+
1374+
@Override
1375+
protected void applyJspecify() {
1376+
super.applyJspecify();
1377+
addPackagInfoSupportingFiles();
1378+
importMapping.put("Nullable", "org.jspecify.annotations.Nullable");
1379+
jSpecifyNullableLambda.setNullableAnnotation("@" + additionalProperties.get(JAVAX_PACKAGE) + ".annotation.Nullable");
1380+
}
1381+
1382+
@Override
1383+
protected void addPackagInfoSupportingFiles() {
1384+
super.addPackagInfoSupportingFiles();
1385+
if (useJspecify) {
1386+
supportingFiles.add(new SupportingFile("invokerPackageInfo.mustache",
1387+
(sourceFolder + File.separator + invokerPackage).replace(".", java.io.File.separator),
1388+
"package-info.java"));
1389+
}
1390+
}
13481391
}

0 commit comments

Comments
 (0)