Skip to content

Commit

Permalink
Add MethodValidationResult
Browse files Browse the repository at this point in the history
  • Loading branch information
rstoyanchev committed Jun 12, 2023
1 parent 8b53fec commit cc8361c
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

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;
Expand Down Expand Up @@ -73,6 +74,8 @@ public class MethodValidationAdapter {

private static final Comparator<ParameterValidationResult> RESULT_COMPARATOR = new ResultComparator();

private static final MethodValidationResult EMPTY_RESULT = new EmptyMethodValidationResult();


private final Supplier<Validator> validator;

Expand Down Expand Up @@ -186,15 +189,19 @@ public static Class<?>[] determineValidationGroups(Object target, Method method)
}

/**
* 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<ConstraintViolation<Object>> result;
try {
Expand All @@ -207,28 +214,26 @@ public void validateMethodArguments(Object target, Method method, Object[] argum
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<ConstraintViolation<Object>> 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(
Expand Down Expand Up @@ -275,7 +280,7 @@ else if (node.getKind().equals(ElementKind.RETURN_VALUE)) {
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);
}

/**
Expand Down Expand Up @@ -470,4 +475,36 @@ private <E> int compareKeys(ParameterErrors errors1, ParameterErrors errors2) {
}
}


/**
* {@link MethodValidationResult} to use when there are no violations.
*/
private static final class EmptyMethodValidationResult implements MethodValidationResult {

@Override
public Set<ConstraintViolation<?>> getConstraintViolations() {
return Collections.emptySet();
}

@Override
public List<ParameterValidationResult> getAllValidationResults() {
return Collections.emptyList();
}

@Override
public List<ParameterValidationResult> getValueResults() {
return Collections.emptyList();
}

@Override
public List<ParameterErrors> getBeanResults() {
return Collections.emptyList();
}

@Override
public void throwIfViolationsPresent() {
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,15 @@
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.
*
* <p>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
Expand All @@ -42,7 +40,7 @@
* @see MethodValidationAdapter
*/
@SuppressWarnings("serial")
public class MethodValidationException extends ConstraintViolationException {
public class MethodValidationException extends ConstraintViolationException implements MethodValidationResult {

private final Object target;

Expand All @@ -52,11 +50,11 @@ public class MethodValidationException extends ConstraintViolationException {


public MethodValidationException(
Object target, Method method,
List<ParameterValidationResult> validationResults,
Set<? extends ConstraintViolation<?>> violations) {
Object target, Method method, Set<? extends ConstraintViolation<?>> violations,
List<ParameterValidationResult> validationResults) {

super(violations);
Assert.notEmpty(violations, "'violations' must not be empty");
this.target = target;
this.method = method;
this.allValidationResults = validationResults;
Expand All @@ -77,42 +75,36 @@ public Method getMethod() {
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<ConstraintViolation<?>> getConstraintViolations() {
return super.getConstraintViolations();
}

@Override
public List<ParameterValidationResult> 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<ParameterValidationResult> 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<ParameterErrors> getBeanResults() {
return this.allValidationResults.stream()
.filter(result -> result instanceof ParameterErrors)
.map(result -> (ParameterErrors) result)
.toList();
}

@Override
public void throwIfViolationsPresent() {
throw this;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,15 @@ public Object invoke(MethodInvocation invocation) throws Throwable {
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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<ConstraintViolation<?>> 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<ParameterValidationResult> 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<ParameterValidationResult> 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<ParameterErrors> getBeanResults();

/**
* Check if {@link #getConstraintViolations()} is empty, and if not, raise
* {@link MethodValidationException}.
* @throws MethodValidationException if the result contains any violations
*/
void throwIfViolationsPresent();

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@
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}.
* @author Rossen Stoyanchev
*/
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");
Expand Down Expand Up @@ -154,23 +155,17 @@ void validateListArgument() {
}

private void validateArguments(
Object target, Method method, Object[] arguments, Consumer<MethodValidationException> assertions) {

MethodValidationAdapter adapter = new MethodValidationAdapter();
Object target, Method method, Object[] arguments, Consumer<MethodValidationResult> assertions) {

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<MethodValidationException> assertions) {

MethodValidationAdapter adapter = new MethodValidationAdapter();
Object target, Method method, @Nullable Object returnValue, Consumer<MethodValidationResult> assertions) {

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(
Expand Down Expand Up @@ -225,12 +220,6 @@ public void addPeople(@Valid List<Person> people) {

@SuppressWarnings("unused")
private record Person(@Size(min = 1, max = 10) String name) {

@Override
public String name() {
return this.name;
}

}

}

0 comments on commit cc8361c

Please sign in to comment.