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