001/*
002 * Copyright 2002-2018 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;
021import javax.validation.ConstraintViolation;
022import javax.validation.ConstraintViolationException;
023import javax.validation.Validation;
024import javax.validation.Validator;
025import javax.validation.ValidatorFactory;
026
027import org.aopalliance.intercept.MethodInterceptor;
028import org.aopalliance.intercept.MethodInvocation;
029import org.hibernate.validator.HibernateValidator;
030
031import org.springframework.beans.factory.FactoryBean;
032import org.springframework.beans.factory.SmartFactoryBean;
033import org.springframework.core.BridgeMethodResolver;
034import org.springframework.core.annotation.AnnotationUtils;
035import org.springframework.util.ClassUtils;
036import org.springframework.util.ReflectionUtils;
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 4.0, this functionality requires either a Bean Validation 1.1 provider
054 * (such as Hibernate Validator 5.x) or the Bean Validation 1.0 API with Hibernate Validator
055 * 4.3. The actual provider will be autodetected and automatically adapted.
056 *
057 * @author Juergen Hoeller
058 * @since 3.1
059 * @see MethodValidationPostProcessor
060 * @see javax.validation.executable.ExecutableValidator
061 * @see org.hibernate.validator.method.MethodValidator
062 */
063public class MethodValidationInterceptor implements MethodInterceptor {
064
065        private static Method forExecutablesMethod;
066
067        private static Method validateParametersMethod;
068
069        private static Method validateReturnValueMethod;
070
071        static {
072                try {
073                        forExecutablesMethod = Validator.class.getMethod("forExecutables");
074                        Class<?> executableValidatorClass = forExecutablesMethod.getReturnType();
075                        validateParametersMethod = executableValidatorClass.getMethod(
076                                        "validateParameters", Object.class, Method.class, Object[].class, Class[].class);
077                        validateReturnValueMethod = executableValidatorClass.getMethod(
078                                        "validateReturnValue", Object.class, Method.class, Object.class, Class[].class);
079                }
080                catch (Exception ex) {
081                        // Bean Validation 1.1 ExecutableValidator API not available
082                }
083        }
084
085
086        private volatile Validator validator;
087
088
089        /**
090         * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
091         */
092        public MethodValidationInterceptor() {
093                this(forExecutablesMethod != null ? Validation.buildDefaultValidatorFactory() :
094                                HibernateValidatorDelegate.buildValidatorFactory());
095        }
096
097        /**
098         * Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
099         * @param validatorFactory the JSR-303 ValidatorFactory to use
100         */
101        public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
102                this(validatorFactory.getValidator());
103        }
104
105        /**
106         * Create a new MethodValidationInterceptor using the given JSR-303 Validator.
107         * @param validator the JSR-303 Validator to use
108         */
109        public MethodValidationInterceptor(Validator validator) {
110                this.validator = validator;
111        }
112
113
114        @Override
115        @SuppressWarnings("unchecked")
116        public Object invoke(MethodInvocation invocation) throws Throwable {
117                // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
118                if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
119                        return invocation.proceed();
120                }
121
122                Class<?>[] groups = determineValidationGroups(invocation);
123
124                if (forExecutablesMethod != null) {
125                        // Standard Bean Validation 1.1 API
126                        Object execVal;
127                        try {
128                                execVal = ReflectionUtils.invokeMethod(forExecutablesMethod, this.validator);
129                        }
130                        catch (AbstractMethodError err) {
131                                // Probably an adapter (maybe a lazy-init proxy) without BV 1.1 support
132                                Validator nativeValidator = this.validator.unwrap(Validator.class);
133                                execVal = ReflectionUtils.invokeMethod(forExecutablesMethod, nativeValidator);
134                                // If successful, store native Validator for further use
135                                this.validator = nativeValidator;
136                        }
137
138                        Method methodToValidate = invocation.getMethod();
139                        Set<ConstraintViolation<?>> result;
140
141                        try {
142                                result = (Set<ConstraintViolation<?>>) ReflectionUtils.invokeMethod(validateParametersMethod,
143                                                execVal, invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
144                        }
145                        catch (IllegalArgumentException ex) {
146                                // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
147                                // Let's try to find the bridged method on the implementation class...
148                                methodToValidate = BridgeMethodResolver.findBridgedMethod(
149                                                ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
150                                result = (Set<ConstraintViolation<?>>) ReflectionUtils.invokeMethod(validateParametersMethod,
151                                                execVal, invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
152                        }
153                        if (!result.isEmpty()) {
154                                throw new ConstraintViolationException(result);
155                        }
156
157                        Object returnValue = invocation.proceed();
158                        result = (Set<ConstraintViolation<?>>) ReflectionUtils.invokeMethod(validateReturnValueMethod,
159                                        execVal, invocation.getThis(), methodToValidate, returnValue, groups);
160                        if (!result.isEmpty()) {
161                                throw new ConstraintViolationException(result);
162                        }
163                        return returnValue;
164                }
165
166                else {
167                        // Hibernate Validator 4.3's native API
168                        return HibernateValidatorDelegate.invokeWithinValidation(invocation, this.validator, groups);
169                }
170        }
171
172        private boolean isFactoryBeanMetadataMethod(Method method) {
173                Class<?> clazz = method.getDeclaringClass();
174
175                // Call from interface-based proxy handle, allowing for an efficient check?
176                if (clazz.isInterface()) {
177                        return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) &&
178                                        !method.getName().equals("getObject"));
179                }
180
181                // Call from CGLIB proxy handle, potentially implementing a FactoryBean method?
182                Class<?> factoryBeanType = null;
183                if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
184                        factoryBeanType = SmartFactoryBean.class;
185                }
186                else if (FactoryBean.class.isAssignableFrom(clazz)) {
187                        factoryBeanType = FactoryBean.class;
188                }
189                return (factoryBeanType != null && !method.getName().equals("getObject") &&
190                                ClassUtils.hasMethod(factoryBeanType, method.getName(), method.getParameterTypes()));
191        }
192
193        /**
194         * Determine the validation groups to validate against for the given method invocation.
195         * <p>Default are the validation groups as specified in the {@link Validated} annotation
196         * on the containing target class of the method.
197         * @param invocation the current MethodInvocation
198         * @return the applicable validation groups as a Class array
199         */
200        protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
201                Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
202                if (validatedAnn == null) {
203                        validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
204                }
205                return (validatedAnn != null ? validatedAnn.value() : new Class<?>[0]);
206        }
207
208
209        /**
210         * Inner class to avoid a hard-coded Hibernate Validator 4.3 dependency.
211         */
212        private static class HibernateValidatorDelegate {
213
214                public static ValidatorFactory buildValidatorFactory() {
215                        return Validation.byProvider(HibernateValidator.class).configure().buildValidatorFactory();
216                }
217
218                @SuppressWarnings("deprecation")
219                public static Object invokeWithinValidation(MethodInvocation invocation, Validator validator, Class<?>[] groups)
220                                throws Throwable {
221
222                        org.hibernate.validator.method.MethodValidator methodValidator =
223                                        validator.unwrap(org.hibernate.validator.method.MethodValidator.class);
224                        Set<org.hibernate.validator.method.MethodConstraintViolation<Object>> result =
225                                        methodValidator.validateAllParameters(
226                                                        invocation.getThis(), invocation.getMethod(), invocation.getArguments(), groups);
227                        if (!result.isEmpty()) {
228                                throw new org.hibernate.validator.method.MethodConstraintViolationException(result);
229                        }
230                        Object returnValue = invocation.proceed();
231                        result = methodValidator.validateReturnValue(
232                                        invocation.getThis(), invocation.getMethod(), returnValue, groups);
233                        if (!result.isEmpty()) {
234                                throw new org.hibernate.validator.method.MethodConstraintViolationException(result);
235                        }
236                        return returnValue;
237                }
238        }
239
240}