All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.victools.jsonschema.module.javax.validation.JavaxValidationModule Maven / Gradle / Ivy

Go to download

Module for the jsonschema-generator – deriving JSON Schema attributes from javax.validation annotations

There is a newer version: 4.37.0
Show newest version
/*
 * Copyright 2019 VicTools.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.github.victools.jsonschema.module.javax.validation;

import com.github.victools.jsonschema.generator.FieldScope;
import com.github.victools.jsonschema.generator.MemberScope;
import com.github.victools.jsonschema.generator.MethodScope;
import com.github.victools.jsonschema.generator.Module;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder;
import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart;
import java.lang.annotation.Annotation;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import javax.validation.constraints.DecimalMax;
import javax.validation.constraints.DecimalMin;
import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.Negative;
import javax.validation.constraints.NegativeOrZero;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Null;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Positive;
import javax.validation.constraints.PositiveOrZero;
import javax.validation.constraints.Size;

/**
 * JSON Schema Generation Module: based on annotations from the {@code javax.validation.constraints} package.
 * 
    *
  • Determine whether a member is not nullable, base assumption being that all fields and method return values are nullable if not annotated.
  • *
  • Optionally: also indicate all explicitly not nullable fields/methods to be required.
  • *
  • Populate "minItems" and "maxItems" for containers (i.e. arrays and collections).
  • *
  • Populate "minLength", "maxLength" and "format" for strings.
  • *
  • Optionally: populate "pattern" for strings.
  • *
  • Populate "minimum"/"exclusiveMinimum" and "maximum"/"exclusiveMaximum" for numbers.
  • *
*/ public class JavaxValidationModule implements Module { private final Set options; private Set> validationGroups; /** * Constructor. * * @param options features to enable */ public JavaxValidationModule(JavaxValidationOption... options) { this.options = options == null ? Collections.emptySet() : new HashSet<>(Arrays.asList(options)); // by default: ignore validation groups this.validationGroups = null; } /** * Add validation groups to be considered. *
    *
  • Never calling this method will result in all annotations to be picked-up.
  • *
  • Calling this without parameters will only consider those annotations where no groups are defined.
  • *
  • Calling this with not-null parameters will only consider those annotations without defined groups or where at least one matches.
  • *
* * @param validationGroups validation groups to consider * @return this module instance (for chaining) */ public JavaxValidationModule forValidationGroups(Class... validationGroups) { if (validationGroups == null) { this.validationGroups = null; } else { this.validationGroups = new HashSet<>(Arrays.asList(validationGroups)); } return this; } @Override public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) { SchemaGeneratorConfigPart fieldConfigPart = builder.forFields(); this.applyToConfigPart(fieldConfigPart); if (this.options.contains(JavaxValidationOption.NOT_NULLABLE_FIELD_IS_REQUIRED)) { fieldConfigPart.withRequiredCheck(this::isRequired); } SchemaGeneratorConfigPart methodConfigPart = builder.forMethods(); this.applyToConfigPart(methodConfigPart); if (this.options.contains(JavaxValidationOption.NOT_NULLABLE_METHOD_IS_REQUIRED)) { methodConfigPart.withRequiredCheck(this::isRequired); } } /** * Apply the various annotation-based resolvers for the given configuration part (this is expected to be executed for both fields and methods). * * @param configPart config builder part to add configurations to */ private void applyToConfigPart(SchemaGeneratorConfigPart configPart) { configPart.withNullableCheck(this::isNullable); configPart.withArrayMinItemsResolver(this::resolveArrayMinItems); configPart.withArrayMaxItemsResolver(this::resolveArrayMaxItems); configPart.withStringMinLengthResolver(this::resolveStringMinLength); configPart.withStringMaxLengthResolver(this::resolveStringMaxLength); configPart.withStringFormatResolver(this::resolveStringFormat); configPart.withNumberInclusiveMinimumResolver(this::resolveNumberInclusiveMinimum); configPart.withNumberExclusiveMinimumResolver(this::resolveNumberExclusiveMinimum); configPart.withNumberInclusiveMaximumResolver(this::resolveNumberInclusiveMaximum); configPart.withNumberExclusiveMaximumResolver(this::resolveNumberExclusiveMaximum); if (this.options.contains(JavaxValidationOption.INCLUDE_PATTERN_EXPRESSIONS)) { configPart.withStringPatternResolver(this::resolveStringPattern); } } /** * Retrieves the annotation instance of the given type, either from the field itself or (if not present) from its getter. *
* If the given field/method represents only a container item of the actual declared type, that container item's annotations are being checked. * * @param type of annotation * @param member field or method to retrieve annotation instance from (or from a field's getter or getter method's field) * @param annotationClass type of annotation * @param validationGroupsLookup how to look-up the associated validation groups of an annotation instance * @return annotation instance (or {@code null}) * @see MemberScope#getAnnotationConsideringFieldAndGetterIfSupported(Class) * @see MemberScope#getContainerItemAnnotationConsideringFieldAndGetterIfSupported(Class) */ protected A getAnnotationFromFieldOrGetter(MemberScope member, Class annotationClass, Function[]> validationGroupsLookup) { A containerItemAnnotation = member.getContainerItemAnnotationConsideringFieldAndGetterIfSupported(annotationClass); if (this.shouldConsiderAnnotation(containerItemAnnotation, validationGroupsLookup)) { return containerItemAnnotation; } A annotation = member.getAnnotationConsideringFieldAndGetterIfSupported(annotationClass); if (this.shouldConsiderAnnotation(annotation, validationGroupsLookup)) { return annotation; } return null; } /** * Check whether a given annotation is supposed to be considered in the schema generation. I.e. if specific validation groups are defined, it must * belong to at least one of them. * * @param type of annotation * @param annotation annotation instance (may be {@code null}, which will result in {@code false} to be returned) * @param validationGroupsLookup how to look-up the associated validation groups of an annotation instance * @return whether the given annotation should be considered in the current schema generation */ private boolean shouldConsiderAnnotation(A annotation, Function[]> validationGroupsLookup) { // avoid repeated null checks by doing it here if (annotation == null) { return false; } // no specific validation groups means: all annotations are fair game if (this.validationGroups == null) { return true; } // check that the annotation's validation groups have at least one common entry to the configured groups on this module Class[] associatedGroups = validationGroupsLookup.apply(annotation); return associatedGroups.length == 0 || !Collections.disjoint(this.validationGroups, Arrays.asList(associatedGroups)); } /** * Determine whether a given field or method is annotated to be not nullable. * * @param member the field or method to check * @return whether member is annotated as nullable or not (returns null if not specified: assumption it is nullable then) */ protected Boolean isNullable(MemberScope member) { Boolean result; if (member.isFakeContainerItemScope()) { // annotations on the field/method are assumed to refer to the surrounding container result = null; } else if (this.getAnnotationFromFieldOrGetter(member, NotNull.class, NotNull::groups) != null || this.getAnnotationFromFieldOrGetter(member, NotBlank.class, NotBlank::groups) != null || this.getAnnotationFromFieldOrGetter(member, NotEmpty.class, NotEmpty::groups) != null) { // field is specifically NOT nullable result = Boolean.FALSE; } else if (this.getAnnotationFromFieldOrGetter(member, Null.class, Null::groups) != null) { // field is specifically null (and thereby nullable) result = Boolean.TRUE; } else { result = null; } return result; } /** * Determine whether a given field or method is deemed to be required in its parent type. * * @param member the field or method to check * @return whether member is deemed to be required or not */ protected boolean isRequired(MemberScope member) { Boolean nullableCheckResult = this.isNullable(member); return Boolean.FALSE.equals(nullableCheckResult); } /** * Determine a given array type's minimum number of items. * * @param member the field or method to check * @return specified minimum number of array items (or null) * @see Size */ protected Integer resolveArrayMinItems(MemberScope member) { if (member.isContainerType()) { Size sizeAnnotation = this.getAnnotationFromFieldOrGetter(member, Size.class, Size::groups); if (sizeAnnotation != null && sizeAnnotation.min() > 0) { // minimum length greater than the default 0 was specified return sizeAnnotation.min(); } if (this.getAnnotationFromFieldOrGetter(member, NotEmpty.class, NotEmpty::groups) != null) { return 1; } } return null; } /** * Determine a given array type's maximum number of items. * * @param member the field or method to check * @return specified maximum number of array items (or null) * @see Size */ protected Integer resolveArrayMaxItems(MemberScope member) { if (member.isContainerType()) { Size sizeAnnotation = this.getAnnotationFromFieldOrGetter(member, Size.class, Size::groups); if (sizeAnnotation != null && sizeAnnotation.max() < 2147483647) { // maximum length below the default 2147483647 was specified return sizeAnnotation.max(); } } return null; } /** * Determine a given text type's minimum number of characters. * * @param member the field or method to check * @return specified minimum number of characters (or null) * @see Size * @see NotEmpty * @see NotBlank */ protected Integer resolveStringMinLength(MemberScope member) { if (member.getType().isInstanceOf(CharSequence.class)) { Size sizeAnnotation = this.getAnnotationFromFieldOrGetter(member, Size.class, Size::groups); if (sizeAnnotation != null && sizeAnnotation.min() > 0) { // minimum length greater than the default 0 was specified return sizeAnnotation.min(); } if (this.getAnnotationFromFieldOrGetter(member, NotEmpty.class, NotEmpty::groups) != null || this.getAnnotationFromFieldOrGetter(member, NotBlank.class, NotBlank::groups) != null) { return 1; } } return null; } /** * Determine a given text type's maximum number of characters. * * @param member the field or method to check * @return specified minimum number of characters (or null) * @see Size */ protected Integer resolveStringMaxLength(MemberScope member) { if (member.getType().isInstanceOf(CharSequence.class)) { Size sizeAnnotation = this.getAnnotationFromFieldOrGetter(member, Size.class, Size::groups); if (sizeAnnotation != null && sizeAnnotation.max() < 2147483647) { // maximum length below the default 2147483647 was specified return sizeAnnotation.max(); } } return null; } /** * Determine a given text type's format. * * @param member the field or method to check * @return specified format (or null) * @see Email */ protected String resolveStringFormat(MemberScope member) { if (member.getType().isInstanceOf(CharSequence.class)) { Email emailAnnotation = this.getAnnotationFromFieldOrGetter(member, Email.class, Email::groups); if (emailAnnotation != null) { // @Email annotation was found, indicate the respective format if (this.options.contains(JavaxValidationOption.PREFER_IDN_EMAIL_FORMAT)) { // the option was set to rather return the value for the internationalised email format return "idn-email"; } // indicate standard internet email address format return "email"; } } return null; } /** * Determine a given text type's pattern. * * @param member the field or method to check * @return specified pattern (or null) * @see Pattern */ protected String resolveStringPattern(MemberScope member) { if (member.getType().isInstanceOf(CharSequence.class)) { Pattern patternAnnotation = this.getAnnotationFromFieldOrGetter(member, Pattern.class, Pattern::groups); if (patternAnnotation != null) { // @Pattern annotation was found, return its (mandatory) regular expression return patternAnnotation.regexp(); } Email emailAnnotation = this.getAnnotationFromFieldOrGetter(member, Email.class, Email::groups); if (emailAnnotation != null && !".*".equals(emailAnnotation.regexp())) { // non-default regular expression on @Email annotation should also be considered return emailAnnotation.regexp(); } } return null; } /** * Determine a number type's minimum (inclusive) value. * * @param member the field or method to check * @return specified inclusive minimum value (or null) * @see Min * @see DecimalMin * @see PositiveOrZero */ protected BigDecimal resolveNumberInclusiveMinimum(MemberScope member) { Min minAnnotation = this.getAnnotationFromFieldOrGetter(member, Min.class, Min::groups); if (minAnnotation != null) { return new BigDecimal(minAnnotation.value()); } DecimalMin decimalMinAnnotation = this.getAnnotationFromFieldOrGetter(member, DecimalMin.class, DecimalMin::groups); if (decimalMinAnnotation != null && decimalMinAnnotation.inclusive()) { return new BigDecimal(decimalMinAnnotation.value()); } PositiveOrZero positiveAnnotation = this.getAnnotationFromFieldOrGetter(member, PositiveOrZero.class, PositiveOrZero::groups); if (positiveAnnotation != null) { return BigDecimal.ZERO; } return null; } /** * Determine a number type's minimum (exclusive) value. * * @param member the field or method to check * @return specified exclusive minimum value (or null) * @see DecimalMin * @see Positive */ protected BigDecimal resolveNumberExclusiveMinimum(MemberScope member) { DecimalMin decimalMinAnnotation = this.getAnnotationFromFieldOrGetter(member, DecimalMin.class, DecimalMin::groups); if (decimalMinAnnotation != null && !decimalMinAnnotation.inclusive()) { return new BigDecimal(decimalMinAnnotation.value()); } Positive positiveAnnotation = this.getAnnotationFromFieldOrGetter(member, Positive.class, Positive::groups); if (positiveAnnotation != null) { return BigDecimal.ZERO; } return null; } /** * Determine a number type's maximum (inclusive) value. * * @param member the field or method to check * @return specified inclusive maximum value (or null) * @see Max * @see DecimalMax#inclusive() * @see NegativeOrZero */ protected BigDecimal resolveNumberInclusiveMaximum(MemberScope member) { Max maxAnnotation = this.getAnnotationFromFieldOrGetter(member, Max.class, Max::groups); if (maxAnnotation != null) { return new BigDecimal(maxAnnotation.value()); } DecimalMax decimalMaxAnnotation = this.getAnnotationFromFieldOrGetter(member, DecimalMax.class, DecimalMax::groups); if (decimalMaxAnnotation != null && decimalMaxAnnotation.inclusive()) { return new BigDecimal(decimalMaxAnnotation.value()); } NegativeOrZero negativeAnnotation = this.getAnnotationFromFieldOrGetter(member, NegativeOrZero.class, NegativeOrZero::groups); if (negativeAnnotation != null) { return BigDecimal.ZERO; } return null; } /** * Determine a number type's maximum (exclusive) value. * * @param member the field or method to check * @return specified exclusive maximum value (or null) * @see DecimalMax#inclusive() * @see Negative */ protected BigDecimal resolveNumberExclusiveMaximum(MemberScope member) { DecimalMax decimalMaxAnnotation = this.getAnnotationFromFieldOrGetter(member, DecimalMax.class, DecimalMax::groups); if (decimalMaxAnnotation != null && !decimalMaxAnnotation.inclusive()) { return new BigDecimal(decimalMaxAnnotation.value()); } Negative negativeAnnotation = this.getAnnotationFromFieldOrGetter(member, Negative.class, Negative::groups); if (negativeAnnotation != null) { return BigDecimal.ZERO; } return null; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy