Skip to content

Commit

Permalink
[4947][java]: adds support for validation of primitives in arrays (Op…
Browse files Browse the repository at this point in the history
…enAPITools#17165)

* [4947][java]: adds support for validation of primitives in arrays

* [4947][java]: prevents generation '@Valid' for Object

* [4947][java]: test against different codegens and stick to primitive

* [4947][java]: code review

* [4947][java]: enhance getBeanValidation

* [4947][java]: adds email

* [4947][java]: removes unnecessary override

* [4947][java]: adds postProcessResponseWithProperty

* [4947][java]: adds missing import {{javaxPackage}}.validation.Valid

* [4947][java]: adds missing useBeanValidation

* [4947][java]: fix use rootJavaEEPackage for helidon
  • Loading branch information
Aliaksie authored Dec 11, 2023
1 parent d4d5196 commit 809b333
Show file tree
Hide file tree
Showing 575 changed files with 1,982 additions and 843 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,8 @@ public interface CodegenConfig {

void postProcessModelProperty(CodegenModel model, CodegenProperty property);

void postProcessResponseWithProperty(CodegenResponse response, CodegenProperty property);

void postProcessParameter(CodegenParameter parameter);

String modelFilename(String templateName, String modelName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,12 @@ public Map<String, Object> postProcessSupportingFileData(Map<String, Object> obj
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
}

// override to post-process any response
@Override
@SuppressWarnings("unused")
public void postProcessResponseWithProperty(CodegenResponse response, CodegenProperty property) {
}

// override to post-process any parameters
@Override
@SuppressWarnings("unused")
Expand Down Expand Up @@ -4999,6 +5005,7 @@ public CodegenResponse fromResponse(String responseCode, ApiResponse response) {
r.simpleType = true;
}

postProcessResponseWithProperty(r, cp);
return r;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.google.common.base.Strings;
import com.google.common.collect.Sets;
import io.swagger.models.Model;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
Expand All @@ -34,7 +33,9 @@
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
import org.openapitools.codegen.languages.features.DocumentationProviderFeatures;
import org.openapitools.codegen.meta.features.*;
import org.openapitools.codegen.model.ModelMap;
Expand Down Expand Up @@ -930,7 +931,7 @@ public String getTypeDeclaration(Schema p) {
Schema<?> target = ModelUtils.isGenerateAliasAsModel() ? p : schema;
if (ModelUtils.isArraySchema(target)) {
Schema<?> items = getSchemaItems((ArraySchema) schema);
return getSchemaType(target) + "<" + getTypeDeclaration(items) + ">";
return getSchemaType(target) + "<" + getBeanValidation(items) + getTypeDeclaration(items) + ">";
} else if (ModelUtils.isMapSchema(target)) {
// Note: ModelUtils.isMapSchema(p) returns true when p is a composed schema that also defines
// additionalproperties: true
Expand All @@ -945,6 +946,128 @@ public String getTypeDeclaration(Schema p) {
return super.getTypeDeclaration(target);
}

/**
* This method stand for resolve bean validation for container(array, set).
* Return empty if there's no bean validation for requested type or prop useBeanValidation false or missed.
*
* @param items type
* @return BeanValidation for declared type in container(array, set)
*/
private String getBeanValidation(Schema<?> items) {
if (Boolean.FALSE.equals(additionalProperties.getOrDefault(BeanValidationFeatures.USE_BEANVALIDATION, Boolean.FALSE))) {
return "";
}

if (ModelUtils.isTypeObjectSchema(items)) {
// prevents generation '@Valid' for Object
return "";
}

if (items.get$ref() != null) {
return "@Valid ";
}

if (ModelUtils.isStringSchema(items)) {
return getStringBeanValidation(items);
}

if (ModelUtils.isNumberSchema(items)) {
return getNumberBeanValidation(items);
}

if (ModelUtils.isIntegerSchema(items)) {
return getIntegerBeanValidation(items);
}

return "";
}

private String getIntegerBeanValidation(Schema<?> items) {
if (items.getMinimum() != null && items.getMaximum() != null) {
return String.format(Locale.ROOT, "@Min(%s) @Max(%s)", items.getMinimum(), items.getMaximum());
}

if (items.getMinimum() != null) {
return String.format(Locale.ROOT, "@Min(%s)", items.getMinimum());
}

if (items.getMaximum() != null) {
return String.format(Locale.ROOT, "@Max(%s)", items.getMaximum());
}
return "";
}

private String getNumberBeanValidation(Schema<?> items) {
if (items.getMinimum() != null && items.getMaximum() != null) {
return String.format(Locale.ROOT, "@DecimalMin(value = \"%s\", inclusive = %s) @DecimalMax(value = \"%s\", inclusive = %s)",
items.getMinimum(),
Optional.ofNullable(items.getExclusiveMinimum()).orElse(Boolean.FALSE),
items.getMaximum(),
Optional.ofNullable(items.getExclusiveMaximum()).orElse(Boolean.FALSE));
}

if (items.getMinimum() != null) {
return String.format(Locale.ROOT, "@DecimalMin( value = \"%s\", inclusive = %s)",
items.getMinimum(),
Optional.ofNullable(items.getExclusiveMinimum()).orElse(Boolean.FALSE));
}

if (items.getMaximum() != null) {
return String.format(Locale.ROOT, "@DecimalMax( value = \"%s\", inclusive = %s)",
items.getMaximum(),
Optional.ofNullable(items.getExclusiveMaximum()).orElse(Boolean.FALSE));
}

return "";
}

private String getStringBeanValidation(Schema<?> items) {
String validations = "";
if (ModelUtils.isByteArraySchema(items) || ModelUtils.isBinarySchema(items)) {
return validations;
}

if (StringUtils.isNotEmpty(items.getPattern())) {
final String pattern = escapeUnsafeCharacters(
StringEscapeUtils.unescapeJava(
StringEscapeUtils.escapeJava(items.getPattern())
.replace("\\/", "/"))
.replaceAll("[\\t\\n\\r]", " ")
.replace("\\", "\\\\")
.replace("\"", "\\\""));

validations = String.format(Locale.ROOT, "@Pattern(regexp = \"%s\")", pattern);
}

if (ModelUtils.isEmailSchema(items)) {
return String.join("", "@Email ");
}

if (ModelUtils.isDecimalSchema(items)) {
return String.join("", validations, getNumberBeanValidation(items));
}

if (items.getMinLength() != null && items.getMaxLength() != null) {
return String.join("",
validations,
String.format(Locale.ROOT, "@Size(min = %d, max = %d)", items.getMinLength(), items.getMaxLength()));
}

if (items.getMinLength() != null) {
return String.join("",
validations,
String.format(Locale.ROOT, "@Size(min = %d)", items.getMinLength()));
}

if (items.getMaxLength() != null) {
return String.join("",
validations,
String.format(Locale.ROOT, "@Size(max = %d)", items.getMaxLength()));
}

return validations;
}

@Override
public String getAlias(String name) {
if (typeAliases != null && typeAliases.containsKey(name)) {
Expand Down Expand Up @@ -1511,6 +1634,21 @@ public void postProcessModelProperty(CodegenModel model, CodegenProperty propert
}
}

public void postProcessResponseWithProperty(CodegenResponse response, CodegenProperty property) {
if (response == null || property == null || response.dataType == null || property.dataType == null) {
return;
}

// the response data types should not contains a bean validation annotation.
if (property.dataType.contains("@")) {
property.dataType = property.dataType.replaceAll("(?:(?i)@[a-z0-9]*+\\s*)*+", "");
}
// the response data types should not contains a bean validation annotation.
if (response.dataType.contains("@")) {
response.dataType = response.dataType.replaceAll("(?:(?i)@[a-z0-9]*+\\s*)*+", "");
}
}

@Override
public ModelsMap postProcessModels(ModelsMap objs) {
// recursively add import for mapping one type to multiple imports
Expand Down Expand Up @@ -2289,7 +2427,7 @@ protected void handleImplicitHeaders(CodegenOperation operation) {
}
operation.hasParams = !operation.allParams.isEmpty();
}

private boolean shouldBeImplicitHeader(CodegenParameter parameter) {
return StringUtils.isNotBlank(implicitHeadersRegex) && parameter.baseName.matches(implicitHeadersRegex);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1420,136 +1420,11 @@ public void setRequestMappingMode(RequestMappingMode requestMappingMode) {
this.requestMappingMode = requestMappingMode;
}

@Override
public CodegenParameter fromParameter( final Parameter parameter, final Set<String> imports ) {
CodegenParameter codegenParameter = super.fromParameter( parameter, imports );
if(!isListOrSet(codegenParameter)){
return codegenParameter;
}
codegenParameter.datatypeWithEnum = replaceBeanValidationCollectionType(codegenParameter.items, codegenParameter.datatypeWithEnum );
codegenParameter.dataType = replaceBeanValidationCollectionType(codegenParameter.items, codegenParameter.dataType );
return codegenParameter;
}
@Override
public CodegenProperty fromProperty( String name, Schema p, boolean required, boolean schemaIsFromAdditionalProperties ) {
CodegenProperty codegenProperty = super.fromProperty( name, p, required, schemaIsFromAdditionalProperties );
if(!isListOrSet(codegenProperty)){
return codegenProperty;
}
codegenProperty.datatypeWithEnum = replaceBeanValidationCollectionType(codegenProperty.items, codegenProperty.datatypeWithEnum );
codegenProperty.dataType = replaceBeanValidationCollectionType(codegenProperty.items, codegenProperty.dataType );
return codegenProperty;
}

// The default validation applied for non-container and non-map types is sufficient for the SpringCodegen.
// Maps are very complex for bean validation, so it's currently not supported.
private static boolean isListOrSet(CodegenProperty codegenProperty) {
return codegenProperty.isContainer && !codegenProperty.isMap;
}

// The default validation applied for non-container and non-map types is sufficient for the SpringCodegen.
// Maps are very complex for bean validation, so it's currently not supported.
private static boolean isListOrSet(CodegenParameter codegenParameter) {
return codegenParameter.isContainer && !codegenParameter.isMap;
}

private String replaceBeanValidationCollectionType(CodegenProperty codegenProperty, String dataType) {
if (!useBeanValidation() || isResponseType(codegenProperty)) {
return dataType;
}

if (StringUtils.isEmpty(dataType) || dataType.contains("@Valid")) {
return dataType;
}

if (codegenProperty.isModel) {
return dataType.replace("<", "<@Valid ");
}
String beanValidation = getPrimitiveBeanValidation(codegenProperty);
if (beanValidation == null) {
return dataType;
}
return dataType.replace("<", "<" + beanValidation + " ");
}

/**
* This method should be in sync with beanValidationCore.mustache
* @param codegenProperty the code property
* @return the bean validation semantic for container primitive types
*/
private String getPrimitiveBeanValidation(CodegenProperty codegenProperty) {

if (StringUtils.isNotEmpty(codegenProperty.pattern) && !codegenProperty.isByteArray) {
return "@Pattern(regexp = \""+codegenProperty.pattern+"\")";
}

if (codegenProperty.minLength != null && codegenProperty.maxLength != null) {
return "@Size(min = " + codegenProperty.minLength + ", max = " + codegenProperty.maxLength + ")";
}

if (codegenProperty.minLength != null) {
return "@Size(min = " + codegenProperty.minLength + ")";
}

if (codegenProperty.maxLength != null) {
return "@Size(max = " + codegenProperty.maxLength + ")";
}


if (codegenProperty.isEmail) {
return "@" + additionalProperties.get(JAVAX_PACKAGE)+".validation.constraints.Email";
}


if (codegenProperty.isLong || codegenProperty.isInteger) {

if (StringUtils.isNotEmpty(codegenProperty.minimum) && StringUtils.isNotEmpty(codegenProperty.maximum)) {
return "@Min("+codegenProperty.minimum+") @Max("+codegenProperty.maximum+")";
}

if (StringUtils.isNotEmpty(codegenProperty.minimum)) {
return "@Min("+codegenProperty.minimum+")";
}

if (StringUtils.isNotEmpty(codegenProperty.maximum)) {
return "@Max("+codegenProperty.maximum+")";
}
}

if (StringUtils.isNotEmpty(codegenProperty.minimum) && StringUtils.isNotEmpty(codegenProperty.maximum)) {
return "@DecimalMin(value = \""+codegenProperty.minimum+"\", inclusive = false) @DecimalMax(value = \""+codegenProperty.maximum+"\", inclusive = false)";
}

if (StringUtils.isNotEmpty(codegenProperty.minimum)) {
return "@DecimalMin( value = \""+codegenProperty.minimum+"\", inclusive = false)";
}

if (StringUtils.isNotEmpty(codegenProperty.maximum)) {
return "@DecimalMax( value = \""+codegenProperty.maximum+"\", inclusive = false)";
}

return null;
}



public void setResourceFolder( String resourceFolder ) {
this.resourceFolder = resourceFolder;
}

public String getResourceFolder() {
return resourceFolder;
}


// This should prevent, that the response data types not contains a @Valid annotation.
// However, the side effect is that attributes with response as name are also affected.
private static boolean isResponseType(CodegenProperty codegenProperty) {
return codegenProperty.baseName.toLowerCase(Locale.ROOT).contains("response");
}

// SPRING_HTTP_INTERFACE does not support bean validation.
public boolean useBeanValidation() {
return useBeanValidation && !SPRING_HTTP_INTERFACE.equals(library);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import java.io.IOException;

{{#useBeanValidation}}
import {{javaxPackage}}.validation.constraints.*;
import {{javaxPackage}}.validation.Valid;
{{/useBeanValidation}}
{{#performBeanValidation}}
import {{javaxPackage}}.validation.ConstraintViolation;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ import io.swagger.v3.oas.annotations.responses.*;
import io.swagger.v3.oas.annotations.security.*;
{{/swagger2AnnotationLibrary}}

{{#useBeanValidation}}
import {{javaxPackage}}.validation.constraints.*;
import {{javaxPackage}}.validation.Valid;
{{/useBeanValidation}}

import java.lang.reflect.Type;
import java.util.function.Consumer;
import java.util.function.Function;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ import okhttp3.MultipartBody;
{{#imports}}import {{import}};
{{/imports}}

{{#useBeanValidation}}
import {{javaxPackage}}.validation.constraints.*;
import {{javaxPackage}}.validation.Valid;
{{/useBeanValidation}}

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {{javaxPackage}}.ws.rs.core.Response;
import {{javaxPackage}}.ws.rs.core.SecurityContext;
{{#useBeanValidation}}
import {{javaxPackage}}.validation.constraints.*;
import {{javaxPackage}}.validation.Valid;
{{/useBeanValidation}}
{{>generatedAnnotation}}
{{#operations}}
Expand Down
Loading

0 comments on commit 809b333

Please sign in to comment.