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 @@ -2,34 +2,32 @@

import io.swagger.v3.oas.models.media.Schema;

import javax.validation.OverridesAttribute;
import javax.validation.constraints.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.*;

import static io.swagger.v3.core.util.SchemaTypeUtils.*;

public class ValidationAnnotationsUtils {

public static final String JAVAX_NOT_NULL = "javax.validation.constraints.NotNull";
public static final String JAVAX_NOT_EMPTY = "javax.validation.constraints.NotEmpty";
public static final String JAVAX_NOT_BLANK = "javax.validation.constraints.NotBlank";
public static final String JAVAX_MIN = "javax.validation.constraints.Min";
public static final String JAVAX_MAX = "javax.validation.constraints.Max";
public static final String JAVAX_SIZE = "javax.validation.constraints.Size";
public static final String JAVAX_DECIMAL_MIN = "javax.validation.constraints.DecimalMin";
public static final String JAVAX_DECIMAL_MAX = "javax.validation.constraints.DecimalMax";
public static final String JAVAX_PATTERN = "javax.validation.constraints.Pattern";
public static final String JAVAX_EMAIL = "javax.validation.constraints.Email";
public static final String JAVAX_POSITIVE = "javax.validation.constraints.Positive";
public static final String JAVAX_POSITIVE_OR_ZERO = "javax.validation.constraints.PositiveOrZero";
public static final String JAVAX_NEGATIVE = "javax.validation.constraints.Negative";
public static final String JAVAX_NEGATIVE_OR_ZERO = "javax.validation.constraints.NegativeOrZero";
private static final String JAVAX_PACKAGE_BASE = "javax.validation.constraints";
public static final String JAVAX_NOT_NULL = JAVAX_PACKAGE_BASE + ".NotNull";
public static final String JAVAX_NOT_EMPTY = JAVAX_PACKAGE_BASE + ".NotEmpty";
public static final String JAVAX_NOT_BLANK = JAVAX_PACKAGE_BASE + ".NotBlank";
public static final String JAVAX_MIN = JAVAX_PACKAGE_BASE + ".Min";
public static final String JAVAX_MAX = JAVAX_PACKAGE_BASE + ".Max";
public static final String JAVAX_SIZE = JAVAX_PACKAGE_BASE + ".Size";
public static final String JAVAX_DECIMAL_MIN = JAVAX_PACKAGE_BASE + ".DecimalMin";
public static final String JAVAX_DECIMAL_MAX = JAVAX_PACKAGE_BASE + ".DecimalMax";
public static final String JAVAX_PATTERN = JAVAX_PACKAGE_BASE + ".Pattern";
public static final String JAVAX_EMAIL = JAVAX_PACKAGE_BASE + ".Email";
public static final String JAVAX_POSITIVE = JAVAX_PACKAGE_BASE + ".Positive";
public static final String JAVAX_POSITIVE_OR_ZERO = JAVAX_PACKAGE_BASE + ".PositiveOrZero";
public static final String JAVAX_NEGATIVE = JAVAX_PACKAGE_BASE + ".Negative";
public static final String JAVAX_NEGATIVE_OR_ZERO = JAVAX_PACKAGE_BASE + ".NegativeOrZero";

private static final String SCHEMA_EMAIL_FORMAT_NAME = "email";

Expand Down Expand Up @@ -249,13 +247,16 @@ public static Annotation[] expandValidationMetaAnnotations(Annotation[] annotati
if (a != null) queue.add(a);
}
while (!queue.isEmpty()) {
Annotation a = queue.poll();
if (!visited.add(a.annotationType())) continue;
for (Annotation meta : a.annotationType().getAnnotations()) {
Annotation annotation = queue.poll();
if (!visited.add(annotation.annotationType())) continue;
List<Class<? extends Annotation>> annotationsThatRelyOnOverride = findOverrides(annotation);
for (Annotation meta : annotation.annotationType().getAnnotations()) {
if (meta == null) continue;
String name = meta.annotationType().getName();
if (name.startsWith("javax.validation.constraints")) {
merged.putIfAbsent(name, meta);
if (name.startsWith(JAVAX_PACKAGE_BASE)) {
if (!annotationsThatRelyOnOverride.contains(meta.annotationType())) {
merged.putIfAbsent(name, meta);
}
} else {
queue.add(meta);
}
Expand All @@ -278,4 +279,25 @@ public static boolean applyNegativeOrZeroConstraint(Schema schema) {
return false;
}

/**
*
* @param annotation the composed constraint annotation
* @return the composing annotations that are overridden with {@link OverridesAttribute}
*/
private static List<Class<? extends Annotation>> findOverrides(Annotation annotation) {
List<Class<? extends Annotation>> overriddenConstraintAnnotations = new ArrayList<>();

Class<? extends Annotation> type = annotation.annotationType();

for (Method method : type.getDeclaredMethods()) {
OverridesAttribute oa = method.getAnnotation(OverridesAttribute.class);

if (oa != null) {
overriddenConstraintAnnotations.add(oa.constraint());
}
}

return overriddenConstraintAnnotations;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@
import java.lang.annotation.Target;
import java.util.Map;

import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.*;

public class ComposedConstraintMetaAnnotationTest {

Expand Down Expand Up @@ -56,11 +55,11 @@ public class ComposedConstraintMetaAnnotationTest {
}

/**
* Mimics how Hibernate Validator's @Range works: meta-annotations @Min/@Max carry default
* Mimics how Hibernate Validator's {@code @Range} works: meta-annotations @Min/@Max carry default
* values, while the actual per-use values are meant to be applied via @OverridesAttribute.
* Our implementation reads meta-annotations from the annotation *type definition*, so it
* always sees the defaults (min=0, max=Long.MAX_VALUE) — not whatever the caller passes
* as @ValidRange(min=5, max=50). This is a known limitation documented by the test below.
* as {@code @ValidRange(min=5, max=50)}. This is a known limitation documented by the test below.
*/
@Min(0)
@Max(Long.MAX_VALUE)
Expand All @@ -79,6 +78,20 @@ public class ComposedConstraintMetaAnnotationTest {
Class<? extends Payload>[] payload() default {};
}

@Min(4)
@Max(Long.MAX_VALUE)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {})
public @interface FourOrMore {
@OverridesAttribute(constraint = Max.class, name = "value")
long max() default Long.MAX_VALUE;

String message() default "Out of range";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

@ValidStoreId
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
Expand All @@ -105,9 +118,6 @@ static class TestStoreDto {
@ValidEmail
private String email;

@ValidRange(min = 5, max = 50)
private Short rangeField;

@ValidStoreIdNested
private Short nestedStoreId;

Expand All @@ -123,14 +133,27 @@ static class TestStoreDto {
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public Short getRangeField() { return rangeField; }
public void setRangeField(Short rangeField) { this.rangeField = rangeField; }
public Short getNestedStoreId() { return nestedStoreId; }
public void setNestedStoreId(Short nestedStoreId) { this.nestedStoreId = nestedStoreId; }
public Short getPriorityStoreId() { return priorityStoreId; }
public void setPriorityStoreId(Short priorityStoreId) { this.priorityStoreId = priorityStoreId; }
}

static class ComposedAnnotationsDto {
@ValidRange(min = 5, max = 50)
private Short rangeField;

@FourOrMore(max = 10)
private Short partiallyOverriddenComposedField;

public Short getRangeField() { return rangeField; }
public void setRangeField(Short rangeField) { this.rangeField = rangeField; }
public Short getPartiallyOverriddenComposedField() { return partiallyOverriddenComposedField; }
public void setPartiallyOverriddenComposedField(Short partiallyOverriddenComposedField) {
this.partiallyOverriddenComposedField = partiallyOverriddenComposedField;
}
}

@Test
public void readsComposedMinMaxConstraintOnDtoField() {
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
Expand Down Expand Up @@ -192,23 +215,36 @@ public void directAnnotationTakesPriorityOverMetaAnnotation() {
}

/**
* Documents a known limitation: for @Range-style constraints that rely on @OverridesAttribute
* Documents a known limitation: for @Range-style constraints that rely on {@link OverridesAttribute}
* to propagate per-use values (e.g. @ValidRange(min=5, max=50)) into their meta-annotations
* (@Min/@Max), our implementation reads constraints from the annotation *type definition*
* and therefore always sees the default values (min=0, max=Long.MAX_VALUE), not the
* caller-supplied ones. Handling @OverridesAttribute is not yet supported.
* caller-supplied ones. Handling {@link OverridesAttribute} is not yet supported, and instead the annotation and
* its composing/meta annotations are ignored entirely.
*/
@Test
public void rangeStyleConstraintUsesDefaultsNotOverriddenValues() {
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(TestStoreDto.class);
Schema model = schemas.get("TestStoreDto");
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(ComposedAnnotationsDto.class);
Schema model = schemas.get("ComposedAnnotationsDto");
Schema range = (Schema) model.getProperties().get("rangeField");
assertNotNull(range, "rangeField property should exist");
// We pick up the *default* values from @Min(0) and @Max(Long.MAX_VALUE) on the type
// definition of @ValidRange, NOT the caller-supplied @ValidRange(min=5, max=50).
assertEquals(range.getMinimum().longValue(), 0L,
"expected default @Min(0) from type definition, not overridden min=5");
assertEquals(range.getMaximum().longValue(), Long.MAX_VALUE,
"expected default @Max(Long.MAX_VALUE) from type definition, not overridden max=50");
// definition of @ValidRange, But we then drop them since we see that they are modified with an OverridesAttribute.
assertNull(range.getMinimum(),
"expected null since we drop the @Min from the overridden composed @ValidRange annotation");
assertNull(range.getMaximum(),
"expected null since we drop the @Max from the overridden composed @ValidRange annotation");
}

@Test
public void composedStyleConstraintUsesOnlyNonOverrideableValues() {
Map<String, Schema> schemas = ModelConverters.getInstance().readAll(ComposedAnnotationsDto.class);
Schema model = schemas.get("ComposedAnnotationsDto");
Schema range = (Schema) model.getProperties().get("partiallyOverriddenComposedField");
assertNotNull(range, "partiallyOverriddenComposedField property should exist");
assertEquals(range.getMinimum().longValue(), 4L,
"expected 4 from type definition since it does not have an OverridesAttribute");
assertNull(range.getMaximum(),
"expected null since we drop the @Max from the overridden composed @FourOrMore annotation");
}
}