Remove Spring MVC path extension content negotiation

See gh-34036
This commit is contained in:
rstoyanchev 2024-12-17 09:34:57 +00:00
parent e9946937e7
commit 7f4da52cc5
21 changed files with 21 additions and 783 deletions

View File

@ -248,36 +248,6 @@ specific than other pattern that do not have double wildcards.
For the full details, follow the above links to the pattern Comparators.
[[mvc-ann-requestmapping-suffix-pattern-match]]
== Suffix Match
Starting in 5.3, by default Spring MVC no longer performs `.{asterisk}` suffix pattern
matching where a controller mapped to `/person` is also implicitly mapped to
`/person.{asterisk}`. As a consequence path extensions are no longer used to interpret
the requested content type for the response -- for example, `/person.pdf`, `/person.xml`,
and so on.
Using file extensions in this way was necessary when browsers used to send `Accept` headers
that were hard to interpret consistently. At present, that is no longer a necessity and
using the `Accept` header should be the preferred choice.
Over time, the use of file name extensions has proven problematic in a variety of ways.
It can cause ambiguity when overlain with the use of URI variables, path parameters, and
URI encoding. Reasoning about URL-based authorization
and security (see next section for more details) also becomes more difficult.
To completely disable the use of path extensions in versions prior to 5.3, set the following:
* `useSuffixPatternMatching(false)`, see xref:web/webmvc/mvc-config/path-matching.adoc[PathMatchConfigurer]
* `favorPathExtension(false)`, see xref:web/webmvc/mvc-config/content-negotiation.adoc[ContentNegotiationConfigurer]
Having a way to request content types other than through the `"Accept"` header can still
be useful, for example, when typing a URL in a browser. A safe alternative to path extensions is
to use the query parameter strategy. If you must use file extensions, consider restricting
them to a list of explicitly registered extensions through the `mediaTypes` property of
xref:web/webmvc/mvc-config/content-negotiation.adoc[ContentNegotiationConfigurer].
[[mvc-ann-requestmapping-rfd]]
== Suffix Match and RFD

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -83,8 +83,7 @@ public abstract class AbstractMappingContentNegotiationStrategy extends MappingM
/**
* Whether to ignore requests with unknown file extension. Setting this to
* {@code false} results in {@code HttpMediaTypeNotAcceptableException}.
* <p>By default this is set to {@literal false} but is overridden in
* {@link PathExtensionContentNegotiationStrategy} to {@literal true}.
* <p>By default, this is set to {@literal false}.
*/
public void setIgnoreUnknownExtensions(boolean ignoreUnknownExtensions) {
this.ignoreUnknownExtensions = ignoreUnknownExtensions;

View File

@ -139,17 +139,6 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
return doResolveExtensions(resolver -> resolver.resolveFileExtensions(mediaType));
}
/**
* {@inheritDoc}
* <p>At startup this method returns extensions explicitly registered with
* either {@link PathExtensionContentNegotiationStrategy} or
* {@link ParameterContentNegotiationStrategy}. At runtime if there is a
* "path extension" strategy and its
* {@link PathExtensionContentNegotiationStrategy#setUseRegisteredExtensionsOnly(boolean)
* useRegisteredExtensionsOnly} property is set to "false", the list of extensions may
* increase as file extensions are resolved via
* {@link org.springframework.http.MediaTypeFactory} and cached.
*/
@Override
public List<String> getAllFileExtensions() {
return doResolveExtensions(MediaTypeFileExtensionResolver::getAllFileExtensions);

View File

@ -23,8 +23,6 @@ import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import jakarta.servlet.ServletContext;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.http.MediaType;
@ -32,7 +30,6 @@ import org.springframework.http.MediaTypeFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.ServletContextAware;
/**
* Factory to create a {@code ContentNegotiationManager} and configure it with
@ -56,12 +53,6 @@ import org.springframework.web.context.ServletContextAware;
* <td>Off</td>
* </tr>
* <tr>
* <td>{@link #setFavorPathExtension favorPathExtension}</td>
* <td>false (as of 5.3)</td>
* <td>{@link PathExtensionContentNegotiationStrategy}</td>
* <td>Off</td>
* </tr>
* <tr>
* <td>{@link #setIgnoreAcceptHeader ignoreAcceptHeader}</td>
* <td>false</td>
* <td>{@link HeaderContentNegotiationStrategy}</td>
@ -85,20 +76,11 @@ import org.springframework.web.context.ServletContextAware;
* methods and set the exact strategies to use via
* {@link #setStrategies(List)}.
*
* <p><strong>Deprecation Note:</strong> As of 5.2.4,
* {@link #setFavorPathExtension(boolean) favorPathExtension} and
* {@link #setIgnoreUnknownPathExtensions(boolean) ignoreUnknownPathExtensions}
* are deprecated in order to discourage using path extensions for content
* negotiation and for request mapping with similar deprecations on
* {@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 Brian Clozel
* @since 3.2
*/
public class ContentNegotiationManagerFactoryBean
implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
public class ContentNegotiationManagerFactoryBean implements FactoryBean<ContentNegotiationManager>, InitializingBean {
@Nullable
private List<ContentNegotiationStrategy> strategies;
@ -108,12 +90,8 @@ public class ContentNegotiationManagerFactoryBean
private String parameterName = "format";
private boolean favorPathExtension = false;
private final Map<String, MediaType> mediaTypes = new HashMap<>();
private boolean ignoreUnknownPathExtensions = true;
@Nullable
private Boolean useRegisteredExtensionsOnly;
@ -125,9 +103,6 @@ public class ContentNegotiationManagerFactoryBean
@Nullable
private ContentNegotiationManager contentNegotiationManager;
@Nullable
private ServletContext servletContext;
/**
* Set the exact list of strategies to use.
@ -161,30 +136,12 @@ public class ContentNegotiationManagerFactoryBean
this.parameterName = parameterName;
}
/**
* Whether the path extension in the URL path should be used to determine
* the requested media type.
* <p>By default this is set to {@code false} in which case path extensions
* have no impact on content negotiation.
* @deprecated as of 5.2.4. See class-level note on the deprecation of path
* extension config options. As there is no replacement for this method,
* in 5.2.x it is necessary to set it to {@code false}. In 5.3 the default
* changes to {@code false} and use of this property becomes unnecessary.
*/
@Deprecated
public void setFavorPathExtension(boolean favorPathExtension) {
this.favorPathExtension = favorPathExtension;
}
/**
* Add a mapping from a key to a MediaType where the key are normalized to
* lowercase and may have been extracted from a path extension, a filename
* extension, or passed as a query parameter.
* <p>The {@link #setFavorParameter(boolean) parameter strategy} requires
* such mappings in order to work while the {@link #setFavorPathExtension(boolean)
* path extension strategy} can fall back on lookups via
* {@link ServletContext#getMimeType} and
* {@link org.springframework.http.MediaTypeFactory}.
* such mappings in order to work.
* <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,
@ -227,35 +184,10 @@ public class ContentNegotiationManagerFactoryBean
}
/**
* Whether to ignore requests with path extension that cannot be resolved
* to any media type. Setting this to {@code false} will result in an
* {@code HttpMediaTypeNotAcceptableException} if there is no match.
* <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) {
this.ignoreUnknownPathExtensions = ignore;
}
/**
* Indicate whether to use the Java Activation Framework as a fallback option
* to map from file extensions to media types.
* @deprecated as of 5.0, in favor of {@link #setUseRegisteredExtensionsOnly(boolean)},
* which has reverse behavior.
*/
@Deprecated
public void setUseJaf(boolean useJaf) {
setUseRegisteredExtensionsOnly(!useJaf);
}
/**
* When {@link #setFavorPathExtension favorPathExtension} or
* {@link #setFavorParameter(boolean)} is set, this property determines
* When {@link #setFavorParameter(boolean)} is set, this property determines
* whether to use only registered {@code MediaType} mappings or to allow
* dynamic resolution, for example, via {@link MediaTypeFactory}.
* <p>By default this is not set in which case dynamic resolution is on.
* <p>By default, this is not set in which case dynamic resolution is on.
*/
public void setUseRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) {
this.useRegisteredExtensionsOnly = useRegisteredExtensionsOnly;
@ -303,14 +235,6 @@ public class ContentNegotiationManagerFactoryBean
this.defaultNegotiationStrategy = strategy;
}
/**
* Invoked by Spring to inject the ServletContext.
*/
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
@Override
public void afterPropertiesSet() {
@ -321,7 +245,6 @@ public class ContentNegotiationManagerFactoryBean
* Create and initialize a {@link ContentNegotiationManager} instance.
* @since 5.0
*/
@SuppressWarnings("deprecation")
public ContentNegotiationManager build() {
List<ContentNegotiationStrategy> strategies = new ArrayList<>();
@ -329,20 +252,6 @@ public class ContentNegotiationManagerFactoryBean
strategies.addAll(this.strategies);
}
else {
if (this.favorPathExtension) {
PathExtensionContentNegotiationStrategy strategy;
if (this.servletContext != null && !useRegisteredExtensionsOnly()) {
strategy = new ServletPathExtensionContentNegotiationStrategy(this.servletContext, this.mediaTypes);
}
else {
strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes);
}
strategy.setIgnoreUnknownExtensions(this.ignoreUnknownPathExtensions);
if (this.useRegisteredExtensionsOnly != null) {
strategy.setUseRegisteredExtensionsOnly(this.useRegisteredExtensionsOnly);
}
strategies.add(strategy);
}
if (this.favorParameter) {
ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
strategy.setParameterName(this.parameterName);
@ -367,7 +276,7 @@ public class ContentNegotiationManagerFactoryBean
// 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) {
if (!CollectionUtils.isEmpty(this.mediaTypes) && !this.favorParameter) {
this.contentNegotiationManager.addFileExtensionResolvers(
new MappingMediaTypeFileExtensionResolver(this.mediaTypes));
}
@ -387,9 +296,4 @@ public class ContentNegotiationManagerFactoryBean
return ContentNegotiationManager.class;
}
@Override
public boolean isSingleton() {
return true;
}
}

View File

@ -1,129 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.accept;
import java.util.Locale;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.MediaTypeFactory;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.util.UriUtils;
import org.springframework.web.util.UrlPathHelper;
/**
* A {@code ContentNegotiationStrategy} that resolves the file extension in the
* request path to a key to be used to look up a media type.
*
* <p>If the file extension is not found in the explicit registrations provided
* to the constructor, the {@link MediaTypeFactory} is used as a fallback
* mechanism.
*
* @author Rossen Stoyanchev
* @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 {
private UrlPathHelper urlPathHelper = new UrlPathHelper();
/**
* Create an instance without any mappings to start with. Mappings may be added
* later on if any extensions are resolved through the Java Activation framework.
*/
public PathExtensionContentNegotiationStrategy() {
this(null);
}
/**
* Create an instance with the given map of file extensions and media types.
*/
public PathExtensionContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) {
super(mediaTypes);
setUseRegisteredExtensionsOnly(false);
setIgnoreUnknownExtensions(true);
this.urlPathHelper.setUrlDecode(false);
}
/**
* Configure a {@code UrlPathHelper} to use in {@link #getMediaTypeKey}
* in order to derive the lookup path for a target request URL path.
* @since 4.2.8
*/
public void setUrlPathHelper(UrlPathHelper urlPathHelper) {
this.urlPathHelper = urlPathHelper;
}
/**
* Indicate whether to use the Java Activation Framework as a fallback option
* to map from file extensions to media types.
* @deprecated as of 5.0, in favor of {@link #setUseRegisteredExtensionsOnly(boolean)}.
*/
@Deprecated
public void setUseJaf(boolean useJaf) {
setUseRegisteredExtensionsOnly(!useJaf);
}
@Override
@Nullable
protected String getMediaTypeKey(NativeWebRequest webRequest) {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
return null;
}
// Ignore LOOKUP_PATH attribute, use our own "fixed" UrlPathHelper with decoding off
String path = this.urlPathHelper.getLookupPathForRequest(request);
String extension = UriUtils.extractFileExtension(path);
return (StringUtils.hasText(extension) ? extension.toLowerCase(Locale.ROOT) : null);
}
/**
* A public method exposing the knowledge of the path extension strategy to
* resolve file extensions to a {@link MediaType} in this case for a given
* {@link Resource}. The method first looks up any explicitly registered
* file extensions first and then falls back on {@link MediaTypeFactory} if available.
* @param resource the resource to look up
* @return the MediaType for the extension, or {@code null} if none found
* @since 4.3
*/
@Nullable
public MediaType getMediaTypeForResource(Resource resource) {
Assert.notNull(resource, "Resource must not be null");
MediaType mediaType = null;
String filename = resource.getFilename();
String extension = StringUtils.getFilenameExtension(filename);
if (extension != null) {
mediaType = lookupMediaType(extension);
}
if (mediaType == null) {
mediaType = MediaTypeFactory.getMediaType(filename).orElse(null);
}
return mediaType;
}
}

View File

@ -1,118 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.accept;
import java.util.Map;
import jakarta.servlet.ServletContext;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Extends {@code PathExtensionContentNegotiationStrategy} that also uses
* {@link ServletContext#getMimeType(String)} to resolve file extensions.
*
* @author Rossen Stoyanchev
* @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 {
private final ServletContext servletContext;
/**
* Create an instance without any mappings to start with. Mappings may be
* added later when extensions are resolved through
* {@link ServletContext#getMimeType(String)} or via
* {@link org.springframework.http.MediaTypeFactory}.
*/
public ServletPathExtensionContentNegotiationStrategy(ServletContext context) {
this(context, null);
}
/**
* Create an instance with the given extension-to-MediaType lookup.
*/
public ServletPathExtensionContentNegotiationStrategy(
ServletContext servletContext, @Nullable Map<String, MediaType> mediaTypes) {
super(mediaTypes);
Assert.notNull(servletContext, "ServletContext is required");
this.servletContext = servletContext;
}
/**
* Resolve file extension via {@link ServletContext#getMimeType(String)}
* and also delegate to base class for a potential
* {@link org.springframework.http.MediaTypeFactory} lookup.
*/
@Override
@Nullable
protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension)
throws HttpMediaTypeNotAcceptableException {
MediaType mediaType = null;
String mimeType = this.servletContext.getMimeType("file." + extension);
if (StringUtils.hasText(mimeType)) {
mediaType = MediaType.parseMediaType(mimeType);
}
if (mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
MediaType superMediaType = super.handleNoMatch(webRequest, extension);
if (superMediaType != null) {
mediaType = superMediaType;
}
}
return mediaType;
}
/**
* Extends the base class
* {@link PathExtensionContentNegotiationStrategy#getMediaTypeForResource}
* with the ability to also look up through the ServletContext.
* @param resource the resource to look up
* @return the MediaType for the extension, or {@code null} if none found
* @since 4.3
*/
@Override
@Nullable
public MediaType getMediaTypeForResource(Resource resource) {
MediaType mediaType = null;
String mimeType = this.servletContext.getMimeType(resource.getFilename());
if (StringUtils.hasText(mimeType)) {
mediaType = MediaType.parseMediaType(mimeType);
}
if (mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) {
MediaType superMediaType = super.getMediaTypeForResource(resource);
if (superMediaType != null) {
mediaType = superMediaType;
}
}
return mediaType;
}
}

View File

@ -3,8 +3,7 @@
*
* <p>{@link org.springframework.web.accept.ContentNegotiationStrategy} is the main
* abstraction for determining requested {@linkplain org.springframework.http.MediaType media types}
* with implementations based on
* {@linkplain org.springframework.web.accept.PathExtensionContentNegotiationStrategy path extensions}, a
* with implementations based on a
* {@linkplain org.springframework.web.accept.ParameterContentNegotiationStrategy a request parameter}, the
* {@linkplain org.springframework.web.accept.HeaderContentNegotiationStrategy 'Accept' header}, or a
* {@linkplain org.springframework.web.accept.FixedContentNegotiationStrategy default content type}.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -60,7 +60,6 @@ class ContentNegotiationManagerFactoryBeanTests {
this.webRequest = new ServletWebRequest(this.servletRequest);
this.factoryBean = new ContentNegotiationManagerFactoryBean();
this.factoryBean.setServletContext(this.servletRequest.getServletContext());
}
@ -115,42 +114,6 @@ class ContentNegotiationManagerFactoryBeanTests {
}
@Test
@SuppressWarnings("deprecation")
void favorPath() throws Exception {
this.factoryBean.setFavorPathExtension(true);
this.factoryBean.addMediaType("bar", new MediaType("application", "bar"));
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
this.servletRequest.setRequestURI("/flower.foo");
assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(new MediaType("application", "foo")));
this.servletRequest.setRequestURI("/flower.bar");
assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(new MediaType("application", "bar")));
this.servletRequest.setRequestURI("/flower.gif");
assertThat(manager.resolveMediaTypes(this.webRequest))
.isEqualTo(Collections.singletonList(MediaType.IMAGE_GIF));
}
@Test // SPR-10170
@SuppressWarnings("deprecation")
void favorPathWithIgnoreUnknownPathExtensionTurnedOff() {
this.factoryBean.setFavorPathExtension(true);
this.factoryBean.setIgnoreUnknownPathExtensions(false);
this.factoryBean.afterPropertiesSet();
ContentNegotiationManager manager = this.factoryBean.getObject();
this.servletRequest.setRequestURI("/flower.foobarbaz");
this.servletRequest.addParameter("format", "json");
assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class).isThrownBy(() ->
manager.resolveMediaTypes(this.webRequest));
}
@Test
void favorParameter() throws Exception {
this.factoryBean.setFavorParameter(true);
@ -180,9 +143,7 @@ class ContentNegotiationManagerFactoryBeanTests {
}
@Test
@SuppressWarnings("deprecation")
void mediaTypeMappingsWithoutPathAndParameterStrategies() {
this.factoryBean.setFavorPathExtension(false);
this.factoryBean.setFavorParameter(false);
Properties properties = new Properties();
@ -201,9 +162,7 @@ class ContentNegotiationManagerFactoryBeanTests {
}
@Test
@SuppressWarnings("deprecation")
void fileExtensions() {
this.factoryBean.setFavorPathExtension(false);
this.factoryBean.setFavorParameter(false);
Properties properties = new Properties();

View File

@ -1,110 +0,0 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.accept;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* A test fixture for {@link PathExtensionContentNegotiationStrategy}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
@SuppressWarnings("deprecation")
class PathExtensionContentNegotiationStrategyTests {
private final MockHttpServletRequest servletRequest = new MockHttpServletRequest();
private final NativeWebRequest webRequest = new ServletWebRequest(servletRequest);
private PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy();
@Test
void resolveMediaTypesFromMapping() throws Exception {
this.servletRequest.setRequestURI("test.html");
List<MediaType> mediaTypes = this.strategy.resolveMediaTypes(this.webRequest);
assertThat(mediaTypes).containsExactly(new MediaType("text", "html"));
Map<String, MediaType> mapping = Collections.singletonMap("HTML", MediaType.APPLICATION_XHTML_XML);
this.strategy = new PathExtensionContentNegotiationStrategy(mapping);
mediaTypes = this.strategy.resolveMediaTypes(this.webRequest);
assertThat(mediaTypes).containsExactly(new MediaType("application", "xhtml+xml"));
}
@Test
void resolveMediaTypesFromMediaTypeFactory() throws Exception {
this.servletRequest.setRequestURI("test.xls");
List<MediaType> mediaTypes = this.strategy.resolveMediaTypes(this.webRequest);
assertThat(mediaTypes).containsExactly(new MediaType("application", "vnd.ms-excel"));
}
@Test // SPR-8678
void getMediaTypeFilenameWithContextPath() throws Exception {
this.servletRequest.setContextPath("/project-1.0.0.M3");
this.servletRequest.setRequestURI("/project-1.0.0.M3/");
assertThat(this.strategy.resolveMediaTypes(webRequest)).as("Context path should be excluded").isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST);
this.servletRequest.setRequestURI("/project-1.0.0.M3");
assertThat(this.strategy.resolveMediaTypes(webRequest)).as("Context path should be excluded").isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST);
}
@Test // SPR-9390
void getMediaTypeFilenameWithEncodedURI() throws Exception {
this.servletRequest.setRequestURI("/quo%20vadis%3f.html");
List<MediaType> result = this.strategy.resolveMediaTypes(webRequest);
assertThat(result).as("Invalid content type").isEqualTo(Collections.singletonList(new MediaType("text", "html")));
}
@Test // SPR-10170
void resolveMediaTypesIgnoreUnknownExtension() throws Exception {
this.servletRequest.setRequestURI("test.foobar");
List<MediaType> mediaTypes = this.strategy.resolveMediaTypes(this.webRequest);
assertThat(mediaTypes).isEqualTo(ContentNegotiationStrategy.MEDIA_TYPE_ALL_LIST);
}
@Test
void resolveMediaTypesDoNotIgnoreUnknownExtension() {
this.servletRequest.setRequestURI("test.foobar");
this.strategy.setIgnoreUnknownExtensions(false);
assertThatExceptionOfType(HttpMediaTypeNotAcceptableException.class)
.isThrownBy(() -> this.strategy.resolveMediaTypes(this.webRequest));
}
}

View File

@ -176,8 +176,7 @@ class ResourceWebHandlerTests {
assertResponseBody(exchange, "foo bar foo bar foo bar");
}
@Test
// SPR-14577
@Test // SPR-14577
void getMediaTypeWithFavorPathExtensionOff() throws Exception {
List<Resource> paths = List.of(new ClassPathResource("test/", getClass()));
ResourceWebHandler handler = new ResourceWebHandler();

View File

@ -188,11 +188,6 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
parseResourceChain(resourceHandlerDef, context, resourceChainElement, source);
}
Object manager = MvcNamespaceUtils.getContentNegotiationManager(context);
if (manager != null) {
values.add("contentNegotiationManager", manager);
}
String beanName = context.getReaderContext().generateBeanName(resourceHandlerDef);
context.getRegistry().registerBeanDefinition(beanName, resourceHandlerDef);
context.registerComponent(new BeanComponentDefinition(resourceHandlerDef, beanName));

View File

@ -55,13 +55,6 @@ import org.springframework.web.accept.ParameterContentNegotiationStrategy;
* <td>Off</td>
* </tr>
* <tr>
* <td>{@link #favorPathExtension}</td>
* <td>false (as of 5.3)</td>
* <td>{@link org.springframework.web.accept.PathExtensionContentNegotiationStrategy
* PathExtensionContentNegotiationStrategy}</td>
* <td>Off</td>
* </tr>
* <tr>
* <td>{@link #ignoreAcceptHeader}</td>
* <td>false</td>
* <td>{@link HeaderContentNegotiationStrategy}</td>
@ -84,13 +77,6 @@ import org.springframework.web.accept.ParameterContentNegotiationStrategy;
* <p>As of 5.0 you can set the exact strategies to use via
* {@link #strategies(List)}.
*
* <p><strong>Note:</strong> if you must use URL-based content type resolution,
* the use of a query parameter is simpler and preferable to the use of a path
* extension since the latter can cause issues with URI variables, path
* parameters, and URI decoding. Consider setting {@link #favorPathExtension}
* to {@literal false} or otherwise set the strategies to use explicitly via
* {@link #strategies(List)}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
@ -101,16 +87,6 @@ public class ContentNegotiationConfigurer {
private final Map<String, MediaType> mediaTypes = new HashMap<>();
/**
* Class constructor with {@link jakarta.servlet.ServletContext}.
*/
public ContentNegotiationConfigurer(@Nullable ServletContext servletContext) {
if (servletContext != null) {
this.factory.setServletContext(servletContext);
}
}
/**
* Set the exact list of strategies to use.
* <p><strong>Note:</strong> use of this method is mutually exclusive with
@ -144,20 +120,6 @@ public class ContentNegotiationConfigurer {
return this;
}
/**
* Whether the path extension in the URL path should be used to determine
* the requested media type.
* <p>By default this is set to {@code false} in which case path extensions
* have no impact on content negotiation.
* @deprecated as of 5.2.4. See deprecation note on
* {@link ContentNegotiationManagerFactoryBean#setFavorPathExtension(boolean)}.
*/
@Deprecated
public ContentNegotiationConfigurer favorPathExtension(boolean favorPathExtension) {
this.factory.setFavorPathExtension(favorPathExtension);
return this;
}
/**
* Add a mapping from a key, extracted from a path extension or a query
* parameter, to a MediaType. This is required in order for the parameter
@ -202,37 +164,11 @@ public class ContentNegotiationConfigurer {
}
/**
* Whether to ignore requests with path extension that cannot be resolved
* to any media type. Setting this to {@code false} will result in an
* {@code HttpMediaTypeNotAcceptableException} if there is no match.
* <p>By default this is set to {@code true}.
* @deprecated as of 5.2.4. See deprecation note on
* {@link ContentNegotiationManagerFactoryBean#setIgnoreUnknownPathExtensions(boolean)}.
*/
@Deprecated
public ContentNegotiationConfigurer ignoreUnknownPathExtensions(boolean ignore) {
this.factory.setIgnoreUnknownPathExtensions(ignore);
return this;
}
/**
* When {@link #favorPathExtension} is set, this property determines whether
* to allow use of JAF (Java Activation Framework) to resolve a path
* extension to a specific MediaType.
* @deprecated as of 5.0, in favor of {@link #useRegisteredExtensionsOnly(boolean)}
* which has reverse behavior
*/
@Deprecated
public ContentNegotiationConfigurer useJaf(boolean useJaf) {
return this.useRegisteredExtensionsOnly(!useJaf);
}
/**
* When {@link #favorPathExtension favorPathExtension} is set, this
* When {@link #favorParameter(boolean)} is set, this
* property determines whether to use only registered {@code MediaType} mappings
* to resolve a path extension to a specific MediaType.
* <p>By default this is not set in which case
* {@code PathExtensionContentNegotiationStrategy} will use defaults if available.
* <p>By default, this is not set in which case
* {@code ParameterContentNegotiationStrategy} will use defaults if available.
*/
public ContentNegotiationConfigurer useRegisteredExtensionsOnly(boolean useRegisteredExtensionsOnly) {
this.factory.setUseRegisteredExtensionsOnly(useRegisteredExtensionsOnly);

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -66,9 +66,6 @@ public class ResourceHandlerRegistry {
private final ApplicationContext applicationContext;
@Nullable
private final ContentNegotiationManager contentNegotiationManager;
@Nullable
private final UrlPathHelper pathHelper;
@ -111,7 +108,6 @@ public class ResourceHandlerRegistry {
Assert.notNull(applicationContext, "ApplicationContext is required");
this.applicationContext = applicationContext;
this.servletContext = servletContext;
this.contentNegotiationManager = contentNegotiationManager;
this.pathHelper = pathHelper;
}
@ -173,15 +169,11 @@ public class ResourceHandlerRegistry {
return new SimpleUrlHandlerMapping(urlMap, this.order);
}
@SuppressWarnings("deprecation")
private ResourceHttpRequestHandler getRequestHandler(ResourceHandlerRegistration registration) {
ResourceHttpRequestHandler handler = registration.getRequestHandler();
if (this.pathHelper != null) {
handler.setUrlPathHelper(this.pathHelper);
}
if (this.contentNegotiationManager != null) {
handler.setContentNegotiationManager(this.contentNegotiationManager);
}
handler.setServletContext(this.servletContext);
handler.setApplicationContext(this.applicationContext);
try {

View File

@ -430,7 +430,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
if (this.contentNegotiationManager == null) {
ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer(this.servletContext);
ContentNegotiationConfigurer configurer = new ContentNegotiationConfigurer();
configurer.mediaTypes(getDefaultMediaTypes());
configureContentNegotiation(configurer);
this.contentNegotiationManager = configurer.buildContentNegotiationManager();

View File

@ -280,31 +280,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return this.resourceRegionHttpMessageConverter;
}
/**
* Configure a {@code ContentNegotiationManager} to help determine the
* media types for resources being served. If the manager contains a path
* extension strategy it will be checked for registered file extension.
* @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) {
this.contentNegotiationManager = contentNegotiationManager;
}
/**
* Return the configured content negotiation manager.
* @since 4.3
* @deprecated as of 5.2.4
*/
@Nullable
@Deprecated
public ContentNegotiationManager getContentNegotiationManager() {
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
@ -459,18 +434,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
if (this.resourceRegionHttpMessageConverter == null) {
this.resourceRegionHttpMessageConverter = new ResourceRegionHttpMessageConverter();
}
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() {
@ -546,21 +509,6 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
}
}
/**
* Initialize the strategy to use to determine the media type for a resource.
* @deprecated as of 5.2.4 this method returns {@code null}, and if a
* subclass returns an actual instance, the instance is used only as a
* 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)}.
*/
@Nullable
@Deprecated
@SuppressWarnings("deprecation")
protected org.springframework.web.accept.PathExtensionContentNegotiationStrategy initContentNegotiationStrategy() {
return null;
}
/**
* Processes a resource request.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -205,7 +205,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
}
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
@Override

View File

@ -380,7 +380,6 @@ public class MvcNamespaceTests {
}
@Test
@SuppressWarnings("deprecation")
void testResources() throws Exception {
loadBeanDefinitions("mvc-config-resources.xml");
@ -390,10 +389,6 @@ public class MvcNamespaceTests {
RequestMappingHandlerMapping mapping = appContext.getBean(RequestMappingHandlerMapping.class);
ContentNegotiationManager manager = mapping.getContentNegotiationManager();
ResourceHttpRequestHandler handler = appContext.getBean(ResourceHttpRequestHandler.class);
assertThat(handler).isNotNull();
assertThat(handler.getContentNegotiationManager()).isSameAs(manager);
SimpleUrlHandlerMapping resourceMapping = appContext.getBean(SimpleUrlHandlerMapping.class);
assertThat(resourceMapping).isNotNull();
assertThat(resourceMapping.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE - 1);

View File

@ -49,7 +49,7 @@ class ContentNegotiationConfigurerTests {
void setup() {
this.servletRequest = new MockHttpServletRequest();
this.webRequest = new ServletWebRequest(this.servletRequest);
this.configurer = new ContentNegotiationConfigurer(this.servletRequest.getServletContext());
this.configurer = new ContentNegotiationConfigurer();
}

View File

@ -75,7 +75,6 @@ import org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionRes
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.servlet.resource.ResourceHttpRequestHandler;
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
@ -264,7 +263,6 @@ class WebMvcConfigurationSupportExtensionTests {
}
@Test
@SuppressWarnings("deprecation")
public void contentNegotiation() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo");
NativeWebRequest webRequest = new ServletWebRequest(request);
@ -287,11 +285,7 @@ class WebMvcConfigurationSupportExtensionTests {
request = new MockHttpServletRequest("GET", "/resources/foo.gif");
HandlerExecutionChain chain = handlerMapping.getHandler(request);
assertThat(chain).isNotNull();
ResourceHttpRequestHandler handler = (ResourceHttpRequestHandler) chain.getHandler();
assertThat(handler).isNotNull();
assertThat(handler.getContentNegotiationManager()).isSameAs(manager);
}
@Test

View File

@ -18,6 +18,7 @@ package org.springframework.web.servlet.resource;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
@ -33,8 +34,6 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
import org.springframework.web.context.support.StaticWebApplicationContext;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
@ -127,18 +126,12 @@ class ResourceHttpRequestHandlerTests {
}
@Test // SPR-13658
@SuppressWarnings("deprecation")
void getResourceWithRegisteredMediaType() throws Exception {
ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
factory.addMediaType("bar", new MediaType("foo", "bar"));
factory.afterPropertiesSet();
ContentNegotiationManager manager = factory.getObject();
List<Resource> paths = List.of(new ClassPathResource("test/", getClass()));
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setServletContext(new MockServletContext());
handler.setMediaTypes(Map.of("bar", new MediaType("foo", "bar")));
handler.setLocations(paths);
handler.setContentNegotiationManager(manager);
handler.afterPropertiesSet();
this.request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "foo.bar");
@ -148,17 +141,12 @@ class ResourceHttpRequestHandlerTests {
assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }");
}
@Test // SPR-14577
@Test // SPR-14577
void getMediaTypeWithFavorPathExtensionOff() throws Exception {
ContentNegotiationManagerFactoryBean factory = new ContentNegotiationManagerFactoryBean();
factory.afterPropertiesSet();
ContentNegotiationManager manager = factory.getObject();
List<Resource> paths = List.of(new ClassPathResource("test/", getClass()));
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setServletContext(new MockServletContext());
handler.setLocations(paths);
handler.setContentNegotiationManager(manager);
handler.afterPropertiesSet();
this.request.addHeader("Accept", "application/json,text/plain,*/*");

View File

@ -84,7 +84,7 @@ class ContentNegotiatingViewResolverTests {
}
@Test
void resolveViewNameWithPathExtension() throws Exception {
void resolveViewNameWithExtensionByParameter() throws Exception {
request.setRequestURI("/test");
request.setParameter("format", "xls");
@ -310,77 +310,6 @@ class ContentNegotiatingViewResolverTests {
assertThat(result).as("Invalid view").isSameAs(viewMock3);
}
@Test
@SuppressWarnings("deprecation")
public void resolveViewNameFilename() throws Exception {
request.setRequestURI("/test.html");
ContentNegotiationManager manager =
new ContentNegotiationManager(new org.springframework.web.accept.PathExtensionContentNegotiationStrategy());
ViewResolver viewResolverMock1 = mock(ViewResolver.class, "viewResolver1");
ViewResolver viewResolverMock2 = mock(ViewResolver.class, "viewResolver2");
viewResolver.setContentNegotiationManager(manager);
viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2));
viewResolver.afterPropertiesSet();
View viewMock1 = mock(View.class, "application_xml");
View viewMock2 = mock(View.class, "text_html");
String viewName = "view";
Locale locale = Locale.ENGLISH;
given(viewResolverMock1.resolveViewName(viewName, locale)).willReturn(viewMock1);
given(viewResolverMock1.resolveViewName(viewName + ".html", locale)).willReturn(null);
given(viewResolverMock2.resolveViewName(viewName, locale)).willReturn(null);
given(viewResolverMock2.resolveViewName(viewName + ".html", locale)).willReturn(viewMock2);
given(viewMock1.getContentType()).willReturn("application/xml");
given(viewMock2.getContentType()).willReturn("text/html;charset=ISO-8859-1");
View result = viewResolver.resolveViewName(viewName, locale);
assertThat(result).as("Invalid view").isSameAs(viewMock2);
}
@Test
@SuppressWarnings("deprecation")
public void resolveViewNameFilenameDefaultView() throws Exception {
request.setRequestURI("/test.json");
Map<String, MediaType> mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON);
org.springframework.web.accept.PathExtensionContentNegotiationStrategy pathStrategy =
new org.springframework.web.accept.PathExtensionContentNegotiationStrategy(mapping);
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(pathStrategy));
ViewResolver viewResolverMock1 = mock();
ViewResolver viewResolverMock2 = mock();
viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2));
View viewMock1 = mock(View.class, "application_xml");
View viewMock2 = mock(View.class, "text_html");
View viewMock3 = mock(View.class, "application_json");
List<View> defaultViews = new ArrayList<>();
defaultViews.add(viewMock3);
viewResolver.setDefaultViews(defaultViews);
viewResolver.afterPropertiesSet();
String viewName = "view";
Locale locale = Locale.ENGLISH;
given(viewResolverMock1.resolveViewName(viewName, locale)).willReturn(viewMock1);
given(viewResolverMock1.resolveViewName(viewName + ".json", locale)).willReturn(null);
given(viewResolverMock2.resolveViewName(viewName, locale)).willReturn(viewMock2);
given(viewResolverMock2.resolveViewName(viewName + ".json", locale)).willReturn(null);
given(viewMock1.getContentType()).willReturn("application/xml");
given(viewMock2.getContentType()).willReturn("text/html;charset=ISO-8859-1");
given(viewMock3.getContentType()).willReturn("application/json");
View result = viewResolver.resolveViewName(viewName, locale);
assertThat(result).as("Invalid view").isSameAs(viewMock3);
}
@Test
void resolveViewContentTypeNull() throws Exception {
request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");