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.lang.reflect.Method;
020import java.util.Set;
021
022import javax.validation.ConstraintViolation;
023import javax.validation.ConstraintViolationException;
024import javax.validation.Validation;
025import javax.validation.Validator;
026import javax.validation.ValidatorFactory;
027import javax.validation.executable.ExecutableValidator;
028
029import org.aopalliance.intercept.MethodInterceptor;
030import org.aopalliance.intercept.MethodInvocation;
031
032import org.springframework.beans.factory.FactoryBean;
033import org.springframework.beans.factory.SmartFactoryBean;
034import org.springframework.core.BridgeMethodResolver;
035import org.springframework.core.annotation.AnnotationUtils;
036import org.springframework.util.ClassUtils;
037import org.springframework.validation.annotation.Validated;
038
039/**
040 * An AOP Alliance {@link MethodInterceptor} implementation that delegates to a
041 * JSR-303 provider for performing method-level validation on annotated methods.
042 *
043 * <p>Applicable methods have JSR-303 constraint annotations on their parameters
044 * and/or on their return value (in the latter case specified at the method level,
045 * typically as inline annotation).
046 *
047 * <p>E.g.: {@code public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)}
048 *
049 * <p>Validation groups can be specified through Spring's {@link Validated} annotation
050 * at the type level of the containing target class, applying to all public service methods
051 * of that class. By default, JSR-303 will validate against its default group only.
052 *
053 * <p>As of Spring 5.0, this functionality requires a Bean Validation 1.1+ provider.
054 *
055 * @author Juergen Hoeller
056 * @since 3.1
057 * @see MethodValidationPostProcessor
058 * @see javax.validation.executable.ExecutableValidator
059 */
060public class MethodValidationInterceptor implements MethodInterceptor {
061
062        private final Validator validator;
063
064
065        /**
066         * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
067         */
068        public MethodValidationInterceptor() {
069                this(Validation.buildDefaultValidatorFactory());
070        }
071
072        /**
073         * Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
074         * @param validatorFactory the JSR-303 ValidatorFactory to use
075         */
076        public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
077                this(validatorFactory.getValidator());
078        }
079
080        /**
081         * Create a new MethodValidationInterceptor using the given JSR-303 Validator.
082         * @param validator the JSR-303 Validator to use
083         */
084        public MethodValidationInterceptor(Validator validator) {
085                this.validator = validator;
086        }
087
088
089        @Override
090        public Object invoke(MethodInvocation invocation) throws Throwable {
091                // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
092                if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
093                        return invocation.proceed();
094                }
095
096                Class<?>[] groups = determineValidationGroups(invocation);
097
098                // Standard Bean Validation 1.1 API
099                ExecutableValidator execVal = this.validator.forExecutables();
100                Method methodToValidate = invocation.getMethod();
101                Set<ConstraintViolation<Object>> result;
102
103                try {
104                        result = execVal.validateParameters(
105                                        invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
106                }
107                catch (IllegalArgumentException ex) {
108                        // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
109                        // Let's try to find the bridged method on the implementation class...
110                        methodToValidate = BridgeMethodResolver.findBridgedMethod(
111                                        ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
112                        result = execVal.validateParameters(
113                                        invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
114                }
115                if (!result.isEmpty()) {
116                        throw new ConstraintViolationException(result);
117                }
118
119                Object returnValue = invocation.proceed();
120
121                result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
122                if (!result.isEmpty()) {
123                        throw new ConstraintViolationException(result);
124                }
125
126                return returnValue;
127        }
128
129        private boolean isFactoryBeanMetadataMethod(Method method) {
130                Class<?> clazz = method.getDeclaringClass();
131
132                // Call from interface-based proxy handle, allowing for an efficient check?
133                if (clazz.isInterface()) {
134                        return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) &&
135                                        !method.getName().equals("getObject"));
136                }
137
138                // Call from CGLIB proxy handle, potentially implementing a FactoryBean method?
139                Class<?> factoryBeanType = null;
140                if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
141                        factoryBeanType = SmartFactoryBean.class;
142                }
143                else if (FactoryBean.class.isAssignableFrom(clazz)) {
144                        factoryBeanType = FactoryBean.class;
145                }
146                return (factoryBeanType != null && !method.getName().equals("getObject") &&
147                                ClassUtils.hasMethod(factoryBeanType, method));
148        }
149
150        /**
151         * Determine the validation groups to validate against for the given method invocation.
152         * <p>Default are the validation groups as specified in the {@link Validated} annotation
153         * on the containing target class of the method.
154         * @param invocation the current MethodInvocation
155         * @return the applicable validation groups as a Class array
156         */
157        protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
158                Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
159                if (validatedAnn == null) {
160                        validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
161                }
162                return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
163        }
164
165}