/*
 * Copyright 2002-2020 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.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import javax.validation.ConstraintViolation;
import javax.validation.ElementKind;
import javax.validation.Path;
import javax.validation.ValidationException;
import javax.validation.executable.ExecutableValidator;
import javax.validation.metadata.BeanDescriptor;
import javax.validation.metadata.ConstraintDescriptor;

import org.springframework.beans.NotReadablePropertyException;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.validation.BindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.validation.SmartValidator;

Adapter that takes a JSR-303 javax.validator.Validator and exposes it as a Spring Validator while also exposing the original JSR-303 Validator interface itself.

Can be used as a programmatic wrapper. Also serves as base class for CustomValidatorBean and LocalValidatorFactoryBean, and as the primary implementation of the 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
See Also:
Since:3.0
/** * Adapter that takes a JSR-303 {@code javax.validator.Validator} and * exposes it as a Spring {@link org.springframework.validation.Validator} * 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}, * 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 { private static final Set<String> internalAnnotationAttributes = new HashSet<>(4); static { internalAnnotationAttributes.add("message"); internalAnnotationAttributes.add("groups"); internalAnnotationAttributes.add("payload"); } @Nullable private javax.validation.Validator targetValidator;
Create a new SpringValidatorAdapter for the given JSR-303 Validator.
Params:
  • targetValidator – the JSR-303 Validator to wrap
/** * Create a new SpringValidatorAdapter for the given JSR-303 Validator. * @param targetValidator the JSR-303 Validator to wrap */
public SpringValidatorAdapter(javax.validation.Validator targetValidator) { Assert.notNull(targetValidator, "Target Validator must not be null"); this.targetValidator = targetValidator; } SpringValidatorAdapter() { } void setTargetValidator(javax.validation.Validator targetValidator) { this.targetValidator = targetValidator; } //--------------------------------------------------------------------- // Implementation of Spring Validator interface //--------------------------------------------------------------------- @Override public boolean supports(Class<?> clazz) { return (this.targetValidator != null); } @Override public void validate(Object target, Errors errors) { if (this.targetValidator != null) { processConstraintViolations(this.targetValidator.validate(target), errors); } } @Override public void validate(Object target, Errors errors, Object... validationHints) { if (this.targetValidator != null) { processConstraintViolations( this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); } } @SuppressWarnings({ "unchecked", "rawtypes" }) @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
/** * 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 Errors object.
Params:
  • violations – the JSR-303 ConstraintViolation results
  • errors – the Spring errors object to register to
/** * Process the given JSR-303 ConstraintViolations, adding corresponding errors to * the provided Spring {@link Errors} object. * @param violations the JSR-303 ConstraintViolation results * @param errors the Spring errors object to register to */
@SuppressWarnings("serial") protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) { for (ConstraintViolation<Object> violation : violations) { String field = determineField(violation); FieldError fieldError = errors.getFieldError(field); if (fieldError == null || !fieldError.isBindingFailure()) { try { ConstraintDescriptor<?> cd = violation.getConstraintDescriptor(); String errorCode = determineErrorCode(cd); Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd); if (errors instanceof BindingResult) { // Can do custom FieldError registration with invalid value from ConstraintViolation, // as necessary for Hibernate Validator compatibility (non-indexed set path in field) BindingResult bindingResult = (BindingResult) errors; String nestedField = bindingResult.getNestedPath() + field; if (nestedField.isEmpty()) { String[] errorCodes = bindingResult.resolveMessageCodes(errorCode); ObjectError error = new ViolationObjectError( errors.getObjectName(), errorCodes, errorArgs, violation, this); bindingResult.addError(error); } else { Object rejectedValue = getRejectedValue(field, violation, bindingResult); String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field); FieldError error = new ViolationFieldError(errors.getObjectName(), nestedField, rejectedValue, errorCodes, errorArgs, violation, this); bindingResult.addError(error); } } else { // got no BindingResult - can only do standard rejectValue call // with automatic extraction of the current field value errors.rejectValue(field, errorCode, errorArgs, violation.getMessage()); } } catch (NotReadablePropertyException ex) { throw new IllegalStateException("JSR-303 validated property '" + field + "' does not have a corresponding accessor for Spring data binding - " + "check your DataBinder's configuration (bean property versus direct field access)", ex); } } } }
Determine a field for the given constraint violation.

The default implementation returns the stringified property path.

Params:
  • violation – the current JSR-303 ConstraintViolation
See Also:
Returns:the Spring-reported field (for use with Errors)
Since:4.2
/** * Determine a field for the given constraint violation. * <p>The default implementation returns the stringified property path. * @param violation the current JSR-303 ConstraintViolation * @return the Spring-reported field (for use with {@link Errors}) * @since 4.2 * @see javax.validation.ConstraintViolation#getPropertyPath() * @see org.springframework.validation.FieldError#getField() */
protected String determineField(ConstraintViolation<Object> violation) { Path path = violation.getPropertyPath(); StringBuilder sb = new StringBuilder(); boolean first = true; for (Path.Node node : path) { if (node.isInIterable()) { sb.append('['); Object index = node.getIndex(); if (index == null) { index = node.getKey(); } if (index != null) { sb.append(index); } sb.append(']'); } String name = node.getName(); if (name != null && node.getKind() == ElementKind.PROPERTY && !name.startsWith("<")) { if (!first) { sb.append('.'); } first = false; sb.append(name); } } return sb.toString(); }
Determine a Spring-reported error code for the given constraint descriptor.

The default implementation returns the simple class name of the descriptor's annotation type. Note that the configured MessageCodesResolver will automatically generate error code variations which include the object name and the field name.

Params:
  • descriptor – the JSR-303 ConstraintDescriptor for the current violation
See Also:
Returns:a corresponding error code (for use with Errors)
Since:4.2
/** * Determine a Spring-reported error code for the given constraint descriptor. * <p>The default implementation returns the simple class name of the descriptor's * annotation type. Note that the configured * {@link org.springframework.validation.MessageCodesResolver} will automatically * generate error code variations which include the object name and the field name. * @param descriptor the JSR-303 ConstraintDescriptor for the current violation * @return a corresponding error code (for use with {@link Errors}) * @since 4.2 * @see javax.validation.metadata.ConstraintDescriptor#getAnnotation() * @see org.springframework.validation.MessageCodesResolver */
protected String determineErrorCode(ConstraintDescriptor<?> descriptor) { return descriptor.getAnnotation().annotationType().getSimpleName(); }
Return FieldError arguments for a validation error on the given field. Invoked for each violated constraint.

The default implementation returns a first argument indicating the field name (see getResolvableField). Afterwards, it adds all actual constraint annotation attributes (i.e. excluding "message", "groups" and "payload") in alphabetical order of their attribute names.

Can be overridden to e.g. add further attributes from the constraint descriptor.

Params:
  • objectName – the name of the target object
  • field – the field that caused the binding error
  • descriptor – the JSR-303 constraint descriptor
See Also:
Returns:the Object array that represents the FieldError arguments
/** * Return FieldError arguments for a validation error on the given field. * Invoked for each violated constraint. * <p>The default implementation returns a first argument indicating the field name * (see {@link #getResolvableField}). Afterwards, it adds all actual constraint * annotation attributes (i.e. excluding "message", "groups" and "payload") in * alphabetical order of their attribute names. * <p>Can be overridden to e.g. add further attributes from the constraint descriptor. * @param objectName the name of the target object * @param field the field that caused the binding error * @param descriptor the JSR-303 constraint descriptor * @return the Object array that represents the FieldError arguments * @see org.springframework.validation.FieldError#getArguments * @see org.springframework.context.support.DefaultMessageSourceResolvable * @see org.springframework.validation.DefaultBindingErrorProcessor#getArgumentsForBindError */
protected Object[] getArgumentsForConstraint(String objectName, String field, ConstraintDescriptor<?> descriptor) { List<Object> arguments = new ArrayList<>(); arguments.add(getResolvableField(objectName, field)); // Using a TreeMap for alphabetical ordering of attribute names Map<String, Object> attributesToExpose = new TreeMap<>(); descriptor.getAttributes().forEach((attributeName, attributeValue) -> { if (!internalAnnotationAttributes.contains(attributeName)) { if (attributeValue instanceof String) { attributeValue = new ResolvableAttribute(attributeValue.toString()); } attributesToExpose.put(attributeName, attributeValue); } }); arguments.addAll(attributesToExpose.values()); return arguments.toArray(); }
Build a resolvable wrapper for the specified field, allowing to resolve the field's name in a MessageSource.

The default implementation returns a first argument indicating the field: of type DefaultMessageSourceResolvable, with "objectName.field" and "field" as codes, and with the plain field name as default message.

Params:
  • objectName – the name of the target object
  • field – the field that caused the binding error
See Also:
  • getArgumentsForConstraint
Returns:a corresponding MessageSourceResolvable for the specified field
Since:4.3
/** * Build a resolvable wrapper for the specified field, allowing to resolve the field's * name in a {@code MessageSource}. * <p>The default implementation returns a first argument indicating the field: * of type {@code DefaultMessageSourceResolvable}, with "objectName.field" and "field" * as codes, and with the plain field name as default message. * @param objectName the name of the target object * @param field the field that caused the binding error * @return a corresponding {@code MessageSourceResolvable} for the specified field * @since 4.3 * @see #getArgumentsForConstraint */
protected MessageSourceResolvable getResolvableField(String objectName, String field) { String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field}; return new DefaultMessageSourceResolvable(codes, field); }
Extract the rejected value behind the given constraint violation, for exposure through the Spring errors representation.
Params:
  • field – the field that caused the binding error
  • violation – the corresponding JSR-303 ConstraintViolation
  • bindingResult – a Spring BindingResult for the backing object which contains the current field's value
See Also:
Returns:the invalid value to expose as part of the field error
Since:4.2
/** * Extract the rejected value behind the given constraint violation, * for exposure through the Spring errors representation. * @param field the field that caused the binding error * @param violation the corresponding JSR-303 ConstraintViolation * @param bindingResult a Spring BindingResult for the backing object * which contains the current field's value * @return the invalid value to expose as part of the field error * @since 4.2 * @see javax.validation.ConstraintViolation#getInvalidValue() * @see org.springframework.validation.FieldError#getRejectedValue() */
@Nullable protected Object getRejectedValue(String field, ConstraintViolation<Object> violation, BindingResult bindingResult) { Object invalidValue = violation.getInvalidValue(); if (!field.isEmpty() && !field.contains("[]") && (invalidValue == violation.getLeafBean() || field.contains("[") || field.contains("."))) { // Possibly a bean constraint with property path: retrieve the actual property value. // However, explicitly avoid this for "address[]" style paths that we can't handle. invalidValue = bindingResult.getRawFieldValue(field); } return invalidValue; }
Indicate whether this violation's interpolated message has remaining placeholders and therefore requires MessageFormat to be applied to it. Called for a Bean Validation defined message (coming out ValidationMessages.properties) when rendered as the default message in Spring's MessageSource.

The default implementation considers a Spring-style "{0}" placeholder for the field name as an indication for MessageFormat. Any other placeholder or escape syntax occurrences are typically a mismatch, coming out of regex pattern values or the like. Note that standard Bean Validation does not support "{0}" style placeholders at all; this is a feature typically used in Spring MessageSource resource bundles.

Params:
  • violation – the Bean Validation constraint violation, including BV-defined interpolation of named attribute references in its message
See Also:
  • getArgumentsForConstraint
Returns:true if java.text.MessageFormat is to be applied, or false if the violation's message should be used as-is
Since:5.1.8
/** * Indicate whether this violation's interpolated message has remaining * placeholders and therefore requires {@link java.text.MessageFormat} * to be applied to it. Called for a Bean Validation defined message * (coming out {@code ValidationMessages.properties}) when rendered * as the default message in Spring's MessageSource. * <p>The default implementation considers a Spring-style "{0}" placeholder * for the field name as an indication for {@link java.text.MessageFormat}. * Any other placeholder or escape syntax occurrences are typically a * mismatch, coming out of regex pattern values or the like. Note that * standard Bean Validation does not support "{0}" style placeholders at all; * this is a feature typically used in Spring MessageSource resource bundles. * @param violation the Bean Validation constraint violation, including * BV-defined interpolation of named attribute references in its message * @return {@code true} if {@code java.text.MessageFormat} is to be applied, * or {@code false} if the violation's message should be used as-is * @since 5.1.8 * @see #getArgumentsForConstraint */
protected boolean requiresMessageFormat(ConstraintViolation<?> violation) { return containsSpringStylePlaceholder(violation.getMessage()); } private static boolean containsSpringStylePlaceholder(@Nullable String message) { return (message != null && message.contains("{0}")); } //--------------------------------------------------------------------- // Implementation of JSR-303 Validator interface //--------------------------------------------------------------------- @Override public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) { Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.validate(object, groups); } @Override public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) { Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.validateProperty(object, propertyName, groups); } @Override public <T> Set<ConstraintViolation<T>> validateValue( Class<T> beanType, String propertyName, Object value, Class<?>... groups) { Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.validateValue(beanType, propertyName, value, groups); } @Override public BeanDescriptor getConstraintsForClass(Class<?> clazz) { Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.getConstraintsForClass(clazz); } @Override @SuppressWarnings("unchecked") public <T> T unwrap(@Nullable Class<T> type) { Assert.state(this.targetValidator != null, "No target Validator set"); try { return (type != null ? this.targetValidator.unwrap(type) : (T) this.targetValidator); } catch (ValidationException ex) { // ignore if just being asked for plain Validator if (javax.validation.Validator.class == type) { return (T) this.targetValidator; } throw ex; } } @Override public ExecutableValidator forExecutables() { Assert.state(this.targetValidator != null, "No target Validator set"); return this.targetValidator.forExecutables(); }
Wrapper for a String attribute which can be resolved via a MessageSource, falling back to the original attribute as a default value otherwise.
/** * Wrapper for a String attribute which can be resolved via a {@code MessageSource}, * falling back to the original attribute as a default value otherwise. */
@SuppressWarnings("serial") private static class ResolvableAttribute implements MessageSourceResolvable, Serializable { private final String resolvableString; public ResolvableAttribute(String resolvableString) { this.resolvableString = resolvableString; } @Override public String[] getCodes() { return new String[] {this.resolvableString}; } @Override @Nullable public Object[] getArguments() { return null; } @Override public String getDefaultMessage() { return this.resolvableString; } @Override public String toString() { return this.resolvableString; } }
Subclass of ObjectError with Spring-style default message rendering.
/** * Subclass of {@code ObjectError} with Spring-style default message rendering. */
@SuppressWarnings("serial") private static class ViolationObjectError extends ObjectError implements Serializable { @Nullable private transient SpringValidatorAdapter adapter; @Nullable private transient ConstraintViolation<?> violation; public ViolationObjectError(String objectName, String[] codes, Object[] arguments, ConstraintViolation<?> violation, SpringValidatorAdapter adapter) { super(objectName, codes, arguments, violation.getMessage()); this.adapter = adapter; this.violation = violation; wrap(violation); } @Override public boolean shouldRenderDefaultMessage() { return (this.adapter != null && this.violation != null ? this.adapter.requiresMessageFormat(this.violation) : containsSpringStylePlaceholder(getDefaultMessage())); } }
Subclass of FieldError with Spring-style default message rendering.
/** * Subclass of {@code FieldError} with Spring-style default message rendering. */
@SuppressWarnings("serial") private static class ViolationFieldError extends FieldError implements Serializable { @Nullable private transient SpringValidatorAdapter adapter; @Nullable private transient ConstraintViolation<?> violation; public ViolationFieldError(String objectName, String field, @Nullable Object rejectedValue, String[] codes, Object[] arguments, ConstraintViolation<?> violation, SpringValidatorAdapter adapter) { super(objectName, field, rejectedValue, false, codes, arguments, violation.getMessage()); this.adapter = adapter; this.violation = violation; wrap(violation); } @Override public boolean shouldRenderDefaultMessage() { return (this.adapter != null && this.violation != null ? this.adapter.requiresMessageFormat(this.violation) : containsSpringStylePlaceholder(getDefaultMessage())); } } }