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.test.web.servlet.setup;
018
019import java.util.ArrayList;
020import java.util.Arrays;
021import java.util.Collections;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026
027import org.springframework.beans.BeansException;
028import org.springframework.beans.factory.BeanInitializationException;
029import org.springframework.beans.factory.InitializingBean;
030import org.springframework.context.ApplicationContextAware;
031import org.springframework.format.support.DefaultFormattingConversionService;
032import org.springframework.format.support.FormattingConversionService;
033import org.springframework.http.converter.HttpMessageConverter;
034import org.springframework.mock.web.MockServletContext;
035import org.springframework.util.PropertyPlaceholderHelper;
036import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
037import org.springframework.util.StringValueResolver;
038import org.springframework.validation.Validator;
039import org.springframework.web.accept.ContentNegotiationManager;
040import org.springframework.web.context.WebApplicationContext;
041import org.springframework.web.context.support.WebApplicationObjectSupport;
042import org.springframework.web.method.support.HandlerMethodArgumentResolver;
043import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
044import org.springframework.web.servlet.DispatcherServlet;
045import org.springframework.web.servlet.FlashMapManager;
046import org.springframework.web.servlet.HandlerExceptionResolver;
047import org.springframework.web.servlet.HandlerInterceptor;
048import org.springframework.web.servlet.LocaleResolver;
049import org.springframework.web.servlet.View;
050import org.springframework.web.servlet.ViewResolver;
051import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
052import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
053import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
054import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
055import org.springframework.web.servlet.handler.AbstractHandlerMapping;
056import org.springframework.web.servlet.handler.MappedInterceptor;
057import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
058import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
059import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
060import org.springframework.web.servlet.support.SessionFlashMapManager;
061import org.springframework.web.servlet.theme.FixedThemeResolver;
062import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator;
063import org.springframework.web.servlet.view.InternalResourceViewResolver;
064
065/**
066 * A {@code MockMvcBuilder} that accepts {@code @Controller} registrations
067 * thus allowing full control over the instantiation and initialization of
068 * controllers and their dependencies similar to plain unit tests, and also
069 * making it possible to test one controller at a time.
070 *
071 * <p>This builder creates the minimum infrastructure required by the
072 * {@link DispatcherServlet} to serve requests with annotated controllers and
073 * also provides methods for customization. The resulting configuration and
074 * customization options are equivalent to using MVC Java config except
075 * using builder style methods.
076 *
077 * <p>To configure view resolution, either select a "fixed" view to use for every
078 * request performed (see {@link #setSingleView(View)}) or provide a list of
079 * {@code ViewResolver}s (see {@link #setViewResolvers(ViewResolver...)}).
080 *
081 * @author Rossen Stoyanchev
082 * @since 3.2
083 */
084public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder<StandaloneMockMvcBuilder> {
085
086        private final Object[] controllers;
087
088        private List<Object> controllerAdvice;
089
090        private List<HttpMessageConverter<?>> messageConverters = new ArrayList<HttpMessageConverter<?>>();
091
092        private List<HandlerMethodArgumentResolver> customArgumentResolvers = new ArrayList<HandlerMethodArgumentResolver>();
093
094        private List<HandlerMethodReturnValueHandler> customReturnValueHandlers = new ArrayList<HandlerMethodReturnValueHandler>();
095
096        private final List<MappedInterceptor> mappedInterceptors = new ArrayList<MappedInterceptor>();
097
098        private Validator validator = null;
099
100        private ContentNegotiationManager contentNegotiationManager;
101
102        private FormattingConversionService conversionService = null;
103
104        private List<HandlerExceptionResolver> handlerExceptionResolvers;
105
106        private Long asyncRequestTimeout;
107
108        private List<ViewResolver> viewResolvers;
109
110        private LocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
111
112        private FlashMapManager flashMapManager = null;
113
114        private boolean useSuffixPatternMatch = true;
115
116        private boolean useTrailingSlashPatternMatch = true;
117
118        private Boolean removeSemicolonContent;
119
120        private Map<String, String> placeholderValues = new HashMap<String, String>();
121
122
123        /**
124         * Protected constructor. Not intended for direct instantiation.
125         * @see MockMvcBuilders#standaloneSetup(Object...)
126         */
127        protected StandaloneMockMvcBuilder(Object... controllers) {
128                this.controllers = controllers;
129        }
130
131
132        /**
133         * Register one or more {@link org.springframework.web.bind.annotation.ControllerAdvice}
134         * instances to be used in tests.
135         * <p>Normally {@code @ControllerAdvice} are auto-detected as long as they're declared
136         * as Spring beans. However since the standalone setup does not load any Spring config,
137         * they need to be registered explicitly here instead much like controllers.
138         * @since 4.2
139         */
140        public StandaloneMockMvcBuilder setControllerAdvice(Object... controllerAdvice) {
141                this.controllerAdvice = Arrays.asList(controllerAdvice);
142                return this;
143        }
144
145        /**
146         * Set the message converters to use in argument resolvers and in return value
147         * handlers, which support reading and/or writing to the body of the request
148         * and response. If no message converters are added to the list, a default
149         * list of converters is added instead.
150         */
151        public StandaloneMockMvcBuilder setMessageConverters(HttpMessageConverter<?>...messageConverters) {
152                this.messageConverters = Arrays.asList(messageConverters);
153                return this;
154        }
155
156        /**
157         * Provide a custom {@link Validator} instead of the one created by default.
158         * The default implementation used, assuming JSR-303 is on the classpath, is
159         * {@link org.springframework.validation.beanvalidation.LocalValidatorFactoryBean}.
160         */
161        public StandaloneMockMvcBuilder setValidator(Validator validator) {
162                this.validator = validator;
163                return this;
164        }
165
166        /**
167         * Provide a conversion service with custom formatters and converters.
168         * If not set, a {@link DefaultFormattingConversionService} is used by default.
169         */
170        public StandaloneMockMvcBuilder setConversionService(FormattingConversionService conversionService) {
171                this.conversionService = conversionService;
172                return this;
173        }
174
175        /**
176         * Add interceptors mapped to all incoming requests.
177         */
178        public StandaloneMockMvcBuilder addInterceptors(HandlerInterceptor... interceptors) {
179                addMappedInterceptors(null, interceptors);
180                return this;
181        }
182
183        /**
184         * Add interceptors mapped to a set of path patterns.
185         */
186        public StandaloneMockMvcBuilder addMappedInterceptors(String[] pathPatterns, HandlerInterceptor... interceptors) {
187                for (HandlerInterceptor interceptor : interceptors) {
188                        this.mappedInterceptors.add(new MappedInterceptor(pathPatterns, interceptor));
189                }
190                return this;
191        }
192
193        /**
194         * Set a ContentNegotiationManager.
195         */
196        public StandaloneMockMvcBuilder setContentNegotiationManager(ContentNegotiationManager manager) {
197                this.contentNegotiationManager = manager;
198                return this;
199        }
200
201        /**
202         * Specify the timeout value for async execution. In Spring MVC Test, this
203         * value is used to determine how to long to wait for async execution to
204         * complete so that a test can verify the results synchronously.
205         * @param timeout the timeout value in milliseconds
206         */
207        public StandaloneMockMvcBuilder setAsyncRequestTimeout(long timeout) {
208                this.asyncRequestTimeout = timeout;
209                return this;
210        }
211
212        /**
213         * Provide custom resolvers for controller method arguments.
214         */
215        public StandaloneMockMvcBuilder setCustomArgumentResolvers(HandlerMethodArgumentResolver... argumentResolvers) {
216                this.customArgumentResolvers = Arrays.asList(argumentResolvers);
217                return this;
218        }
219
220        /**
221         * Provide custom handlers for controller method return values.
222         */
223        public StandaloneMockMvcBuilder setCustomReturnValueHandlers(HandlerMethodReturnValueHandler... handlers) {
224                this.customReturnValueHandlers = Arrays.asList(handlers);
225                return this;
226        }
227
228        /**
229         * Set the HandlerExceptionResolver types to use as a list.
230         */
231        public StandaloneMockMvcBuilder setHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
232                this.handlerExceptionResolvers = exceptionResolvers;
233                return this;
234        }
235
236        /**
237         * Set the HandlerExceptionResolver types to use as an array.
238         */
239        public StandaloneMockMvcBuilder setHandlerExceptionResolvers(HandlerExceptionResolver... exceptionResolvers) {
240                this.handlerExceptionResolvers = Arrays.asList(exceptionResolvers);
241                return this;
242        }
243
244        /**
245         * Set up view resolution with the given {@link ViewResolver}s.
246         * If not set, an {@link InternalResourceViewResolver} is used by default.
247         */
248        public StandaloneMockMvcBuilder setViewResolvers(ViewResolver...resolvers) {
249                this.viewResolvers = Arrays.asList(resolvers);
250                return this;
251        }
252
253        /**
254         * Sets up a single {@link ViewResolver} that always returns the provided
255         * view instance. This is a convenient shortcut if you need to use one
256         * View instance only -- e.g. rendering generated content (JSON, XML, Atom).
257         */
258        public StandaloneMockMvcBuilder setSingleView(View view) {
259                this.viewResolvers = Collections.<ViewResolver>singletonList(new StaticViewResolver(view));
260                return this;
261        }
262
263        /**
264         * Provide a LocaleResolver instance.
265         * If not provided, the default one used is {@link AcceptHeaderLocaleResolver}.
266         */
267        public StandaloneMockMvcBuilder setLocaleResolver(LocaleResolver localeResolver) {
268                this.localeResolver = localeResolver;
269                return this;
270        }
271
272        /**
273         * Provide a custom FlashMapManager instance.
274         * If not provided, {@code SessionFlashMapManager} is used by default.
275         */
276        public StandaloneMockMvcBuilder setFlashMapManager(FlashMapManager flashMapManager) {
277                this.flashMapManager = flashMapManager;
278                return this;
279        }
280
281        /**
282         * Whether to use suffix pattern match (".*") when matching patterns to
283         * requests. If enabled a method mapped to "/users" also matches to "/users.*".
284         * <p>The default value is {@code true}.
285         */
286        public StandaloneMockMvcBuilder setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
287                this.useSuffixPatternMatch = useSuffixPatternMatch;
288                return this;
289        }
290
291        /**
292         * Whether to match to URLs irrespective of the presence of a trailing slash.
293         * If enabled a method mapped to "/users" also matches to "/users/".
294         * <p>The default value is {@code true}.
295         */
296        public StandaloneMockMvcBuilder setUseTrailingSlashPatternMatch(boolean useTrailingSlashPatternMatch) {
297                this.useTrailingSlashPatternMatch = useTrailingSlashPatternMatch;
298                return this;
299        }
300
301        /**
302         * Set if ";" (semicolon) content should be stripped from the request URI. The value,
303         * if provided, is in turn set on
304         * {@link AbstractHandlerMapping#setRemoveSemicolonContent(boolean)}.
305         */
306        public StandaloneMockMvcBuilder setRemoveSemicolonContent(boolean removeSemicolonContent) {
307                this.removeSemicolonContent = removeSemicolonContent;
308                return this;
309        }
310
311        /**
312         * In a standalone setup there is no support for placeholder values embedded in
313         * request mappings. This method allows manually provided placeholder values so they
314         * can be resolved. Alternatively consider creating a test that initializes a
315         * {@link WebApplicationContext}.
316         * @since 4.2.8
317         */
318        public StandaloneMockMvcBuilder addPlaceholderValue(String name, String value) {
319                this.placeholderValues.put(name, value);
320                return this;
321        }
322
323        /**
324         * @deprecated as of 4.2.8, in favor of {@link #addPlaceholderValue(String, String)}
325         */
326        @Deprecated
327        public StandaloneMockMvcBuilder addPlaceHolderValue(String name, String value) {
328                this.placeholderValues.put(name, value);
329                return this;
330        }
331
332
333        @Override
334        protected WebApplicationContext initWebAppContext() {
335                MockServletContext servletContext = new MockServletContext();
336                StubWebApplicationContext wac = new StubWebApplicationContext(servletContext);
337                registerMvcSingletons(wac);
338                servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, wac);
339                return wac;
340        }
341
342        private void registerMvcSingletons(StubWebApplicationContext wac) {
343                StandaloneConfiguration config = new StandaloneConfiguration();
344                config.setApplicationContext(wac);
345
346                wac.addBeans(this.controllerAdvice);
347
348                StaticRequestMappingHandlerMapping hm = config.getHandlerMapping();
349                hm.setServletContext(wac.getServletContext());
350                hm.setApplicationContext(wac);
351                hm.afterPropertiesSet();
352                hm.registerHandlers(this.controllers);
353                wac.addBean("requestMappingHandlerMapping", hm);
354
355                RequestMappingHandlerAdapter handlerAdapter = config.requestMappingHandlerAdapter();
356                handlerAdapter.setServletContext(wac.getServletContext());
357                handlerAdapter.setApplicationContext(wac);
358                handlerAdapter.afterPropertiesSet();
359                wac.addBean("requestMappingHandlerAdapter", handlerAdapter);
360
361                wac.addBean("handlerExceptionResolver", config.handlerExceptionResolver());
362
363                wac.addBeans(initViewResolvers(wac));
364                wac.addBean(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, this.localeResolver);
365                wac.addBean(DispatcherServlet.THEME_RESOLVER_BEAN_NAME, new FixedThemeResolver());
366                wac.addBean(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, new DefaultRequestToViewNameTranslator());
367
368                this.flashMapManager = new SessionFlashMapManager();
369                wac.addBean(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, this.flashMapManager);
370        }
371
372        private List<ViewResolver> initViewResolvers(WebApplicationContext wac) {
373                this.viewResolvers = (this.viewResolvers != null ? this.viewResolvers :
374                                Collections.<ViewResolver>singletonList(new InternalResourceViewResolver()));
375                for (Object viewResolver : this.viewResolvers) {
376                        if (viewResolver instanceof WebApplicationObjectSupport) {
377                                ((WebApplicationObjectSupport) viewResolver).setApplicationContext(wac);
378                        }
379                }
380                return this.viewResolvers;
381        }
382
383
384        /** Using the MVC Java configuration as the starting point for the "standalone" setup */
385        private class StandaloneConfiguration extends WebMvcConfigurationSupport {
386
387                public StaticRequestMappingHandlerMapping getHandlerMapping() {
388                        StaticRequestMappingHandlerMapping handlerMapping = new StaticRequestMappingHandlerMapping();
389                        handlerMapping.setEmbeddedValueResolver(new StaticStringValueResolver(placeholderValues));
390                        handlerMapping.setUseSuffixPatternMatch(useSuffixPatternMatch);
391                        handlerMapping.setUseTrailingSlashMatch(useTrailingSlashPatternMatch);
392                        handlerMapping.setOrder(0);
393                        handlerMapping.setInterceptors(getInterceptors());
394                        if (removeSemicolonContent != null) {
395                                handlerMapping.setRemoveSemicolonContent(removeSemicolonContent);
396                        }
397                        return handlerMapping;
398                }
399
400                @Override
401                protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
402                        converters.addAll(messageConverters);
403                }
404
405                @Override
406                protected void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
407                        argumentResolvers.addAll(customArgumentResolvers);
408                }
409
410                @Override
411                protected void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> returnValueHandlers) {
412                        returnValueHandlers.addAll(customReturnValueHandlers);
413                }
414
415                @Override
416                protected void addInterceptors(InterceptorRegistry registry) {
417                        for (MappedInterceptor interceptor : mappedInterceptors) {
418                                InterceptorRegistration registration = registry.addInterceptor(interceptor.getInterceptor());
419                                if (interceptor.getPathPatterns() != null) {
420                                        registration.addPathPatterns(interceptor.getPathPatterns());
421                                }
422                        }
423                }
424
425                @Override
426                public ContentNegotiationManager mvcContentNegotiationManager() {
427                        return (contentNegotiationManager != null) ? contentNegotiationManager : super.mvcContentNegotiationManager();
428                }
429
430                @Override
431                public FormattingConversionService mvcConversionService() {
432                        return (conversionService != null) ? conversionService : super.mvcConversionService();
433                }
434
435                @Override
436                public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
437                        if (asyncRequestTimeout != null) {
438                                configurer.setDefaultTimeout(asyncRequestTimeout);
439                        }
440                }
441
442                @Override
443                public Validator mvcValidator() {
444                        Validator mvcValidator = (validator != null) ? validator : super.mvcValidator();
445                        if (mvcValidator instanceof InitializingBean) {
446                                try {
447                                        ((InitializingBean) mvcValidator).afterPropertiesSet();
448                                }
449                                catch (Exception ex) {
450                                        throw new BeanInitializationException("Failed to initialize Validator", ex);
451                                }
452                        }
453                        return mvcValidator;
454                }
455
456                @Override
457                protected void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
458                        if (handlerExceptionResolvers == null) {
459                                return;
460                        }
461                        for (HandlerExceptionResolver resolver : handlerExceptionResolvers) {
462                                if (resolver instanceof ApplicationContextAware) {
463                                        ((ApplicationContextAware) resolver).setApplicationContext(getApplicationContext());
464                                }
465                                if (resolver instanceof InitializingBean) {
466                                        try {
467                                                ((InitializingBean) resolver).afterPropertiesSet();
468                                        }
469                                        catch (Exception ex) {
470                                                throw new IllegalStateException("Failure from afterPropertiesSet", ex);
471                                        }
472                                }
473                                exceptionResolvers.add(resolver);
474                        }
475                }
476        }
477
478
479        /**
480         * A {@code RequestMappingHandlerMapping} that allows registration of controllers.
481         */
482        private static class StaticRequestMappingHandlerMapping extends RequestMappingHandlerMapping {
483
484                public void registerHandlers(Object...handlers) {
485                        for (Object handler : handlers) {
486                                detectHandlerMethods(handler);
487                        }
488                }
489        }
490
491
492        /**
493         * A static resolver placeholder for values embedded in request mappings.
494         */
495        private static class StaticStringValueResolver implements StringValueResolver {
496
497                private final PropertyPlaceholderHelper helper;
498
499                private final PlaceholderResolver resolver;
500
501                public StaticStringValueResolver(final Map<String, String> values) {
502                        this.helper = new PropertyPlaceholderHelper("${", "}", ":", false);
503                        this.resolver = new PlaceholderResolver() {
504                                @Override
505                                public String resolvePlaceholder(String placeholderName) {
506                                        return values.get(placeholderName);
507                                }
508                        };
509                }
510
511                @Override
512                public String resolveStringValue(String strVal) throws BeansException {
513                        return this.helper.replacePlaceholders(strVal, this.resolver);
514                }
515        }
516
517
518        /**
519         * A {@link ViewResolver} that always returns same View.
520         */
521        private static class StaticViewResolver implements ViewResolver {
522
523                private final View view;
524
525                public StaticViewResolver(View view) {
526                        this.view = view;
527                }
528
529                @Override
530                public View resolveViewName(String viewName, Locale locale) {
531                        return this.view;
532                }
533        }
534
535}