Browse Source

Consistently find @⁠Validated as a meta-annotation at arbitrary depths

Prior to this commit, ValidationAnnotationUtils looked up @⁠Validated
as a meta-annotation at arbitrary depths (e.g., when used as a
meta-meta-annotation) in determineValidationGroups() but only as a
"directly present" meta-annotation in determineValidationHints().

For consistency, this commit revises determineValidationHints() so that
it finds @⁠Validated as a meta-annotation at arbitrary depths as well.

Closes gh-36274
pull/36325/head
Sam Brannen 1 month ago
parent
commit
2d3a2c5cbd
  1. 7
      spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java
  2. 196
      spring-context/src/test/java/org/springframework/validation/annotation/ValidationAnnotationUtilsTests.java

7
spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java

@ -27,7 +27,8 @@ import org.springframework.core.annotation.AnnotationUtils; @@ -27,7 +27,8 @@ import org.springframework.core.annotation.AnnotationUtils;
/**
* Utility class for handling validation annotations.
* Mainly for internal use within the framework.
*
* <p>Mainly for internal use within the framework.
*
* @author Christoph Dreis
* @author Juergen Hoeller
@ -41,7 +42,7 @@ public abstract class ValidationAnnotationUtils { @@ -41,7 +42,7 @@ public abstract class ValidationAnnotationUtils {
/**
* Determine any validation hints by the given annotation.
* Determine any validation hints for the given annotation.
* <p>This implementation checks for Spring's
* {@link org.springframework.validation.annotation.Validated},
* {@code @jakarta.validation.Valid}, and custom annotations whose
@ -62,7 +63,7 @@ public abstract class ValidationAnnotationUtils { @@ -62,7 +63,7 @@ public abstract class ValidationAnnotationUtils {
return EMPTY_OBJECT_ARRAY;
}
// Meta presence of @Validated ?
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
Validated validatedAnn = AnnotationUtils.findAnnotation(annotationType, Validated.class);
if (validatedAnn != null) {
return validatedAnn.value();
}

196
spring-context/src/test/java/org/springframework/validation/annotation/ValidationAnnotationUtilsTests.java

@ -0,0 +1,196 @@ @@ -0,0 +1,196 @@
/*
* Copyright 2002-present the original author or authors.
*
* 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
*
* https://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 org.springframework.validation.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import jakarta.validation.Valid;
import jakarta.validation.groups.Default;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.RegisterExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link ValidationAnnotationUtils}.
*
* @author Sam Brannen
* @since 7.0.4
*/
class ValidationAnnotationUtilsTests {
@Nested
class DetermineValidationHintsTests {
Method method;
@RegisterExtension
BeforeTestExecutionCallback extension = context -> this.method = context.getRequiredTestMethod();
@Test
void nonValidatedMethod(TestInfo testInfo) {
var annotation = this.method.getAnnotation(Test.class);
var hints = ValidationAnnotationUtils.determineValidationHints(annotation);
assertThat(hints).isNull();
}
@Test
@Validated({ GroupA.class, Default.class })
void springValidated(TestInfo testInfo) {
var annotation = this.method.getAnnotation(Validated.class);
var hints = ValidationAnnotationUtils.determineValidationHints(annotation);
assertThat(hints).containsExactly(GroupA.class, Default.class);
}
@Test
@MetaValidated
void springMetaValidated(TestInfo testInfo) {
var annotation = this.method.getAnnotation(MetaValidated.class);
var hints = ValidationAnnotationUtils.determineValidationHints(annotation);
assertThat(hints).containsExactly(GroupB.class, Default.class);
}
@Test // gh-36274
@MetaMetaValidated
void springMetaMetaValidated(TestInfo testInfo) {
var annotation = this.method.getAnnotation(MetaMetaValidated.class);
var hints = ValidationAnnotationUtils.determineValidationHints(annotation);
assertThat(hints).containsExactly(GroupB.class, Default.class);
}
@Test
@Valid
void jakartaValid(TestInfo testInfo) {
var annotation = this.method.getAnnotation(Valid.class);
var hints = ValidationAnnotationUtils.determineValidationHints(annotation);
assertThat(hints).isEmpty();
}
@Test
@ValidPlain
void plainCustomValidAnnotation(TestInfo testInfo) {
var annotation = this.method.getAnnotation(ValidPlain.class);
var hints = ValidationAnnotationUtils.determineValidationHints(annotation);
assertThat(hints).isEmpty();
}
@Test
@ValidParameterized
void parameterizedCustomValidAnnotationWithEmptyGroups(TestInfo testInfo) {
var annotation = this.method.getAnnotation(ValidParameterized.class);
var hints = ValidationAnnotationUtils.determineValidationHints(annotation);
assertThat(hints).isEmpty();
}
@Test
@ValidParameterized({ GroupA.class, GroupB.class })
void parameterizedCustomValidAnnotationWithNonEmptyGroups(TestInfo testInfo) {
var annotation = this.method.getAnnotation(ValidParameterized.class);
var hints = ValidationAnnotationUtils.determineValidationHints(annotation);
assertThat(hints).containsExactly(GroupA.class, GroupB.class);
}
@Retention(RetentionPolicy.RUNTIME)
@interface ValidPlain {
}
@Retention(RetentionPolicy.RUNTIME)
@interface ValidParameterized {
Class<?>[] value() default {};
}
}
@Nested
class DetermineValidationGroupsTests {
Method method;
@RegisterExtension
BeforeTestExecutionCallback extension = context -> this.method = context.getRequiredTestMethod();
@Test
void nonValidatedMethod(TestInfo testInfo) {
var hints = ValidationAnnotationUtils.determineValidationGroups(this, this.method);
assertThat(hints).isEmpty();
}
@Test
@Validated({ GroupA.class, Default.class })
void springValidated(TestInfo testInfo) {
var hints = ValidationAnnotationUtils.determineValidationGroups(this, this.method);
assertThat(hints).containsExactly(GroupA.class, Default.class);
}
@Test
@MetaValidated
void springMetaValidated(TestInfo testInfo) {
var hints = ValidationAnnotationUtils.determineValidationGroups(this, this.method);
assertThat(hints).containsExactly(GroupB.class, Default.class);
}
@Test
@MetaMetaValidated
void springMetaMetaValidated(TestInfo testInfo) {
var hints = ValidationAnnotationUtils.determineValidationGroups(this, this.method);
assertThat(hints).containsExactly(GroupB.class, Default.class);
}
@Test
@Valid
void jakartaValid(TestInfo testInfo) {
var hints = ValidationAnnotationUtils.determineValidationGroups(this, this.method);
assertThat(hints).isEmpty();
}
}
interface GroupA {
}
interface GroupB {
}
@Validated({ GroupB.class, Default.class })
@Retention(RetentionPolicy.RUNTIME)
@interface MetaValidated {
}
@MetaValidated
@Retention(RetentionPolicy.RUNTIME)
@interface MetaMetaValidated {
}
}
Loading…
Cancel
Save