diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java index 82a92aba86..ece1663b27 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java @@ -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); } diff --git a/spring-context/src/main/java/org/springframework/validation/BindException.java b/spring-context/src/main/java/org/springframework/validation/BindException.java index d31ee8e552..f9111ecd22 100644 --- a/spring-context/src/main/java/org/springframework/validation/BindException.java +++ b/spring-context/src/main/java/org/springframework/validation/BindException.java @@ -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); } diff --git a/spring-context/src/main/java/org/springframework/validation/BindingResult.java b/spring-context/src/main/java/org/springframework/validation/BindingResult.java index ebe5594eee..6227396994 100644 --- a/spring-context/src/main/java/org/springframework/validation/BindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/BindingResult.java @@ -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) { } /** diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 4f01b64d7d..ab62c932ba 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -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. *
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); } } } diff --git a/spring-context/src/main/java/org/springframework/validation/SmartValidator.java b/spring-context/src/main/java/org/springframework/validation/SmartValidator.java index 16f7c7580e..d59026319f 100644 --- a/spring-context/src/main/java/org/springframework/validation/SmartValidator.java +++ b/spring-context/src/main/java/org/springframework/validation/SmartValidator.java @@ -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 { *
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); + } } diff --git a/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java b/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java index 3ab00680c0..716bb1acdd 100644 --- a/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java +++ b/spring-context/src/main/java/org/springframework/validation/ValidationUtils.java @@ -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()) { diff --git a/spring-context/src/main/java/org/springframework/validation/Validator.java b/spring-context/src/main/java/org/springframework/validation/Validator.java index d5511c602b..d3379ddc03 100644 --- a/spring-context/src/main/java/org/springframework/validation/Validator.java +++ b/spring-context/src/main/java/org/springframework/validation/Validator.java @@ -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}. *
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); } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java index 3f6463bc8a..b06c276684 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java @@ -50,13 +50,17 @@ import org.springframework.validation.SmartValidator; * while also exposing the original JSR-303 Validator interface itself. * *
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. * *
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 The default implementation checks for {@code @javax.validation.Valid},
+ * Spring's {@link org.springframework.validation.annotation.Validated},
+ * and custom annotations whose name starts with "Valid".
+ * @param binder the DataBinder to be used
+ * @param parameter the method parameter declaration
+ * @param targetType the target type
+ * @param fieldName the name of the field
+ * @param value the candidate value
+ * @since 5.1
+ * @see #validateIfApplicable(WebDataBinder, MethodParameter)
+ * @see SmartValidator#validateValue(Class, String, Object, Errors, Object...)
+ */
+ protected void validateValueIfApplicable(WebDataBinder binder, MethodParameter parameter,
+ Class> targetType, String fieldName, @Nullable Object value) {
+
+ for (Annotation ann : parameter.getParameterAnnotations()) {
+ Object[] validationHints = determineValidationHints(ann);
+ if (validationHints != null) {
+ for (Validator validator : binder.getValidators()) {
+ if (validator instanceof SmartValidator) {
+ try {
+ ((SmartValidator) validator).validateValue(targetType, fieldName, value,
+ binder.getBindingResult(), validationHints);
+ }
+ catch (IllegalArgumentException ex) {
+ // No corresponding field on the target class...
+ }
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ /**
+ * Determine any validation triggered by the given annotation.
+ * @param ann the annotation (potentially a validation annotation)
+ * @return the validation hints to apply (possibly an empty array),
+ * or {@code null} if this annotation does not trigger any validation
+ * @since 5.1
+ */
+ @Nullable
+ private Object[] determineValidationHints(Annotation ann) {
+ Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
+ if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
+ Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
+ return (hints instanceof Object[] ? (Object[]) hints : new Object[]{hints});
+ }
+ return null;
+ }
+
/**
* Whether to raise a fatal bind exception on validation errors.
* The default implementation delegates to {@link #isBindExceptionRequired(MethodParameter)}.
@@ -380,4 +464,61 @@ public class ModelAttributeMethodProcessor implements HandlerMethodArgumentResol
}
}
+
+ /**
+ * {@link MethodParameter} subclass which detects field annotations as well.
+ * @since 5.1
+ */
+ private static class FieldAwareConstructorParameter extends MethodParameter {
+
+ private final String parameterName;
+
+ @Nullable
+ private volatile Annotation[] combinedAnnotations;
+
+ public FieldAwareConstructorParameter(Constructor> constructor, int parameterIndex, String parameterName) {
+ super(constructor, parameterIndex);
+ this.parameterName = parameterName;
+ }
+
+ @Override
+ public Annotation[] getParameterAnnotations() {
+ Annotation[] anns = this.combinedAnnotations;
+ if (anns == null) {
+ anns = super.getParameterAnnotations();
+ try {
+ Field field = getDeclaringClass().getDeclaredField(this.parameterName);
+ Annotation[] fieldAnns = field.getAnnotations();
+ if (fieldAnns.length > 0) {
+ List