Polish ContentNegotiationStrategy support

Issue: SPR-8410, SPR-8417, SPR-8418,SPR-8416, SPR-8419,SPR-7722
This commit is contained in:
Rossen Stoyanchev 2012-06-25 15:24:05 -04:00
parent 4623568bce
commit f94aed8386
11 changed files with 80 additions and 61 deletions

View File

@ -31,14 +31,14 @@ import org.springframework.web.context.request.NativeWebRequest;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
*/ */
public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeExtensionsResolver public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver
implements ContentNegotiationStrategy, MediaTypeExtensionsResolver { implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
/** /**
* Create an instance with the given extension-to-MediaType lookup. * Create an instance with the given extension-to-MediaType lookup.
* @throws IllegalArgumentException if a media type string cannot be parsed * @throws IllegalArgumentException if a media type string cannot be parsed
*/ */
public AbstractMappingContentNegotiationStrategy(Map<String, String> mediaTypes) { public AbstractMappingContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes); super(mediaTypes);
} }

View File

@ -33,28 +33,30 @@ import org.springframework.web.context.request.NativeWebRequest;
* in a request by delegating to a list of {@link ContentNegotiationStrategy} instances. * in a request by delegating to a list of {@link ContentNegotiationStrategy} instances.
* *
* <p>It may also be used to determine the extensions associated with a MediaType by * <p>It may also be used to determine the extensions associated with a MediaType by
* delegating to a list of {@link MediaTypeExtensionsResolver} instances. * delegating to a list of {@link MediaTypeFileExtensionResolver} instances.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
*/ */
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeExtensionsResolver { public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {
private final List<ContentNegotiationStrategy> contentNegotiationStrategies = new ArrayList<ContentNegotiationStrategy>(); private final List<ContentNegotiationStrategy> contentNegotiationStrategies =
new ArrayList<ContentNegotiationStrategy>();
private final Set<MediaTypeExtensionsResolver> extensionResolvers = new LinkedHashSet<MediaTypeExtensionsResolver>(); private final Set<MediaTypeFileExtensionResolver> fileExtensionResolvers =
new LinkedHashSet<MediaTypeFileExtensionResolver>();
/** /**
* Create an instance with the given ContentNegotiationStrategy instances. * Create an instance with the given ContentNegotiationStrategy instances.
* <p>Each instance is checked to see if it is also an implementation of * <p>Each instance is checked to see if it is also an implementation of
* MediaTypeExtensionsResolver, and if so it is registered as such. * MediaTypeFileExtensionResolver, and if so it is registered as such.
*/ */
public ContentNegotiationManager(ContentNegotiationStrategy... strategies) { public ContentNegotiationManager(ContentNegotiationStrategy... strategies) {
Assert.notEmpty(strategies, "At least one ContentNegotiationStrategy is expected"); Assert.notEmpty(strategies, "At least one ContentNegotiationStrategy is expected");
this.contentNegotiationStrategies.addAll(Arrays.asList(strategies)); this.contentNegotiationStrategies.addAll(Arrays.asList(strategies));
for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) { for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) {
if (strategy instanceof MediaTypeExtensionsResolver) { if (strategy instanceof MediaTypeFileExtensionResolver) {
this.extensionResolvers.add((MediaTypeExtensionsResolver) strategy); this.fileExtensionResolvers.add((MediaTypeFileExtensionResolver) strategy);
} }
} }
} }
@ -67,10 +69,10 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
} }
/** /**
* Add MediaTypeExtensionsResolver instances. * Add MediaTypeFileExtensionResolver instances.
*/ */
public void addExtensionsResolver(MediaTypeExtensionsResolver... resolvers) { public void addFileExtensionResolvers(MediaTypeFileExtensionResolver... resolvers) {
this.extensionResolvers.addAll(Arrays.asList(resolvers)); this.fileExtensionResolvers.addAll(Arrays.asList(resolvers));
} }
/** /**
@ -91,13 +93,13 @@ public class ContentNegotiationManager implements ContentNegotiationStrategy, Me
} }
/** /**
* Delegate to all configured MediaTypeExtensionsResolver instances and aggregate * Delegate to all configured MediaTypeFileExtensionResolver instances and aggregate
* the list of all extensions found. * the list of all file extensions found.
*/ */
public List<String> resolveExtensions(MediaType mediaType) { public List<String> resolveFileExtensions(MediaType mediaType) {
Set<String> extensions = new LinkedHashSet<String>(); Set<String> extensions = new LinkedHashSet<String>();
for (MediaTypeExtensionsResolver resolver : this.extensionResolvers) { for (MediaTypeFileExtensionResolver resolver : this.fileExtensionResolvers) {
extensions.addAll(resolver.resolveExtensions(mediaType)); extensions.addAll(resolver.resolveFileExtensions(mediaType));
} }
return new ArrayList<String>(extensions); return new ArrayList<String>(extensions);
} }

View File

@ -15,7 +15,7 @@
*/ */
package org.springframework.web.accept; package org.springframework.web.accept;
import java.util.ArrayList; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@ -24,28 +24,32 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentMap;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/** /**
* An implementation of {@link MediaTypeExtensionsResolver} that maintains a lookup * An implementation of {@link MediaTypeFileExtensionResolver} that maintains a lookup
* from extension to MediaType. * from extension to MediaType.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
*/ */
public class MappingMediaTypeExtensionsResolver implements MediaTypeExtensionsResolver { public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExtensionResolver {
private ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>(); private final ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>();
private final MultiValueMap<MediaType, String> fileExtensions = new LinkedMultiValueMap<MediaType, String>();
/** /**
* Create an instance with the given mappings between extensions and media types. * Create an instance with the given mappings between extensions and media types.
* @throws IllegalArgumentException if a media type string cannot be parsed * @throws IllegalArgumentException if a media type string cannot be parsed
*/ */
public MappingMediaTypeExtensionsResolver(Map<String, String> mediaTypes) { public MappingMediaTypeFileExtensionResolver(Map<String, MediaType> mediaTypes) {
if (mediaTypes != null) { if (mediaTypes != null) {
for (Map.Entry<String, String> entry : mediaTypes.entrySet()) { for (Entry<String, MediaType> entries : mediaTypes.entrySet()) {
String extension = entry.getKey().toLowerCase(Locale.ENGLISH); String extension = entries.getKey().toLowerCase(Locale.ENGLISH);
MediaType mediaType = MediaType.parseMediaType(entry.getValue()); MediaType mediaType = entries.getValue();
this.mediaTypes.put(extension, mediaType); addMapping(extension, mediaType);
} }
} }
} }
@ -54,14 +58,9 @@ public class MappingMediaTypeExtensionsResolver implements MediaTypeExtensionsRe
* Find the extensions applicable to the given MediaType. * Find the extensions applicable to the given MediaType.
* @return 0 or more extensions, never {@code null} * @return 0 or more extensions, never {@code null}
*/ */
public List<String> resolveExtensions(MediaType mediaType) { public List<String> resolveFileExtensions(MediaType mediaType) {
List<String> result = new ArrayList<String>(); List<String> fileExtensions = this.fileExtensions.get(mediaType);
for (Entry<String, MediaType> entry : this.mediaTypes.entrySet()) { return (fileExtensions != null) ? fileExtensions : Collections.<String>emptyList();
if (mediaType.includes(entry.getValue())) {
result.add(entry.getKey());
}
}
return result;
} }
/** /**
@ -76,7 +75,10 @@ public class MappingMediaTypeExtensionsResolver implements MediaTypeExtensionsRe
* Map a MediaType to an extension or ignore if the extensions is already mapped. * Map a MediaType to an extension or ignore if the extensions is already mapped.
*/ */
protected void addMapping(String extension, MediaType mediaType) { protected void addMapping(String extension, MediaType mediaType) {
this.mediaTypes.putIfAbsent(extension, mediaType); MediaType previous = this.mediaTypes.putIfAbsent(extension, mediaType);
if (previous == null) {
this.fileExtensions.add(mediaType, extension);
}
} }
} }

View File

@ -27,7 +27,7 @@ import org.springframework.http.MediaType;
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 3.2 * @since 3.2
*/ */
public interface MediaTypeExtensionsResolver { public interface MediaTypeFileExtensionResolver {
/** /**
* Resolve the given media type to a list of path extensions. * Resolve the given media type to a list of path extensions.
@ -35,6 +35,6 @@ public interface MediaTypeExtensionsResolver {
* @param mediaType the media type to resolve * @param mediaType the media type to resolve
* @return a list of extensions or an empty list, never {@code null} * @return a list of extensions or an empty list, never {@code null}
*/ */
List<String> resolveExtensions(MediaType mediaType); List<String> resolveFileExtensions(MediaType mediaType);
} }

View File

@ -42,7 +42,7 @@ public class ParameterContentNegotiationStrategy extends AbstractMappingContentN
* Create an instance with the given extension-to-MediaType lookup. * Create an instance with the given extension-to-MediaType lookup.
* @throws IllegalArgumentException if a media type string cannot be parsed * @throws IllegalArgumentException if a media type string cannot be parsed
*/ */
public ParameterContentNegotiationStrategy(Map<String, String> mediaTypes) { public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes); super(mediaTypes);
Assert.notEmpty(mediaTypes, "Cannot look up media types without any mappings"); Assert.notEmpty(mediaTypes, "Cannot look up media types without any mappings");
} }

View File

@ -72,7 +72,7 @@ public class PathExtensionContentNegotiationStrategy extends AbstractMappingCont
* Create an instance with the given extension-to-MediaType lookup. * Create an instance with the given extension-to-MediaType lookup.
* @throws IllegalArgumentException if a media type string cannot be parsed * @throws IllegalArgumentException if a media type string cannot be parsed
*/ */
public PathExtensionContentNegotiationStrategy(Map<String, String> mediaTypes) { public PathExtensionContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes); super(mediaTypes);
} }

View File

@ -35,7 +35,7 @@ public class AbstractMappingContentNegotiationStrategyTests {
@Test @Test
public void resolveMediaTypes() { public void resolveMediaTypes() {
Map<String, String> mapping = Collections.singletonMap("json", "application/json"); Map<String, MediaType> mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON);
TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("json", mapping); TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("json", mapping);
List<MediaType> mediaTypes = strategy.resolveMediaTypes(null); List<MediaType> mediaTypes = strategy.resolveMediaTypes(null);
@ -46,7 +46,7 @@ public class AbstractMappingContentNegotiationStrategyTests {
@Test @Test
public void resolveMediaTypesNoMatch() { public void resolveMediaTypesNoMatch() {
Map<String, String> mapping = null; Map<String, MediaType> mapping = null;
TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("blah", mapping); TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("blah", mapping);
List<MediaType> mediaTypes = strategy.resolveMediaTypes(null); List<MediaType> mediaTypes = strategy.resolveMediaTypes(null);
@ -56,7 +56,7 @@ public class AbstractMappingContentNegotiationStrategyTests {
@Test @Test
public void resolveMediaTypesNoKey() { public void resolveMediaTypesNoKey() {
Map<String, String> mapping = Collections.singletonMap("json", "application/json"); Map<String, MediaType> mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON);
TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy(null, mapping); TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy(null, mapping);
List<MediaType> mediaTypes = strategy.resolveMediaTypes(null); List<MediaType> mediaTypes = strategy.resolveMediaTypes(null);
@ -66,7 +66,7 @@ public class AbstractMappingContentNegotiationStrategyTests {
@Test @Test
public void resolveMediaTypesHandleNoMatch() { public void resolveMediaTypesHandleNoMatch() {
Map<String, String> mapping = null; Map<String, MediaType> mapping = null;
TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("xml", mapping); TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("xml", mapping);
List<MediaType> mediaTypes = strategy.resolveMediaTypes(null); List<MediaType> mediaTypes = strategy.resolveMediaTypes(null);
@ -80,7 +80,7 @@ public class AbstractMappingContentNegotiationStrategyTests {
private final String extension; private final String extension;
public TestMappingContentNegotiationStrategy(String extension, Map<String, String> mapping) { public TestMappingContentNegotiationStrategy(String extension, Map<String, MediaType> mapping) {
super(mapping); super(mapping);
this.extension = extension; this.extension = extension;
} }

View File

@ -16,6 +16,7 @@
package org.springframework.web.accept; package org.springframework.web.accept;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -25,20 +26,29 @@ import org.junit.Test;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
/** /**
* Test fixture for MappingMediaTypeExtensionsResolver. * Test fixture for {@link MappingMediaTypeFileExtensionResolver}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
*/ */
public class MappingMediaTypeExtensionsResolverTests { public class MappingMediaTypeFileExtensionResolverTests {
@Test @Test
public void resolveExtensions() { public void resolveExtensions() {
Map<String, String> mapping = Collections.singletonMap("json", "application/json"); Map<String, MediaType> mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON);
MappingMediaTypeExtensionsResolver resolver = new MappingMediaTypeExtensionsResolver(mapping); MappingMediaTypeFileExtensionResolver resolver = new MappingMediaTypeFileExtensionResolver(mapping);
List<String> extensions = resolver.resolveExtensions(MediaType.APPLICATION_JSON); List<String> extensions = resolver.resolveFileExtensions(MediaType.APPLICATION_JSON);
assertEquals(1, extensions.size()); assertEquals(1, extensions.size());
assertEquals("json", extensions.get(0)); assertEquals("json", extensions.get(0));
} }
@Test
public void resolveExtensionsNoMatch() {
Map<String, MediaType> mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON);
MappingMediaTypeFileExtensionResolver resolver = new MappingMediaTypeFileExtensionResolver(mapping);
List<String> extensions = resolver.resolveFileExtensions(MediaType.TEXT_HTML);
assertTrue(extensions.isEmpty());
}
} }

View File

@ -56,7 +56,7 @@ public class PathExtensionContentNegotiationStrategyTests {
assertEquals(Arrays.asList(new MediaType("text", "html")), mediaTypes); assertEquals(Arrays.asList(new MediaType("text", "html")), mediaTypes);
strategy = new PathExtensionContentNegotiationStrategy(Collections.singletonMap("HTML", "application/xhtml+xml")); strategy = new PathExtensionContentNegotiationStrategy(Collections.singletonMap("HTML", MediaType.APPLICATION_XHTML_XML));
mediaTypes = strategy.resolveMediaTypes(this.webRequest); mediaTypes = strategy.resolveMediaTypes(this.webRequest);
assertEquals(Arrays.asList(new MediaType("application", "xhtml+xml")), mediaTypes); assertEquals(Arrays.asList(new MediaType("application", "xhtml+xml")), mediaTypes);

View File

@ -102,7 +102,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
private boolean favorPathExtension = true; private boolean favorPathExtension = true;
private boolean favorParameter = false; private boolean favorParameter = false;
private boolean ignoreAcceptHeader = false; private boolean ignoreAcceptHeader = false;
private Map<String, String> mediaTypes = new HashMap<String, String>(); private Map<String, MediaType> mediaTypes = new HashMap<String, MediaType>();
private Boolean useJaf; private Boolean useJaf;
private String parameterName; private String parameterName;
private MediaType defaultContentType; private MediaType defaultContentType;
@ -200,7 +200,13 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
* @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)}
*/ */
public void setMediaTypes(Map<String, String> mediaTypes) { public void setMediaTypes(Map<String, String> mediaTypes) {
this.mediaTypes = mediaTypes; if (mediaTypes != null) {
for (Map.Entry<String, String> entry : mediaTypes.entrySet()) {
String extension = entry.getKey().toLowerCase(Locale.ENGLISH);
MediaType mediaType = MediaType.parseMediaType(entry.getValue());
this.mediaTypes.put(extension, mediaType);
}
}
} }
/** /**
@ -389,7 +395,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
candidateViews.add(view); candidateViews.add(view);
} }
for (MediaType requestedMediaType : requestedMediaTypes) { for (MediaType requestedMediaType : requestedMediaTypes) {
List<String> extensions = this.contentNegotiationManager.resolveExtensions(requestedMediaType); List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
for (String extension : extensions) { for (String extension : extensions) {
String viewNameWithExtension = viewName + "." + extension; String viewNameWithExtension = viewName + "." + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale); view = viewResolver.resolveViewName(viewNameWithExtension, locale);

View File

@ -43,7 +43,7 @@ import org.springframework.mock.web.MockServletContext;
import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.accept.ContentNegotiationManager;
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.MappingMediaTypeExtensionsResolver; import org.springframework.web.accept.MappingMediaTypeFileExtensionResolver;
import org.springframework.web.accept.ParameterContentNegotiationStrategy; import org.springframework.web.accept.ParameterContentNegotiationStrategy;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
@ -117,10 +117,10 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameWithAcceptHeader() throws Exception { public void resolveViewNameWithAcceptHeader() throws Exception {
request.addHeader("Accept", "application/vnd.ms-excel"); request.addHeader("Accept", "application/vnd.ms-excel");
Map<String, String> mapping = Collections.singletonMap("xls", "application/vnd.ms-excel"); Map<String, MediaType> mapping = Collections.singletonMap("xls", MediaType.valueOf("application/vnd.ms-excel"));
MappingMediaTypeExtensionsResolver extensionsResolver = new MappingMediaTypeExtensionsResolver(mapping); MappingMediaTypeFileExtensionResolver extensionsResolver = new MappingMediaTypeFileExtensionResolver(mapping);
ContentNegotiationManager manager = new ContentNegotiationManager(new HeaderContentNegotiationStrategy()); ContentNegotiationManager manager = new ContentNegotiationManager(new HeaderContentNegotiationStrategy());
manager.addExtensionsResolver(extensionsResolver); manager.addFileExtensionResolvers(extensionsResolver);
viewResolver.setContentNegotiationManager(manager); viewResolver.setContentNegotiationManager(manager);
ViewResolver viewResolverMock = createMock(ViewResolver.class); ViewResolver viewResolverMock = createMock(ViewResolver.class);
@ -155,7 +155,7 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameWithRequestParameter() throws Exception { public void resolveViewNameWithRequestParameter() throws Exception {
request.addParameter("format", "xls"); request.addParameter("format", "xls");
Map<String, String> mapping = Collections.singletonMap("xls", "application/vnd.ms-excel"); Map<String, MediaType> mapping = Collections.singletonMap("xls", MediaType.valueOf("application/vnd.ms-excel"));
ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mapping); ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mapping);
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(paramStrategy)); viewResolver.setContentNegotiationManager(new ContentNegotiationManager(paramStrategy));
@ -343,8 +343,7 @@ public class ContentNegotiatingViewResolverTests {
public void resolveViewNameFilenameDefaultView() throws Exception { public void resolveViewNameFilenameDefaultView() throws Exception {
request.setRequestURI("/test.json"); request.setRequestURI("/test.json");
Map<String, MediaType> mapping = Collections.singletonMap("json", MediaType.APPLICATION_JSON);
Map<String, String> mapping = Collections.singletonMap("json", "application/json");
PathExtensionContentNegotiationStrategy pathStrategy = new PathExtensionContentNegotiationStrategy(mapping); PathExtensionContentNegotiationStrategy pathStrategy = new PathExtensionContentNegotiationStrategy(mapping);
viewResolver.setContentNegotiationManager(new ContentNegotiationManager(pathStrategy)); viewResolver.setContentNegotiationManager(new ContentNegotiationManager(pathStrategy));