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.io.IOException;
020import java.lang.reflect.Constructor;
021import java.lang.reflect.Method;
022import java.util.Arrays;
023import java.util.HashMap;
024import java.util.List;
025import java.util.Map;
026import java.util.Properties;
027
028import javax.validation.Configuration;
029import javax.validation.ConstraintValidatorFactory;
030import javax.validation.MessageInterpolator;
031import javax.validation.ParameterNameProvider;
032import javax.validation.TraversableResolver;
033import javax.validation.Validation;
034import javax.validation.ValidationException;
035import javax.validation.ValidationProviderResolver;
036import javax.validation.Validator;
037import javax.validation.ValidatorContext;
038import javax.validation.ValidatorFactory;
039import javax.validation.bootstrap.GenericBootstrap;
040import javax.validation.bootstrap.ProviderSpecificBootstrap;
041
042import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator;
043
044import org.springframework.beans.factory.DisposableBean;
045import org.springframework.beans.factory.InitializingBean;
046import org.springframework.context.ApplicationContext;
047import org.springframework.context.ApplicationContextAware;
048import org.springframework.context.MessageSource;
049import org.springframework.core.DefaultParameterNameDiscoverer;
050import org.springframework.core.ParameterNameDiscoverer;
051import org.springframework.core.io.Resource;
052import org.springframework.lang.Nullable;
053import org.springframework.util.Assert;
054import org.springframework.util.CollectionUtils;
055import org.springframework.util.ReflectionUtils;
056
057/**
058 * This is the central class for {@code javax.validation} (JSR-303) setup in a Spring
059 * application context: It bootstraps a {@code javax.validation.ValidationFactory} and
060 * exposes it through the Spring {@link org.springframework.validation.Validator} interface
061 * as well as through the JSR-303 {@link javax.validation.Validator} interface and the
062 * {@link javax.validation.ValidatorFactory} interface itself.
063 *
064 * <p>When talking to an instance of this bean through the Spring or JSR-303 Validator interfaces,
065 * you'll be talking to the default Validator of the underlying ValidatorFactory. This is very
066 * convenient in that you don't have to perform yet another call on the factory, assuming that
067 * you will almost always use the default Validator anyway. This can also be injected directly
068 * into any target dependency of type {@link org.springframework.validation.Validator}!
069 *
070 * <p><b>As of Spring 5.0, this class requires Bean Validation 1.1+, with special support
071 * for Hibernate Validator 5.x</b> (see {@link #setValidationMessageSource}).
072 * This class is also runtime-compatible with Bean Validation 2.0 and Hibernate Validator 6.0,
073 * with one special note: If you'd like to call BV 2.0's {@code getClockProvider()} method,
074 * obtain the native {@code ValidatorFactory} through {@code #unwrap(ValidatorFactory.class)}
075 * and call the {@code getClockProvider()} method on the returned native reference there.
076 *
077 * <p>This class is also being used by Spring's MVC configuration namespace, in case of the
078 * {@code javax.validation} API being present but no explicit Validator having been configured.
079 *
080 * @author Juergen Hoeller
081 * @since 3.0
082 * @see javax.validation.ValidatorFactory
083 * @see javax.validation.Validator
084 * @see javax.validation.Validation#buildDefaultValidatorFactory()
085 * @see javax.validation.ValidatorFactory#getValidator()
086 */
087public class LocalValidatorFactoryBean extends SpringValidatorAdapter
088                implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean {
089
090        @SuppressWarnings("rawtypes")
091        @Nullable
092        private Class providerClass;
093
094        @Nullable
095        private ValidationProviderResolver validationProviderResolver;
096
097        @Nullable
098        private MessageInterpolator messageInterpolator;
099
100        @Nullable
101        private TraversableResolver traversableResolver;
102
103        @Nullable
104        private ConstraintValidatorFactory constraintValidatorFactory;
105
106        @Nullable
107        private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
108
109        @Nullable
110        private Resource[] mappingLocations;
111
112        private final Map<String, String> validationPropertyMap = new HashMap<>();
113
114        @Nullable
115        private ApplicationContext applicationContext;
116
117        @Nullable
118        private ValidatorFactory validatorFactory;
119
120
121        /**
122         * Specify the desired provider class, if any.
123         * <p>If not specified, JSR-303's default search mechanism will be used.
124         * @see javax.validation.Validation#byProvider(Class)
125         * @see javax.validation.Validation#byDefaultProvider()
126         */
127        @SuppressWarnings("rawtypes")
128        public void setProviderClass(Class providerClass) {
129                this.providerClass = providerClass;
130        }
131
132        /**
133         * Specify a JSR-303 {@link ValidationProviderResolver} for bootstrapping the
134         * provider of choice, as an alternative to {@code META-INF} driven resolution.
135         * @since 4.3
136         */
137        public void setValidationProviderResolver(ValidationProviderResolver validationProviderResolver) {
138                this.validationProviderResolver = validationProviderResolver;
139        }
140
141        /**
142         * Specify a custom MessageInterpolator to use for this ValidatorFactory
143         * and its exposed default Validator.
144         */
145        public void setMessageInterpolator(MessageInterpolator messageInterpolator) {
146                this.messageInterpolator = messageInterpolator;
147        }
148
149        /**
150         * Specify a custom Spring MessageSource for resolving validation messages,
151         * instead of relying on JSR-303's default "ValidationMessages.properties" bundle
152         * in the classpath. This may refer to a Spring context's shared "messageSource" bean,
153         * or to some special MessageSource setup for validation purposes only.
154         * <p><b>NOTE:</b> This feature requires Hibernate Validator 4.3 or higher on the classpath.
155         * You may nevertheless use a different validation provider but Hibernate Validator's
156         * {@link ResourceBundleMessageInterpolator} class must be accessible during configuration.
157         * <p>Specify either this property or {@link #setMessageInterpolator "messageInterpolator"},
158         * not both. If you would like to build a custom MessageInterpolator, consider deriving from
159         * Hibernate Validator's {@link ResourceBundleMessageInterpolator} and passing in a
160         * Spring-based {@code ResourceBundleLocator} when constructing your interpolator.
161         * <p>In order for Hibernate's default validation messages to be resolved still, your
162         * {@link MessageSource} must be configured for optional resolution (usually the default).
163         * In particular, the {@code MessageSource} instance specified here should not apply
164         * {@link org.springframework.context.support.AbstractMessageSource#setUseCodeAsDefaultMessage
165         * "useCodeAsDefaultMessage"} behavior. Please double-check your setup accordingly.
166         * @see ResourceBundleMessageInterpolator
167         */
168        public void setValidationMessageSource(MessageSource messageSource) {
169                this.messageInterpolator = HibernateValidatorDelegate.buildMessageInterpolator(messageSource);
170        }
171
172        /**
173         * Specify a custom TraversableResolver to use for this ValidatorFactory
174         * and its exposed default Validator.
175         */
176        public void setTraversableResolver(TraversableResolver traversableResolver) {
177                this.traversableResolver = traversableResolver;
178        }
179
180        /**
181         * Specify a custom ConstraintValidatorFactory to use for this ValidatorFactory.
182         * <p>Default is a {@link SpringConstraintValidatorFactory}, delegating to the
183         * containing ApplicationContext for creating autowired ConstraintValidator instances.
184         */
185        public void setConstraintValidatorFactory(ConstraintValidatorFactory constraintValidatorFactory) {
186                this.constraintValidatorFactory = constraintValidatorFactory;
187        }
188
189        /**
190         * Set the ParameterNameDiscoverer to use for resolving method and constructor
191         * parameter names if needed for message interpolation.
192         * <p>Default is a {@link org.springframework.core.DefaultParameterNameDiscoverer}.
193         */
194        public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
195                this.parameterNameDiscoverer = parameterNameDiscoverer;
196        }
197
198        /**
199         * Specify resource locations to load XML constraint mapping files from, if any.
200         */
201        public void setMappingLocations(Resource... mappingLocations) {
202                this.mappingLocations = mappingLocations;
203        }
204
205        /**
206         * Specify bean validation properties to be passed to the validation provider.
207         * <p>Can be populated with a String "value" (parsed via PropertiesEditor)
208         * or a "props" element in XML bean definitions.
209         * @see javax.validation.Configuration#addProperty(String, String)
210         */
211        public void setValidationProperties(Properties jpaProperties) {
212                CollectionUtils.mergePropertiesIntoMap(jpaProperties, this.validationPropertyMap);
213        }
214
215        /**
216         * Specify bean validation properties to be passed to the validation provider as a Map.
217         * <p>Can be populated with a "map" or "props" element in XML bean definitions.
218         * @see javax.validation.Configuration#addProperty(String, String)
219         */
220        public void setValidationPropertyMap(@Nullable Map<String, String> validationProperties) {
221                if (validationProperties != null) {
222                        this.validationPropertyMap.putAll(validationProperties);
223                }
224        }
225
226        /**
227         * Allow Map access to the bean validation properties to be passed to the validation provider,
228         * with the option to add or override specific entries.
229         * <p>Useful for specifying entries directly, for example via "validationPropertyMap[myKey]".
230         */
231        public Map<String, String> getValidationPropertyMap() {
232                return this.validationPropertyMap;
233        }
234
235        @Override
236        public void setApplicationContext(ApplicationContext applicationContext) {
237                this.applicationContext = applicationContext;
238        }
239
240
241        @Override
242        @SuppressWarnings({"rawtypes", "unchecked"})
243        public void afterPropertiesSet() {
244                Configuration<?> configuration;
245                if (this.providerClass != null) {
246                        ProviderSpecificBootstrap bootstrap = Validation.byProvider(this.providerClass);
247                        if (this.validationProviderResolver != null) {
248                                bootstrap = bootstrap.providerResolver(this.validationProviderResolver);
249                        }
250                        configuration = bootstrap.configure();
251                }
252                else {
253                        GenericBootstrap bootstrap = Validation.byDefaultProvider();
254                        if (this.validationProviderResolver != null) {
255                                bootstrap = bootstrap.providerResolver(this.validationProviderResolver);
256                        }
257                        configuration = bootstrap.configure();
258                }
259
260                // Try Hibernate Validator 5.2's externalClassLoader(ClassLoader) method
261                if (this.applicationContext != null) {
262                        try {
263                                Method eclMethod = configuration.getClass().getMethod("externalClassLoader", ClassLoader.class);
264                                ReflectionUtils.invokeMethod(eclMethod, configuration, this.applicationContext.getClassLoader());
265                        }
266                        catch (NoSuchMethodException ex) {
267                                // Ignore - no Hibernate Validator 5.2+ or similar provider
268                        }
269                }
270
271                MessageInterpolator targetInterpolator = this.messageInterpolator;
272                if (targetInterpolator == null) {
273                        targetInterpolator = configuration.getDefaultMessageInterpolator();
274                }
275                configuration.messageInterpolator(new LocaleContextMessageInterpolator(targetInterpolator));
276
277                if (this.traversableResolver != null) {
278                        configuration.traversableResolver(this.traversableResolver);
279                }
280
281                ConstraintValidatorFactory targetConstraintValidatorFactory = this.constraintValidatorFactory;
282                if (targetConstraintValidatorFactory == null && this.applicationContext != null) {
283                        targetConstraintValidatorFactory =
284                                        new SpringConstraintValidatorFactory(this.applicationContext.getAutowireCapableBeanFactory());
285                }
286                if (targetConstraintValidatorFactory != null) {
287                        configuration.constraintValidatorFactory(targetConstraintValidatorFactory);
288                }
289
290                if (this.parameterNameDiscoverer != null) {
291                        configureParameterNameProvider(this.parameterNameDiscoverer, configuration);
292                }
293
294                if (this.mappingLocations != null) {
295                        for (Resource location : this.mappingLocations) {
296                                try {
297                                        configuration.addMapping(location.getInputStream());
298                                }
299                                catch (IOException ex) {
300                                        throw new IllegalStateException("Cannot read mapping resource: " + location);
301                                }
302                        }
303                }
304
305                this.validationPropertyMap.forEach(configuration::addProperty);
306
307                // Allow for custom post-processing before we actually build the ValidatorFactory.
308                postProcessConfiguration(configuration);
309
310                this.validatorFactory = configuration.buildValidatorFactory();
311                setTargetValidator(this.validatorFactory.getValidator());
312        }
313
314        private void configureParameterNameProvider(ParameterNameDiscoverer discoverer, Configuration<?> configuration) {
315                final ParameterNameProvider defaultProvider = configuration.getDefaultParameterNameProvider();
316                configuration.parameterNameProvider(new ParameterNameProvider() {
317                        @Override
318                        public List<String> getParameterNames(Constructor<?> constructor) {
319                                String[] paramNames = discoverer.getParameterNames(constructor);
320                                return (paramNames != null ? Arrays.asList(paramNames) :
321                                                defaultProvider.getParameterNames(constructor));
322                        }
323                        @Override
324                        public List<String> getParameterNames(Method method) {
325                                String[] paramNames = discoverer.getParameterNames(method);
326                                return (paramNames != null ? Arrays.asList(paramNames) :
327                                                defaultProvider.getParameterNames(method));
328                        }
329                });
330        }
331
332        /**
333         * Post-process the given Bean Validation configuration,
334         * adding to or overriding any of its settings.
335         * <p>Invoked right before building the {@link ValidatorFactory}.
336         * @param configuration the Configuration object, pre-populated with
337         * settings driven by LocalValidatorFactoryBean's properties
338         */
339        protected void postProcessConfiguration(Configuration<?> configuration) {
340        }
341
342
343        @Override
344        public Validator getValidator() {
345                Assert.notNull(this.validatorFactory, "No target ValidatorFactory set");
346                return this.validatorFactory.getValidator();
347        }
348
349        @Override
350        public ValidatorContext usingContext() {
351                Assert.notNull(this.validatorFactory, "No target ValidatorFactory set");
352                return this.validatorFactory.usingContext();
353        }
354
355        @Override
356        public MessageInterpolator getMessageInterpolator() {
357                Assert.notNull(this.validatorFactory, "No target ValidatorFactory set");
358                return this.validatorFactory.getMessageInterpolator();
359        }
360
361        @Override
362        public TraversableResolver getTraversableResolver() {
363                Assert.notNull(this.validatorFactory, "No target ValidatorFactory set");
364                return this.validatorFactory.getTraversableResolver();
365        }
366
367        @Override
368        public ConstraintValidatorFactory getConstraintValidatorFactory() {
369                Assert.notNull(this.validatorFactory, "No target ValidatorFactory set");
370                return this.validatorFactory.getConstraintValidatorFactory();
371        }
372
373        @Override
374        public ParameterNameProvider getParameterNameProvider() {
375                Assert.notNull(this.validatorFactory, "No target ValidatorFactory set");
376                return this.validatorFactory.getParameterNameProvider();
377        }
378
379        // Bean Validation 2.0: currently not implemented here since it would imply
380        // a hard dependency on the new javax.validation.ClockProvider interface.
381        // To be resolved once Spring Framework requires Bean Validation 2.0+.
382        // Obtain the native ValidatorFactory through unwrap(ValidatorFactory.class)
383        // instead which will fully support a getClockProvider() call as well.
384        /*
385        @Override
386        public javax.validation.ClockProvider getClockProvider() {
387                Assert.notNull(this.validatorFactory, "No target ValidatorFactory set");
388                return this.validatorFactory.getClockProvider();
389        }
390        */
391
392        @Override
393        @SuppressWarnings("unchecked")
394        public <T> T unwrap(@Nullable Class<T> type) {
395                if (type == null || !ValidatorFactory.class.isAssignableFrom(type)) {
396                        try {
397                                return super.unwrap(type);
398                        }
399                        catch (ValidationException ex) {
400                                // ignore - we'll try ValidatorFactory unwrapping next
401                        }
402                }
403                if (this.validatorFactory != null) {
404                        try {
405                                return this.validatorFactory.unwrap(type);
406                        }
407                        catch (ValidationException ex) {
408                                // ignore if just being asked for ValidatorFactory
409                                if (ValidatorFactory.class == type) {
410                                        return (T) this.validatorFactory;
411                                }
412                                throw ex;
413                        }
414                }
415                throw new ValidationException("Cannot unwrap to " + type);
416        }
417
418        @Override
419        public void close() {
420                if (this.validatorFactory != null) {
421                        this.validatorFactory.close();
422                }
423        }
424
425        @Override
426        public void destroy() {
427                close();
428        }
429
430
431        /**
432         * Inner class to avoid a hard-coded Hibernate Validator dependency.
433         */
434        private static class HibernateValidatorDelegate {
435
436                public static MessageInterpolator buildMessageInterpolator(MessageSource messageSource) {
437                        return new ResourceBundleMessageInterpolator(new MessageSourceResourceBundleLocator(messageSource));
438                }
439        }
440
441}