Skip to content

Commit 542e187

Browse files
committed
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
1 parent 214ba63 commit 542e187

File tree

5 files changed

+185
-72
lines changed

5 files changed

+185
-72
lines changed

spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,13 +20,17 @@
2020
import java.util.Arrays;
2121
import java.util.Collection;
2222
import java.util.Collections;
23+
import java.util.HashMap;
2324
import java.util.LinkedHashSet;
2425
import java.util.List;
26+
import java.util.Map;
2527
import java.util.Set;
28+
import java.util.function.Function;
2629

2730
import org.springframework.http.MediaType;
2831
import org.springframework.lang.Nullable;
2932
import org.springframework.util.Assert;
33+
import org.springframework.util.CollectionUtils;
3034
import org.springframework.web.HttpMediaTypeNotAcceptableException;
3135
import org.springframework.web.context.request.NativeWebRequest;
3236

@@ -132,11 +136,7 @@ public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMe
132136

133137
@Override
134138
public List<String> resolveFileExtensions(MediaType mediaType) {
135-
Set<String> result = new LinkedHashSet<>();
136-
for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
137-
result.addAll(resolver.resolveFileExtensions(mediaType));
138-
}
139-
return new ArrayList<>(result);
139+
return doResolveExtensions(resolver -> resolver.resolveFileExtensions(mediaType));
140140
}
141141

142142
/**
@@ -152,11 +152,44 @@ public List<String> resolveFileExtensions(MediaType mediaType) {
152152
*/
153153
@Override
154154
public List<String> getAllFileExtensions() {
155-
Set<String> result = new LinkedHashSet<>();
155+
return doResolveExtensions(MediaTypeFileExtensionResolver::getAllFileExtensions);
156+
}
157+
158+
private List<String> doResolveExtensions(Function<MediaTypeFileExtensionResolver, List<String>> extractor) {
159+
List<String> result = null;
160+
for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
161+
List<String> extensions = extractor.apply(resolver);
162+
if (CollectionUtils.isEmpty(extensions)) {
163+
continue;
164+
}
165+
result = (result != null ? result : new ArrayList<>(4));
166+
for (String extension : extensions) {
167+
if (!result.contains(extension)) {
168+
result.add(extension);
169+
}
170+
}
171+
}
172+
return (result != null ? result : Collections.emptyList());
173+
}
174+
175+
/**
176+
* Return all registered lookup key to media type mappings by iterating
177+
* {@link MediaTypeFileExtensionResolver}s.
178+
* @since 5.2.4
179+
*/
180+
public Map<String, MediaType> getMediaTypeMappings() {
181+
Map<String, MediaType> result = null;
156182
for (MediaTypeFileExtensionResolver resolver : this.resolvers) {
157-
result.addAll(resolver.getAllFileExtensions());
183+
if (resolver instanceof MappingMediaTypeFileExtensionResolver) {
184+
Map<String, MediaType> map = ((MappingMediaTypeFileExtensionResolver) resolver).getMediaTypes();
185+
if (CollectionUtils.isEmpty(map)) {
186+
continue;
187+
}
188+
result = (result != null ? result : new HashMap<>(4));
189+
result.putAll(map);
190+
}
158191
}
159-
return new ArrayList<>(result);
192+
return (result != null ? result : Collections.emptyMap());
160193
}
161194

162195
}

spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManagerFactoryBean.java

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
* {@link #setFavorPathExtension(boolean) favorPathExtension} and
9090
* {@link #setIgnoreUnknownPathExtensions(boolean) ignoreUnknownPathExtensions}
9191
* are deprecated in order to discourage use of path extensions for content
92-
* negotiation and for request mapping (with similar deprecations in
92+
* negotiation as well as for request mapping (with similar deprecations in
9393
* {@link org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping
9494
* RequestMappingHandlerMapping}). For further context, please read issue
9595
* <a href="https://github.com/spring-projects/spring-framework/issues/24179">#24719</a>.
@@ -157,46 +157,52 @@ public void setFavorPathExtension(boolean favorPathExtension) {
157157
}
158158

159159
/**
160-
* Add a mapping from a key, extracted from a path extension or a query
161-
* parameter, to a MediaType. This is required in order for the parameter
162-
* strategy to work. Any extensions explicitly registered here are also
163-
* whitelisted for the purpose of Reflected File Download attack detection
164-
* (see Spring Framework reference documentation for more details on RFD
165-
* attack protection).
166-
* <p>The path extension strategy will also try to use
160+
* Add a mapping from a key to a MediaType where the key are normalized to
161+
* lowercase and may have been extracted from a path extension, a filename
162+
* extension, or passed as a query parameter.
163+
* <p>The {@link #setFavorParameter(boolean) parameter strategy} requires
164+
* such mappings in order to work while the {@link #setFavorPathExtension(boolean)
165+
* path extension strategy} can fall back on lookups via
167166
* {@link ServletContext#getMimeType} and
168-
* {@link org.springframework.http.MediaTypeFactory} to resolve path extensions.
167+
* {@link org.springframework.http.MediaTypeFactory}.
168+
* <p><strong>Note:</strong> Mappings registered here may be accessed via
169+
* {@link ContentNegotiationManager#getMediaTypeMappings()} and may be used
170+
* not only in the parameter and path extension strategies. For example,
171+
* with the Spring MVC config, e.g. {@code @EnableWebMvc} or
172+
* {@code <mvc:annotation-driven>}, the media type mappings are also plugged
173+
* in to:
174+
* <ul>
175+
* <li>Determine the media type of static resources served with
176+
* {@code ResourceHttpRequestHandler}.
177+
* <li>Determine the media type of views rendered with
178+
* {@code ContentNegotiatingViewResolver}.
179+
* <li>Whitelist extensions for RFD attack detection (check the Spring
180+
* Framework reference docs for details).
181+
* </ul>
169182
* @param mediaTypes media type mappings
170183
* @see #addMediaType(String, MediaType)
171184
* @see #addMediaTypes(Map)
172185
*/
173186
public void setMediaTypes(Properties mediaTypes) {
174187
if (!CollectionUtils.isEmpty(mediaTypes)) {
175-
mediaTypes.forEach((key, value) -> {
176-
String extension = ((String) key).toLowerCase(Locale.ENGLISH);
177-
MediaType mediaType = MediaType.valueOf((String) value);
178-
this.mediaTypes.put(extension, mediaType);
179-
});
188+
mediaTypes.forEach((key, value) ->
189+
addMediaType((String) key, MediaType.valueOf((String) value)));
180190
}
181191
}
182192

183193
/**
184-
* An alternative to {@link #setMediaTypes} for use in Java code.
185-
* @see #setMediaTypes
186-
* @see #addMediaTypes
194+
* An alternative to {@link #setMediaTypes} for programmatic registrations.
187195
*/
188-
public void addMediaType(String fileExtension, MediaType mediaType) {
189-
this.mediaTypes.put(fileExtension, mediaType);
196+
public void addMediaType(String key, MediaType mediaType) {
197+
this.mediaTypes.put(key.toLowerCase(Locale.ENGLISH), mediaType);
190198
}
191199

192200
/**
193-
* An alternative to {@link #setMediaTypes} for use in Java code.
194-
* @see #setMediaTypes
195-
* @see #addMediaType
201+
* An alternative to {@link #setMediaTypes} for programmatic registrations.
196202
*/
197203
public void addMediaTypes(@Nullable Map<String, MediaType> mediaTypes) {
198204
if (mediaTypes != null) {
199-
this.mediaTypes.putAll(mediaTypes);
205+
mediaTypes.forEach(this::addMediaType);
200206
}
201207
}
202208

@@ -315,6 +321,7 @@ public void afterPropertiesSet() {
315321
* Create and initialize a {@link ContentNegotiationManager} instance.
316322
* @since 5.0
317323
*/
324+
@SuppressWarnings("deprecation")
318325
public ContentNegotiationManager build() {
319326
List<ContentNegotiationStrategy> strategies = new ArrayList<>();
320327

@@ -336,7 +343,6 @@ public ContentNegotiationManager build() {
336343
}
337344
strategies.add(strategy);
338345
}
339-
340346
if (this.favorParameter) {
341347
ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes);
342348
strategy.setParameterName(this.parameterName);
@@ -348,17 +354,24 @@ public ContentNegotiationManager build() {
348354
}
349355
strategies.add(strategy);
350356
}
351-
352357
if (!this.ignoreAcceptHeader) {
353358
strategies.add(new HeaderContentNegotiationStrategy());
354359
}
355-
356360
if (this.defaultNegotiationStrategy != null) {
357361
strategies.add(this.defaultNegotiationStrategy);
358362
}
359363
}
360364

361365
this.contentNegotiationManager = new ContentNegotiationManager(strategies);
366+
367+
// Ensure media type mappings are available via ContentNegotiationManager#getMediaTypeMappings()
368+
// independent of path extension or parameter strategies.
369+
370+
if (!CollectionUtils.isEmpty(this.mediaTypes) && !this.favorPathExtension && !this.favorParameter) {
371+
this.contentNegotiationManager.addFileExtensionResolvers(
372+
new MappingMediaTypeFileExtensionResolver(this.mediaTypes));
373+
}
374+
362375
return this.contentNegotiationManager;
363376
}
364377

spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeFileExtensionResolver.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,9 +18,11 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.Collections;
21+
import java.util.HashSet;
2122
import java.util.List;
2223
import java.util.Locale;
2324
import java.util.Map;
25+
import java.util.Set;
2426
import java.util.concurrent.ConcurrentHashMap;
2527
import java.util.concurrent.ConcurrentMap;
2628
import java.util.concurrent.CopyOnWriteArrayList;
@@ -53,7 +55,7 @@ public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExten
5355
*/
5456
public MappingMediaTypeFileExtensionResolver(@Nullable Map<String, MediaType> mediaTypes) {
5557
if (mediaTypes != null) {
56-
List<String> allFileExtensions = new ArrayList<>();
58+
Set<String> allFileExtensions = new HashSet<>(mediaTypes.size());
5759
mediaTypes.forEach((extension, mediaType) -> {
5860
String lowerCaseExtension = extension.toLowerCase(Locale.ENGLISH);
5961
this.mediaTypes.put(lowerCaseExtension, mediaType);

0 commit comments

Comments
 (0)