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