Consistent processing of binding/validation failures for data classes

Includes an extension of SmartValidator for candidate value validation, as well as nullability refinements in Validator and BindingResult.

Issue: SPR-16840
Issue: SPR-16841
Issue: SPR-16854
This commit is contained in:
Juergen Hoeller
2018-07-17 17:01:34 +02:00
parent d8a2927dd3
commit 955665b419
12 changed files with 341 additions and 99 deletions

View File

@@ -222,7 +222,7 @@ public abstract class AbstractBindingResult extends AbstractErrors implements Bi
if (fieldError != null) {
Object value = fieldError.getRejectedValue();
// Do not apply formatting on binding failures like type mismatches.
return (fieldError.isBindingFailure() ? value : formatFieldValue(field, value));
return (fieldError.isBindingFailure() || getTarget() == null ? value : formatFieldValue(field, value));
}
else if (getTarget() != null) {
Object value = getActualFieldValue(fixedField(field));
@@ -321,9 +321,8 @@ public abstract class AbstractBindingResult extends AbstractErrors implements Bi
@Override
public String[] resolveMessageCodes(String errorCode, @Nullable String field) {
Class<?> fieldType = (getTarget() != null ? getFieldType(field) : null);
return getMessageCodesResolver().resolveMessageCodes(
errorCode, getObjectName(), fixedField(field), fieldType);
errorCode, getObjectName(), fixedField(field), getFieldType(field));
}
@Override
@@ -332,7 +331,7 @@ public abstract class AbstractBindingResult extends AbstractErrors implements Bi
}
@Override
public void recordFieldValue(String field, Class<?> type, Object value) {
public void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
this.fieldTypes.put(field, type);
this.fieldValues.put(field, value);
}

View File

@@ -275,7 +275,7 @@ public class BindException extends Exception implements BindingResult {
}
@Override
public void recordFieldValue(String field, Class<?> type, Object value) {
public void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
this.bindingResult.recordFieldValue(field, type, value);
}

View File

@@ -143,7 +143,7 @@ public interface BindingResult extends Errors {
* @param value the original value
* @since 5.0.4
*/
default void recordFieldValue(String field, Class<?> type, Object value) {
default void recordFieldValue(String field, Class<?> type, @Nullable Object value) {
}
/**

View File

@@ -853,8 +853,12 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* @see #getBindingResult()
*/
public void validate() {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// Call each validator with the same binding result
for (Validator validator : this.validators) {
validator.validate(getTarget(), getBindingResult());
validator.validate(target, bindingResult);
}
}
@@ -862,16 +866,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* Invoke the specified Validators, if any, with the given validation hints.
* <p>Note: Validation hints may get ignored by the actual target Validator.
* @param validationHints one or more hint objects to be passed to a {@link SmartValidator}
* @since 3.1
* @see #setValidator(Validator)
* @see SmartValidator#validate(Object, Errors, Object...)
*/
public void validate(Object... validationHints) {
Object target = getTarget();
Assert.state(target != null, "No target to validate");
BindingResult bindingResult = getBindingResult();
// Call each validator with the same binding result
for (Validator validator : getValidators()) {
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(getTarget(), getBindingResult(), validationHints);
((SmartValidator) validator).validate(target, bindingResult, validationHints);
}
else if (validator != null) {
validator.validate(getTarget(), getBindingResult());
validator.validate(target, bindingResult);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2018 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.
@@ -39,11 +39,29 @@ public interface SmartValidator extends Validator {
* <p>Note: Validation hints may get ignored by the actual target {@code Validator},
* in which case this method should behave just like its regular
* {@link #validate(Object, Errors)} sibling.
* @param target the object that is to be validated (can be {@code null})
* @param errors contextual state about the validation process (never {@code null})
* @param target the object that is to be validated
* @param errors contextual state about the validation process
* @param validationHints one or more hint objects to be passed to the validation engine
* @see ValidationUtils
* @see javax.validation.Validator#validate(Object, Class[])
*/
void validate(@Nullable Object target, Errors errors, Object... validationHints);
void validate(Object target, Errors errors, Object... validationHints);
/**
* Validate the supplied value for the specified field on the target type,
* reporting the same validation errors as if the value would be bound to
* the field on an instance of the target class.
* @param targetType the target type
* @param fieldName the name of the field
* @param value the candidate value
* @param errors contextual state about the validation process
* @param validationHints one or more hint objects to be passed to the validation engine
* @since 5.1
* @see javax.validation.Validator#validateValue(Class, String, Object, Class[])
*/
default void validateValue(
Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
throw new IllegalArgumentException("Cannot validate individual value for " + targetType);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 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.
@@ -45,30 +45,30 @@ public abstract class ValidationUtils {
/**
* Invoke the given {@link Validator} for the supplied object and
* {@link Errors} instance.
* @param validator the {@code Validator} to be invoked (must not be {@code null})
* @param obj the object to bind the parameters to
* @param errors the {@link Errors} instance that should store the errors (must not be {@code null})
* @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors} arguments is
* {@code null}, or if the supplied {@code Validator} does not {@link Validator#supports(Class) support}
* the validation of the supplied object's type
* @param validator the {@code Validator} to be invoked
* @param target the object to bind the parameters to
* @param errors the {@link Errors} instance that should store the errors
* @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors}
* arguments is {@code null}, or if the supplied {@code Validator} does not
* {@link Validator#supports(Class) support} the validation of the supplied object's type
*/
public static void invokeValidator(Validator validator, Object obj, Errors errors) {
invokeValidator(validator, obj, errors, (Object[]) null);
public static void invokeValidator(Validator validator, Object target, Errors errors) {
invokeValidator(validator, target, errors, (Object[]) null);
}
/**
* Invoke the given {@link Validator}/{@link SmartValidator} for the supplied object and
* {@link Errors} instance.
* @param validator the {@code Validator} to be invoked (must not be {@code null})
* @param obj the object to bind the parameters to
* @param errors the {@link Errors} instance that should store the errors (must not be {@code null})
* @param validator the {@code Validator} to be invoked
* @param target the object to bind the parameters to
* @param errors the {@link Errors} instance that should store the errors
* @param validationHints one or more hint objects to be passed to the validation engine
* @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors} arguments is
* {@code null}, or if the supplied {@code Validator} does not {@link Validator#supports(Class) support}
* the validation of the supplied object's type
* @throws IllegalArgumentException if either of the {@code Validator} or {@code Errors}
* arguments is {@code null}, or if the supplied {@code Validator} does not
* {@link Validator#supports(Class) support} the validation of the supplied object's type
*/
public static void invokeValidator(
Validator validator, @Nullable Object obj, Errors errors, @Nullable Object... validationHints) {
Validator validator, Object target, Errors errors, @Nullable Object... validationHints) {
Assert.notNull(validator, "Validator must not be null");
Assert.notNull(errors, "Errors object must not be null");
@@ -76,16 +76,16 @@ public abstract class ValidationUtils {
if (logger.isDebugEnabled()) {
logger.debug("Invoking validator [" + validator + "]");
}
if (obj != null && !validator.supports(obj.getClass())) {
if (!validator.supports(target.getClass())) {
throw new IllegalArgumentException(
"Validator [" + validator.getClass() + "] does not support [" + obj.getClass() + "]");
"Validator [" + validator.getClass() + "] does not support [" + target.getClass() + "]");
}
if (!ObjectUtils.isEmpty(validationHints) && validator instanceof SmartValidator) {
((SmartValidator) validator).validate(obj, errors, validationHints);
((SmartValidator) validator).validate(target, errors, validationHints);
}
else {
validator.validate(obj, errors);
validator.validate(target, errors);
}
if (logger.isDebugEnabled()) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2018 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.
@@ -16,8 +16,6 @@
package org.springframework.validation;
import org.springframework.lang.Nullable;
/**
* A validator for application-specific objects.
*
@@ -61,6 +59,7 @@ import org.springframework.lang.Nullable;
* application.
*
* @author Rod Johnson
* @see SmartValidator
* @see Errors
* @see ValidationUtils
*/
@@ -87,10 +86,10 @@ public interface Validator {
* typically has (or would) return {@code true}.
* <p>The supplied {@link Errors errors} instance can be used to report
* any resulting validation errors.
* @param target the object that is to be validated (can be {@code null})
* @param errors contextual state about the validation process (never {@code null})
* @param target the object that is to be validated
* @param errors contextual state about the validation process
* @see ValidationUtils
*/
void validate(@Nullable Object target, Errors errors);
void validate(Object target, Errors errors);
}

View File

@@ -50,13 +50,17 @@ import org.springframework.validation.SmartValidator;
* while also exposing the original JSR-303 Validator interface itself.
*
* <p>Can be used as a programmatic wrapper. Also serves as base class for
* {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean}.
* {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean},
* and as the primary implementation of the {@link SmartValidator} interface.
*
* <p>As of Spring Framework 5.0, this adapter is fully compatible with
* Bean Validation 1.1 as well as 2.0.
*
* @author Juergen Hoeller
* @since 3.0
* @see SmartValidator
* @see CustomValidatorBean
* @see LocalValidatorFactoryBean
*/
public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {
@@ -99,28 +103,45 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation.
}
@Override
public void validate(@Nullable Object target, Errors errors) {
public void validate(Object target, Errors errors) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validate(target), errors);
}
}
@Override
public void validate(@Nullable Object target, Errors errors, @Nullable Object... validationHints) {
public void validate(Object target, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
Set<Class<?>> groups = new LinkedHashSet<>();
if (validationHints != null) {
for (Object hint : validationHints) {
if (hint instanceof Class) {
groups.add((Class<?>) hint);
}
}
}
processConstraintViolations(
this.targetValidator.validate(target, ClassUtils.toClassArray(groups)), errors);
this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
}
}
@SuppressWarnings("unchecked")
@Override
public void validateValue(
Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
if (this.targetValidator != null) {
processConstraintViolations(this.targetValidator.validateValue(
(Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors);
}
}
/**
* Turn the specified validation hints into JSR-303 validation groups.
* @since 5.1
*/
private Class<?>[] asValidationGroups(Object... validationHints) {
Set<Class<?>> groups = new LinkedHashSet<>(4);
for (Object hint : validationHints) {
if (hint instanceof Class) {
groups.add((Class<?>) hint);
}
}
return ClassUtils.toClassArray(groups);
}
/**
* Process the given JSR-303 ConstraintViolations, adding corresponding errors to
* the provided Spring {@link Errors} object.