001/*
002 * Copyright 2012-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 *      http://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.boot.autoconfigure.web.servlet;
018
019import java.time.Duration;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.List;
025import java.util.ListIterator;
026import java.util.Map;
027import java.util.Optional;
028
029import javax.servlet.Servlet;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033
034import org.springframework.beans.factory.BeanFactory;
035import org.springframework.beans.factory.ListableBeanFactory;
036import org.springframework.beans.factory.NoSuchBeanDefinitionException;
037import org.springframework.beans.factory.ObjectProvider;
038import org.springframework.beans.factory.annotation.Autowired;
039import org.springframework.boot.autoconfigure.AutoConfigureAfter;
040import org.springframework.boot.autoconfigure.AutoConfigureOrder;
041import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
042import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
043import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
044import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
045import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
046import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
047import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
048import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
049import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
050import org.springframework.boot.autoconfigure.template.TemplateAvailabilityProviders;
051import org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration;
052import org.springframework.boot.autoconfigure.validation.ValidatorAdapter;
053import org.springframework.boot.autoconfigure.web.ConditionalOnEnabledResourceChain;
054import org.springframework.boot.autoconfigure.web.ResourceProperties;
055import org.springframework.boot.autoconfigure.web.ResourceProperties.Strategy;
056import org.springframework.boot.autoconfigure.web.format.WebConversionService;
057import org.springframework.boot.context.properties.EnableConfigurationProperties;
058import org.springframework.boot.web.servlet.filter.OrderedFormContentFilter;
059import org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter;
060import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
061import org.springframework.context.ApplicationContext;
062import org.springframework.context.ResourceLoaderAware;
063import org.springframework.context.annotation.Bean;
064import org.springframework.context.annotation.Configuration;
065import org.springframework.context.annotation.Import;
066import org.springframework.context.annotation.Primary;
067import org.springframework.core.Ordered;
068import org.springframework.core.annotation.Order;
069import org.springframework.core.convert.converter.Converter;
070import org.springframework.core.convert.converter.GenericConverter;
071import org.springframework.core.io.ClassPathResource;
072import org.springframework.core.io.Resource;
073import org.springframework.core.io.ResourceLoader;
074import org.springframework.core.task.AsyncTaskExecutor;
075import org.springframework.format.Formatter;
076import org.springframework.format.FormatterRegistry;
077import org.springframework.format.support.FormattingConversionService;
078import org.springframework.http.CacheControl;
079import org.springframework.http.MediaType;
080import org.springframework.http.converter.HttpMessageConverter;
081import org.springframework.util.ClassUtils;
082import org.springframework.validation.DefaultMessageCodesResolver;
083import org.springframework.validation.MessageCodesResolver;
084import org.springframework.validation.Validator;
085import org.springframework.web.HttpMediaTypeNotAcceptableException;
086import org.springframework.web.accept.ContentNegotiationManager;
087import org.springframework.web.accept.ContentNegotiationStrategy;
088import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
089import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
090import org.springframework.web.context.request.NativeWebRequest;
091import org.springframework.web.context.request.RequestAttributes;
092import org.springframework.web.context.request.RequestContextListener;
093import org.springframework.web.filter.FormContentFilter;
094import org.springframework.web.filter.HiddenHttpMethodFilter;
095import org.springframework.web.filter.RequestContextFilter;
096import org.springframework.web.servlet.DispatcherServlet;
097import org.springframework.web.servlet.HandlerExceptionResolver;
098import org.springframework.web.servlet.LocaleResolver;
099import org.springframework.web.servlet.View;
100import org.springframework.web.servlet.ViewResolver;
101import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
102import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
103import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;
104import org.springframework.web.servlet.config.annotation.EnableWebMvc;
105import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
106import org.springframework.web.servlet.config.annotation.ResourceChainRegistration;
107import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistration;
108import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
109import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
110import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
111import org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver;
112import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
113import org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver;
114import org.springframework.web.servlet.i18n.FixedLocaleResolver;
115import org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver;
116import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
117import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
118import org.springframework.web.servlet.resource.AppCacheManifestTransformer;
119import org.springframework.web.servlet.resource.EncodedResourceResolver;
120import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
121import org.springframework.web.servlet.resource.ResourceResolver;
122import org.springframework.web.servlet.resource.VersionResourceResolver;
123import org.springframework.web.servlet.view.BeanNameViewResolver;
124import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
125import org.springframework.web.servlet.view.InternalResourceViewResolver;
126
127/**
128 * {@link EnableAutoConfiguration Auto-configuration} for {@link EnableWebMvc Web MVC}.
129 *
130 * @author Phillip Webb
131 * @author Dave Syer
132 * @author Andy Wilkinson
133 * @author Sébastien Deleuze
134 * @author Eddú Meléndez
135 * @author Stephane Nicoll
136 * @author Kristine Jetzke
137 * @author Bruce Brouwer
138 * @author Artsiom Yudovin
139 */
140@Configuration
141@ConditionalOnWebApplication(type = Type.SERVLET)
142@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
143@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
144@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
145@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class,
146                TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class })
147public class WebMvcAutoConfiguration {
148
149        public static final String DEFAULT_PREFIX = "";
150
151        public static final String DEFAULT_SUFFIX = "";
152
153        private static final String[] SERVLET_LOCATIONS = { "/" };
154
155        @Bean
156        @ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
157        @ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = true)
158        public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
159                return new OrderedHiddenHttpMethodFilter();
160        }
161
162        @Bean
163        @ConditionalOnMissingBean(FormContentFilter.class)
164        @ConditionalOnProperty(prefix = "spring.mvc.formcontent.filter", name = "enabled", matchIfMissing = true)
165        public OrderedFormContentFilter formContentFilter() {
166                return new OrderedFormContentFilter();
167        }
168
169        // Defined as a nested config to ensure WebMvcConfigurer is not read when not
170        // on the classpath
171        @Configuration
172        @Import(EnableWebMvcConfiguration.class)
173        @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
174        @Order(0)
175        public static class WebMvcAutoConfigurationAdapter
176                        implements WebMvcConfigurer, ResourceLoaderAware {
177
178                private static final Log logger = LogFactory.getLog(WebMvcConfigurer.class);
179
180                private final ResourceProperties resourceProperties;
181
182                private final WebMvcProperties mvcProperties;
183
184                private final ListableBeanFactory beanFactory;
185
186                private final ObjectProvider<HttpMessageConverters> messageConvertersProvider;
187
188                final ResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer;
189
190                private ResourceLoader resourceLoader;
191
192                public WebMvcAutoConfigurationAdapter(ResourceProperties resourceProperties,
193                                WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,
194                                ObjectProvider<HttpMessageConverters> messageConvertersProvider,
195                                ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider) {
196                        this.resourceProperties = resourceProperties;
197                        this.mvcProperties = mvcProperties;
198                        this.beanFactory = beanFactory;
199                        this.messageConvertersProvider = messageConvertersProvider;
200                        this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider
201                                        .getIfAvailable();
202                }
203
204                @Override
205                public void setResourceLoader(ResourceLoader resourceLoader) {
206                        this.resourceLoader = resourceLoader;
207                }
208
209                @Override
210                public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
211                        this.messageConvertersProvider.ifAvailable((customConverters) -> converters
212                                        .addAll(customConverters.getConverters()));
213                }
214
215                @Override
216                public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
217                        if (this.beanFactory.containsBean(
218                                        TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME)) {
219                                Object taskExecutor = this.beanFactory.getBean(
220                                                TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME);
221                                if (taskExecutor instanceof AsyncTaskExecutor) {
222                                        configurer.setTaskExecutor(((AsyncTaskExecutor) taskExecutor));
223                                }
224                        }
225                        Duration timeout = this.mvcProperties.getAsync().getRequestTimeout();
226                        if (timeout != null) {
227                                configurer.setDefaultTimeout(timeout.toMillis());
228                        }
229                }
230
231                @Override
232                public void configurePathMatch(PathMatchConfigurer configurer) {
233                        configurer.setUseSuffixPatternMatch(
234                                        this.mvcProperties.getPathmatch().isUseSuffixPattern());
235                        configurer.setUseRegisteredSuffixPatternMatch(
236                                        this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern());
237                }
238
239                @Override
240                public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
241                        WebMvcProperties.Contentnegotiation contentnegotiation = this.mvcProperties
242                                        .getContentnegotiation();
243                        configurer.favorPathExtension(contentnegotiation.isFavorPathExtension());
244                        configurer.favorParameter(contentnegotiation.isFavorParameter());
245                        if (contentnegotiation.getParameterName() != null) {
246                                configurer.parameterName(contentnegotiation.getParameterName());
247                        }
248                        Map<String, MediaType> mediaTypes = this.mvcProperties.getContentnegotiation()
249                                        .getMediaTypes();
250                        mediaTypes.forEach(configurer::mediaType);
251                }
252
253                @Bean
254                @ConditionalOnMissingBean
255                public InternalResourceViewResolver defaultViewResolver() {
256                        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
257                        resolver.setPrefix(this.mvcProperties.getView().getPrefix());
258                        resolver.setSuffix(this.mvcProperties.getView().getSuffix());
259                        return resolver;
260                }
261
262                @Bean
263                @ConditionalOnBean(View.class)
264                @ConditionalOnMissingBean
265                public BeanNameViewResolver beanNameViewResolver() {
266                        BeanNameViewResolver resolver = new BeanNameViewResolver();
267                        resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
268                        return resolver;
269                }
270
271                @Bean
272                @ConditionalOnBean(ViewResolver.class)
273                @ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
274                public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
275                        ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
276                        resolver.setContentNegotiationManager(
277                                        beanFactory.getBean(ContentNegotiationManager.class));
278                        // ContentNegotiatingViewResolver uses all the other view resolvers to locate
279                        // a view so it should have a high precedence
280                        resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
281                        return resolver;
282                }
283
284                @Bean
285                @ConditionalOnMissingBean
286                @ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
287                public LocaleResolver localeResolver() {
288                        if (this.mvcProperties
289                                        .getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
290                                return new FixedLocaleResolver(this.mvcProperties.getLocale());
291                        }
292                        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
293                        localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
294                        return localeResolver;
295                }
296
297                @Override
298                public MessageCodesResolver getMessageCodesResolver() {
299                        if (this.mvcProperties.getMessageCodesResolverFormat() != null) {
300                                DefaultMessageCodesResolver resolver = new DefaultMessageCodesResolver();
301                                resolver.setMessageCodeFormatter(
302                                                this.mvcProperties.getMessageCodesResolverFormat());
303                                return resolver;
304                        }
305                        return null;
306                }
307
308                @Override
309                public void addFormatters(FormatterRegistry registry) {
310                        for (Converter<?, ?> converter : getBeansOfType(Converter.class)) {
311                                registry.addConverter(converter);
312                        }
313                        for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
314                                registry.addConverter(converter);
315                        }
316                        for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
317                                registry.addFormatter(formatter);
318                        }
319                }
320
321                private <T> Collection<T> getBeansOfType(Class<T> type) {
322                        return this.beanFactory.getBeansOfType(type).values();
323                }
324
325                @Override
326                public void addResourceHandlers(ResourceHandlerRegistry registry) {
327                        if (!this.resourceProperties.isAddMappings()) {
328                                logger.debug("Default resource handling disabled");
329                                return;
330                        }
331                        Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
332                        CacheControl cacheControl = this.resourceProperties.getCache()
333                                        .getCachecontrol().toHttpCacheControl();
334                        if (!registry.hasMappingForPattern("/webjars/**")) {
335                                customizeResourceHandlerRegistration(registry
336                                                .addResourceHandler("/webjars/**")
337                                                .addResourceLocations("classpath:/META-INF/resources/webjars/")
338                                                .setCachePeriod(getSeconds(cachePeriod))
339                                                .setCacheControl(cacheControl));
340                        }
341                        String staticPathPattern = this.mvcProperties.getStaticPathPattern();
342                        if (!registry.hasMappingForPattern(staticPathPattern)) {
343                                customizeResourceHandlerRegistration(
344                                                registry.addResourceHandler(staticPathPattern)
345                                                                .addResourceLocations(getResourceLocations(
346                                                                                this.resourceProperties.getStaticLocations()))
347                                                                .setCachePeriod(getSeconds(cachePeriod))
348                                                                .setCacheControl(cacheControl));
349                        }
350                }
351
352                private Integer getSeconds(Duration cachePeriod) {
353                        return (cachePeriod != null) ? (int) cachePeriod.getSeconds() : null;
354                }
355
356                @Bean
357                public WelcomePageHandlerMapping welcomePageHandlerMapping(
358                                ApplicationContext applicationContext) {
359                        return new WelcomePageHandlerMapping(
360                                        new TemplateAvailabilityProviders(applicationContext),
361                                        applicationContext, getWelcomePage(),
362                                        this.mvcProperties.getStaticPathPattern());
363                }
364
365                static String[] getResourceLocations(String[] staticLocations) {
366                        String[] locations = new String[staticLocations.length
367                                        + SERVLET_LOCATIONS.length];
368                        System.arraycopy(staticLocations, 0, locations, 0, staticLocations.length);
369                        System.arraycopy(SERVLET_LOCATIONS, 0, locations, staticLocations.length,
370                                        SERVLET_LOCATIONS.length);
371                        return locations;
372                }
373
374                private Optional<Resource> getWelcomePage() {
375                        String[] locations = getResourceLocations(
376                                        this.resourceProperties.getStaticLocations());
377                        return Arrays.stream(locations).map(this::getIndexHtml)
378                                        .filter(this::isReadable).findFirst();
379                }
380
381                private Resource getIndexHtml(String location) {
382                        return this.resourceLoader.getResource(location + "index.html");
383                }
384
385                private boolean isReadable(Resource resource) {
386                        try {
387                                return resource.exists() && (resource.getURL() != null);
388                        }
389                        catch (Exception ex) {
390                                return false;
391                        }
392                }
393
394                private void customizeResourceHandlerRegistration(
395                                ResourceHandlerRegistration registration) {
396                        if (this.resourceHandlerRegistrationCustomizer != null) {
397                                this.resourceHandlerRegistrationCustomizer.customize(registration);
398                        }
399                }
400
401                @Bean
402                @ConditionalOnMissingBean({ RequestContextListener.class,
403                                RequestContextFilter.class })
404                public static RequestContextFilter requestContextFilter() {
405                        return new OrderedRequestContextFilter();
406                }
407
408                @Configuration
409                @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
410                public static class FaviconConfiguration implements ResourceLoaderAware {
411
412                        private final ResourceProperties resourceProperties;
413
414                        private ResourceLoader resourceLoader;
415
416                        public FaviconConfiguration(ResourceProperties resourceProperties) {
417                                this.resourceProperties = resourceProperties;
418                        }
419
420                        @Override
421                        public void setResourceLoader(ResourceLoader resourceLoader) {
422                                this.resourceLoader = resourceLoader;
423                        }
424
425                        @Bean
426                        public SimpleUrlHandlerMapping faviconHandlerMapping() {
427                                SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
428                                mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
429                                mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
430                                                faviconRequestHandler()));
431                                return mapping;
432                        }
433
434                        @Bean
435                        public ResourceHttpRequestHandler faviconRequestHandler() {
436                                ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
437                                requestHandler.setLocations(resolveFaviconLocations());
438                                return requestHandler;
439                        }
440
441                        private List<Resource> resolveFaviconLocations() {
442                                String[] staticLocations = getResourceLocations(
443                                                this.resourceProperties.getStaticLocations());
444                                List<Resource> locations = new ArrayList<>(staticLocations.length + 1);
445                                Arrays.stream(staticLocations).map(this.resourceLoader::getResource)
446                                                .forEach(locations::add);
447                                locations.add(new ClassPathResource("/"));
448                                return Collections.unmodifiableList(locations);
449                        }
450
451                }
452
453        }
454
455        /**
456         * Configuration equivalent to {@code @EnableWebMvc}.
457         */
458        @Configuration
459        public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
460
461                private final WebMvcProperties mvcProperties;
462
463                private final ListableBeanFactory beanFactory;
464
465                private final WebMvcRegistrations mvcRegistrations;
466
467                public EnableWebMvcConfiguration(
468                                ObjectProvider<WebMvcProperties> mvcPropertiesProvider,
469                                ObjectProvider<WebMvcRegistrations> mvcRegistrationsProvider,
470                                ListableBeanFactory beanFactory) {
471                        this.mvcProperties = mvcPropertiesProvider.getIfAvailable();
472                        this.mvcRegistrations = mvcRegistrationsProvider.getIfUnique();
473                        this.beanFactory = beanFactory;
474                }
475
476                @Bean
477                @Override
478                public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
479                        RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter();
480                        adapter.setIgnoreDefaultModelOnRedirect(this.mvcProperties == null
481                                        || this.mvcProperties.isIgnoreDefaultModelOnRedirect());
482                        return adapter;
483                }
484
485                @Override
486                protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
487                        if (this.mvcRegistrations != null
488                                        && this.mvcRegistrations.getRequestMappingHandlerAdapter() != null) {
489                                return this.mvcRegistrations.getRequestMappingHandlerAdapter();
490                        }
491                        return super.createRequestMappingHandlerAdapter();
492                }
493
494                @Bean
495                @Primary
496                @Override
497                public RequestMappingHandlerMapping requestMappingHandlerMapping() {
498                        // Must be @Primary for MvcUriComponentsBuilder to work
499                        return super.requestMappingHandlerMapping();
500                }
501
502                @Bean
503                @Override
504                public FormattingConversionService mvcConversionService() {
505                        WebConversionService conversionService = new WebConversionService(
506                                        this.mvcProperties.getDateFormat());
507                        addFormatters(conversionService);
508                        return conversionService;
509                }
510
511                @Bean
512                @Override
513                public Validator mvcValidator() {
514                        if (!ClassUtils.isPresent("javax.validation.Validator",
515                                        getClass().getClassLoader())) {
516                                return super.mvcValidator();
517                        }
518                        return ValidatorAdapter.get(getApplicationContext(), getValidator());
519                }
520
521                @Override
522                protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
523                        if (this.mvcRegistrations != null
524                                        && this.mvcRegistrations.getRequestMappingHandlerMapping() != null) {
525                                return this.mvcRegistrations.getRequestMappingHandlerMapping();
526                        }
527                        return super.createRequestMappingHandlerMapping();
528                }
529
530                @Override
531                protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
532                        try {
533                                return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class);
534                        }
535                        catch (NoSuchBeanDefinitionException ex) {
536                                return super.getConfigurableWebBindingInitializer();
537                        }
538                }
539
540                @Override
541                protected ExceptionHandlerExceptionResolver createExceptionHandlerExceptionResolver() {
542                        if (this.mvcRegistrations != null && this.mvcRegistrations
543                                        .getExceptionHandlerExceptionResolver() != null) {
544                                return this.mvcRegistrations.getExceptionHandlerExceptionResolver();
545                        }
546                        return super.createExceptionHandlerExceptionResolver();
547                }
548
549                @Override
550                protected void configureHandlerExceptionResolvers(
551                                List<HandlerExceptionResolver> exceptionResolvers) {
552                        super.configureHandlerExceptionResolvers(exceptionResolvers);
553                        if (exceptionResolvers.isEmpty()) {
554                                addDefaultHandlerExceptionResolvers(exceptionResolvers);
555                        }
556                        if (this.mvcProperties.isLogResolvedException()) {
557                                for (HandlerExceptionResolver resolver : exceptionResolvers) {
558                                        if (resolver instanceof AbstractHandlerExceptionResolver) {
559                                                ((AbstractHandlerExceptionResolver) resolver)
560                                                                .setWarnLogCategory(resolver.getClass().getName());
561                                        }
562                                }
563                        }
564                }
565
566                @Bean
567                @Override
568                public ContentNegotiationManager mvcContentNegotiationManager() {
569                        ContentNegotiationManager manager = super.mvcContentNegotiationManager();
570                        List<ContentNegotiationStrategy> strategies = manager.getStrategies();
571                        ListIterator<ContentNegotiationStrategy> iterator = strategies.listIterator();
572                        while (iterator.hasNext()) {
573                                ContentNegotiationStrategy strategy = iterator.next();
574                                if (strategy instanceof PathExtensionContentNegotiationStrategy) {
575                                        iterator.set(new OptionalPathExtensionContentNegotiationStrategy(
576                                                        strategy));
577                                }
578                        }
579                        return manager;
580                }
581
582        }
583
584        @Configuration
585        @ConditionalOnEnabledResourceChain
586        static class ResourceChainCustomizerConfiguration {
587
588                @Bean
589                public ResourceChainResourceHandlerRegistrationCustomizer resourceHandlerRegistrationCustomizer() {
590                        return new ResourceChainResourceHandlerRegistrationCustomizer();
591                }
592
593        }
594
595        interface ResourceHandlerRegistrationCustomizer {
596
597                void customize(ResourceHandlerRegistration registration);
598
599        }
600
601        static class ResourceChainResourceHandlerRegistrationCustomizer
602                        implements ResourceHandlerRegistrationCustomizer {
603
604                @Autowired
605                private ResourceProperties resourceProperties = new ResourceProperties();
606
607                @Override
608                public void customize(ResourceHandlerRegistration registration) {
609                        ResourceProperties.Chain properties = this.resourceProperties.getChain();
610                        configureResourceChain(properties,
611                                        registration.resourceChain(properties.isCache()));
612                }
613
614                private void configureResourceChain(ResourceProperties.Chain properties,
615                                ResourceChainRegistration chain) {
616                        Strategy strategy = properties.getStrategy();
617                        if (properties.isCompressed()) {
618                                chain.addResolver(new EncodedResourceResolver());
619                        }
620                        if (strategy.getFixed().isEnabled() || strategy.getContent().isEnabled()) {
621                                chain.addResolver(getVersionResourceResolver(strategy));
622                        }
623                        if (properties.isHtmlApplicationCache()) {
624                                chain.addTransformer(new AppCacheManifestTransformer());
625                        }
626                }
627
628                private ResourceResolver getVersionResourceResolver(
629                                ResourceProperties.Strategy properties) {
630                        VersionResourceResolver resolver = new VersionResourceResolver();
631                        if (properties.getFixed().isEnabled()) {
632                                String version = properties.getFixed().getVersion();
633                                String[] paths = properties.getFixed().getPaths();
634                                resolver.addFixedVersionStrategy(version, paths);
635                        }
636                        if (properties.getContent().isEnabled()) {
637                                String[] paths = properties.getContent().getPaths();
638                                resolver.addContentVersionStrategy(paths);
639                        }
640                        return resolver;
641                }
642
643        }
644
645        /**
646         * Decorator to make {@link PathExtensionContentNegotiationStrategy} optional
647         * depending on a request attribute.
648         */
649        static class OptionalPathExtensionContentNegotiationStrategy
650                        implements ContentNegotiationStrategy {
651
652                private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
653                                .getName() + ".SKIP";
654
655                private final ContentNegotiationStrategy delegate;
656
657                OptionalPathExtensionContentNegotiationStrategy(
658                                ContentNegotiationStrategy delegate) {
659                        this.delegate = delegate;
660                }
661
662                @Override
663                public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
664                                throws HttpMediaTypeNotAcceptableException {
665                        Object skip = webRequest.getAttribute(SKIP_ATTRIBUTE,
666                                        RequestAttributes.SCOPE_REQUEST);
667                        if (skip != null && Boolean.parseBoolean(skip.toString())) {
668                                return MEDIA_TYPE_ALL_LIST;
669                        }
670                        return this.delegate.resolveMediaTypes(webRequest);
671                }
672
673        }
674
675}