diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index 128abc400e..6d9180850c 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java @@ -18,6 +18,7 @@ package org.springframework.validation.beanvalidation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; @@ -73,6 +74,8 @@ public class MethodValidationAdapter { private static final Comparator RESULT_COMPARATOR = new ResultComparator(); + private static final MethodValidationResult EMPTY_RESULT = new EmptyMethodValidationResult(); + private final Supplier validator; @@ -186,15 +189,19 @@ public class MethodValidationAdapter { } /** - * Validate the given method arguments and raise {@link ConstraintViolation} - * in case of any errors. + * Validate the given method arguments and return the result of validation. * @param target the target Object * @param method the target method * @param arguments candidate arguments for a method invocation * @param groups groups for validation determined via * {@link #determineValidationGroups(Object, Method)} + * @return a result with {@link ConstraintViolation violations} and + * {@link ParameterValidationResult validationResults}, both possibly empty + * in case there are no violations */ - public void validateMethodArguments(Object target, Method method, Object[] arguments, Class[] groups) { + public MethodValidationResult validateMethodArguments( + Object target, Method method, Object[] arguments, Class[] groups) { + ExecutableValidator execVal = this.validator.get().forExecutables(); Set> result; try { @@ -207,28 +214,26 @@ public class MethodValidationAdapter { Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(mostSpecificMethod); result = execVal.validateParameters(target, bridgedMethod, arguments, groups); } - if (!result.isEmpty()) { - throw createException(target, method, result, i -> arguments[i]); - } + return (result.isEmpty() ? EMPTY_RESULT : createException(target, method, result, i -> arguments[i])); } /** - * Validate the given return value and raise {@link ConstraintViolation} - * in case of any errors. + * Validate the given return value and return the result of validation. * @param target the target Object * @param method the target method * @param returnValue value returned from invoking the target method * @param groups groups for validation determined via * {@link #determineValidationGroups(Object, Method)} + * @return a result with {@link ConstraintViolation violations} and + * {@link ParameterValidationResult validationResults}, both possibly empty + * in case there are no violations */ - public void validateMethodReturnValue( + public MethodValidationResult validateMethodReturnValue( Object target, Method method, @Nullable Object returnValue, Class[] groups) { ExecutableValidator execVal = this.validator.get().forExecutables(); Set> result = execVal.validateReturnValue(target, method, returnValue, groups); - if (!result.isEmpty()) { - throw createException(target, method, result, i -> returnValue); - } + return (result.isEmpty() ? EMPTY_RESULT : createException(target, method, result, i -> returnValue)); } private MethodValidationException createException( @@ -275,7 +280,7 @@ public class MethodValidationAdapter { cascadedViolations.forEach((node, builder) -> validatonResultList.add(builder.build())); validatonResultList.sort(RESULT_COMPARATOR); - return new MethodValidationException(target, method, validatonResultList, violations); + return new MethodValidationException(target, method, violations, validatonResultList); } /** @@ -470,4 +475,36 @@ public class MethodValidationAdapter { } } + + /** + * {@link MethodValidationResult} to use when there are no violations. + */ + private static final class EmptyMethodValidationResult implements MethodValidationResult { + + @Override + public Set> getConstraintViolations() { + return Collections.emptySet(); + } + + @Override + public List getAllValidationResults() { + return Collections.emptyList(); + } + + @Override + public List getValueResults() { + return Collections.emptyList(); + } + + @Override + public List getBeanResults() { + return Collections.emptyList(); + } + + @Override + public void throwIfViolationsPresent() { + } + + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java index 89b4460429..03729a9a66 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationException.java @@ -23,17 +23,15 @@ import java.util.Set; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; +import org.springframework.util.Assert; + /** - * Extension of {@link ConstraintViolationException} that exposes an additional - * list of {@link ParameterValidationResult} with violations adapted to + * Extension of {@link ConstraintViolationException} that implements + * {@link MethodValidationResult} exposing an additional list of + * {@link ParameterValidationResult} that represents violations adapted to * {@link org.springframework.context.MessageSourceResolvable} and grouped by * method parameter. * - *

For {@link jakarta.validation.Valid @Valid}-annotated, Object method - * parameters or return types with cascaded violations, the {@link ParameterErrors} - * subclass of {@link ParameterValidationResult} implements - * {@link org.springframework.validation.Errors} and exposes - * {@link org.springframework.validation.FieldError field errors}. * * @author Rossen Stoyanchev * @since 6.1 @@ -42,7 +40,7 @@ import jakarta.validation.ConstraintViolationException; * @see MethodValidationAdapter */ @SuppressWarnings("serial") -public class MethodValidationException extends ConstraintViolationException { +public class MethodValidationException extends ConstraintViolationException implements MethodValidationResult { private final Object target; @@ -52,11 +50,11 @@ public class MethodValidationException extends ConstraintViolationException { public MethodValidationException( - Object target, Method method, - List validationResults, - Set> violations) { + Object target, Method method, Set> violations, + List validationResults) { super(violations); + Assert.notEmpty(violations, "'violations' must not be empty"); this.target = target; this.method = method; this.allValidationResults = validationResults; @@ -77,37 +75,26 @@ public class MethodValidationException extends ConstraintViolationException { return this.method; } - /** - * Return all validation results. This includes method parameters with - * constraints declared on them, as well as - * {@link jakarta.validation.Valid @Valid} method parameters with - * cascaded constraints. - * @see #getValueResults() - * @see #getBeanResults() - */ + // re-declare parent class method for NonNull treatment of interface + + @Override + public Set> getConstraintViolations() { + return super.getConstraintViolations(); + } + + @Override public List getAllValidationResults() { return this.allValidationResults; } - /** - * Return only validation results for method parameters with constraints - * declared directly on them. This excludes - * {@link jakarta.validation.Valid @Valid} method parameters with cascaded - * constraints. - * @see #getAllValidationResults() - */ + @Override public List getValueResults() { return this.allValidationResults.stream() .filter(result -> !(result instanceof ParameterErrors)) .toList(); } - /** - * Return only validation results for {@link jakarta.validation.Valid @Valid} - * method parameters with cascaded constraints. This excludes method - * parameters with constraints declared directly on them. - * @see #getAllValidationResults() - */ + @Override public List getBeanResults() { return this.allValidationResults.stream() .filter(result -> result instanceof ParameterErrors) @@ -115,4 +102,9 @@ public class MethodValidationException extends ConstraintViolationException { .toList(); } + @Override + public void throwIfViolationsPresent() { + throw this; + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java index 3a07fed041..e37b688230 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java @@ -103,11 +103,15 @@ public class MethodValidationInterceptor implements MethodInterceptor { Object target = getTarget(invocation); Method method = invocation.getMethod(); Class[] groups = determineValidationGroups(invocation); - this.delegate.validateMethodArguments(target, method, invocation.getArguments(), groups); + + this.delegate.validateMethodArguments(target, method, invocation.getArguments(), groups) + .throwIfViolationsPresent(); Object returnValue = invocation.proceed(); - this.delegate.validateMethodReturnValue(target, method, returnValue, groups); + this.delegate.validateMethodReturnValue(target, method, returnValue, groups) + .throwIfViolationsPresent(); + return returnValue; } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationResult.java new file mode 100644 index 0000000000..f197417ca9 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationResult.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2023 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.beanvalidation; + +import java.util.List; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; + +/** + * Container for method validation results where underlying + * {@link ConstraintViolation violations} have been adapted to + * {@link ParameterValidationResult} each containing a list of + * {@link org.springframework.context.MessageSourceResolvable} grouped by method + * parameter. + * + *

For {@link jakarta.validation.Valid @Valid}-annotated, Object method + * parameters or return types with cascaded violations, the {@link ParameterErrors} + * subclass of {@link ParameterValidationResult} implements + * {@link org.springframework.validation.Errors} and exposes + * {@link org.springframework.validation.FieldError field errors}. + * + * @author Rossen Stoyanchev + * @since 6.1 + */ +public interface MethodValidationResult { + + /** + * Returns the set of constraint violations reported during a validation. + * @return the {@code Set} of {@link ConstraintViolation}s, or an empty Set + */ + Set> getConstraintViolations(); + + /** + * Return all validation results. This includes method parameters with + * constraints declared on them, as well as + * {@link jakarta.validation.Valid @Valid} method parameters with + * cascaded constraints. + * @see #getValueResults() + * @see #getBeanResults() + */ + List getAllValidationResults(); + + /** + * Return only validation results for method parameters with constraints + * declared directly on them. This excludes + * {@link jakarta.validation.Valid @Valid} method parameters with cascaded + * constraints. + * @see #getAllValidationResults() + */ + List getValueResults(); + + /** + * Return only validation results for {@link jakarta.validation.Valid @Valid} + * method parameters with cascaded constraints. This excludes method + * parameters with constraints declared directly on them. + * @see #getAllValidationResults() + */ + List getBeanResults(); + + /** + * Check if {@link #getConstraintViolations()} is empty, and if not, raise + * {@link MethodValidationException}. + * @throws MethodValidationException if the result contains any violations + */ + void throwIfViolationsPresent(); + +} diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java index cd0f3512bb..df10ce558a 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java @@ -32,7 +32,6 @@ import org.springframework.util.ClassUtils; import org.springframework.validation.FieldError; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** * Unit tests for {@link MethodValidationAdapter}. @@ -40,6 +39,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; */ public class MethodValidationAdapterTests { + private static final MethodValidationAdapter validationAdapter = new MethodValidationAdapter(); + private static final Person faustino1234 = new Person("Faustino1234"); private static final Person cayetana6789 = new Person("Cayetana6789"); @@ -154,23 +155,17 @@ public class MethodValidationAdapterTests { } private void validateArguments( - Object target, Method method, Object[] arguments, Consumer assertions) { + Object target, Method method, Object[] arguments, Consumer assertions) { - MethodValidationAdapter adapter = new MethodValidationAdapter(); - - assertThatExceptionOfType(MethodValidationException.class) - .isThrownBy(() -> adapter.validateMethodArguments(target, method, arguments, new Class[0])) - .satisfies(assertions); + assertions.accept( + validationAdapter.validateMethodArguments(target, method, arguments, new Class[0])); } private void validateReturnValue( - Object target, Method method, @Nullable Object returnValue, Consumer assertions) { + Object target, Method method, @Nullable Object returnValue, Consumer assertions) { - MethodValidationAdapter adapter = new MethodValidationAdapter(); - - assertThatExceptionOfType(MethodValidationException.class) - .isThrownBy(() -> adapter.validateMethodReturnValue(target, method, returnValue, new Class[0])) - .satisfies(assertions); + assertions.accept( + validationAdapter.validateMethodReturnValue(target, method, returnValue, new Class[0])); } private static void assertBeanResult( @@ -225,12 +220,6 @@ public class MethodValidationAdapterTests { @SuppressWarnings("unused") private record Person(@Size(min = 1, max = 10) String name) { - - @Override - public String name() { - return this.name; - } - } }