Expose media type mappings in ContentNegotiationManager
ContentNegotiationManagerFactoryBean now ensures that ContentNegotiationManager contains the MediaType mappings even if the path extension and the parameter strategies are off. There are also minor fixes to ensure the media type mappings in ContentNegotiationManagerFactoryBean aren't polluted when mapping keys are not lowercase, and likewise MappingMediaTypeFileExtensionResolver filters out duplicates in the list of all file extensions. See gh-24179
This commit is contained in:
@@ -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");
|
||||
* 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.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.HttpMediaTypeNotAcceptableException;
|
||||
import org.springframework.web.context.request.NativeWebRequest;
|
||||
|
||||
@@ -132,11 +136,7 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
|
||||
|
||||
@Override
|
||||
public List<String> resolveFileExtensions(MediaType mediaType) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
|
||||
result.addAll(resolver.resolveFileExtensions(mediaType));
|
||||
}
|
||||
return new ArrayList<>(result);
|
||||
return doResolveExtensions(resolver -> resolver.resolveFileExtensions(mediaType));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,11 +152,44 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
|
||||
*/
|
||||
@Override
|
||||
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) {
|
||||
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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ import org.springframework.web.context.ServletContextAware;
|
||||
* {@link #setFavorPathExtension(boolean) favorPathExtension} and
|
||||
* {@link #setIgnoreUnknownPathExtensions(boolean) ignoreUnknownPathExtensions}
|
||||
* are deprecated in order to discourage use of path extensions for content
|
||||
* negotiation and for request mapping (with similar deprecations in
|
||||
* negotiation as well as for request mapping (with similar deprecations in
|
||||
* {@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>.
|
||||
@@ -157,46 +157,52 @@ public class ContentNegotiationManagerFactoryBean
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* strategy to work. Any extensions explicitly registered here are also
|
||||
* whitelisted for the purpose of Reflected File Download attack detection
|
||||
* (see Spring Framework reference documentation for more details on RFD
|
||||
* attack protection).
|
||||
* <p>The path extension strategy will also try to use
|
||||
* 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} 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
|
||||
* @see #addMediaType(String, MediaType)
|
||||
* @see #addMediaTypes(Map)
|
||||
*/
|
||||
public void setMediaTypes(Properties mediaTypes) {
|
||||
if (!CollectionUtils.isEmpty(mediaTypes)) {
|
||||
mediaTypes.forEach((key, value) -> {
|
||||
String extension = ((String) key).toLowerCase(Locale.ENGLISH);
|
||||
MediaType mediaType = MediaType.valueOf((String) value);
|
||||
this.mediaTypes.put(extension, mediaType);
|
||||
});
|
||||
mediaTypes.forEach((key, value) ->
|
||||
addMediaType((String) key, MediaType.valueOf((String) value)));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative to {@link #setMediaTypes} for use in Java code.
|
||||
* @see #setMediaTypes
|
||||
* @see #addMediaTypes
|
||||
* An alternative to {@link #setMediaTypes} for programmatic registrations.
|
||||
*/
|
||||
public void addMediaType(String fileExtension, MediaType mediaType) {
|
||||
this.mediaTypes.put(fileExtension, mediaType);
|
||||
public void addMediaType(String key, MediaType mediaType) {
|
||||
this.mediaTypes.put(key.toLowerCase(Locale.ENGLISH), mediaType);
|
||||
}
|
||||
|
||||
/**
|
||||
* An alternative to {@link #setMediaTypes} for use in Java code.
|
||||
* @see #setMediaTypes
|
||||
* @see #addMediaType
|
||||
* An alternative to {@link #setMediaTypes} for programmatic registrations.
|
||||
*/
|
||||
public void addMediaTypes(@Nullable Map<String, MediaType> mediaTypes) {
|
||||
if (mediaTypes != null) {
|
||||
this.mediaTypes.putAll(mediaTypes);
|
||||
mediaTypes.forEach(this::addMediaType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +321,7 @@ public class ContentNegotiationManagerFactoryBean
|
||||
* Create and initialize a {@link ContentNegotiationManager} instance.
|
||||
* @since 5.0
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
public ContentNegotiationManager build() {
|
||||
List<ContentNegotiationStrategy> strategies = new ArrayList<>();
|
||||
|
||||
@@ -336,7 +343,6 @@ public class ContentNegotiationManagerFactoryBean
|
||||
}
|
||||
strategies.add(strategy);
|
||||
}
|
||||
|
||||
if (this.favorParameter) {
|
||||
ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
|
||||
strategy.setParameterName(this.parameterName);
|
||||
@@ -348,17 +354,24 @@ public class ContentNegotiationManagerFactoryBean
|
||||
}
|
||||
strategies.add(strategy);
|
||||
}
|
||||
|
||||
if (!this.ignoreAcceptHeader) {
|
||||
strategies.add(new HeaderContentNegotiationStrategy());
|
||||
}
|
||||
|
||||
if (this.defaultNegotiationStrategy != null) {
|
||||
strategies.add(this.defaultNegotiationStrategy);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
* 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.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
@@ -53,7 +55,7 @@ public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExten
|
||||
*/
|
||||
public MappingMediaTypeFileExtensionResolver(@Nullable Map<String, MediaType> mediaTypes) {
|
||||
if (mediaTypes != null) {
|
||||
List<String> allFileExtensions = new ArrayList<>();
|
||||
Set<String> allFileExtensions = new HashSet<>(mediaTypes.size());
|
||||
mediaTypes.forEach((extension, mediaType) -> {
|
||||
String lowerCaseExtension = extension.toLowerCase(Locale.ENGLISH);
|
||||
this.mediaTypes.put(lowerCaseExtension, mediaType);
|
||||
|
||||
Reference in New Issue
Block a user