Merge path extension related deprecation changes

Closes gh-24179
This commit is contained in:
Rossen Stoyanchev 2020-01-22 13:35:21 +00:00
commit 77517d6cff
19 changed files with 486 additions and 191 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -310,7 +310,11 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder<StandaloneM
* Whether to use suffix pattern match (".*") when matching patterns to * Whether to use suffix pattern match (".*") when matching patterns to
* requests. If enabled a method mapped to "/users" also matches to "/users.*". * requests. If enabled a method mapped to "/users" also matches to "/users.*".
* <p>The default value is {@code true}. * <p>The default value is {@code true}.
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path extension
* config options.
*/ */
@Deprecated
public StandaloneMockMvcBuilder setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { public StandaloneMockMvcBuilder setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
this.useSuffixPatternMatch = useSuffixPatternMatch; this.useSuffixPatternMatch = useSuffixPatternMatch;
return this; return this;
@ -442,6 +446,7 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder<StandaloneM
/** Using the MVC Java configuration as the starting point for the "standalone" setup. */ /** Using the MVC Java configuration as the starting point for the "standalone" setup. */
private class StandaloneConfiguration extends WebMvcConfigurationSupport { private class StandaloneConfiguration extends WebMvcConfigurationSupport {
@SuppressWarnings("deprecation")
public RequestMappingHandlerMapping getHandlerMapping( public RequestMappingHandlerMapping getHandlerMapping(
FormattingConversionService mvcConversionService, FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) { ResourceUrlProvider mvcResourceUrlProvider) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -20,13 +20,17 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.function.Function;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
@ -132,11 +136,7 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
@Override @Override
public List<String> resolveFileExtensions(MediaType mediaType) { public List<String> resolveFileExtensions(MediaType mediaType) {
Set<String> result = new LinkedHashSet<>(); return doResolveExtensions(resolver -> resolver.resolveFileExtensions(mediaType));
for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
result.addAll(resolver.resolveFileExtensions(mediaType));
}
return new ArrayList<>(result);
} }
/** /**
@ -152,11 +152,44 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
*/ */
@Override @Override
public List<String> getAllFileExtensions() { public List<String> getAllFileExtensions() {
Set<String> result = new LinkedHashSet<>(); return doResolveExtensions(MediaTypeFileExtensionResolver::getAllFileExtensions);
}
private List<String> doResolveExtensions(Function<MediaTypeFileExtensionResolver, List<String>> extractor) {
List<String> result = null;
for (MediaTypeFileExtensionResolver resolver : this.resolvers) { for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
result.addAll(resolver.getAllFileExtensions()); List<String> extensions = extractor.apply(resolver);
if (CollectionUtils.isEmpty(extensions)) {
continue;
}
result = (result != null ? result : new ArrayList<>(4));
for (String extension : extensions) {
if (!result.contains(extension)) {
result.add(extension);
}
}
} }
return new ArrayList<>(result); return (result != null ? result : Collections.emptyList());
}
/**
* Return all registered lookup key to media type mappings by iterating
* {@link MediaTypeFileExtensionResolver}s.
* @since 5.2.4
*/
public Map<String, MediaType> getMediaTypeMappings() {
Map<String, MediaType> result = null;
for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
if (resolver instanceof MappingMediaTypeFileExtensionResolver) {
Map<String, MediaType> map = ((MappingMediaTypeFileExtensionResolver) resolver).getMediaTypes();
if (CollectionUtils.isEmpty(map)) {
continue;
}
result = (result != null ? result : new HashMap<>(4));
result.putAll(map);
}
}
return (result != null ? result : Collections.emptyMap());
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -81,15 +81,18 @@ import org.springframework.web.context.ServletContextAware;
* </tr> * </tr>
* </table> * </table>
* *
* <p>As of 5.0 you can set the exact strategies to use via * <p>Alternatively you can avoid use of the above convenience builder
* methods and set the exact strategies to use via
* {@link #setStrategies(List)}. * {@link #setStrategies(List)}.
* *
* <p><strong>Note:</strong> if you must use URL-based content type resolution, * <p><strong>Note:</strong> As of 5.2.4,
* the use of a query parameter is simpler and preferable to the use of a path * {@link #setFavorPathExtension(boolean) favorPathExtension} and
* extension since the latter can cause issues with URI variables, path * {@link #setIgnoreUnknownPathExtensions(boolean) ignoreUnknownPathExtensions}
* parameters, and URI decoding. Consider setting {@link #setFavorPathExtension} * are deprecated in order to discourage use of path extensions for content
* to {@literal false} or otherwise set the strategies to use explicitly via * negotiation as well as for request mapping (with similar deprecations in
* {@link #setStrategies(List)}. * {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
* RequestMappingHandlerMapping}). For further context, please read issue
* <a href="https://github.com/spring-projects/spring-framework/issues/24179">#24719</a>.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Brian Clozel * @author Brian Clozel
@ -145,52 +148,61 @@ public class ContentNegotiationManagerFactoryBean
* <p>By default this is set to {@code true} in which case a request * <p>By default this is set to {@code true} in which case a request
* for {@code /hotels.pdf} will be interpreted as a request for * for {@code /hotels.pdf} will be interpreted as a request for
* {@code "application/pdf"} regardless of the 'Accept' header. * {@code "application/pdf"} regardless of the 'Accept' header.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options.
*/ */
@Deprecated
public void setFavorPathExtension(boolean favorPathExtension) { public void setFavorPathExtension(boolean favorPathExtension) {
this.favorPathExtension = favorPathExtension; this.favorPathExtension = favorPathExtension;
} }
/** /**
* Add a mapping from a key, extracted from a path extension or a query * Add a mapping from a key to a MediaType where the key are normalized to
* parameter, to a MediaType. This is required in order for the parameter * lowercase and may have been extracted from a path extension, a filename
* strategy to work. Any extensions explicitly registered here are also * extension, or passed as a query parameter.
* whitelisted for the purpose of Reflected File Download attack detection * <p>The {@link #setFavorParameter(boolean) parameter strategy} requires
* (see Spring Framework reference documentation for more details on RFD * such mappings in order to work while the {@link #setFavorPathExtension(boolean)
* attack protection). * path extension strategy} can fall back on lookups via
* <p>The path extension strategy will also try to use
* {@link ServletContext#getMimeType} and * {@link ServletContext#getMimeType} and
* {@link org.springframework.http.MediaTypeFactory} to resolve path extensions. * {@link org.springframework.http.MediaTypeFactory}.
* <p><strong>Note:</strong> Mappings registered here may be accessed via
* {@link ContentNegotiationManager#getMediaTypeMappings()} and may be used
* not only in the parameter and path extension strategies. For example,
* with the Spring MVC config, e.g. {@code @EnableWebMvc} or
* {@code <mvc:annotation-driven>}, the media type mappings are also plugged
* in to:
* <ul>
* <li>Determine the media type of static resources served with
* {@code ResourceHttpRequestHandler}.
* <li>Determine the media type of views rendered with
* {@code ContentNegotiatingViewResolver}.
* <li>Whitelist extensions for RFD attack detection (check the Spring
* Framework reference docs for details).
* </ul>
* @param mediaTypes media type mappings * @param mediaTypes media type mappings
* @see #addMediaType(String, MediaType) * @see #addMediaType(String, MediaType)
* @see #addMediaTypes(Map) * @see #addMediaTypes(Map)
*/ */
public void setMediaTypes(Properties mediaTypes) { public void setMediaTypes(Properties mediaTypes) {
if (!CollectionUtils.isEmpty(mediaTypes)) { if (!CollectionUtils.isEmpty(mediaTypes)) {
mediaTypes.forEach((key, value) -> { mediaTypes.forEach((key, value) ->
String extension = ((String) key).toLowerCase(Locale.ENGLISH); addMediaType((String) key, MediaType.valueOf((String) value)));
MediaType mediaType = MediaType.valueOf((String) value);
this.mediaTypes.put(extension, mediaType);
});
} }
} }
/** /**
* An alternative to {@link #setMediaTypes} for use in Java code. * An alternative to {@link #setMediaTypes} for programmatic registrations.
* @see #setMediaTypes
* @see #addMediaTypes
*/ */
public void addMediaType(String fileExtension, MediaType mediaType) { public void addMediaType(String key, MediaType mediaType) {
this.mediaTypes.put(fileExtension, mediaType); this.mediaTypes.put(key.toLowerCase(Locale.ENGLISH), mediaType);
} }
/** /**
* An alternative to {@link #setMediaTypes} for use in Java code. * An alternative to {@link #setMediaTypes} for programmatic registrations.
* @see #setMediaTypes
* @see #addMediaType
*/ */
public void addMediaTypes(@Nullable Map<String, MediaType> mediaTypes) { public void addMediaTypes(@Nullable Map<String, MediaType> mediaTypes) {
if (mediaTypes != null) { if (mediaTypes != null) {
this.mediaTypes.putAll(mediaTypes); mediaTypes.forEach(this::addMediaType);
} }
} }
@ -199,7 +211,10 @@ public class ContentNegotiationManagerFactoryBean
* to any media type. Setting this to {@code false} will result in an * to any media type. Setting this to {@code false} will result in an
* {@code HttpMediaTypeNotAcceptableException} if there is no match. * {@code HttpMediaTypeNotAcceptableException} if there is no match.
* <p>By default this is set to {@code true}. * <p>By default this is set to {@code true}.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options.
*/ */
@Deprecated
public void setIgnoreUnknownPathExtensions(boolean ignore) { public void setIgnoreUnknownPathExtensions(boolean ignore) {
this.ignoreUnknownPathExtensions = ignore; this.ignoreUnknownPathExtensions = ignore;
} }
@ -303,9 +318,10 @@ public class ContentNegotiationManagerFactoryBean
} }
/** /**
* Actually build the {@link ContentNegotiationManager}. * Create and initialize a {@link ContentNegotiationManager} instance.
* @since 5.0 * @since 5.0
*/ */
@SuppressWarnings("deprecation")
public ContentNegotiationManager build() { public ContentNegotiationManager build() {
List<ContentNegotiationStrategy> strategies = new ArrayList<>(); List<ContentNegotiationStrategy> strategies = new ArrayList<>();
@ -327,7 +343,6 @@ public class ContentNegotiationManagerFactoryBean
} }
strategies.add(strategy); strategies.add(strategy);
} }
if (this.favorParameter) { if (this.favorParameter) {
ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes); ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
strategy.setParameterName(this.parameterName); strategy.setParameterName(this.parameterName);
@ -339,17 +354,24 @@ public class ContentNegotiationManagerFactoryBean
} }
strategies.add(strategy); strategies.add(strategy);
} }
if (!this.ignoreAcceptHeader) { if (!this.ignoreAcceptHeader) {
strategies.add(new HeaderContentNegotiationStrategy()); strategies.add(new HeaderContentNegotiationStrategy());
} }
if (this.defaultNegotiationStrategy != null) { if (this.defaultNegotiationStrategy != null) {
strategies.add(this.defaultNegotiationStrategy); strategies.add(this.defaultNegotiationStrategy);
} }
} }
this.contentNegotiationManager = new ContentNegotiationManager(strategies); this.contentNegotiationManager = new ContentNegotiationManager(strategies);
// Ensure media type mappings are available via ContentNegotiationManager#getMediaTypeMappings()
// independent of path extension or parameter strategies.
if (!CollectionUtils.isEmpty(this.mediaTypes) && !this.favorPathExtension && !this.favorParameter) {
this.contentNegotiationManager.addFileExtensionResolvers(
new MappingMediaTypeFileExtensionResolver(this.mediaTypes));
}
return this.contentNegotiationManager; return this.contentNegotiationManager;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -18,9 +18,11 @@ package org.springframework.web.accept;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -53,7 +55,7 @@ public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExten
*/ */
public MappingMediaTypeFileExtensionResolver(@Nullable Map<String, MediaType> mediaTypes) { public MappingMediaTypeFileExtensionResolver(@Nullable Map<String, MediaType> mediaTypes) {
if (mediaTypes != null) { if (mediaTypes != null) {
List<String> allFileExtensions = new ArrayList<>(); Set<String> allFileExtensions = new HashSet<>(mediaTypes.size());
mediaTypes.forEach((extension, mediaType) -> { mediaTypes.forEach((extension, mediaType) -> {
String lowerCaseExtension = extension.toLowerCase(Locale.ENGLISH); String lowerCaseExtension = extension.toLowerCase(Locale.ENGLISH);
this.mediaTypes.put(lowerCaseExtension, mediaType); this.mediaTypes.put(lowerCaseExtension, mediaType);

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,7 +41,11 @@ import org.springframework.web.util.UrlPathHelper;
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
* @deprecated as of 5.2.4. See class-level note in
* {@link ContentNegotiationManagerFactoryBean} on the deprecation of path
* extension config options.
*/ */
@Deprecated
public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
private UrlPathHelper urlPathHelper = new UrlPathHelper(); private UrlPathHelper urlPathHelper = new UrlPathHelper();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -34,7 +34,11 @@ import org.springframework.web.context.request.NativeWebRequest;
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
* @deprecated as of 5.2.4. See class-level note in
* {@link ContentNegotiationManagerFactoryBean} on the deprecation of path
* extension config options.
*/ */
@Deprecated
public class ServletPathExtensionContentNegotiationStrategy extends PathExtensionContentNegotiationStrategy { public class ServletPathExtensionContentNegotiationStrategy extends PathExtensionContentNegotiationStrategy {
private final ServletContext servletContext; private final ServletContext servletContext;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -70,21 +71,29 @@ public class ContentNegotiationManagerFactoryBeanTests {
this.servletRequest.setRequestURI("/flower.gif"); this.servletRequest.setRequestURI("/flower.gif");
assertThat(manager.resolveMediaTypes(this.webRequest)).as("Should be able to resolve file extensions by default").isEqualTo(Collections.singletonList(MediaType.IMAGE_GIF)); assertThat(manager.resolveMediaTypes(this.webRequest))
.as("Should be able to resolve file extensions by default")
.isEqualTo(Collections.singletonList(MediaType.IMAGE_GIF));
this.servletRequest.setRequestURI("/flower.foobarbaz"); this.servletRequest.setRequestURI("/flower.foobarbaz");
assertThat(manager.resolveMediaTypes(this.webRequest)).as("Should ignore unknown extensions by default").isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST); assertThat(manager.resolveMediaTypes(this.webRequest))
.as("Should ignore unknown extensions by default")
.isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST);
this.servletRequest.setRequestURI("/flower"); this.servletRequest.setRequestURI("/flower");
this.servletRequest.setParameter("format", "gif"); this.servletRequest.setParameter("format", "gif");
assertThat(manager.resolveMediaTypes(this.webRequest)).as("Should not resolve request parameters by default").isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST); assertThat(manager.resolveMediaTypes(this.webRequest))
.as("Should not resolve request parameters by default")
.isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST);
this.servletRequest.setRequestURI("/flower"); this.servletRequest.setRequestURI("/flower");
this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE); this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE);
assertThat(manager.resolveMediaTypes(this.webRequest)).as("Should resolve Accept header by default").isEqualTo(Collections.singletonList(MediaType.IMAGE_GIF)); assertThat(manager.resolveMediaTypes(this.webRequest))
.as("Should resolve Accept header by default")
.isEqualTo(Collections.singletonList(MediaType.IMAGE_GIF));
} }
@Test @Test
@ -101,29 +110,33 @@ public class ContentNegotiationManagerFactoryBeanTests {
this.servletRequest.setRequestURI("/flower"); this.servletRequest.setRequestURI("/flower");
this.servletRequest.addParameter("format", "bar"); this.servletRequest.addParameter("format", "bar");
assertThat(manager.resolveMediaTypes(this.webRequest)).isEqualTo(Collections.singletonList(new MediaType("application", "bar"))); assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(new MediaType("application", "bar")));
} }
@Test @Test
public void favorPath() throws Exception { public void favorPath() throws Exception {
this.factoryBean.setFavorPathExtension(true); this.factoryBean.setFavorPathExtension(true);
this.factoryBean.addMediaTypes(Collections.singletonMap("bar", new MediaType("application", "bar"))); this.factoryBean.addMediaType("bar", new MediaType("application", "bar"));
this.factoryBean.afterPropertiesSet(); this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject(); ContentNegotiationManager manager = this.factoryBean.getObject();
this.servletRequest.setRequestURI("/flower.foo"); this.servletRequest.setRequestURI("/flower.foo");
assertThat(manager.resolveMediaTypes(this.webRequest)).isEqualTo(Collections.singletonList(new MediaType("application", "foo"))); assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(new MediaType("application", "foo")));
this.servletRequest.setRequestURI("/flower.bar"); this.servletRequest.setRequestURI("/flower.bar");
assertThat(manager.resolveMediaTypes(this.webRequest)).isEqualTo(Collections.singletonList(new MediaType("application", "bar"))); assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(new MediaType("application", "bar")));
this.servletRequest.setRequestURI("/flower.gif"); this.servletRequest.setRequestURI("/flower.gif");
assertThat(manager.resolveMediaTypes(this.webRequest)).isEqualTo(Collections.singletonList(MediaType.IMAGE_GIF)); assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(MediaType.IMAGE_GIF));
} }
@Test // SPR-10170 @Test // SPR-10170
public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() throws Exception { public void favorPathWithIgnoreUnknownPathExtensionTurnedOff() {
this.factoryBean.setFavorPathExtension(true); this.factoryBean.setFavorPathExtension(true);
this.factoryBean.setIgnoreUnknownPathExtensions(false); this.factoryBean.setIgnoreUnknownPathExtensions(false);
this.factoryBean.afterPropertiesSet(); this.factoryBean.afterPropertiesSet();
@ -139,10 +152,7 @@ public class ContentNegotiationManagerFactoryBeanTests {
@Test @Test
public void favorParameter() throws Exception { public void favorParameter() throws Exception {
this.factoryBean.setFavorParameter(true); this.factoryBean.setFavorParameter(true);
this.factoryBean.addMediaType("json", MediaType.APPLICATION_JSON);
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json", MediaType.APPLICATION_JSON);
this.factoryBean.addMediaTypes(mediaTypes);
this.factoryBean.afterPropertiesSet(); this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject(); ContentNegotiationManager manager = this.factoryBean.getObject();
@ -150,11 +160,12 @@ public class ContentNegotiationManagerFactoryBeanTests {
this.servletRequest.setRequestURI("/flower"); this.servletRequest.setRequestURI("/flower");
this.servletRequest.addParameter("format", "json"); this.servletRequest.addParameter("format", "json");
assertThat(manager.resolveMediaTypes(this.webRequest)).isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON)); assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON));
} }
@Test // SPR-10170 @Test // SPR-10170
public void favorParameterWithUnknownMediaType() throws HttpMediaTypeNotAcceptableException { public void favorParameterWithUnknownMediaType() {
this.factoryBean.setFavorParameter(true); this.factoryBean.setFavorParameter(true);
this.factoryBean.afterPropertiesSet(); this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject(); ContentNegotiationManager manager = this.factoryBean.getObject();
@ -162,8 +173,52 @@ public class ContentNegotiationManagerFactoryBeanTests {
this.servletRequest.setRequestURI("/flower"); this.servletRequest.setRequestURI("/flower");
this.servletRequest.setParameter("format", "invalid"); this.servletRequest.setParameter("format", "invalid");
assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class).isThrownBy(() -> assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class)
manager.resolveMediaTypes(this.webRequest)); .isThrownBy(() -> manager.resolveMediaTypes(this.webRequest));
}
@Test
public void mediaTypeMappingsWithoutPathAndParameterStrategies() {
this.factoryBean.setFavorPathExtension(false);
this.factoryBean.setFavorParameter(false);
Properties properties = new Properties();
properties.put("JSon", "application/json");
this.factoryBean.setMediaTypes(properties);
this.factoryBean.addMediaType("pdF", MediaType.APPLICATION_PDF);
this.factoryBean.addMediaTypes(Collections.singletonMap("xML", MediaType.APPLICATION_XML));
ContentNegotiationManager manager = this.factoryBean.build();
assertThat(manager.getMediaTypeMappings())
.hasSize(3)
.containsEntry("json", MediaType.APPLICATION_JSON)
.containsEntry("pdf", MediaType.APPLICATION_PDF)
.containsEntry("xml", MediaType.APPLICATION_XML);
}
@Test
public void fileExtensions() {
this.factoryBean.setFavorPathExtension(false);
this.factoryBean.setFavorParameter(false);
Properties properties = new Properties();
properties.put("json", "application/json");
properties.put("pdf", "application/pdf");
properties.put("xml", "application/xml");
this.factoryBean.setMediaTypes(properties);
this.factoryBean.addMediaType("jsON", MediaType.APPLICATION_JSON);
this.factoryBean.addMediaType("pdF", MediaType.APPLICATION_PDF);
this.factoryBean.addMediaTypes(Collections.singletonMap("JSon", MediaType.APPLICATION_JSON));
this.factoryBean.addMediaTypes(Collections.singletonMap("xML", MediaType.APPLICATION_XML));
ContentNegotiationManager manager = this.factoryBean.build();
assertThat(manager.getAllFileExtensions()).containsExactlyInAnyOrder("json", "xml", "pdf");
} }
@Test @Test
@ -175,7 +230,8 @@ public class ContentNegotiationManagerFactoryBeanTests {
this.servletRequest.setRequestURI("/flower"); this.servletRequest.setRequestURI("/flower");
this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE); this.servletRequest.addHeader("Accept", MediaType.IMAGE_GIF_VALUE);
assertThat(manager.resolveMediaTypes(this.webRequest)).isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST); assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST);
} }
@Test @Test
@ -210,10 +266,12 @@ public class ContentNegotiationManagerFactoryBeanTests {
this.factoryBean.afterPropertiesSet(); this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject(); ContentNegotiationManager manager = this.factoryBean.getObject();
assertThat(manager.resolveMediaTypes(this.webRequest)).isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON)); assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON));
this.servletRequest.addHeader("Accept", MediaType.ALL_VALUE); this.servletRequest.addHeader("Accept", MediaType.ALL_VALUE);
assertThat(manager.resolveMediaTypes(this.webRequest)).isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON)); assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(MediaType.APPLICATION_JSON));
} }

View File

@ -17,6 +17,7 @@
package org.springframework.web.accept; package org.springframework.web.accept;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -34,12 +35,14 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
public class MappingMediaTypeFileExtensionResolverTests { public class MappingMediaTypeFileExtensionResolverTests {
private final Map<String, MediaType> mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON); private static final Map<String, MediaType> DEFAULT_MAPPINGS =
private final MappingMediaTypeFileExtensionResolver resolver = new MappingMediaTypeFileExtensionResolver(this.mapping); Collections.singletonMap("json", MediaType.APPLICATION_JSON);
@Test @Test
public void resolveExtensions() { public void resolveExtensions() {
List<String> extensions = this.resolver.resolveFileExtensions(MediaType.APPLICATION_JSON); List<String> extensions = new MappingMediaTypeFileExtensionResolver(DEFAULT_MAPPINGS)
.resolveFileExtensions(MediaType.APPLICATION_JSON);
assertThat(extensions).hasSize(1); assertThat(extensions).hasSize(1);
assertThat(extensions.get(0)).isEqualTo("json"); assertThat(extensions.get(0)).isEqualTo("json");
@ -47,20 +50,24 @@ public class MappingMediaTypeFileExtensionResolverTests {
@Test @Test
public void resolveExtensionsNoMatch() { public void resolveExtensionsNoMatch() {
List<String> extensions = this.resolver.resolveFileExtensions(MediaType.TEXT_HTML); assertThat(new MappingMediaTypeFileExtensionResolver(DEFAULT_MAPPINGS)
.resolveFileExtensions(MediaType.TEXT_HTML)).isEmpty();
assertThat(extensions).isEmpty();
} }
/** @Test // SPR-13747
* Unit test for SPR-13747 - ensures that reverse lookup of media type from media
* type key is case-insensitive.
*/
@Test
public void lookupMediaTypeCaseInsensitive() { public void lookupMediaTypeCaseInsensitive() {
MediaType mediaType = this.resolver.lookupMediaType("JSON"); assertThat(new MappingMediaTypeFileExtensionResolver(DEFAULT_MAPPINGS).lookupMediaType("JSON"))
.isEqualTo(MediaType.APPLICATION_JSON);
assertThat(mediaType).isEqualTo(MediaType.APPLICATION_JSON);
} }
@Test
public void allFileExtensions() {
Map<String, MediaType> mappings = new HashMap<>();
mappings.put("json", MediaType.APPLICATION_JSON);
mappings.put("JsOn", MediaType.APPLICATION_JSON);
mappings.put("jSoN", MediaType.APPLICATION_JSON);
MappingMediaTypeFileExtensionResolver resolver = new MappingMediaTypeFileExtensionResolver(mappings);
assertThat(resolver.getAllFileExtensions()).containsExactly("json");
}
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -32,7 +32,6 @@ import org.springframework.web.accept.ContentNegotiationStrategy;
import org.springframework.web.accept.FixedContentNegotiationStrategy; import org.springframework.web.accept.FixedContentNegotiationStrategy;
import org.springframework.web.accept.HeaderContentNegotiationStrategy; import org.springframework.web.accept.HeaderContentNegotiationStrategy;
import org.springframework.web.accept.ParameterContentNegotiationStrategy; import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
/** /**
* Creates a {@code ContentNegotiationManager} and configures it with * Creates a {@code ContentNegotiationManager} and configures it with
@ -52,7 +51,8 @@ import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
* <tr> * <tr>
* <td>{@link #favorPathExtension}</td> * <td>{@link #favorPathExtension}</td>
* <td>true</td> * <td>true</td>
* <td>{@link PathExtensionContentNegotiationStrategy}</td> * <td>{@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy
* PathExtensionContentNegotiationStrategy}</td>
* <td>Enabled</td> * <td>Enabled</td>
* </tr> * </tr>
* <tr> * <tr>
@ -129,7 +129,11 @@ public class ContentNegotiationConfigurer {
* <p>By default this is set to {@code true} in which case a request * <p>By default this is set to {@code true} in which case a request
* for {@code /hotels.pdf} will be interpreted as a request for * for {@code /hotels.pdf} will be interpreted as a request for
* {@code "application/pdf"} regardless of the 'Accept' header. * {@code "application/pdf"} regardless of the 'Accept' header.
* @deprecated as of 5.2.4. See class-level note in
* {@link ContentNegotiationManagerFactoryBean} on the deprecation of path
* extension config options.
*/ */
@Deprecated
public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) { public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) {
this.factory.setFavorPathExtension(favorPathExtension); this.factory.setFavorPathExtension(favorPathExtension);
return this; return this;
@ -183,7 +187,11 @@ public class ContentNegotiationConfigurer {
* to any media type. Setting this to {@code false} will result in an * to any media type. Setting this to {@code false} will result in an
* {@code HttpMediaTypeNotAcceptableException} if there is no match. * {@code HttpMediaTypeNotAcceptableException} if there is no match.
* <p>By default this is set to {@code true}. * <p>By default this is set to {@code true}.
* @deprecated as of 5.2.4. See class-level note in
* {@link ContentNegotiationManagerFactoryBean} on the deprecation of path
* extension config options.
*/ */
@Deprecated
public ContentNegotiationConfigurer ignoreUnknownPathExtensions(boolean ignore) { public ContentNegotiationConfigurer ignoreUnknownPathExtensions(boolean ignore) {
this.factory.setIgnoreUnknownPathExtensions(ignore); this.factory.setIgnoreUnknownPathExtensions(ignore);
return this; return this;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -22,6 +22,7 @@ import java.util.function.Predicate;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.PathMatcher; import org.springframework.util.PathMatcher;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.UrlPathHelper;
/** /**
@ -37,7 +38,7 @@ import org.springframework.web.util.UrlPathHelper;
* *
* @author Brian Clozel * @author Brian Clozel
* @since 4.0.3 * @since 4.0.3
* @see org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping * @see RequestMappingHandlerMapping
* @see org.springframework.web.servlet.handler.SimpleUrlHandlerMapping * @see org.springframework.web.servlet.handler.SimpleUrlHandlerMapping
*/ */
public class PathMatchConfigurer { public class PathMatchConfigurer {
@ -46,10 +47,10 @@ public class PathMatchConfigurer {
private Boolean suffixPatternMatch; private Boolean suffixPatternMatch;
@Nullable @Nullable
private Boolean trailingSlashMatch; private Boolean registeredSuffixPatternMatch;
@Nullable @Nullable
private Boolean registeredSuffixPatternMatch; private Boolean trailingSlashMatch;
@Nullable @Nullable
private UrlPathHelper urlPathHelper; private UrlPathHelper urlPathHelper;
@ -66,22 +67,16 @@ public class PathMatchConfigurer {
* requests. If enabled a method mapped to "/users" also matches to "/users.*". * requests. If enabled a method mapped to "/users" also matches to "/users.*".
* <p>By default this is set to {@code true}. * <p>By default this is set to {@code true}.
* @see #registeredSuffixPatternMatch * @see #registeredSuffixPatternMatch
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path extension
* config options.
*/ */
@Deprecated
public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) { public PathMatchConfigurer setUseSuffixPatternMatch(Boolean suffixPatternMatch) {
this.suffixPatternMatch = suffixPatternMatch; this.suffixPatternMatch = suffixPatternMatch;
return this; return this;
} }
/**
* Whether to match to URLs irrespective of the presence of a trailing slash.
* If enabled a method mapped to "/users" also matches to "/users/".
* <p>The default value is {@code true}.
*/
public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) {
this.trailingSlashMatch = trailingSlashMatch;
return this;
}
/** /**
* Whether suffix pattern matching should work only against path extensions * Whether suffix pattern matching should work only against path extensions
* explicitly registered when you * explicitly registered when you
@ -90,12 +85,26 @@ public class PathMatchConfigurer {
* avoid issues such as when a "." appears in the path for other reasons. * avoid issues such as when a "." appears in the path for other reasons.
* <p>By default this is set to "false". * <p>By default this is set to "false".
* @see WebMvcConfigurer#configureContentNegotiation * @see WebMvcConfigurer#configureContentNegotiation
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path extension
* config options.
*/ */
@Deprecated
public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean registeredSuffixPatternMatch) { public PathMatchConfigurer setUseRegisteredSuffixPatternMatch(Boolean registeredSuffixPatternMatch) {
this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; this.registeredSuffixPatternMatch = registeredSuffixPatternMatch;
return this; return this;
} }
/**
* Whether to match to URLs irrespective of the presence of a trailing slash.
* If enabled a method mapped to "/users" also matches to "/users/".
* <p>The default value is {@code true}.
*/
public PathMatchConfigurer setUseTrailingSlashMatch(Boolean trailingSlashMatch) {
this.trailingSlashMatch = trailingSlashMatch;
return this;
}
/** /**
* Set the UrlPathHelper to use for resolution of lookup paths. * Set the UrlPathHelper to use for resolution of lookup paths.
* <p>Use this to override the default UrlPathHelper with a custom subclass, * <p>Use this to override the default UrlPathHelper with a custom subclass,
@ -137,21 +146,35 @@ public class PathMatchConfigurer {
} }
/**
* Whether to use registered suffixes for pattern matching.
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path extension
* config options.
*/
@Nullable @Nullable
@Deprecated
public Boolean isUseSuffixPatternMatch() { public Boolean isUseSuffixPatternMatch() {
return this.suffixPatternMatch; return this.suffixPatternMatch;
} }
/**
* Whether to use registered suffixes for pattern matching.
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path extension
* config options.
*/
@Nullable
@Deprecated
public Boolean isUseRegisteredSuffixPatternMatch() {
return this.registeredSuffixPatternMatch;
}
@Nullable @Nullable
public Boolean isUseTrailingSlashMatch() { public Boolean isUseTrailingSlashMatch() {
return this.trailingSlashMatch; return this.trailingSlashMatch;
} }
@Nullable
public Boolean isUseRegisteredSuffixPatternMatch() {
return this.registeredSuffixPatternMatch;
}
@Nullable @Nullable
public UrlPathHelper getUrlPathHelper() { public UrlPathHelper getUrlPathHelper() {
return this.urlPathHelper; return this.urlPathHelper;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -151,6 +151,7 @@ public class ResourceHandlerRegistry {
* of no registrations. * of no registrations.
*/ */
@Nullable @Nullable
@SuppressWarnings("deprecation")
protected AbstractHandlerMapping getHandlerMapping() { protected AbstractHandlerMapping getHandlerMapping() {
if (this.registrations.isEmpty()) { if (this.registrations.isEmpty()) {
return null; return null;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -275,6 +275,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
* requests to annotated controllers. * requests to annotated controllers.
*/ */
@Bean @Bean
@SuppressWarnings("deprecation")
public RequestMappingHandlerMapping requestMappingHandlerMapping( public RequestMappingHandlerMapping requestMappingHandlerMapping(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService, @Qualifier("mvcConversionService") FormattingConversionService conversionService,

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -33,6 +33,7 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher; import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.UrlPathHelper;
/** /**
@ -58,23 +59,42 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
/** /**
* Creates a new instance with the given URL patterns. * Creates a new instance with the given URL patterns. Each pattern that is
* Each pattern that is not empty and does not start with "/" is prepended with "/". * not empty and does not start with "/" is prepended with "/".
* @param patterns 0 or more URL patterns; if 0 the condition will match to every request. * @param patterns 0 or more URL patterns; if 0 the condition will match to
* every request.
*/ */
public PatternsRequestCondition(String... patterns) { public PatternsRequestCondition(String... patterns) {
this(Arrays.asList(patterns), null, null, true, true, null); this(Arrays.asList(patterns), null, null, true, true, null);
} }
/** /**
* Additional constructor with flags for using suffix pattern (.*) and * Alternative constructor with additional, optional {@link UrlPathHelper},
* trailing slash matches. * {@link PathMatcher}, and whether to automatically match trailing slashes.
* @param patterns the URL patterns to use; if 0, the condition will match to every request.
* @param urlPathHelper a {@link UrlPathHelper} for determining the lookup path for a request
* @param pathMatcher a {@link PathMatcher} for pattern path matching
* @param useTrailingSlashMatch whether to match irrespective of a trailing slash
* @since 5.2.4
*/
public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper,
@Nullable PathMatcher pathMatcher, boolean useTrailingSlashMatch) {
this(Arrays.asList(patterns), urlPathHelper, pathMatcher, false, useTrailingSlashMatch, null);
}
/**
* Alternative constructor with additional optional parameters.
* @param patterns the URL patterns to use; if 0, the condition will match to every request. * @param patterns the URL patterns to use; if 0, the condition will match to every request.
* @param urlPathHelper for determining the lookup path of a request * @param urlPathHelper for determining the lookup path of a request
* @param pathMatcher for path matching with patterns * @param pathMatcher for path matching with patterns
* @param useSuffixPatternMatch whether to enable matching by suffix (".*") * @param useSuffixPatternMatch whether to enable matching by suffix (".*")
* @param useTrailingSlashMatch whether to match irrespective of a trailing slash * @param useTrailingSlashMatch whether to match irrespective of a trailing slash
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path extension
* config options.
*/ */
@Deprecated
public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper, public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper,
@Nullable PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch) { @Nullable PathMatcher pathMatcher, boolean useSuffixPatternMatch, boolean useTrailingSlashMatch) {
@ -82,15 +102,18 @@ public final class PatternsRequestCondition extends AbstractRequestCondition<Pat
} }
/** /**
* Creates a new instance with the given URL patterns. * Alternative constructor with additional optional parameters.
* Each pattern that is not empty and does not start with "/" is pre-pended with "/".
* @param patterns the URL patterns to use; if 0, the condition will match to every request. * @param patterns the URL patterns to use; if 0, the condition will match to every request.
* @param urlPathHelper a {@link UrlPathHelper} for determining the lookup path for a request * @param urlPathHelper a {@link UrlPathHelper} for determining the lookup path for a request
* @param pathMatcher a {@link PathMatcher} for pattern path matching * @param pathMatcher a {@link PathMatcher} for pattern path matching
* @param useSuffixPatternMatch whether to enable matching by suffix (".*") * @param useSuffixPatternMatch whether to enable matching by suffix (".*")
* @param useTrailingSlashMatch whether to match irrespective of a trailing slash * @param useTrailingSlashMatch whether to match irrespective of a trailing slash
* @param fileExtensions a list of file extensions to consider for path matching * @param fileExtensions a list of file extensions to consider for path matching
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path extension
* config options.
*/ */
@Deprecated
public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper, public PatternsRequestCondition(String[] patterns, @Nullable UrlPathHelper urlPathHelper,
@Nullable PathMatcher pathMatcher, boolean useSuffixPatternMatch, @Nullable PathMatcher pathMatcher, boolean useSuffixPatternMatch,
boolean useTrailingSlashMatch, @Nullable List<String> fileExtensions) { boolean useTrailingSlashMatch, @Nullable List<String> fileExtensions) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -35,6 +35,7 @@ import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestCondition; import org.springframework.web.servlet.mvc.condition.RequestCondition;
import org.springframework.web.servlet.mvc.condition.RequestConditionHolder; import org.springframework.web.servlet.mvc.condition.RequestConditionHolder;
import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.UrlPathHelper; import org.springframework.web.util.UrlPathHelper;
/** /**
@ -505,6 +506,7 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
} }
@Override @Override
@SuppressWarnings("deprecation")
public RequestMappingInfo build() { public RequestMappingInfo build() {
ContentNegotiationManager manager = this.options.getContentNegotiationManager(); ContentNegotiationManager manager = this.options.getContentNegotiationManager();
@ -600,14 +602,22 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
* Set whether to apply suffix pattern matching in PatternsRequestCondition. * Set whether to apply suffix pattern matching in PatternsRequestCondition.
* <p>By default this is set to 'true'. * <p>By default this is set to 'true'.
* @see #setRegisteredSuffixPatternMatch(boolean) * @see #setRegisteredSuffixPatternMatch(boolean)
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path
* extension config options.
*/ */
@Deprecated
public void setSuffixPatternMatch(boolean suffixPatternMatch) { public void setSuffixPatternMatch(boolean suffixPatternMatch) {
this.suffixPatternMatch = suffixPatternMatch; this.suffixPatternMatch = suffixPatternMatch;
} }
/** /**
* Return whether to apply suffix pattern matching in PatternsRequestCondition. * Return whether to apply suffix pattern matching in PatternsRequestCondition.
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path
* extension config options.
*/ */
@Deprecated
public boolean useSuffixPatternMatch() { public boolean useSuffixPatternMatch() {
return this.suffixPatternMatch; return this.suffixPatternMatch;
} }
@ -618,7 +628,12 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
* {@code suffixPatternMatch=true} and requires that a * {@code suffixPatternMatch=true} and requires that a
* {@link #setContentNegotiationManager} is also configured in order to * {@link #setContentNegotiationManager} is also configured in order to
* obtain the registered file extensions. * obtain the registered file extensions.
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path
* extension config options; note also that in 5.3 the default for this
* property switches from {@code false} to {@code true}.
*/ */
@Deprecated
public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) { public void setRegisteredSuffixPatternMatch(boolean registeredSuffixPatternMatch) {
this.registeredSuffixPatternMatch = registeredSuffixPatternMatch; this.registeredSuffixPatternMatch = registeredSuffixPatternMatch;
this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch); this.suffixPatternMatch = (registeredSuffixPatternMatch || this.suffixPatternMatch);
@ -627,7 +642,11 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
/** /**
* Return whether suffix pattern matching should be restricted to registered * Return whether suffix pattern matching should be restricted to registered
* file extensions only. * file extensions only.
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path
* extension config options.
*/ */
@Deprecated
public boolean useRegisteredSuffixPatternMatch() { public boolean useRegisteredSuffixPatternMatch() {
return this.registeredSuffixPatternMatch; return this.registeredSuffixPatternMatch;
} }
@ -636,8 +655,12 @@ public final class RequestMappingInfo implements RequestCondition<RequestMapping
* Return the file extensions to use for suffix pattern matching. If * Return the file extensions to use for suffix pattern matching. If
* {@code registeredSuffixPatternMatch=true}, the extensions are obtained * {@code registeredSuffixPatternMatch=true}, the extensions are obtained
* from the configured {@code contentNegotiationManager}. * from the configured {@code contentNegotiationManager}.
* @deprecated as of 5.2.4. See class-level note in
* {@link RequestMappingHandlerMapping} on the deprecation of path
* extension config options.
*/ */
@Nullable @Nullable
@Deprecated
public List<String> getFileExtensions() { public List<String> getFileExtensions() {
if (useRegisteredSuffixPatternMatch() && this.contentNegotiationManager != null) { if (useRegisteredSuffixPatternMatch() && this.contentNegotiationManager != null) {
return this.contentNegotiationManager.getAllFileExtensions(); return this.contentNegotiationManager.getAllFileExtensions();

View File

@ -26,6 +26,7 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set; import java.util.Set;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -43,6 +44,7 @@ import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpRange; import org.springframework.http.HttpRange;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.converter.HttpMessageNotWritableException;
@ -54,7 +56,6 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
@ -102,8 +103,6 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
private final ContentNegotiationManager contentNegotiationManager; private final ContentNegotiationManager contentNegotiationManager;
private final PathExtensionContentNegotiationStrategy pathStrategy;
private final Set<String> safeExtensions = new HashSet<>(); private final Set<String> safeExtensions = new HashSet<>();
@ -133,17 +132,10 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
super(converters, requestResponseBodyAdvice); super(converters, requestResponseBodyAdvice);
this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager()); this.contentNegotiationManager = (manager != null ? manager : new ContentNegotiationManager());
this.pathStrategy = initPathStrategy(this.contentNegotiationManager);
this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions()); this.safeExtensions.addAll(this.contentNegotiationManager.getAllFileExtensions());
this.safeExtensions.addAll(WHITELISTED_EXTENSIONS); this.safeExtensions.addAll(WHITELISTED_EXTENSIONS);
} }
private static PathExtensionContentNegotiationStrategy initPathStrategy(ContentNegotiationManager manager) {
Class<PathExtensionContentNegotiationStrategy> clazz = PathExtensionContentNegotiationStrategy.class;
PathExtensionContentNegotiationStrategy strategy = manager.getStrategy(clazz);
return (strategy != null ? strategy : new PathExtensionContentNegotiationStrategy());
}
/** /**
* Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}. * Creates a new {@link HttpOutputMessage} from the given {@link NativeWebRequest}.
@ -481,26 +473,21 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe
return true; return true;
} }
} }
return safeMediaTypesForExtension(new ServletWebRequest(request), extension); MediaType mediaType = resolveMediaType(request, extension);
return (mediaType != null && (safeMediaType(mediaType)));
} }
private boolean safeMediaTypesForExtension(NativeWebRequest request, String extension) { @Nullable
List<MediaType> mediaTypes = null; private MediaType resolveMediaType(ServletRequest request, String extension) {
try { MediaType result = null;
mediaTypes = this.pathStrategy.resolveMediaTypeKey(request, extension); String rawMimeType = request.getServletContext().getMimeType("file." + extension);
if (StringUtils.hasText(rawMimeType)) {
result = MediaType.parseMediaType(rawMimeType);
} }
catch (HttpMediaTypeNotAcceptableException ex) { if (result == null || MediaType.APPLICATION_OCTET_STREAM.equals(result)) {
// Ignore result = MediaTypeFactory.getMediaType("file." + extension).orElse(null);
} }
if (CollectionUtils.isEmpty(mediaTypes)) { return result;
return false;
}
for (MediaType mediaType : mediaTypes) {
if (!safeMediaType(mediaType)) {
return false;
}
}
return true;
} }
private boolean safeMediaType(MediaType mediaType) { private boolean safeMediaType(MediaType mediaType) {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -59,6 +59,18 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMappi
* {@link RequestMapping @RequestMapping} annotations in * {@link RequestMapping @RequestMapping} annotations in
* {@link Controller @Controller} classes. * {@link Controller @Controller} classes.
* *
* <p><strong>Note:</strong></p> In 5.2.4,
* {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch} and
* {@link #setUseRegisteredSuffixPatternMatch(boolean) useRegisteredSuffixPatternMatch}
* are deprecated in order to discourage use of path extensions for request
* mapping and for content negotiation (with similar deprecations in
* {@link ContentNegotiationManager}). For further context, please read issue
* <a href="https://github.com/spring-projects/spring-framework/issues/24179">#24719</a>.
*
* <p>In 5.3, {@link #setUseRegisteredSuffixPatternMatch(boolean) useRegisteredSuffixPatternMatch}
* switches to being on by default so that path matching becomes constrained
* to registered suffixes only.
*
* @author Arjen Poutsma * @author Arjen Poutsma
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sam Brannen * @author Sam Brannen
@ -89,7 +101,10 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
* <p>The default value is {@code true}. * <p>The default value is {@code true}.
* <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for * <p>Also see {@link #setUseRegisteredSuffixPatternMatch(boolean)} for
* more fine-grained control over specific suffixes to allow. * more fine-grained control over specific suffixes to allow.
* @deprecated as of 5.2.4. See class level comment about deprecation of
* path extension config options.
*/ */
@Deprecated
public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { public void setUseSuffixPatternMatch(boolean useSuffixPatternMatch) {
this.useSuffixPatternMatch = useSuffixPatternMatch; this.useSuffixPatternMatch = useSuffixPatternMatch;
} }
@ -100,7 +115,11 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
* is generally recommended to reduce ambiguity and to avoid issues such as * is generally recommended to reduce ambiguity and to avoid issues such as
* when a "." appears in the path for other reasons. * when a "." appears in the path for other reasons.
* <p>By default this is set to "false". * <p>By default this is set to "false".
* @deprecated as of 5.2.4. See class level comment about deprecation of
* path extension config options note also that in 5.3 the default for this
* property will switch from {@code false} to {@code true}.
*/ */
@Deprecated
public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) { public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch; this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch); this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
@ -159,13 +178,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
} }
@Override @Override
@SuppressWarnings("deprecation")
public void afterPropertiesSet() { public void afterPropertiesSet() {
this.config = new RequestMappingInfo.BuilderConfiguration(); this.config = new RequestMappingInfo.BuilderConfiguration();
this.config.setUrlPathHelper(getUrlPathHelper()); this.config.setUrlPathHelper(getUrlPathHelper());
this.config.setPathMatcher(getPathMatcher()); this.config.setPathMatcher(getPathMatcher());
this.config.setSuffixPatternMatch(this.useSuffixPatternMatch); this.config.setSuffixPatternMatch(useSuffixPatternMatch());
this.config.setTrailingSlashMatch(this.useTrailingSlashMatch); this.config.setTrailingSlashMatch(useTrailingSlashMatch());
this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch); this.config.setRegisteredSuffixPatternMatch(useRegisteredSuffixPatternMatch());
this.config.setContentNegotiationManager(getContentNegotiationManager()); this.config.setContentNegotiationManager(getContentNegotiationManager());
super.afterPropertiesSet(); super.afterPropertiesSet();
@ -173,15 +193,21 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
/** /**
* Whether to use suffix pattern matching. * Whether to use registered suffixes for pattern matching.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options.
*/ */
@Deprecated
public boolean useSuffixPatternMatch() { public boolean useSuffixPatternMatch() {
return this.useSuffixPatternMatch; return this.useSuffixPatternMatch;
} }
/** /**
* Whether to use registered suffixes for pattern matching. * Whether to use registered suffixes for pattern matching.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options.
*/ */
@Deprecated
public boolean useRegisteredSuffixPatternMatch() { public boolean useRegisteredSuffixPatternMatch() {
return this.useRegisteredSuffixPatternMatch; return this.useRegisteredSuffixPatternMatch;
} }
@ -195,8 +221,12 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi
/** /**
* Return the file extensions to use for suffix pattern matching. * Return the file extensions to use for suffix pattern matching.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options.
*/ */
@Nullable @Nullable
@Deprecated
@SuppressWarnings("deprecation")
public List<String> getFileExtensions() { public List<String> getFileExtensions() {
return this.config.getFileExtensions(); return this.config.getFileExtensions();
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,6 +24,7 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -43,6 +44,7 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRange; import org.springframework.http.HttpRange;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.ResourceRegionHttpMessageConverter; import org.springframework.http.converter.ResourceRegionHttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpRequest;
@ -56,8 +58,6 @@ import org.springframework.util.StringUtils;
import org.springframework.util.StringValueResolver; import org.springframework.util.StringValueResolver;
import org.springframework.web.HttpRequestHandler; import org.springframework.web.HttpRequestHandler;
import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.accept.ServletPathExtensionContentNegotiationStrategy;
import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
@ -129,8 +129,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
@Nullable @Nullable
private ContentNegotiationManager contentNegotiationManager; private ContentNegotiationManager contentNegotiationManager;
@Nullable private final Map<String, MediaType> mediaTypes = new HashMap<>(4);
private PathExtensionContentNegotiationStrategy contentNegotiationStrategy;
@Nullable @Nullable
private CorsConfiguration corsConfiguration; private CorsConfiguration corsConfiguration;
@ -262,7 +261,11 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
* media types for resources being served. If the manager contains a path * media types for resources being served. If the manager contains a path
* extension strategy it will be checked for registered file extension. * extension strategy it will be checked for registered file extension.
* @since 4.3 * @since 4.3
* @deprecated as of 5.2.4 in favor of using {@link #setMediaTypes(Map)}
* with mappings possibly obtained from
* {@link ContentNegotiationManager#getMediaTypeMappings()}.
*/ */
@Deprecated
public void setContentNegotiationManager(@Nullable ContentNegotiationManager contentNegotiationManager) { public void setContentNegotiationManager(@Nullable ContentNegotiationManager contentNegotiationManager) {
this.contentNegotiationManager = contentNegotiationManager; this.contentNegotiationManager = contentNegotiationManager;
} }
@ -270,12 +273,38 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/** /**
* Return the configured content negotiation manager. * Return the configured content negotiation manager.
* @since 4.3 * @since 4.3
* @deprecated as of 5.2.4.
*/ */
@Nullable @Nullable
@Deprecated
public ContentNegotiationManager getContentNegotiationManager() { public ContentNegotiationManager getContentNegotiationManager() {
return this.contentNegotiationManager; return this.contentNegotiationManager;
} }
/**
* Add mappings between file extensions, extracted from the filename of a
* static {@link Resource}, and corresponding media type to set on the
* response.
* <p>Use of this method is typically not necessary since mappings are
* otherwise determined via
* {@link javax.servlet.ServletContext#getMimeType(String)} or via
* {@link MediaTypeFactory#getMediaType(Resource)}.
* @param mediaTypes media type mappings
* @since 5.2.4
*/
public void setMediaTypes(Map<String, MediaType> mediaTypes) {
mediaTypes.forEach((ext, mediaType) ->
this.mediaTypes.put(ext.toLowerCase(Locale.ENGLISH), mediaType));
}
/**
* Return the {@link #setMediaTypes(Map) configured} media types.
* @since 5.2.4
*/
public Map<String, MediaType> getMediaTypes() {
return this.mediaTypes;
}
/** /**
* Specify the CORS configuration for resources served by this handler. * Specify the CORS configuration for resources served by this handler.
* <p>By default this is not set in which allows cross-origin requests. * <p>By default this is not set in which allows cross-origin requests.
@ -344,7 +373,17 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter(); this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter();
} }
this.contentNegotiationStrategy = initContentNegotiationStrategy(); ContentNegotiationManager manager = getContentNegotiationManager();
if (manager != null) {
setMediaTypes(manager.getMediaTypeMappings());
}
@SuppressWarnings("deprecation")
org.springframework.web.accept.PathExtensionContentNegotiationStrategy strategy =
initContentNegotiationStrategy();
if (strategy != null) {
setMediaTypes(strategy.getMediaTypes());
}
} }
private void resolveResourceLocations() { private void resolveResourceLocations() {
@ -412,26 +451,20 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
} }
/** /**
* Initialize the content negotiation strategy depending on the {@code ContentNegotiationManager} * Initialize the strategy to use to determine the media type for a resource.
* setup and the availability of a {@code ServletContext}. * @deprecated as of 5.2.4 this method returns {@code null}, and if a
* @see ServletPathExtensionContentNegotiationStrategy * sub-class returns an actual instance,the instance is used only as a
* @see PathExtensionContentNegotiationStrategy * source of media type mappings, if it contains any. Please, use
* {@link #setMediaTypes(Map)} instead, or if you need to change behavior,
* you can override {@link #getMediaType(HttpServletRequest, Resource)}.
*/ */
protected PathExtensionContentNegotiationStrategy initContentNegotiationStrategy() { @Nullable
Map<String, MediaType> mediaTypes = null; @Deprecated
if (getContentNegotiationManager() != null) { @SuppressWarnings("deprecation")
PathExtensionContentNegotiationStrategy strategy = protected org.springframework.web.accept.PathExtensionContentNegotiationStrategy initContentNegotiationStrategy() {
getContentNegotiationManager().getStrategy(PathExtensionContentNegotiationStrategy.class); return null;
if (strategy != null) {
mediaTypes = new HashMap<>(strategy.getMediaTypes());
}
}
return (getServletContext() != null ?
new ServletPathExtensionContentNegotiationStrategy(getServletContext(), mediaTypes) :
new PathExtensionContentNegotiationStrategy(mediaTypes));
} }
/** /**
* Processes a resource request. * Processes a resource request.
* <p>Checks for the existence of the requested resource in the configured list of locations. * <p>Checks for the existence of the requested resource in the configured list of locations.
@ -659,17 +692,40 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/** /**
* Determine the media type for the given request and the resource matched * Determine the media type for the given request and the resource matched
* to it. This implementation tries to determine the MediaType based on the * to it. This implementation tries to determine the MediaType using one of
* file extension of the Resource via * the following lookups based on the resource filename and its path
* {@link ServletPathExtensionContentNegotiationStrategy#getMediaTypeForResource}. * extension:
* <ol>
* <li>{@link javax.servlet.ServletContext#getMimeType(String)}
* <li>{@link #getMediaTypes()}
* <li>{@link MediaTypeFactory#getMediaType(String)}
* </ol>
* @param request the current request * @param request the current request
* @param resource the resource to check * @param resource the resource to check
* @return the corresponding media type, or {@code null} if none found * @return the corresponding media type, or {@code null} if none found
*/ */
@Nullable @Nullable
protected MediaType getMediaType(HttpServletRequest request, Resource resource) { protected MediaType getMediaType(HttpServletRequest request, Resource resource) {
return (this.contentNegotiationStrategy != null ? MediaType result = null;
this.contentNegotiationStrategy.getMediaTypeForResource(resource) : null); String mimeType = request.getServletContext().getMimeType(resource.getFilename());
if (StringUtils.hasText(mimeType)) {
result = MediaType.parseMediaType(mimeType);
}
if (result == null || MediaType.APPLICATION_OCTET_STREAM.equals(result)) {
MediaType mediaType = null;
String filename = resource.getFilename();
String ext = StringUtils.getFilenameExtension(filename);
if (ext != null) {
mediaType = this.mediaTypes.get(ext.toLowerCase(Locale.ENGLISH));
}
if (mediaType == null) {
mediaType = MediaTypeFactory.getMediaType(filename).orElse(null);
}
if (mediaType != null) {
result = mediaType;
}
}
return result;
} }
/** /**

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2020 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -74,13 +74,15 @@ public class ResourceHttpRequestHandlerTests {
paths.add(new ClassPathResource("testalternatepath/", getClass())); paths.add(new ClassPathResource("testalternatepath/", getClass()));
paths.add(new ClassPathResource("META-INF/resources/webjars/")); paths.add(new ClassPathResource("META-INF/resources/webjars/"));
TestServletContext servletContext = new TestServletContext();
this.handler = new ResourceHttpRequestHandler(); this.handler = new ResourceHttpRequestHandler();
this.handler.setLocations(paths); this.handler.setLocations(paths);
this.handler.setCacheSeconds(3600); this.handler.setCacheSeconds(3600);
this.handler.setServletContext(new TestServletContext()); this.handler.setServletContext(servletContext);
this.handler.afterPropertiesSet(); this.handler.afterPropertiesSet();
this.request = new MockHttpServletRequest("GET", ""); this.request = new MockHttpServletRequest(servletContext, "GET", "");
this.response = new MockHttpServletResponse(); this.response = new MockHttpServletResponse();
} }
@ -283,15 +285,12 @@ public class ResourceHttpRequestHandlerTests {
@Test // SPR-14368 @Test // SPR-14368
public void getResourceWithMediaTypeResolvedThroughServletContext() throws Exception { public void getResourceWithMediaTypeResolvedThroughServletContext() throws Exception {
MockServletContext servletContext = new MockServletContext() { MockServletContext servletContext = new MockServletContext() {
@Override @Override
public String getMimeType(String filePath) { public String getMimeType(String filePath) {
return "foo/bar"; return "foo/bar";
} }
@Override
public String getVirtualServerName() {
return "";
}
}; };
List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass())); List<Resource> paths = Collections.singletonList(new ClassPathResource("test/", getClass()));
@ -300,8 +299,9 @@ public class ResourceHttpRequestHandlerTests {
handler.setLocations(paths); handler.setLocations(paths);
handler.afterPropertiesSet(); handler.afterPropertiesSet();
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css"); MockHttpServletRequest request = new MockHttpServletRequest(servletContext, "GET", "");
handler.handleRequest(this.request, this.response); request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.css");
handler.handleRequest(request, this.response);
assertThat(this.response.getContentType()).isEqualTo("foo/bar"); assertThat(this.response.getContentType()).isEqualTo("foo/bar");
assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }"); assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }");

View File

@ -1683,6 +1683,18 @@ the issues that come with file extensions. Alternatively, if you must use file e
restricting them to a list of explicitly registered extensions through the restricting them to a list of explicitly registered extensions through the
`mediaTypes` property of <<mvc-config-content-negotiation,ContentNegotiationConfigurer>>. `mediaTypes` property of <<mvc-config-content-negotiation,ContentNegotiationConfigurer>>.
[INFO]
====
Starting in 5.2.4, path extension related options for request mapping in
{api-spring-framework}/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java[RequestMappingHandlerMapping]
and for content negotiation in
{api-spring-framework}/org.springframework.web.accept/ContentNegotiationManagerFactoryBean.java[ContentNegotiationManagerFactoryBean]
are deprecated. See Spring Framework issue
https://github.com/spring-projects/spring-framework/issues/24179[#24179] and related
issues for further plans.
====
[[mvc-ann-requestmapping-rfd]] [[mvc-ann-requestmapping-rfd]]
==== Suffix Match and RFD ==== Suffix Match and RFD
@ -5779,13 +5791,11 @@ The following example shows how to customize path matching in Java configuration
@Override @Override
public void configurePathMatch(PathMatchConfigurer configurer) { public void configurePathMatch(PathMatchConfigurer configurer) {
configurer configurer
.setUseSuffixPatternMatch(true)
.setUseTrailingSlashMatch(false) .setUseTrailingSlashMatch(false)
.setUseRegisteredSuffixPatternMatch(true) .setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher()) .setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper()) .setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api", .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController.class));
HandlerTypePredicate.forAnnotation(RestController.class));
} }
@Bean @Bean
@ -5813,8 +5823,7 @@ The following example shows how to customize path matching in Java configuration
.setUseRegisteredSuffixPatternMatch(true) .setUseRegisteredSuffixPatternMatch(true)
.setPathMatcher(antPathMatcher()) .setPathMatcher(antPathMatcher())
.setUrlPathHelper(urlPathHelper()) .setUrlPathHelper(urlPathHelper())
.addPathPrefix("/api", .addPathPrefix("/api", HandlerTypePredicate.forAnnotation(RestController::class.java))
HandlerTypePredicate.forAnnotation(RestController::class.java))
} }
@Bean @Bean
@ -5835,7 +5844,6 @@ The following example shows how to achieve the same configuration in XML:
---- ----
<mvc:annotation-driven> <mvc:annotation-driven>
<mvc:path-matching <mvc:path-matching
suffix-pattern="true"
trailing-slash="false" trailing-slash="false"
registered-suffixes-only="true" registered-suffixes-only="true"
path-helper="pathHelper" path-helper="pathHelper"