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}