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
Original file line number Diff line number Diff line change
Expand Up @@ -2948,14 +2948,109 @@ protected ImmutableMap.Builder<String, Mustache.Lambda> addMustacheLambdas() {
};
Mustache.Lambda javaStringLiteralLambda = (fragment, writer) ->
writer.write(toEnumValue(fragment.execute(), "String"));
// Places type-use annotations rendered in front of a (possibly fully qualified) type where Java
// allows them, e.g. {{#lambda.typeUseAnnotations}}Optional<@Valid {{{dataType}}}{{/lambda.typeUseAnnotations}}
// -> Optional<@Valid Pet> or Optional<java.time.@Valid Instant> (Optional<@Valid java.time.Instant> does not compile).
Mustache.Lambda typeUseAnnotationsLambda = (fragment, writer) ->
writer.write(placeTypeUseAnnotations(fragment.execute()));
return super.addMustacheLambdas()
.put("javaStringLiteral", javaStringLiteralLambda)
.put("typeUseAnnotations", typeUseAnnotationsLambda)
.put("jSpecifyDatatype", jSpecifyDatatypeLambda)
.put("jSpecifyNullable", jSpecifyNullableLambda)
.put("escapeJavaDoc", new EscapeJavaDocLambda());

}

/**
* Moves the type-use annotations written in front of the innermost type argument of {@code declaration} after the
* package qualifier when that type is fully qualified, as required by the Java grammar:
* {@code Optional<@Valid java.time.Instant>} becomes {@code Optional<java.time.@Valid Instant>}. Declarations whose
* annotated type is not qualified, or that carry no annotations, are returned unchanged.
*
* @param declaration rendered type declaration, e.g. {@code JsonNullable<@Valid @Size(max = 3) com.acme.Code}
* @return the declaration with its type-use annotations at a legal position
*/
public static String placeTypeUseAnnotations(String declaration) {
int firstAnnotation = declaration.indexOf('@');
if (firstAnnotation < 0) {
return declaration;
}
// keep the wrapper (e.g. "Optional<") untouched; the annotations apply to the type argument that follows
int start = declaration.lastIndexOf('<', firstAnnotation) + 1;
if (!StringUtils.isBlank(declaration.substring(start, firstAnnotation))) {
// something else precedes the first annotation (e.g. "java.time.@Nullable Instant"): already placed
return declaration;
}
int end = skipAnnotations(declaration, firstAnnotation);
String annotations = declaration.substring(firstAnnotation, end);
String type = declaration.substring(end);
int idx = getLastIndexOfQualifier(type);
if (idx <= 0) {
return declaration;
}
return declaration.substring(0, firstAnnotation) + type.substring(0, idx + 1) + annotations + type.substring(idx + 1);
}

/**
* @return the index right after the annotations (and their trailing whitespace) starting at {@code index}
*/
private static int skipAnnotations(String declaration, int index) {
int i = index;
int length = declaration.length();
while (i < length && declaration.charAt(i) == '@') {
i++;
while (i < length && (Character.isJavaIdentifierPart(declaration.charAt(i)) || declaration.charAt(i) == '.')) {
i++;
}
if (i < length && declaration.charAt(i) == '(') {
i = skipParentheses(declaration, i);
}
while (i < length && Character.isWhitespace(declaration.charAt(i))) {
i++;
}
}
return i;
}

/**
* @return the index right after the parenthesis group opened at {@code index}, ignoring parentheses in string literals
*/
private static int skipParentheses(String declaration, int index) {
int depth = 0;
boolean inString = false;
for (int i = index; i < declaration.length(); i++) {
char c = declaration.charAt(i);
if (inString) {
if (c == '\\') {
i++;
} else if (c == '"') {
inString = false;
}
} else if (c == '"') {
inString = true;
} else if (c == '(') {
depth++;
} else if (c == ')' && --depth == 0) {
return i + 1;
}
}
return declaration.length();
}

private static int getLastIndexOfQualifier(String dataType) {
int index = dataType.indexOf('<');
if (index >= 0) {
dataType = dataType.substring(0, index);
}
int at = dataType.indexOf('@');
if (at >= 0) {
// e.g. "java.time.@Nullable Instant": the qualifier ends before the existing annotation
dataType = dataType.substring(0, at);
}
return dataType.lastIndexOf('.');
}

private int getLastIndex(String dataType) {
int index = dataType.indexOf('<');
if (index >= 0) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{>notNull}}{{^isContainer}}{{^isPrimitiveType}}@Valid {{/isPrimitiveType}}{{/isContainer}}{{^openApiNullable}}{{>beanValidationCore}}{{/openApiNullable}}{{#openApiNullable}}{{^useOptional}}{{>beanValidationCore}}{{/useOptional}}{{/openApiNullable}}{{#useOptional}}{{#openApiNullable}}{{#isContainer}}{{^required}}{{>beanValidationCore}}{{/required}}{{/isContainer}}{{/openApiNullable}}{{#openApiNullable}}{{#required}}{{>beanValidationCore}}{{/required}}{{/openApiNullable}}{{/useOptional}}
{{>notNull}}{{^isContainer}}{{^isPrimitiveType}}{{^openApiNullable}}@Valid {{/openApiNullable}}{{#openApiNullable}}{{^isNullable}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isNullable}}{{/openApiNullable}}{{/isPrimitiveType}}{{/isContainer}}{{^openApiNullable}}{{>beanValidationCore}}{{/openApiNullable}}{{#openApiNullable}}{{^useOptional}}{{>beanValidationCore}}{{/useOptional}}{{/openApiNullable}}{{#useOptional}}{{#openApiNullable}}{{#isContainer}}{{^required}}{{>beanValidationCore}}{{/required}}{{/isContainer}}{{/openApiNullable}}{{#openApiNullable}}{{#required}}{{>beanValidationCore}}{{/required}}{{/openApiNullable}}{{/useOptional}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isBodyParam}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{#reactive}}@Valid {{/reactive}}{{^reactive}}{{^isContainer}}@Valid {{/isContainer}}{{/reactive}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}@RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{>nullableAnnotation}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}}
{{#isBodyParam}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{#reactive}}@Valid {{/reactive}}{{^reactive}}{{^isContainer}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isContainer}}{{/reactive}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}@RequestBody{{^required}}(required = false){{/required}} {{^reactive}}{{>nullableAnnotation}}{{>optionalDataType}}{{/reactive}}{{#reactive}}{{^isArray}}Mono<{{{dataType}}}>{{/isArray}}{{#isArray}}Flux<{{{baseType}}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isBodyParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{^isModel}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isModel}}{{/isContainer}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}{{#isArray}}@RequestPart{{/isArray}}{{^isArray}}{{#reactive}}@RequestPart{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/isArray}}{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{^required}}{{#useOptional}}Optional<{{/useOptional}}{{/required}}{{{dataType}}}{{^required}}{{#useOptional}}>{{/useOptional}}{{/required}} {{paramName}}{{/isFile}}{{#isFile}}{{>paramDoc}} {{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}@RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}MultipartFile{{#isArray}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isFile}}{{/isFormParam}}
{{#isFormParam}}{{^isFile}}{{>paramDoc}}{{#useBeanValidation}} {{>beanValidationBodyParams}}{{^isContainer}}{{^useOptional}}@Valid {{/useOptional}}{{#useOptional}}{{#required}}@Valid {{/required}}{{/useOptional}}{{/isContainer}}{{/useBeanValidation}}{{^useBeanValidation}} {{/useBeanValidation}}{{#isModel}}@RequestPart{{/isModel}}{{^isModel}}{{#isArray}}@RequestPart{{/isArray}}{{^isArray}}{{#reactive}}@RequestPart{{/reactive}}{{^reactive}}@RequestParam{{/reactive}}{{/isArray}}{{/isModel}}(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}){{>dateTimeParam}} {{#lambda.typeUseAnnotations}}{{^required}}{{#useOptional}}Optional<{{#useBeanValidation}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{/isContainer}}{{/useBeanValidation}}{{/useOptional}}{{/required}}{{{dataType}}}{{/lambda.typeUseAnnotations}}{{^required}}{{#useOptional}}>{{/useOptional}}{{/required}} {{paramName}}{{/isFile}}{{#isFile}}{{>paramDoc}} {{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}@RequestPart(value = "{{baseName}}"{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}) {{#reactive}}{{#isArray}}Flux<{{/isArray}}Part{{#isArray}}>{{/isArray}}{{/reactive}}{{^reactive}}{{#isArray}}List<{{/isArray}}MultipartFile{{#isArray}}>{{/isArray}}{{/reactive}} {{paramName}}{{/isFile}}{{/isFormParam}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#lambda.trim}}{{#openApiNullable}}{{#isNullable}}{{^isContainer}}JsonNullable<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{#isContainer}}JsonNullable<{{/isContainer}}{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.trim}}
{{#lambda.trim}}{{#lambda.typeUseAnnotations}}{{#openApiNullable}}{{#isNullable}}{{^isContainer}}JsonNullable<{{#useBeanValidation}}{{#isModel}}@Valid {{/isModel}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{#isContainer}}JsonNullable<{{/isContainer}}{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}Optional<{{#useBeanValidation}}{{#isModel}}@Valid {{/isModel}}{{>beanValidationCore}}{{/useBeanValidation}}{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{#lambda.jSpecifyDatatype}}{{{datatypeWithEnum}}}{{/lambda.jSpecifyDatatype}}{{#openApiNullable}}{{#isNullable}}>{{/isNullable}}{{#useOptional}}{{^required}}{{^isNullable}}{{^isContainer}}>{{/isContainer}}{{/isNullable}}{{/required}}{{/useOptional}}{{/openApiNullable}}{{/lambda.typeUseAnnotations}}{{/lambda.trim}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#useOptional}}{{#required}}{{{dataType}}}{{/required}}{{^required}}Optional<{{#useBeanValidation}}{{>beanValidationCore}}{{/useBeanValidation}}{{{dataType}}}>{{/required}}{{/useOptional}}{{^useOptional}}{{#lambda.jSpecifyDatatype}}{{{dataType}}}{{/lambda.jSpecifyDatatype}}{{/useOptional}}
{{#useOptional}}{{#required}}{{{dataType}}}{{/required}}{{^required}}{{#lambda.typeUseAnnotations}}Optional<{{#useBeanValidation}}{{^isContainer}}{{#isModel}}@Valid {{/isModel}}{{/isContainer}}{{>beanValidationCore}}{{/useBeanValidation}}{{{dataType}}}{{/lambda.typeUseAnnotations}}>{{/required}}{{/useOptional}}{{^useOptional}}{{#lambda.jSpecifyDatatype}}{{{dataType}}}{{/lambda.jSpecifyDatatype}}{{/useOptional}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{{#isQueryParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}}{{#useBeanValidation}}{{^isContainer}}{{#isModel}} @Valid{{/isModel}}{{^isModel}}{{^useOptional}} @Valid{{/useOptional}}{{#useOptional}}{{#required}} @Valid{{/required}}{{/useOptional}}{{/isModel}}{{/isContainer}}{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/isModel}}{{>dateTimeParam}}{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isQueryParam}}
{{#isQueryParam}}{{#vendorExtensions.x-field-extra-annotation}}{{{.}}} {{/vendorExtensions.x-field-extra-annotation}}{{#useBeanValidation}}{{>beanValidationQueryParams}}{{/useBeanValidation}}{{>paramDoc}}{{#useBeanValidation}}{{^isContainer}}{{^useOptional}} @Valid{{/useOptional}}{{#useOptional}}{{#required}} @Valid{{/required}}{{/useOptional}}{{/isContainer}}{{/useBeanValidation}}{{^isModel}} @RequestParam(value = {{#isMap}}""{{/isMap}}{{^isMap}}"{{baseName}}"{{/isMap}}{{#required}}, required = true{{/required}}{{^required}}, required = false{{/required}}{{#defaultValue}}, defaultValue = "{{{.}}}"{{/defaultValue}}){{/isModel}}{{>dateTimeParam}}{{#isDeprecated}} @Deprecated{{/isDeprecated}} {{>nullableAnnotation}}{{>optionalDataType}} {{paramName}}{{/isQueryParam}}
Original file line number Diff line number Diff line change
Expand Up @@ -1251,4 +1251,32 @@ public void testNullTypeMapsToObject() {
assertThat(codegen.getTypeDeclaration(nullSchema)).isEqualTo("Object");
assertThat(codegen.getTypeDeclaration(new ArraySchema().items(nullSchema))).isEqualTo("List<Object>");
}

@Test
public void placeTypeUseAnnotations_issue25097() {
// simple names and declarations without annotations are left untouched
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("Optional<@Valid Pet")).isEqualTo("Optional<@Valid Pet");
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("@Valid Pet")).isEqualTo("@Valid Pet");
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("Optional<java.time.Instant>")).isEqualTo("Optional<java.time.Instant>");
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("java.time.Instant")).isEqualTo("java.time.Instant");
// fully qualified type arguments get the annotations after the package qualifier
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("Optional<@Valid java.time.Instant"))
.isEqualTo("Optional<java.time.@Valid Instant");
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("JsonNullable<@Valid @Size(max = 3) com.acme.Code"))
.isEqualTo("JsonNullable<com.acme.@Valid @Size(max = 3) Code");
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("@Valid com.acme.Container<java.lang.Object>"))
.isEqualTo("com.acme.@Valid Container<java.lang.Object>");
// dots and parentheses inside annotation names and string attributes are not mistaken for the qualifier
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations(
"Optional<@Pattern(regexp = \"^(a.b\\\"c)$\") @jakarta.validation.constraints.Email java.lang.String"))
.isEqualTo("Optional<java.lang.@Pattern(regexp = \"^(a.b\\\"c)$\") @jakarta.validation.constraints.Email String");
// an existing jSpecify placement (java.time.@Nullable Instant) is kept and extended
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("JsonNullable<@Valid java.time.@Nullable Instant"))
.isEqualTo("JsonNullable<java.time.@Valid @Nullable Instant");
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("java.time.@Nullable Instant"))
.isEqualTo("java.time.@Nullable Instant");
// annotations of a nested container element are not the wrapper's type argument: untouched
assertThat(AbstractJavaCodegen.placeTypeUseAnnotations("JsonNullable<Map<String, @Valid com.acme.Item>>"))
.isEqualTo("JsonNullable<Map<String, @Valid com.acme.Item>>");
}
}
Loading
Loading