001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.validation.beanvalidation;
018
019import java.io.Serializable;
020import java.util.ArrayList;
021import java.util.HashSet;
022import java.util.LinkedHashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026import java.util.TreeMap;
027
028import javax.validation.ConstraintViolation;
029import javax.validation.ElementKind;
030import javax.validation.Path;
031import javax.validation.ValidationException;
032import javax.validation.executable.ExecutableValidator;
033import javax.validation.metadata.BeanDescriptor;
034import javax.validation.metadata.ConstraintDescriptor;
035
036import org.springframework.beans.NotReadablePropertyException;
037import org.springframework.context.MessageSourceResolvable;
038import org.springframework.context.support.DefaultMessageSourceResolvable;
039import org.springframework.lang.Nullable;
040import org.springframework.util.Assert;
041import org.springframework.util.ClassUtils;
042import org.springframework.validation.BindingResult;
043import org.springframework.validation.Errors;
044import org.springframework.validation.FieldError;
045import org.springframework.validation.ObjectError;
046import org.springframework.validation.SmartValidator;
047
048/**
049 * Adapter that takes a JSR-303 {@code javax.validator.Validator} and
050 * exposes it as a Spring {@link org.springframework.validation.Validator}
051 * while also exposing the original JSR-303 Validator interface itself.
052 *
053 * <p>Can be used as a programmatic wrapper. Also serves as base class for
054 * {@link CustomValidatorBean} and {@link LocalValidatorFactoryBean},
055 * and as the primary implementation of the {@link SmartValidator} interface.
056 *
057 * <p>As of Spring Framework 5.0, this adapter is fully compatible with
058 * Bean Validation 1.1 as well as 2.0.
059 *
060 * @author Juergen Hoeller
061 * @since 3.0
062 * @see SmartValidator
063 * @see CustomValidatorBean
064 * @see LocalValidatorFactoryBean
065 */
066public class SpringValidatorAdapter implements SmartValidator, javax.validation.Validator {
067
068        private static final Set<String> internalAnnotationAttributes = new HashSet<>(4);
069
070        static {
071                internalAnnotationAttributes.add("message");
072                internalAnnotationAttributes.add("groups");
073                internalAnnotationAttributes.add("payload");
074        }
075
076        @Nullable
077        private javax.validation.Validator targetValidator;
078
079
080        /**
081         * Create a new SpringValidatorAdapter for the given JSR-303 Validator.
082         * @param targetValidator the JSR-303 Validator to wrap
083         */
084        public SpringValidatorAdapter(javax.validation.Validator targetValidator) {
085                Assert.notNull(targetValidator, "Target Validator must not be null");
086                this.targetValidator = targetValidator;
087        }
088
089        SpringValidatorAdapter() {
090        }
091
092        void setTargetValidator(javax.validation.Validator targetValidator) {
093                this.targetValidator = targetValidator;
094        }
095
096
097        //---------------------------------------------------------------------
098        // Implementation of Spring Validator interface
099        //---------------------------------------------------------------------
100
101        @Override
102        public boolean supports(Class<?> clazz) {
103                return (this.targetValidator != null);
104        }
105
106        @Override
107        public void validate(Object target, Errors errors) {
108                if (this.targetValidator != null) {
109                        processConstraintViolations(this.targetValidator.validate(target), errors);
110                }
111        }
112
113        @Override
114        public void validate(Object target, Errors errors, Object... validationHints) {
115                if (this.targetValidator != null) {
116                        processConstraintViolations(
117                                        this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
118                }
119        }
120
121        @SuppressWarnings({ "unchecked", "rawtypes" })
122        @Override
123        public void validateValue(
124                        Class<?> targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) {
125
126                if (this.targetValidator != null) {
127                        processConstraintViolations(this.targetValidator.validateValue(
128                                        (Class) targetType, fieldName, value, asValidationGroups(validationHints)), errors);
129                }
130        }
131
132        /**
133         * Turn the specified validation hints into JSR-303 validation groups.
134         * @since 5.1
135         */
136        private Class<?>[] asValidationGroups(Object... validationHints) {
137                Set<Class<?>> groups = new LinkedHashSet<>(4);
138                for (Object hint : validationHints) {
139                        if (hint instanceof Class) {
140                                groups.add((Class<?>) hint);
141                        }
142                }
143                return ClassUtils.toClassArray(groups);
144        }
145
146        /**
147         * Process the given JSR-303 ConstraintViolations, adding corresponding errors to
148         * the provided Spring {@link Errors} object.
149         * @param violations the JSR-303 ConstraintViolation results
150         * @param errors the Spring errors object to register to
151         */
152        @SuppressWarnings("serial")
153        protected void processConstraintViolations(Set<ConstraintViolation<Object>> violations, Errors errors) {
154                for (ConstraintViolation<Object> violation : violations) {
155                        String field = determineField(violation);
156                        FieldError fieldError = errors.getFieldError(field);
157                        if (fieldError == null || !fieldError.isBindingFailure()) {
158                                try {
159                                        ConstraintDescriptor<?> cd = violation.getConstraintDescriptor();
160                                        String errorCode = determineErrorCode(cd);
161                                        Object[] errorArgs = getArgumentsForConstraint(errors.getObjectName(), field, cd);
162                                        if (errors instanceof BindingResult) {
163                                                // Can do custom FieldError registration with invalid value from ConstraintViolation,
164                                                // as necessary for Hibernate Validator compatibility (non-indexed set path in field)
165                                                BindingResult bindingResult = (BindingResult) errors;
166                                                String nestedField = bindingResult.getNestedPath() + field;
167                                                if (nestedField.isEmpty()) {
168                                                        String[] errorCodes = bindingResult.resolveMessageCodes(errorCode);
169                                                        ObjectError error = new ViolationObjectError(
170                                                                        errors.getObjectName(), errorCodes, errorArgs, violation, this);
171                                                        bindingResult.addError(error);
172                                                }
173                                                else {
174                                                        Object rejectedValue = getRejectedValue(field, violation, bindingResult);
175                                                        String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field);
176                                                        FieldError error = new ViolationFieldError(errors.getObjectName(), nestedField,
177                                                                        rejectedValue, errorCodes, errorArgs, violation, this);
178                                                        bindingResult.addError(error);
179                                                }
180                                        }
181                                        else {
182                                                // got no BindingResult - can only do standard rejectValue call
183                                                // with automatic extraction of the current field value
184                                                errors.rejectValue(field, errorCode, errorArgs, violation.getMessage());
185                                        }
186                                }
187                                catch (NotReadablePropertyException ex) {
188                                        throw new IllegalStateException("JSR-303 validated property '" + field +
189                                                        "' does not have a corresponding accessor for Spring data binding - " +
190                                                        "check your DataBinder's configuration (bean property versus direct field access)", ex);
191                                }
192                        }
193                }
194        }
195
196        /**
197         * Determine a field for the given constraint violation.
198         * <p>The default implementation returns the stringified property path.
199         * @param violation the current JSR-303 ConstraintViolation
200         * @return the Spring-reported field (for use with {@link Errors})
201         * @since 4.2
202         * @see javax.validation.ConstraintViolation#getPropertyPath()
203         * @see org.springframework.validation.FieldError#getField()
204         */
205        protected String determineField(ConstraintViolation<Object> violation) {
206                Path path = violation.getPropertyPath();
207                StringBuilder sb = new StringBuilder();
208                boolean first = true;
209                for (Path.Node node : path) {
210                        if (node.isInIterable()) {
211                                sb.append('[');
212                                Object index = node.getIndex();
213                                if (index == null) {
214                                        index = node.getKey();
215                                }
216                                if (index != null) {
217                                        sb.append(index);
218                                }
219                                sb.append(']');
220                        }
221                        String name = node.getName();
222                        if (name != null && node.getKind() == ElementKind.PROPERTY && !name.startsWith("<")) {
223                                if (!first) {
224                                        sb.append('.');
225                                }
226                                first = false;
227                                sb.append(name);
228                        }
229                }
230                return sb.toString();
231        }
232
233        /**
234         * Determine a Spring-reported error code for the given constraint descriptor.
235         * <p>The default implementation returns the simple class name of the descriptor's
236         * annotation type. Note that the configured
237         * {@link org.springframework.validation.MessageCodesResolver} will automatically
238         * generate error code variations which include the object name and the field name.
239         * @param descriptor the JSR-303 ConstraintDescriptor for the current violation
240         * @return a corresponding error code (for use with {@link Errors})
241         * @since 4.2
242         * @see javax.validation.metadata.ConstraintDescriptor#getAnnotation()
243         * @see org.springframework.validation.MessageCodesResolver
244         */
245        protected String determineErrorCode(ConstraintDescriptor<?> descriptor) {
246                return descriptor.getAnnotation().annotationType().getSimpleName();
247        }
248
249        /**
250         * Return FieldError arguments for a validation error on the given field.
251         * Invoked for each violated constraint.
252         * <p>The default implementation returns a first argument indicating the field name
253         * (see {@link #getResolvableField}). Afterwards, it adds all actual constraint
254         * annotation attributes (i.e. excluding "message", "groups" and "payload") in
255         * alphabetical order of their attribute names.
256         * <p>Can be overridden to e.g. add further attributes from the constraint descriptor.
257         * @param objectName the name of the target object
258         * @param field the field that caused the binding error
259         * @param descriptor the JSR-303 constraint descriptor
260         * @return the Object array that represents the FieldError arguments
261         * @see org.springframework.validation.FieldError#getArguments
262         * @see org.springframework.context.support.DefaultMessageSourceResolvable
263         * @see org.springframework.validation.DefaultBindingErrorProcessor#getArgumentsForBindError
264         */
265        protected Object[] getArgumentsForConstraint(String objectName, String field, ConstraintDescriptor<?> descriptor) {
266                List<Object> arguments = new ArrayList<>();
267                arguments.add(getResolvableField(objectName, field));
268                // Using a TreeMap for alphabetical ordering of attribute names
269                Map<String, Object> attributesToExpose = new TreeMap<>();
270                descriptor.getAttributes().forEach((attributeName, attributeValue) -> {
271                        if (!internalAnnotationAttributes.contains(attributeName)) {
272                                if (attributeValue instanceof String) {
273                                        attributeValue = new ResolvableAttribute(attributeValue.toString());
274                                }
275                                attributesToExpose.put(attributeName, attributeValue);
276                        }
277                });
278                arguments.addAll(attributesToExpose.values());
279                return arguments.toArray();
280        }
281
282        /**
283         * Build a resolvable wrapper for the specified field, allowing to resolve the field's
284         * name in a {@code MessageSource}.
285         * <p>The default implementation returns a first argument indicating the field:
286         * of type {@code DefaultMessageSourceResolvable}, with "objectName.field" and "field"
287         * as codes, and with the plain field name as default message.
288         * @param objectName the name of the target object
289         * @param field the field that caused the binding error
290         * @return a corresponding {@code MessageSourceResolvable} for the specified field
291         * @since 4.3
292         * @see #getArgumentsForConstraint
293         */
294        protected MessageSourceResolvable getResolvableField(String objectName, String field) {
295                String[] codes = new String[] {objectName + Errors.NESTED_PATH_SEPARATOR + field, field};
296                return new DefaultMessageSourceResolvable(codes, field);
297        }
298
299        /**
300         * Extract the rejected value behind the given constraint violation,
301         * for exposure through the Spring errors representation.
302         * @param field the field that caused the binding error
303         * @param violation the corresponding JSR-303 ConstraintViolation
304         * @param bindingResult a Spring BindingResult for the backing object
305         * which contains the current field's value
306         * @return the invalid value to expose as part of the field error
307         * @since 4.2
308         * @see javax.validation.ConstraintViolation#getInvalidValue()
309         * @see org.springframework.validation.FieldError#getRejectedValue()
310         */
311        @Nullable
312        protected Object getRejectedValue(String field, ConstraintViolation<Object> violation, BindingResult bindingResult) {
313                Object invalidValue = violation.getInvalidValue();
314                if (!field.isEmpty() && !field.contains("[]") &&
315                                (invalidValue == violation.getLeafBean() || field.contains("[") || field.contains("."))) {
316                        // Possibly a bean constraint with property path: retrieve the actual property value.
317                        // However, explicitly avoid this for "address[]" style paths that we can't handle.
318                        invalidValue = bindingResult.getRawFieldValue(field);
319                }
320                return invalidValue;
321        }
322
323        /**
324         * Indicate whether this violation's interpolated message has remaining
325         * placeholders and therefore requires {@link java.text.MessageFormat}
326         * to be applied to it. Called for a Bean Validation defined message
327         * (coming out {@code ValidationMessages.properties}) when rendered
328         * as the default message in Spring's MessageSource.
329         * <p>The default implementation considers a Spring-style "{0}" placeholder
330         * for the field name as an indication for {@link java.text.MessageFormat}.
331         * Any other placeholder or escape syntax occurrences are typically a
332         * mismatch, coming out of regex pattern values or the like. Note that
333         * standard Bean Validation does not support "{0}" style placeholders at all;
334         * this is a feature typically used in Spring MessageSource resource bundles.
335         * @param violation the Bean Validation constraint violation, including
336         * BV-defined interpolation of named attribute references in its message
337         * @return {@code true} if {@code java.text.MessageFormat} is to be applied,
338         * or {@code false} if the violation's message should be used as-is
339         * @since 5.1.8
340         * @see #getArgumentsForConstraint
341         */
342        protected boolean requiresMessageFormat(ConstraintViolation<?> violation) {
343                return containsSpringStylePlaceholder(violation.getMessage());
344        }
345
346        private static boolean containsSpringStylePlaceholder(@Nullable String message) {
347                return (message != null && message.contains("{0}"));
348        }
349
350
351        //---------------------------------------------------------------------
352        // Implementation of JSR-303 Validator interface
353        //---------------------------------------------------------------------
354
355        @Override
356        public <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
357                Assert.state(this.targetValidator != null, "No target Validator set");
358                return this.targetValidator.validate(object, groups);
359        }
360
361        @Override
362        public <T> Set<ConstraintViolation<T>> validateProperty(T object, String propertyName, Class<?>... groups) {
363                Assert.state(this.targetValidator != null, "No target Validator set");
364                return this.targetValidator.validateProperty(object, propertyName, groups);
365        }
366
367        @Override
368        public <T> Set<ConstraintViolation<T>> validateValue(
369                        Class<T> beanType, String propertyName, Object value, Class<?>... groups) {
370
371                Assert.state(this.targetValidator != null, "No target Validator set");
372                return this.targetValidator.validateValue(beanType, propertyName, value, groups);
373        }
374
375        @Override
376        public BeanDescriptor getConstraintsForClass(Class<?> clazz) {
377                Assert.state(this.targetValidator != null, "No target Validator set");
378                return this.targetValidator.getConstraintsForClass(clazz);
379        }
380
381        @Override
382        @SuppressWarnings("unchecked")
383        public <T> T unwrap(@Nullable Class<T> type) {
384                Assert.state(this.targetValidator != null, "No target Validator set");
385                try {
386                        return (type != null ? this.targetValidator.unwrap(type) : (T) this.targetValidator);
387                }
388                catch (ValidationException ex) {
389                        // ignore if just being asked for plain Validator
390                        if (javax.validation.Validator.class == type) {
391                                return (T) this.targetValidator;
392                        }
393                        throw ex;
394                }
395        }
396
397        @Override
398        public ExecutableValidator forExecutables() {
399                Assert.state(this.targetValidator != null, "No target Validator set");
400                return this.targetValidator.forExecutables();
401        }
402
403
404        /**
405         * Wrapper for a String attribute which can be resolved via a {@code MessageSource},
406         * falling back to the original attribute as a default value otherwise.
407         */
408        @SuppressWarnings("serial")
409        private static class ResolvableAttribute implements MessageSourceResolvable, Serializable {
410
411                private final String resolvableString;
412
413                public ResolvableAttribute(String resolvableString) {
414                        this.resolvableString = resolvableString;
415                }
416
417                @Override
418                public String[] getCodes() {
419                        return new String[] {this.resolvableString};
420                }
421
422                @Override
423                @Nullable
424                public Object[] getArguments() {
425                        return null;
426                }
427
428                @Override
429                public String getDefaultMessage() {
430                        return this.resolvableString;
431                }
432
433                @Override
434                public String toString() {
435                        return this.resolvableString;
436                }
437        }
438
439
440        /**
441         * Subclass of {@code ObjectError} with Spring-style default message rendering.
442         */
443        @SuppressWarnings("serial")
444        private static class ViolationObjectError extends ObjectError implements Serializable {
445
446                @Nullable
447                private transient SpringValidatorAdapter adapter;
448
449                @Nullable
450                private transient ConstraintViolation<?> violation;
451
452                public ViolationObjectError(String objectName, String[] codes, Object[] arguments,
453                                ConstraintViolation<?> violation, SpringValidatorAdapter adapter) {
454
455                        super(objectName, codes, arguments, violation.getMessage());
456                        this.adapter = adapter;
457                        this.violation = violation;
458                        wrap(violation);
459                }
460
461                @Override
462                public boolean shouldRenderDefaultMessage() {
463                        return (this.adapter != null && this.violation != null ?
464                                        this.adapter.requiresMessageFormat(this.violation) :
465                                        containsSpringStylePlaceholder(getDefaultMessage()));
466                }
467        }
468
469
470        /**
471         * Subclass of {@code FieldError} with Spring-style default message rendering.
472         */
473        @SuppressWarnings("serial")
474        private static class ViolationFieldError extends FieldError implements Serializable {
475
476                @Nullable
477                private transient SpringValidatorAdapter adapter;
478
479                @Nullable
480                private transient ConstraintViolation<?> violation;
481
482                public ViolationFieldError(String objectName, String field, @Nullable Object rejectedValue, String[] codes,
483                                Object[] arguments, ConstraintViolation<?> violation, SpringValidatorAdapter adapter) {
484
485                        super(objectName, field, rejectedValue, false, codes, arguments, violation.getMessage());
486                        this.adapter = adapter;
487                        this.violation = violation;
488                        wrap(violation);
489                }
490
491                @Override
492                public boolean shouldRenderDefaultMessage() {
493                        return (this.adapter != null && this.violation != null ?
494                                        this.adapter.requiresMessageFormat(this.violation) :
495                                        containsSpringStylePlaceholder(getDefaultMessage()));
496                }
497        }
498
499}