diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java index 1cc392278f..a5d7944599 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/LocalValidatorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -17,7 +17,14 @@ package org.springframework.validation.beanvalidation; import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.Properties; import javax.validation.Configuration; @@ -31,12 +38,18 @@ import javax.validation.ValidatorFactory; import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.MessageSource; +import org.springframework.context.support.MessageSourceResourceBundle; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.io.Resource; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; /** * This is the central class for {@code javax.validation} (JSR-303) setup @@ -51,6 +64,15 @@ import org.springframework.util.CollectionUtils; * you will almost always use the default Validator anyway. This can also be injected directly * into any target dependency of type {@link org.springframework.validation.Validator}! * + *

As of Spring 4.0, this class supports Bean Validation 1.0 and 1.1, with special support + * for Hibernate Validator 4.x and 5.0 (see {@link #setValidationMessageSource}). + * + *

Note that Bean Validation 1.1's {@code #forExecutables} method isn't supported: We do not + * expect that method to be called by application code; consider {@link MethodValidationInterceptor} + * instead. If you really need programmatic {@code #forExecutables} access, inject this class as + * a {@link ValidatorFactory} and call {@link #getValidator()} on it, then {@code #forExecutables} + * on the returned native {@link Validator} reference instead of directly on this class. + * * @author Juergen Hoeller * @since 3.0 * @see javax.validation.ValidatorFactory @@ -59,7 +81,10 @@ import org.springframework.util.CollectionUtils; * @see javax.validation.ValidatorFactory#getValidator() */ public class LocalValidatorFactoryBean extends SpringValidatorAdapter - implements ValidatorFactory, ApplicationContextAware, InitializingBean { + implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean { + + private static final Method closeMethod = ClassUtils.getMethodIfAvailable(ValidatorFactory.class, "close"); + @SuppressWarnings("rawtypes") private Class providerClass; @@ -70,6 +95,8 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter private ConstraintValidatorFactory constraintValidatorFactory; + private ParameterNameDiscoverer parameterNameDiscoverer; + private Resource[] mappingLocations; private final Map validationPropertyMap = new HashMap(); @@ -109,9 +136,8 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter *

Specify either this property or {@link #setMessageInterpolator "messageInterpolator"}, * not both. If you would like to build a custom MessageInterpolator, consider deriving from * Hibernate Validator's {@link ResourceBundleMessageInterpolator} and passing in a - * Spring {@link MessageSourceResourceBundleLocator} when constructing your interpolator. + * Spring-based {@code ResourceBundleLocator} when constructing your interpolator. * @see ResourceBundleMessageInterpolator - * @see MessageSourceResourceBundleLocator */ public void setValidationMessageSource(MessageSource messageSource) { this.messageInterpolator = HibernateValidatorDelegate.buildMessageInterpolator(messageSource); @@ -134,6 +160,15 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter this.constraintValidatorFactory = constraintValidatorFactory; } + /** + * Set the ParameterNameDiscoverer to use for resolving method and constructor + * parameter names if needed for message interpolation. + *

Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}. + */ + public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { + this.parameterNameDiscoverer = parameterNameDiscoverer; + } + /** * Specify resource locations to load XML constraint mapping files from, if any. */ @@ -202,6 +237,8 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter configuration.constraintValidatorFactory(targetConstraintValidatorFactory); } + configureParameterNameProviderIfPossible(configuration); + if (this.mappingLocations != null) { for (Resource location : this.mappingLocations) { try { @@ -221,6 +258,58 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter setTargetValidator(this.validatorFactory.getValidator()); } + private void configureParameterNameProviderIfPossible(Configuration configuration) { + try { + Class parameterNameProviderClass = + ClassUtils.forName("javax.validation.ParameterNameProvider", getClass().getClassLoader()); + Method parameterNameProviderMethod = + Configuration.class.getMethod("parameterNameProvider", parameterNameProviderClass); + final Object defaultProvider = ReflectionUtils.invokeMethod( + Configuration.class.getMethod("getDefaultParameterNameProvider"), configuration); + final ParameterNameDiscoverer discoverer = (this.parameterNameDiscoverer != null ? + this.parameterNameDiscoverer : new LocalVariableTableParameterNameDiscoverer()); + Object parameterNameProvider = Proxy.newProxyInstance(getClass().getClassLoader(), + new Class[] {parameterNameProviderClass}, new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("getParameterNames")) { + String[] result = null; + if (args[0] instanceof Constructor) { + result = discoverer.getParameterNames((Constructor) args[0]); + } + else if (args[0] instanceof Method) { + result = discoverer.getParameterNames((Method) args[0]); + } + if (result != null) { + return Arrays.asList(result); + } + else { + try { + return method.invoke(defaultProvider, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + else { + // toString, equals, hashCode + try { + return method.invoke(this, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + }); + ReflectionUtils.invokeMethod(parameterNameProviderMethod, configuration, parameterNameProvider); + + } + catch (Exception ex) { + // Bean Validation 1.1 API not available - simply not applying the ParameterNameDiscoverer + } + } + public Validator getValidator() { return this.validatorFactory.getValidator(); @@ -242,14 +331,61 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter return this.validatorFactory.getConstraintValidatorFactory(); } + public void close() { + ReflectionUtils.invokeMethod(closeMethod, this.validatorFactory); + } + + public void destroy() { + close(); + } + /** - * Inner class to avoid a hard-coded Hibernate Validator 4.1 dependency. + * Inner class to avoid a hard-coded Hibernate Validator dependency. */ private static class HibernateValidatorDelegate { - public static MessageInterpolator buildMessageInterpolator(MessageSource messageSource) { - return new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(messageSource)); + public static MessageInterpolator buildMessageInterpolator(final MessageSource messageSource) { + Class locatorClass; + try { + // Hibernate Validator 5.x + locatorClass = ClassUtils.forName( + "org.hibernate.validator.spi.resourceloading.ResourceBundleLocator", + HibernateValidatorDelegate.class.getClassLoader()); + } + catch (ClassNotFoundException ex) { + try { + // Hibernate Validator 4.x + locatorClass = ClassUtils.forName( + "org.hibernate.validator.resourceloading.ResourceBundleLocator", + HibernateValidatorDelegate.class.getClassLoader()); + } + catch (ClassNotFoundException ex2) { + throw new IllegalStateException("Neither Hibernate Validator 5.x nor 4.x API found"); + } + } + Object locator = Proxy.newProxyInstance(HibernateValidatorDelegate.class.getClassLoader(), + new Class[] {locatorClass}, new InvocationHandler() { + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if (method.getName().equals("getResourceBundle")) { + return new MessageSourceResourceBundle(messageSource, (Locale) args[0]); + } + else { + try { + return method.invoke(this, args); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } + } + }); + try { + return ResourceBundleMessageInterpolator.class.getConstructor(locatorClass).newInstance(locator); + } + catch (Exception ex) { + throw new IllegalStateException("Unexpected Hibernate Validator API mismatch", ex); + } } } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MessageSourceResourceBundleLocator.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MessageSourceResourceBundleLocator.java deleted file mode 100644 index 9b9dac21b5..0000000000 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MessageSourceResourceBundleLocator.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2002-2012 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 - * - * http://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.util.Locale; -import java.util.ResourceBundle; - -import org.hibernate.validator.resourceloading.ResourceBundleLocator; - -import org.springframework.context.MessageSource; -import org.springframework.context.support.MessageSourceResourceBundle; -import org.springframework.util.Assert; - -/** - * Implementation of Hibernate Validator 4.1's {@link ResourceBundleLocator} interface, - * exposing a Spring {@link MessageSource} as localized {@link MessageSourceResourceBundle}. - * - * @author Juergen Hoeller - * @since 3.0.4 - * @see ResourceBundleLocator - * @see MessageSource - * @see MessageSourceResourceBundle - */ -public class MessageSourceResourceBundleLocator implements ResourceBundleLocator { - - private final MessageSource messageSource; - - /** - * Build a MessageSourceResourceBundleLocator for the given MessageSource. - * @param messageSource the Spring MessageSource to wrap - */ - public MessageSourceResourceBundleLocator(MessageSource messageSource) { - Assert.notNull(messageSource, "MessageSource must not be null"); - this.messageSource = messageSource; - } - - public ResourceBundle getResourceBundle(Locale locale) { - return new MessageSourceResourceBundle(this.messageSource, locale); - } - -} diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java index 3d4b907fa2..688ae6eab5 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java @@ -16,7 +16,10 @@ package org.springframework.validation.beanvalidation; +import java.lang.reflect.Method; import java.util.Set; +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; @@ -29,6 +32,7 @@ import org.hibernate.validator.method.MethodConstraintViolationException; import org.hibernate.validator.method.MethodValidator; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.validation.annotation.Validated; /** @@ -45,25 +49,48 @@ import org.springframework.validation.annotation.Validated; * at the type level of the containing target class, applying to all public service methods * of that class. By default, JSR-303 will validate against its default group only. * - *

As of Spring 3.1, this functionality requires Hibernate Validator 4.2 or higher. - * Once Bean Validation 1.1 becomes available, this class will autodetect a compliant - * provider and automatically use the standard method validation support there. + *

As of Spring 4.0, this functionality requires either a Bean Validation 1.1 provider + * or Bean Validation 1.0 with Hibernate Validator 4.2 or higher. The actual provider + * will be autodetected and automatically adapted. * * @author Juergen Hoeller * @since 3.1 * @see MethodValidationPostProcessor + * @see javax.validation.executable.ExecutableValidator * @see org.hibernate.validator.method.MethodValidator */ public class MethodValidationInterceptor implements MethodInterceptor { - private final MethodValidator validator; + private static Method forExecutablesMethod; + + private static Method validateParametersMethod; + + private static Method validateReturnValueMethod; + + static { + try { + forExecutablesMethod = Validator.class.getMethod("forExecutables"); + Class executableValidatorClass = forExecutablesMethod.getReturnType(); + validateParametersMethod = executableValidatorClass.getMethod( + "validateParameters", Object.class, Method.class, Object[].class, Class[].class); + validateReturnValueMethod = executableValidatorClass.getMethod( + "validateReturnValue", Object.class, Method.class, Object.class, Class[].class); + } + catch (Exception ex) { + // Bean Validation 1.1 ExecutableValidator API not available + } + } + + + private final Validator validator; /** * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath. */ public MethodValidationInterceptor() { - this(Validation.byProvider(HibernateValidator.class).configure().buildValidatorFactory()); + this(forExecutablesMethod != null ? Validation.buildDefaultValidatorFactory() : + HibernateValidatorDelegate.buildValidatorFactory()); } /** @@ -79,24 +106,33 @@ public class MethodValidationInterceptor implements MethodInterceptor { * @param validator the JSR-303 Validator to use */ public MethodValidationInterceptor(Validator validator) { - this.validator = validator.unwrap(MethodValidator.class); + this.validator = validator; } + @SuppressWarnings("unchecked") public Object invoke(MethodInvocation invocation) throws Throwable { Class[] groups = determineValidationGroups(invocation); - Set> result = this.validator.validateAllParameters( - invocation.getThis(), invocation.getMethod(), invocation.getArguments(), groups); - if (!result.isEmpty()) { - throw new MethodConstraintViolationException(result); + if (forExecutablesMethod != null) { + Object executableValidator = ReflectionUtils.invokeMethod(forExecutablesMethod, this.validator); + Set> result = (Set>) + ReflectionUtils.invokeMethod(validateParametersMethod, executableValidator, + invocation.getThis(), invocation.getMethod(), invocation.getArguments(), groups); + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + Object returnValue = invocation.proceed(); + result = (Set>) + ReflectionUtils.invokeMethod(validateReturnValueMethod, executableValidator, + invocation.getThis(), invocation.getMethod(), returnValue, groups); + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + return returnValue; } - Object returnValue = invocation.proceed(); - result = this.validator.validateReturnValue( - invocation.getThis(), invocation.getMethod(), returnValue, groups); - if (!result.isEmpty()) { - throw new MethodConstraintViolationException(result); + else { + return HibernateValidatorDelegate.invokeWithinValidation(invocation, this.validator, groups); } - return returnValue; } /** @@ -111,4 +147,33 @@ public class MethodValidationInterceptor implements MethodInterceptor { return (valid != null ? valid.value() : new Class[0]); } + + /** + * Inner class to avoid a hard-coded Hibernate Validator dependency. + */ + private static class HibernateValidatorDelegate { + + public static ValidatorFactory buildValidatorFactory() { + return Validation.byProvider(HibernateValidator.class).configure().buildValidatorFactory(); + } + + public static Object invokeWithinValidation(MethodInvocation invocation, Validator validator, Class[] groups) + throws Throwable { + + MethodValidator methodValidator = validator.unwrap(MethodValidator.class); + Set> result = methodValidator.validateAllParameters( + invocation.getThis(), invocation.getMethod(), invocation.getArguments(), groups); + if (!result.isEmpty()) { + throw new MethodConstraintViolationException(result); + } + Object returnValue = invocation.proceed(); + result = methodValidator.validateReturnValue( + invocation.getThis(), invocation.getMethod(), returnValue, groups); + if (!result.isEmpty()) { + throw new MethodConstraintViolationException(result); + } + return returnValue; + } + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java index 3c4cff91d2..f6d6407f6b 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringConstraintValidatorFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -50,4 +50,8 @@ public class SpringConstraintValidatorFactory implements ConstraintValidatorFact return this.beanFactory.createBean(key); } + public void releaseInstance(ConstraintValidator instance) { + this.beanFactory.destroyBean(instance); + } + } diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java index 214f797fa0..83b650c6aa 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2013 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. @@ -22,7 +22,6 @@ import javax.validation.constraints.Max; import javax.validation.constraints.NotNull; import javax.validation.groups.Default; -import org.hibernate.validator.method.MethodConstraintViolationException; import org.junit.Test; import org.springframework.aop.framework.ProxyFactory; @@ -61,21 +60,21 @@ public class MethodValidationTests { assertNotNull(proxy.myValidMethod("value", 15)); fail("Should have thrown MethodConstraintViolationException"); } - catch (MethodConstraintViolationException ex) { + catch (javax.validation.ValidationException ex) { // expected } try { assertNotNull(proxy.myValidMethod(null, 5)); fail("Should have thrown MethodConstraintViolationException"); } - catch (MethodConstraintViolationException ex) { + catch (javax.validation.ValidationException ex) { // expected } try { assertNotNull(proxy.myValidMethod("value", 0)); fail("Should have thrown MethodConstraintViolationException"); } - catch (MethodConstraintViolationException ex) { + catch (javax.validation.ValidationException ex) { // expected } }