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}