From f05e2bc56f8e03466977d73a5e99c37651248803 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 8 Jun 2012 08:56:57 -0400 Subject: [PATCH] Add abstractions for content negotiation Introduced ContentNeogtiationStrategy for resolving the requested media types from an incoming request. The available implementations are based on path extension, request parameter, 'Accept' header, and a fixed default content type. The logic for these implementations is based on equivalent options, previously available only in the ContentNegotiatingViewResolver. Also in this commit is ContentNegotiationManager, the central class to use when configuring content negotiation options. It accepts one or more ContentNeogtiationStrategy instances and delegates to them. The ContentNeogiationManager can now be used to configure the following classes: - RequestMappingHandlerMappingm - RequestMappingHandlerAdapter - ExceptionHandlerExceptionResolver - ContentNegotiatingViewResolver Issue: SPR-8410, SPR-8417, SPR-8418,SPR-8416, SPR-8419,SPR-7722 --- build.gradle | 1 + ...ractMappingContentNegotiationStrategy.java | 82 ++++ .../web/accept/ContentNegotiationManager.java | 105 +++++ .../accept/ContentNegotiationStrategy.java | 44 +++ .../FixedContentNegotiationStrategy.java | 53 +++ .../HeaderContentNegotiationStrategy.java | 57 +++ .../MappingMediaTypeExtensionsResolver.java | 82 ++++ .../accept/MediaTypeExtensionsResolver.java | 40 ++ .../ParameterContentNegotiationStrategy.java | 71 ++++ ...thExtensionContentNegotiationStrategy.java | 187 +++++++++ .../web/accept/package-info.java | 17 + ...appingContentNegotiationStrategyTests.java | 99 +++++ ...HeaderContentNegotiationStrategyTests.java | 68 ++++ ...ppingMediaTypeExtensionsResolverTests.java | 44 +++ ...ensionContentNegotiationStrategyTests.java | 113 ++++++ .../AbstractMediaTypeExpression.java | 8 +- .../condition/ConsumesRequestCondition.java | 65 ++-- .../condition/ProducesRequestCondition.java | 170 ++++---- ...stractMessageConverterMethodProcessor.java | 37 +- .../ExceptionHandlerExceptionResolver.java | 15 +- .../annotation/HttpEntityMethodProcessor.java | 9 +- .../RequestMappingHandlerAdapter.java | 15 +- .../RequestMappingHandlerMapping.java | 21 +- .../RequestResponseBodyMethodProcessor.java | 7 + .../view/ContentNegotiatingViewResolver.java | 364 ++++++------------ .../ParamsRequestConditionTests.java | 4 +- .../ProducesRequestConditionTests.java | 29 +- .../ContentNegotiatingViewResolverTests.java | 149 +++---- src/dist/changelog.txt | 2 +- 29 files changed, 1469 insertions(+), 489 deletions(-) create mode 100644 spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeExtensionsResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/MediaTypeExtensionsResolver.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java create mode 100644 spring-web/src/main/java/org/springframework/web/accept/package-info.java create mode 100644 spring-web/src/test/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategyTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/accept/MappingMediaTypeExtensionsResolverTests.java create mode 100644 spring-web/src/test/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategyTests.java diff --git a/build.gradle b/build.gradle index c2b1bcbcdbc..cd33239f50a 100644 --- a/build.gradle +++ b/build.gradle @@ -368,6 +368,7 @@ project('spring-web') { optional dep exclude group: 'org.mortbay.jetty', module: 'servlet-api-2.5' } + testCompile project(":spring-context-support") // for JafMediaTypeFactory testCompile "xmlunit:xmlunit:1.2" } diff --git a/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java new file mode 100644 index 00000000000..da1dc114ab4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategy.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.springframework.http.MediaType; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * A base class for ContentNegotiationStrategy types that maintain a map with keys + * such as "json" and media types such as "application/json". + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeExtensionsResolver + implements ContentNegotiationStrategy, MediaTypeExtensionsResolver { + + /** + * Create an instance with the given extension-to-MediaType lookup. + * @throws IllegalArgumentException if a media type string cannot be parsed + */ + public AbstractMappingContentNegotiationStrategy(Map mediaTypes) { + super(mediaTypes); + } + + public List resolveMediaTypes(NativeWebRequest webRequest) { + String key = getMediaTypeKey(webRequest); + if (StringUtils.hasText(key)) { + MediaType mediaType = lookupMediaType(key); + if (mediaType != null) { + handleMatch(key, mediaType); + return Collections.singletonList(mediaType); + } + mediaType = handleNoMatch(webRequest, key); + if (mediaType != null) { + addMapping(key, mediaType); + return Collections.singletonList(mediaType); + } + } + return Collections.emptyList(); + } + + /** + * Sub-classes must extract the key to use to look up a media type. + * @return the lookup key or {@code null} if the key cannot be derived + */ + protected abstract String getMediaTypeKey(NativeWebRequest request); + + /** + * Invoked when a matching media type is found in the lookup map. + */ + protected void handleMatch(String mappingKey, MediaType mediaType) { + } + + /** + * Invoked when no matching media type is found in the lookup map. + * Sub-classes can take further steps to determine the media type. + */ + protected MediaType handleNoMatch(NativeWebRequest request, String mappingKey) { + return null; + } + +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java new file mode 100644 index 00000000000..e789bcba502 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationManager.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * This class is used to determine the requested {@linkplain MediaType media types} + * in a request by delegating to a list of {@link ContentNegotiationStrategy} instances. + * + *

It may also be used to determine the extensions associated with a MediaType by + * delegating to a list of {@link MediaTypeExtensionsResolver} instances. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeExtensionsResolver { + + private final List contentNegotiationStrategies = new ArrayList(); + + private final Set extensionResolvers = new LinkedHashSet(); + + /** + * Create an instance with the given ContentNegotiationStrategy instances. + *

Each instance is checked to see if it is also an implementation of + * MediaTypeExtensionsResolver, and if so it is registered as such. + */ + public ContentNegotiationManager(ContentNegotiationStrategy... strategies) { + Assert.notEmpty(strategies, "At least one ContentNegotiationStrategy is expected"); + this.contentNegotiationStrategies.addAll(Arrays.asList(strategies)); + for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) { + if (strategy instanceof MediaTypeExtensionsResolver) { + this.extensionResolvers.add((MediaTypeExtensionsResolver) strategy); + } + } + } + + /** + * Create an instance with a {@link HeaderContentNegotiationStrategy}. + */ + public ContentNegotiationManager() { + this(new HeaderContentNegotiationStrategy()); + } + + /** + * Add MediaTypeExtensionsResolver instances. + */ + public void addExtensionsResolver(MediaTypeExtensionsResolver... resolvers) { + this.extensionResolvers.addAll(Arrays.asList(resolvers)); + } + + /** + * Delegate to all configured ContentNegotiationStrategy instances until one + * returns a non-empty list. + * @param request the current request + * @return the requested media types or an empty list, never {@code null} + * @throws HttpMediaTypeNotAcceptableException if the requested media types cannot be parsed + */ + public List resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { + for (ContentNegotiationStrategy strategy : this.contentNegotiationStrategies) { + List mediaTypes = strategy.resolveMediaTypes(webRequest); + if (!mediaTypes.isEmpty()) { + return mediaTypes; + } + } + return Collections.emptyList(); + } + + /** + * Delegate to all configured MediaTypeExtensionsResolver instances and aggregate + * the list of all extensions found. + */ + public List resolveExtensions(MediaType mediaType) { + Set extensions = new LinkedHashSet(); + for (MediaTypeExtensionsResolver resolver : this.extensionResolvers) { + extensions.addAll(resolver.resolveExtensions(mediaType)); + } + return new ArrayList(extensions); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.java new file mode 100644 index 00000000000..0f990e0849c --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/ContentNegotiationStrategy.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.List; + +import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * A strategy for resolving the requested media types in a request. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public interface ContentNegotiationStrategy { + + /** + * Resolve the given request to a list of media types. The returned list is + * ordered by specificity first and by quality parameter second. + * + * @param request the current request + * @return the requested media types or an empty list, never {@code null} + * + * @throws HttpMediaTypeNotAcceptableException if the requested media types cannot be parsed + */ + List resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException; + +} diff --git a/spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.java new file mode 100644 index 00000000000..84ac58cfee3 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/FixedContentNegotiationStrategy.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2012 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 + * + * http://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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.MediaType; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * A ContentNegotiationStrategy that returns a fixed content type. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class FixedContentNegotiationStrategy implements ContentNegotiationStrategy { + + private static final Log logger = LogFactory.getLog(FixedContentNegotiationStrategy.class); + + private final MediaType defaultContentType; + + /** + * Create an instance that always returns the given content type. + */ + public FixedContentNegotiationStrategy(MediaType defaultContentType) { + this.defaultContentType = defaultContentType; + } + + public List resolveMediaTypes(NativeWebRequest webRequest) { + if (logger.isDebugEnabled()) { + logger.debug("Requested media types is " + this.defaultContentType + " (based on default MediaType)"); + } + return Collections.singletonList(this.defaultContentType); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java new file mode 100644 index 00000000000..ef84fb179da --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/HeaderContentNegotiationStrategy.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2012 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 + * + * http://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 org.springframework.http.MediaType; +import org.springframework.util.StringUtils; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * A ContentNegotiationStrategy that parses the 'Accept' header of the request. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy { + + private static final String ACCEPT_HEADER = "Accept"; + + /** + * {@inheritDoc} + * @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed. + */ + public List resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException { + String acceptHeader = webRequest.getHeader(ACCEPT_HEADER); + try { + if (StringUtils.hasText(acceptHeader)) { + List mediaTypes = MediaType.parseMediaTypes(acceptHeader); + MediaType.sortBySpecificityAndQuality(mediaTypes); + return mediaTypes; + } + } + catch (IllegalArgumentException ex) { + throw new HttpMediaTypeNotAcceptableException( + "Could not parse accept header [" + acceptHeader + "]: " + ex.getMessage()); + } + return Collections.emptyList(); + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeExtensionsResolver.java b/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeExtensionsResolver.java new file mode 100644 index 00000000000..0a401a2e735 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/MappingMediaTypeExtensionsResolver.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import org.springframework.http.MediaType; + +/** + * An implementation of {@link MediaTypeExtensionsResolver} that maintains a lookup + * from extension to MediaType. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class MappingMediaTypeExtensionsResolver implements MediaTypeExtensionsResolver { + + private ConcurrentMap mediaTypes = new ConcurrentHashMap(); + + /** + * Create an instance with the given mappings between extensions and media types. + * @throws IllegalArgumentException if a media type string cannot be parsed + */ + public MappingMediaTypeExtensionsResolver(Map mediaTypes) { + if (mediaTypes != null) { + for (Map.Entry entry : mediaTypes.entrySet()) { + String extension = entry.getKey().toLowerCase(Locale.ENGLISH); + MediaType mediaType = MediaType.parseMediaType(entry.getValue()); + this.mediaTypes.put(extension, mediaType); + } + } + } + + /** + * Find the extensions applicable to the given MediaType. + * @return 0 or more extensions, never {@code null} + */ + public List resolveExtensions(MediaType mediaType) { + List result = new ArrayList(); + for (Entry entry : this.mediaTypes.entrySet()) { + if (mediaType.includes(entry.getValue())) { + result.add(entry.getKey()); + } + } + return result; + } + + /** + * Return the MediaType mapped to the given extension. + * @return a MediaType for the key or {@code null} + */ + public MediaType lookupMediaType(String extension) { + return this.mediaTypes.get(extension); + } + + /** + * Map a MediaType to an extension or ignore if the extensions is already mapped. + */ + protected void addMapping(String extension, MediaType mediaType) { + this.mediaTypes.putIfAbsent(extension, mediaType); + } + +} \ No newline at end of file diff --git a/spring-web/src/main/java/org/springframework/web/accept/MediaTypeExtensionsResolver.java b/spring-web/src/main/java/org/springframework/web/accept/MediaTypeExtensionsResolver.java new file mode 100644 index 00000000000..476695b947b --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/MediaTypeExtensionsResolver.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.List; + +import org.springframework.http.MediaType; + +/** + * A strategy for resolving a {@link MediaType} to one or more path extensions. + * For example "application/json" to "json". + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public interface MediaTypeExtensionsResolver { + + /** + * Resolve the given media type to a list of path extensions. + * + * @param mediaType the media type to resolve + * @return a list of extensions or an empty list, never {@code null} + */ + List resolveExtensions(MediaType mediaType); + +} diff --git a/spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java new file mode 100644 index 00000000000..7e0ce5f9d5a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/ParameterContentNegotiationStrategy.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2012 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 + * + * http://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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.http.MediaType; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * A ContentNegotiationStrategy that uses a request parameter to determine what + * media types are requested. The default parameter name is {@code format}. + * Its value is used to look up the media type in the map given to the constructor. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { + + private static final Log logger = LogFactory.getLog(ParameterContentNegotiationStrategy.class); + + private String parameterName = "format"; + + /** + * Create an instance with the given extension-to-MediaType lookup. + * @throws IllegalArgumentException if a media type string cannot be parsed + */ + public ParameterContentNegotiationStrategy(Map mediaTypes) { + super(mediaTypes); + Assert.notEmpty(mediaTypes, "Cannot look up media types without any mappings"); + } + + /** + * Set the parameter name that can be used to determine the requested media type. + *

The default parameter name is {@code format}. + */ + public void setParameterName(String parameterName) { + this.parameterName = parameterName; + } + + @Override + protected String getMediaTypeKey(NativeWebRequest webRequest) { + return webRequest.getParameter(this.parameterName); + } + + @Override + protected void handleMatch(String mediaTypeKey, MediaType mediaType) { + if (logger.isDebugEnabled()) { + logger.debug("Requested media type is '" + mediaType + "' (based on parameter '" + + this.parameterName + "'='" + mediaTypeKey + "')"); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java b/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java new file mode 100644 index 00000000000..d2ddca7baed --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategy.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2012 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 + * + * http://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.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.Map; + +import javax.activation.FileTypeMap; +import javax.activation.MimetypesFileTypeMap; +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.util.UrlPathHelper; +import org.springframework.web.util.WebUtils; + +/** + * A ContentNegotiationStrategy that uses the path extension of the URL to determine + * what media types are requested. The path extension is used as follows: + * + *

    + *
  1. Look upin the map of media types provided to the constructor + *
  2. Call to {@link ServletContext#getMimeType(String)} + *
  3. Use the Java Activation framework + *
+ * + *

The presence of the Java Activation framework is detected and enabled automatically + * but the {@link #setUseJaf(boolean)} property may be used to override that setting. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class PathExtensionContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { + + private static final boolean JAF_PRESENT = + ClassUtils.isPresent("javax.activation.FileTypeMap", PathExtensionContentNegotiationStrategy.class.getClassLoader()); + + private static final Log logger = LogFactory.getLog(PathExtensionContentNegotiationStrategy.class); + + private static final UrlPathHelper urlPathHelper = new UrlPathHelper(); + + static { + urlPathHelper.setUrlDecode(false); + } + + private boolean useJaf = JAF_PRESENT; + + /** + * Create an instance with the given extension-to-MediaType lookup. + * @throws IllegalArgumentException if a media type string cannot be parsed + */ + public PathExtensionContentNegotiationStrategy(Map mediaTypes) { + super(mediaTypes); + } + + /** + * Create an instance without any mappings to start with. Mappings may be added + * later on if any extensions are resolved through {@link ServletContext#getMimeType(String)} + * or through the Java Activation framework. + */ + public PathExtensionContentNegotiationStrategy() { + super(null); + } + + /** + * Indicate whether to use the Java Activation Framework to map from file extensions to media types. + *

Default is {@code true}, i.e. the Java Activation Framework is used (if available). + */ + public void setUseJaf(boolean useJaf) { + this.useJaf = useJaf; + } + + @Override + protected String getMediaTypeKey(NativeWebRequest webRequest) { + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + if (servletRequest == null) { + logger.warn("An HttpServletRequest is required to determine the media type key"); + return null; + } + String path = urlPathHelper.getLookupPathForRequest(servletRequest); + String filename = WebUtils.extractFullFilenameFromUrlPath(path); + String extension = StringUtils.getFilenameExtension(filename); + return (StringUtils.hasText(extension)) ? extension.toLowerCase(Locale.ENGLISH) : null; + } + + @Override + protected void handleMatch(String extension, MediaType mediaType) { + if (logger.isDebugEnabled()) { + logger.debug("Requested media type is '" + mediaType + "' (based on file extension '" + extension + "')"); + } + } + + @Override + protected MediaType handleNoMatch(NativeWebRequest webRequest, String extension) { + MediaType mediaType = null; + HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); + if (servletRequest != null) { + String mimeType = servletRequest.getServletContext().getMimeType("file." + extension); + if (StringUtils.hasText(mimeType)) { + mediaType = MediaType.parseMediaType(mimeType); + } + } + if ((mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType)) && this.useJaf) { + MediaType jafMediaType = JafMediaTypeFactory.getMediaType("file." + extension); + if (jafMediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(jafMediaType)) { + mediaType = jafMediaType; + } + } + return mediaType; + } + + + /** + * Inner class to avoid hard-coded dependency on JAF. + */ + private static class JafMediaTypeFactory { + + private static final FileTypeMap fileTypeMap; + + static { + fileTypeMap = initFileTypeMap(); + } + + /** + * Find extended mime.types from the spring-context-support module. + */ + private static FileTypeMap initFileTypeMap() { + Resource resource = new ClassPathResource("org/springframework/mail/javamail/mime.types"); + if (resource.exists()) { + if (logger.isTraceEnabled()) { + logger.trace("Loading Java Activation Framework FileTypeMap from " + resource); + } + InputStream inputStream = null; + try { + inputStream = resource.getInputStream(); + return new MimetypesFileTypeMap(inputStream); + } + catch (IOException ex) { + // ignore + } + finally { + if (inputStream != null) { + try { + inputStream.close(); + } + catch (IOException ex) { + // ignore + } + } + } + } + if (logger.isTraceEnabled()) { + logger.trace("Loading default Java Activation Framework FileTypeMap"); + } + return FileTypeMap.getDefaultFileTypeMap(); + } + + public static MediaType getMediaType(String filename) { + String mediaType = fileTypeMap.getContentType(filename); + return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null); + } + } + +} diff --git a/spring-web/src/main/java/org/springframework/web/accept/package-info.java b/spring-web/src/main/java/org/springframework/web/accept/package-info.java new file mode 100644 index 00000000000..f4061338247 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/accept/package-info.java @@ -0,0 +1,17 @@ + +/** + * This package contains classes used to determine the requested the media types in a request. + * + *

{@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 + * {@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}. + * + *

{@link org.springframework.web.accept.ContentNegotiationManager} is used to delegate to one + * ore more of the above strategies in a specific order. + */ +package org.springframework.web.accept; + diff --git a/spring-web/src/test/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategyTests.java b/spring-web/src/test/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategyTests.java new file mode 100644 index 00000000000..4088ddebcbb --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/accept/AbstractMappingContentNegotiationStrategyTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2012 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 + * + * http://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 static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * A test fixture with a test sub-class of AbstractMappingContentNegotiationStrategy. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class AbstractMappingContentNegotiationStrategyTests { + + @Test + public void resolveMediaTypes() { + Map mapping = Collections.singletonMap("json", "application/json"); + TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("json", mapping); + + List mediaTypes = strategy.resolveMediaTypes(null); + + assertEquals(1, mediaTypes.size()); + assertEquals("application/json", mediaTypes.get(0).toString()); + } + + @Test + public void resolveMediaTypesNoMatch() { + Map mapping = null; + TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("blah", mapping); + + List mediaTypes = strategy.resolveMediaTypes(null); + + assertEquals(0, mediaTypes.size()); + } + + @Test + public void resolveMediaTypesNoKey() { + Map mapping = Collections.singletonMap("json", "application/json"); + TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy(null, mapping); + + List mediaTypes = strategy.resolveMediaTypes(null); + + assertEquals(0, mediaTypes.size()); + } + + @Test + public void resolveMediaTypesHandleNoMatch() { + Map mapping = null; + TestMappingContentNegotiationStrategy strategy = new TestMappingContentNegotiationStrategy("xml", mapping); + + List mediaTypes = strategy.resolveMediaTypes(null); + + assertEquals(1, mediaTypes.size()); + assertEquals("application/xml", mediaTypes.get(0).toString()); + } + + + private static class TestMappingContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy { + + private final String extension; + + public TestMappingContentNegotiationStrategy(String extension, Map mapping) { + super(mapping); + this.extension = extension; + } + + @Override + protected String getMediaTypeKey(NativeWebRequest request) { + return this.extension; + } + + @Override + protected MediaType handleNoMatch(NativeWebRequest request, String mappingKey) { + return "xml".equals(mappingKey) ? MediaType.APPLICATION_XML : null; + } + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java new file mode 100644 index 00000000000..b4479e60c4c --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/accept/HeaderContentNegotiationStrategyTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2012 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 + * + * http://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 static org.junit.Assert.assertEquals; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * Test fixture for HeaderContentNegotiationStrategy tests. + * + * @author Rossen Stoyanchev + */ +public class HeaderContentNegotiationStrategyTests { + + private HeaderContentNegotiationStrategy strategy; + + private NativeWebRequest webRequest; + + private MockHttpServletRequest servletRequest; + + @Before + public void setup() { + this.strategy = new HeaderContentNegotiationStrategy(); + this.servletRequest = new MockHttpServletRequest(); + this.webRequest = new ServletWebRequest(servletRequest ); + } + + @Test + public void resolveMediaTypes() throws Exception { + this.servletRequest.addHeader("Accept", "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"); + List mediaTypes = this.strategy.resolveMediaTypes(this.webRequest); + + assertEquals(4, mediaTypes.size()); + assertEquals("text/html", mediaTypes.get(0).toString()); + assertEquals("text/x-c", mediaTypes.get(1).toString()); + assertEquals("text/x-dvi;q=0.8", mediaTypes.get(2).toString()); + assertEquals("text/plain;q=0.5", mediaTypes.get(3).toString()); + } + + @Test(expected=HttpMediaTypeNotAcceptableException.class) + public void resolveMediaTypesParseError() throws Exception { + this.servletRequest.addHeader("Accept", "textplain; q=0.5"); + this.strategy.resolveMediaTypes(this.webRequest); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/accept/MappingMediaTypeExtensionsResolverTests.java b/spring-web/src/test/java/org/springframework/web/accept/MappingMediaTypeExtensionsResolverTests.java new file mode 100644 index 00000000000..6aca4875357 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/accept/MappingMediaTypeExtensionsResolverTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2012 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 + * + * http://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 static org.junit.Assert.assertEquals; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.springframework.http.MediaType; + +/** + * Test fixture for MappingMediaTypeExtensionsResolver. + * + * @author Rossen Stoyanchev + */ +public class MappingMediaTypeExtensionsResolverTests { + + @Test + public void resolveExtensions() { + Map mapping = Collections.singletonMap("json", "application/json"); + MappingMediaTypeExtensionsResolver resolver = new MappingMediaTypeExtensionsResolver(mapping); + List extensions = resolver.resolveExtensions(MediaType.APPLICATION_JSON); + + assertEquals(1, extensions.size()); + assertEquals("json", extensions.get(0)); + } + +} diff --git a/spring-web/src/test/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategyTests.java b/spring-web/src/test/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategyTests.java new file mode 100644 index 00000000000..47dc26e899d --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/accept/PathExtensionContentNegotiationStrategyTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2012 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 + * + * http://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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; + +/** + * A test fixture for PathExtensionContentNegotiationStrategy. + * + * @author Rossen Stoyanchev + * @since 3.2 + */ +public class PathExtensionContentNegotiationStrategyTests { + + private NativeWebRequest webRequest; + + private MockHttpServletRequest servletRequest; + + @Before + public void setup() { + this.servletRequest = new MockHttpServletRequest(); + this.webRequest = new ServletWebRequest(servletRequest ); + } + + @Test + public void resolveMediaTypesFromMapping() { + this.servletRequest.setRequestURI("test.html"); + PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(); + + List mediaTypes = strategy.resolveMediaTypes(this.webRequest); + + assertEquals(Arrays.asList(new MediaType("text", "html")), mediaTypes); + + strategy = new PathExtensionContentNegotiationStrategy(Collections.singletonMap("HTML", "application/xhtml+xml")); + mediaTypes = strategy.resolveMediaTypes(this.webRequest); + + assertEquals(Arrays.asList(new MediaType("application", "xhtml+xml")), mediaTypes); + } + + @Test + public void resolveMediaTypesFromJaf() { + this.servletRequest.setRequestURI("test.xls"); + PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(); + + List mediaTypes = strategy.resolveMediaTypes(this.webRequest); + + assertEquals(Arrays.asList(new MediaType("application", "vnd.ms-excel")), mediaTypes); + } + + @Test + public void getMediaTypeFromFilenameNoJaf() { + this.servletRequest.setRequestURI("test.xls"); + PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(); + strategy.setUseJaf(false); + + List mediaTypes = strategy.resolveMediaTypes(this.webRequest); + + assertEquals(Arrays.asList(MediaType.APPLICATION_OCTET_STREAM), mediaTypes); + } + + // SPR-8678 + + @Test + public void getMediaTypeFilenameWithContextPath() { + this.servletRequest.setContextPath("/project-1.0.0.M3"); + this.servletRequest.setRequestURI("/project-1.0.0.M3/"); + PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(); + + assertTrue("Context path should be excluded", strategy.resolveMediaTypes(webRequest).isEmpty()); + + this.servletRequest.setRequestURI("/project-1.0.0.M3"); + + assertTrue("Context path should be excluded", strategy.resolveMediaTypes(webRequest).isEmpty()); + } + + // SPR-9390 + + @Test + public void getMediaTypeFilenameWithEncodedURI() { + this.servletRequest.setRequestURI("/quo%20vadis%3f.html"); + PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(); + + List result = strategy.resolveMediaTypes(webRequest); + + assertEquals("Invalid content type", Collections.singletonList(new MediaType("text", "html")), result); + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java index 1e36253feac..15aee22ec25 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/condition/AbstractMediaTypeExpression.java @@ -21,6 +21,7 @@ import javax.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.http.MediaType; +import org.springframework.web.HttpMediaTypeException; import org.springframework.web.bind.annotation.RequestMapping; /** @@ -68,15 +69,12 @@ abstract class AbstractMediaTypeExpression implements Comparable0 if the two conditions have the same number of expressions *

  • Less than 0 if "this" has more or more specific media type expressions *
  • Greater than 0 if "other" has more or more specific media type expressions - * - * - *

    It is assumed that both instances have been obtained via - * {@link #getMatchingCondition(HttpServletRequest)} and each instance contains + * + * + *

    It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(HttpServletRequest)} and each instance contains * the matching consumable media type expression only or is otherwise empty. */ public int compareTo(ConsumesRequestCondition other, HttpServletRequest request) { @@ -197,7 +198,7 @@ public final class ConsumesRequestCondition extends AbstractRequestCondition expressions; + private final ContentNegotiationManager contentNegotiationManager; + /** - * Creates a new instance from 0 or more "produces" expressions. - * @param produces expressions with the syntax described in {@link RequestMapping#produces()} - * if 0 expressions are provided, the condition matches to every request + * Creates a new instance from "produces" expressions. If 0 expressions + * are provided in total, this condition will match to any request. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} */ public ProducesRequestCondition(String... produces) { - this(parseExpressions(produces, null)); + this(produces, (String[]) null); } - + /** - * Creates a new instance with "produces" and "header" expressions. "Header" expressions - * where the header name is not 'Accept' or have no header value defined are ignored. - * If 0 expressions are provided in total, the condition matches to every request - * @param produces expressions with the syntax described in {@link RequestMapping#produces()} - * @param headers expressions with the syntax described in {@link RequestMapping#headers()} + * Creates a new instance with "produces" and "header" expressions. "Header" + * expressions where the header name is not 'Accept' or have no header value + * defined are ignored. If 0 expressions are provided in total, this condition + * will match to any request. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} + * @param headers expressions with syntax defined by {@link RequestMapping#headers()} */ public ProducesRequestCondition(String[] produces, String[] headers) { - this(parseExpressions(produces, headers)); + this(produces, headers, null); } /** - * Private constructor accepting parsed media type expressions. + * Same as {@link #ProducesRequestCondition(String[], String[])} but also + * accepting a {@link ContentNegotiationManager}. + * @param produces expressions with syntax defined by {@link RequestMapping#produces()} + * @param headers expressions with syntax defined by {@link RequestMapping#headers()} + * @param contentNegotiationManager used to determine requested media types */ - private ProducesRequestCondition(Collection expressions) { - this.expressions = new ArrayList(expressions); + public ProducesRequestCondition(String[] produces, String[] headers, + ContentNegotiationManager manager) { + + this.expressions = new ArrayList(parseExpressions(produces, headers)); Collections.sort(this.expressions); + this.contentNegotiationManager = (manager != null) ? manager : new ContentNegotiationManager(); } - private static Set parseExpressions(String[] produces, String[] headers) { + private Set parseExpressions(String[] produces, String[] headers) { Set result = new LinkedHashSet(); if (headers != null) { for (String header : headers) { @@ -95,6 +106,17 @@ public final class ProducesRequestCondition extends AbstractRequestCondition expressions, + ContentNegotiationManager manager) { + + this.expressions = new ArrayList(expressions); + Collections.sort(this.expressions); + this.contentNegotiationManager = (manager != null) ? manager : new ContentNegotiationManager(); + } + /** * Return the contained "produces" expressions. */ @@ -133,8 +155,8 @@ public final class ProducesRequestCondition extends AbstractRequestCondition *

  • Sort 'Accept' header media types by quality value via * {@link MediaType#sortByQualityValue(List)} and iterate the list. *
  • Get the first index of matching media types in each "produces" - * condition first matching with {@link MediaType#equals(Object)} and + * condition first matching with {@link MediaType#equals(Object)} and * then with {@link MediaType#includes(MediaType)}. *
  • If a lower index is found, the condition at that index wins. - *
  • If both indexes are equal, the media types at the index are + *
  • If both indexes are equal, the media types at the index are * compared further with {@link MediaType#SPECIFICITY_COMPARATOR}. * - * - *

    It is assumed that both instances have been obtained via - * {@link #getMatchingCondition(HttpServletRequest)} and each instance - * contains the matching producible media type expression only or + * + *

    It is assumed that both instances have been obtained via + * {@link #getMatchingCondition(HttpServletRequest)} and each instance + * contains the matching producible media type expression only or * is otherwise empty. */ public int compareTo(ProducesRequestCondition other, HttpServletRequest request) { - List acceptedMediaTypes = getAcceptedMediaTypes(request); - MediaType.sortByQualityValue(acceptedMediaTypes); + try { + List acceptedMediaTypes = getAcceptedMediaTypes(request); - for (MediaType acceptedMediaType : acceptedMediaTypes) { - int thisIndex = this.indexOfEqualMediaType(acceptedMediaType); - int otherIndex = other.indexOfEqualMediaType(acceptedMediaType); - int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex); - if (result != 0) { - return result; - } - thisIndex = this.indexOfIncludedMediaType(acceptedMediaType); - otherIndex = other.indexOfIncludedMediaType(acceptedMediaType); - result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex); - if (result != 0) { - return result; + for (MediaType acceptedMediaType : acceptedMediaTypes) { + int thisIndex = this.indexOfEqualMediaType(acceptedMediaType); + int otherIndex = other.indexOfEqualMediaType(acceptedMediaType); + int result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex); + if (result != 0) { + return result; + } + thisIndex = this.indexOfIncludedMediaType(acceptedMediaType); + otherIndex = other.indexOfIncludedMediaType(acceptedMediaType); + result = compareMatchingMediaTypes(this, thisIndex, other, otherIndex); + if (result != 0) { + return result; + } } + return 0; + } + catch (HttpMediaTypeNotAcceptableException e) { + // should never happen + throw new IllegalStateException("Cannot compare without having any requested media types"); } - - return 0; } - private static List getAcceptedMediaTypes(HttpServletRequest request) { - String acceptHeader = request.getHeader("Accept"); - if (StringUtils.hasLength(acceptHeader)) { - return MediaType.parseMediaTypes(acceptHeader); - } - else { - return Collections.singletonList(MediaType.ALL); - } + private List getAcceptedMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { + List mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); + return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes; } private int indexOfEqualMediaType(MediaType mediaType) { @@ -238,8 +259,9 @@ public final class ProducesRequestCondition extends AbstractRequestCondition getExpressionsToCompare() { - return this.expressions.isEmpty() ? DEFAULT_EXPRESSION_LIST : this.expressions; + return this.expressions.isEmpty() ? MEDIA_TYPE_ALL_LIST : this.expressions; } - private static final List DEFAULT_EXPRESSION_LIST = - Arrays.asList(new ProduceMediaTypeExpression("*/*")); + private final List MEDIA_TYPE_ALL_LIST = + Collections.singletonList(new ProduceMediaTypeExpression("*/*")); + - /** - * Parses and matches a single media type expression to a request's 'Accept' header. + * Parses and matches a single media type expression to a request's 'Accept' header. */ - static class ProduceMediaTypeExpression extends AbstractMediaTypeExpression { - + class ProduceMediaTypeExpression extends AbstractMediaTypeExpression { + ProduceMediaTypeExpression(MediaType mediaType, boolean negated) { super(mediaType, negated); } @@ -279,7 +301,7 @@ public final class ProducesRequestCondition extends AbstractRequestCondition acceptedMediaTypes = getAcceptedMediaTypes(request); for (MediaType acceptedMediaType : acceptedMediaTypes) { if (getMediaType().isCompatibleWith(acceptedMediaType)) { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java index d1aa721bbf1..8634ead5ecc 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.java @@ -27,7 +27,6 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.core.MethodParameter; -import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; @@ -35,7 +34,9 @@ import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.util.CollectionUtils; import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.support.HandlerMethodReturnValueHandler; import org.springframework.web.servlet.HandlerMapping; @@ -52,8 +53,17 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe private static final MediaType MEDIA_TYPE_APPLICATION = new MediaType("application"); + private final ContentNegotiationManager contentNegotiationManager; + protected AbstractMessageConverterMethodProcessor(List> messageConverters) { + this(messageConverters, null); + } + + protected AbstractMessageConverterMethodProcessor(List> messageConverters, + ContentNegotiationManager manager) { + super(messageConverters); + this.contentNegotiationManager = (manager != null) ? manager : new ContentNegotiationManager(); } /** @@ -100,14 +110,15 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe Class returnValueClass = returnValue.getClass(); - List acceptableMediaTypes = getAcceptableMediaTypes(inputMessage); - List producibleMediaTypes = getProducibleMediaTypes(inputMessage.getServletRequest(), returnValueClass); + HttpServletRequest servletRequest = inputMessage.getServletRequest(); + List requestedMediaTypes = getAcceptableMediaTypes(servletRequest); + List producibleMediaTypes = getProducibleMediaTypes(servletRequest, returnValueClass); Set compatibleMediaTypes = new LinkedHashSet(); - for (MediaType a : acceptableMediaTypes) { + for (MediaType r : requestedMediaTypes) { for (MediaType p : producibleMediaTypes) { - if (a.isCompatibleWith(p)) { - compatibleMediaTypes.add(getMostSpecificMediaType(a, p)); + if (r.isCompatibleWith(p)) { + compatibleMediaTypes.add(getMostSpecificMediaType(r, p)); } } } @@ -175,17 +186,9 @@ public abstract class AbstractMessageConverterMethodProcessor extends AbstractMe } } - private List getAcceptableMediaTypes(HttpInputMessage inputMessage) { - try { - List result = inputMessage.getHeaders().getAccept(); - return result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result; - } - catch (IllegalArgumentException ex) { - if (logger.isDebugEnabled()) { - logger.debug("Could not parse Accept header: " + ex.getMessage()); - } - return Collections.emptyList(); - } + private List getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { + List mediaTypes = this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); + return mediaTypes.isEmpty() ? Collections.singletonList(MediaType.ALL) : mediaTypes; } /** diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java index 80b26b3794f..c105ba1d820 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/ExceptionHandlerExceptionResolver.java @@ -32,6 +32,7 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; import org.springframework.http.converter.xml.XmlAwareFormHttpMessageConverter; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.method.HandlerMethod; @@ -69,6 +70,8 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce private List> messageConverters; + private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); + private final Map, ExceptionHandlerMethodResolver> exceptionHandlerMethodResolvers = new ConcurrentHashMap, ExceptionHandlerMethodResolver>(); @@ -182,6 +185,14 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce return messageConverters; } + /** + * Set the {@link ContentNegotiationManager} to use to determine requested media types. + * If not set, the default constructor is used. + */ + public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { + this.contentNegotiationManager = contentNegotiationManager; + } + public void afterPropertiesSet() { if (this.argumentResolvers == null) { List resolvers = getDefaultArgumentResolvers(); @@ -223,11 +234,11 @@ public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExce handlers.add(new ModelAndViewMethodReturnValueHandler()); handlers.add(new ModelMethodProcessor()); handlers.add(new ViewMethodReturnValueHandler()); - handlers.add(new HttpEntityMethodProcessor(getMessageConverters())); + handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager)); // Annotation-based return value types handlers.add(new ModelAttributeMethodProcessor(false)); - handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters())); + handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager)); // Multi-purpose return value types handlers.add(new ViewNameMethodReturnValueHandler()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java index 04601a4d473..2be7e00b80d 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/HttpEntityMethodProcessor.java @@ -33,6 +33,7 @@ import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.http.server.ServletServerHttpResponse; import org.springframework.util.Assert; import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.ModelAndViewContainer; @@ -56,6 +57,12 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro super(messageConverters); } + public HttpEntityMethodProcessor(List> messageConverters, + ContentNegotiationManager contentNegotiationManager) { + + super(messageConverters, contentNegotiationManager); + } + public boolean supportsParameter(MethodParameter parameter) { Class parameterType = parameter.getParameterType(); return HttpEntity.class.equals(parameterType); @@ -123,7 +130,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro if (!entityHeaders.isEmpty()) { outputMessage.getHeaders().putAll(entityHeaders); } - + Object body = responseEntity.getBody(); if (body != null) { writeWithMessageConverters(body, returnType, inputMessage, outputMessage); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java index 683f0304810..f582ae07ce5 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerAdapter.java @@ -47,6 +47,7 @@ import org.springframework.ui.ModelMap; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; @@ -147,6 +148,8 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i private Long asyncRequestTimeout; + private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); + /** * Default constructor. */ @@ -410,6 +413,14 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i this.asyncRequestTimeout = asyncRequestTimeout; } + /** + * Set the {@link ContentNegotiationManager} to use to determine requested media types. + * If not set, the default constructor is used. + */ + public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { + this.contentNegotiationManager = contentNegotiationManager; + } + /** * {@inheritDoc} *

    A {@link ConfigurableBeanFactory} is expected for resolving @@ -525,12 +536,12 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter i handlers.add(new ModelAndViewMethodReturnValueHandler()); handlers.add(new ModelMethodProcessor()); handlers.add(new ViewMethodReturnValueHandler()); - handlers.add(new HttpEntityMethodProcessor(getMessageConverters())); + handlers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.contentNegotiationManager)); handlers.add(new AsyncMethodReturnValueHandler()); // Annotation-based return value types handlers.add(new ModelAttributeMethodProcessor(false)); - handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters())); + handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.contentNegotiationManager)); // Multi-purpose return value types handlers.add(new ViewNameMethodReturnValueHandler()); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java index 263b988762b..211644612b3 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestMappingHandlerMapping.java @@ -20,6 +20,8 @@ import java.lang.reflect.Method; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.stereotype.Controller; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.servlet.mvc.condition.AbstractRequestCondition; import org.springframework.web.servlet.mvc.condition.CompositeRequestCondition; @@ -48,6 +50,8 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi private boolean useTrailingSlashMatch = true; + private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager(); + /** * Whether to use suffix pattern match (".*") when matching patterns to * requests. If enabled a method mapped to "/users" also matches to "/users.*". @@ -66,6 +70,14 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi this.useTrailingSlashMatch = useTrailingSlashMatch; } + /** + * Set the {@link ContentNegotiationManager} to use to determine requested media types. + * If not set, the default constructor is used. + */ + public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { + this.contentNegotiationManager = contentNegotiationManager; + } + /** * Whether to use suffix pattern matching. */ @@ -79,6 +91,13 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi return this.useTrailingSlashMatch; } + /** + * Return the configured {@link ContentNegotiationManager}. + */ + public ContentNegotiationManager getContentNegotiationManager() { + return contentNegotiationManager; + } + /** * {@inheritDoc} * Expects a handler to have a type-level @{@link Controller} annotation. @@ -160,7 +179,7 @@ public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMappi new ParamsRequestCondition(annotation.params()), new HeadersRequestCondition(annotation.headers()), new ConsumesRequestCondition(annotation.consumes(), annotation.headers()), - new ProducesRequestCondition(annotation.produces(), annotation.headers()), + new ProducesRequestCondition(annotation.produces(), annotation.headers(), getContentNegotiationManager()), customCondition); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java index 722e85b6bca..bfaf4aa86a8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/RequestResponseBodyMethodProcessor.java @@ -29,6 +29,7 @@ import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindingResult; import org.springframework.web.HttpMediaTypeNotAcceptableException; import org.springframework.web.HttpMediaTypeNotSupportedException; +import org.springframework.web.accept.ContentNegotiationManager; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.RequestBody; @@ -60,6 +61,12 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter super(messageConverters); } + public RequestResponseBodyMethodProcessor(List> messageConverters, + ContentNegotiationManager contentNegotiationManager) { + + super(messageConverters, contentNegotiationManager); + } + public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(RequestBody.class); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java index 588489d4716..7d652aae997 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java @@ -16,48 +16,47 @@ package org.springframework.web.servlet.view; -import java.io.IOException; -import java.io.InputStream; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Map.Entry; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; + import javax.activation.FileTypeMap; -import javax.activation.MimetypesFileTypeMap; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.InitializingBean; import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; -import org.springframework.core.io.ClassPathResource; -import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; +import org.springframework.web.HttpMediaTypeNotAcceptableException; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.accept.FixedContentNegotiationStrategy; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; +import org.springframework.web.accept.ParameterContentNegotiationStrategy; +import org.springframework.web.accept.ContentNegotiationStrategy; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.context.support.WebApplicationObjectSupport; import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.SmartView; import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; -import org.springframework.web.util.UrlPathHelper; -import org.springframework.web.util.WebUtils; /** * Implementation of {@link ViewResolver} that resolves a view based on the request file name or {@code Accept} header. @@ -69,23 +68,8 @@ import org.springframework.web.util.WebUtils; * property needs to be set to a higher precedence than the others (the default is {@link Ordered#HIGHEST_PRECEDENCE}.) * *

    This view resolver uses the requested {@linkplain MediaType media type} to select a suitable {@link View} for a - * request. This media type is determined by using the following criteria: - *

      - *
    1. If the requested path has a file extension and if the {@link #setFavorPathExtension} property is - * {@code true}, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching media type.
    2. - *
    3. If the request contains a parameter defining the extension and if the {@link #setFavorParameter} - * property is true, the {@link #setMediaTypes(Map) mediaTypes} property is inspected for a matching - * media type. The default name of the parameter is format and it can be configured using the - * {@link #setParameterName(String) parameterName} property.
    4. - *
    5. If there is no match in the {@link #setMediaTypes(Map) mediaTypes} property and if the Java Activation - * Framework (JAF) is both {@linkplain #setUseJaf enabled} and present on the classpath, - * {@link FileTypeMap#getContentType(String)} is used instead.
    6. - *
    7. If the previous steps did not result in a media type, and - * {@link #setIgnoreAcceptHeader ignoreAcceptHeader} is {@code false}, the request {@code Accept} header is - * used.
    8. - *
    - * - *

    Once the requested media type has been determined, this resolver queries each delegate view resolver for a + * request. The requested media type is determined through the configured {@link ContentNegotiationManager}. + * Once the requested media type has been determined, this resolver queries each delegate view resolver for a * {@link View} and determines if the requested media type is {@linkplain MediaType#includes(MediaType) compatible} * with the view's {@linkplain View#getContentType() content type}). The most compatible view is returned. * @@ -107,44 +91,33 @@ import org.springframework.web.util.WebUtils; * @see InternalResourceViewResolver * @see BeanNameViewResolver */ -public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered { +public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean { private static final Log logger = LogFactory.getLog(ContentNegotiatingViewResolver.class); - private static final String ACCEPT_HEADER = "Accept"; - - private static final boolean jafPresent = - ClassUtils.isPresent("javax.activation.FileTypeMap", ContentNegotiatingViewResolver.class.getClassLoader()); - - private static final UrlPathHelper urlPathHelper = new UrlPathHelper(); - - static { - urlPathHelper.setUrlDecode(false); - } - private int order = Ordered.HIGHEST_PRECEDENCE; + private ContentNegotiationManager contentNegotiationManager; + private boolean favorPathExtension = true; - private boolean favorParameter = false; - - private String parameterName = "format"; + private boolean ignoreAcceptHeader = false; + private Map mediaTypes = new HashMap(); + private Boolean useJaf; + private String parameterName; + private MediaType defaultContentType; private boolean useNotAcceptableStatusCode = false; - private boolean ignoreAcceptHeader = false; - - private boolean useJaf = jafPresent; - - private ConcurrentMap mediaTypes = new ConcurrentHashMap(); - private List defaultViews; - private MediaType defaultContentType; - private List viewResolvers; + public ContentNegotiatingViewResolver() { + super(); + } + public void setOrder(int order) { this.order = order; } @@ -153,23 +126,45 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport return this.order; } + /** + * Set the {@link ContentNegotiationManager} to use to determine requested media types. + * If not set, the default constructor is used. + */ + public void setContentNegotiationManager(ContentNegotiationManager contentNegotiationManager) { + this.contentNegotiationManager = contentNegotiationManager; + } + /** * Indicate whether the extension of the request path should be used to determine the requested media type, * in favor of looking at the {@code Accept} header. The default value is {@code true}. *

    For instance, when this flag is true (the default), a request for {@code /hotels.pdf} * will result in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the * browser-defined {@code text/html,application/xhtml+xml}. + * + * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} */ public void setFavorPathExtension(boolean favorPathExtension) { this.favorPathExtension = favorPathExtension; } + /** + * Indicate whether to use the Java Activation Framework to map from file extensions to media types. + *

    Default is {@code true}, i.e. the Java Activation Framework is used (if available). + * + * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} + */ + public void setUseJaf(boolean useJaf) { + this.useJaf = useJaf; + } + /** * Indicate whether a request parameter should be used to determine the requested media type, * in favor of looking at the {@code Accept} header. The default value is {@code false}. *

    For instance, when this flag is true, a request for {@code /hotels?format=pdf} will result * in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined * {@code text/html,application/xhtml+xml}. + * + * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} */ public void setFavorParameter(boolean favorParameter) { this.favorParameter = favorParameter; @@ -178,6 +173,8 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport /** * Set the parameter name that can be used to determine the requested media type if the {@link * #setFavorParameter} property is {@code true}. The default parameter name is {@code format}. + * + * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} */ public void setParameterName(String parameterName) { this.parameterName = parameterName; @@ -188,11 +185,35 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport *

    If set to {@code true}, this view resolver will only refer to the file extension and/or * parameter, as indicated by the {@link #setFavorPathExtension favorPathExtension} and * {@link #setFavorParameter favorParameter} properties. + * + * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} */ public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) { this.ignoreAcceptHeader = ignoreAcceptHeader; } + /** + * Set the mapping from file extensions to media types. + *

    When this mapping is not set or when an extension is not present, this view resolver + * will fall back to using a {@link FileTypeMap} when the Java Action Framework is available. + * + * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} + */ + public void setMediaTypes(Map mediaTypes) { + this.mediaTypes = mediaTypes; + } + + /** + * Set the default content type. + *

    This content type will be used when file extension, parameter, nor {@code Accept} + * header define a content-type, either through being disabled or empty. + * + * @deprecated use {@link #setContentNegotiationManager(ContentNegotiationManager)} + */ + public void setDefaultContentType(MediaType defaultContentType) { + this.defaultContentType = defaultContentType; + } + /** * Indicate whether a {@link HttpServletResponse#SC_NOT_ACCEPTABLE 406 Not Acceptable} * status code should be returned if no suitable view can be found. @@ -206,20 +227,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport this.useNotAcceptableStatusCode = useNotAcceptableStatusCode; } - /** - * Set the mapping from file extensions to media types. - *

    When this mapping is not set or when an extension is not present, this view resolver - * will fall back to using a {@link FileTypeMap} when the Java Action Framework is available. - */ - public void setMediaTypes(Map mediaTypes) { - Assert.notNull(mediaTypes, "'mediaTypes' must not be null"); - for (Map.Entry entry : mediaTypes.entrySet()) { - String extension = entry.getKey().toLowerCase(Locale.ENGLISH); - MediaType mediaType = MediaType.parseMediaType(entry.getValue()); - this.mediaTypes.put(extension, mediaType); - } - } - /** * Set the default views to use when a more specific view can not be obtained * from the {@link ViewResolver} chain. @@ -228,23 +235,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport this.defaultViews = defaultViews; } - /** - * Set the default content type. - *

    This content type will be used when file extension, parameter, nor {@code Accept} - * header define a content-type, either through being disabled or empty. - */ - public void setDefaultContentType(MediaType defaultContentType) { - this.defaultContentType = defaultContentType; - } - - /** - * Indicate whether to use the Java Activation Framework to map from file extensions to media types. - *

    Default is {@code true}, i.e. the Java Activation Framework is used (if available). - */ - public void setUseJaf(boolean useJaf) { - this.useJaf = useJaf; - } - /** * Sets the view resolvers to be wrapped by this view resolver. *

    If this property is not set, view resolvers will be detected automatically. @@ -283,6 +273,32 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport OrderComparator.sort(this.viewResolvers); } + public void afterPropertiesSet() throws Exception { + if (this.contentNegotiationManager == null) { + List strategies = new ArrayList(); + if (this.favorPathExtension) { + PathExtensionContentNegotiationStrategy strategy = new PathExtensionContentNegotiationStrategy(this.mediaTypes); + if (this.useJaf != null) { + strategy.setUseJaf(this.useJaf); + } + strategies.add(strategy); + } + if (this.favorParameter) { + ParameterContentNegotiationStrategy strategy = new ParameterContentNegotiationStrategy(this.mediaTypes); + strategy.setParameterName(this.parameterName); + strategies.add(strategy); + } + if (!this.ignoreAcceptHeader) { + strategies.add(new HeaderContentNegotiationStrategy()); + } + if (this.defaultContentType != null) { + strategies.add(new FixedContentNegotiationStrategy(this.defaultContentType)); + } + ContentNegotiationStrategy[] array = strategies.toArray(new ContentNegotiationStrategy[strategies.size()]); + this.contentNegotiationManager = new ContentNegotiationManager(array); + } + } + public View resolveViewName(String viewName, Locale locale) throws Exception { RequestAttributes attrs = RequestContextHolder.getRequestAttributes(); Assert.isInstanceOf(ServletRequestAttributes.class, attrs); @@ -317,69 +333,28 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport * @return the list of media types requested, if any */ protected List getMediaTypes(HttpServletRequest request) { - if (this.favorPathExtension) { - String requestUri = urlPathHelper.getLookupPathForRequest(request); - String filename = WebUtils.extractFullFilenameFromUrlPath(requestUri); - MediaType mediaType = getMediaTypeFromFilename(filename); - if (mediaType != null) { - if (logger.isDebugEnabled()) { - logger.debug("Requested media type is '" + mediaType + "' (based on filename '" + filename + "')"); - } - return Collections.singletonList(mediaType); - } - } - if (this.favorParameter) { - if (request.getParameter(this.parameterName) != null) { - String parameterValue = request.getParameter(this.parameterName); - MediaType mediaType = getMediaTypeFromParameter(parameterValue); - if (mediaType != null) { - if (logger.isDebugEnabled()) { - logger.debug("Requested media type is '" + mediaType + "' (based on parameter '" + - this.parameterName + "'='" + parameterValue + "')"); + try { + ServletWebRequest webRequest = new ServletWebRequest(request); + List acceptableMediaTypes = this.contentNegotiationManager.resolveMediaTypes(webRequest); + List producibleMediaTypes = getProducibleMediaTypes(request); + Set compatibleMediaTypes = new LinkedHashSet(); + for (MediaType acceptable : acceptableMediaTypes) { + for (MediaType producible : producibleMediaTypes) { + if (acceptable.isCompatibleWith(producible)) { + compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible)); } - return Collections.singletonList(mediaType); } } - } - if (!this.ignoreAcceptHeader) { - String acceptHeader = request.getHeader(ACCEPT_HEADER); - if (StringUtils.hasText(acceptHeader)) { - try { - List acceptableMediaTypes = MediaType.parseMediaTypes(acceptHeader); - List producibleMediaTypes = getProducibleMediaTypes(request); - Set compatibleMediaTypes = new LinkedHashSet(); - for (MediaType acceptable : acceptableMediaTypes) { - for (MediaType producible : producibleMediaTypes) { - if (acceptable.isCompatibleWith(producible)) { - compatibleMediaTypes.add(getMostSpecificMediaType(acceptable, producible)); - } - } - } - List selectedMediaTypes = new ArrayList(compatibleMediaTypes); - MediaType.sortBySpecificityAndQuality(selectedMediaTypes); - if (logger.isDebugEnabled()) { - logger.debug("Requested media types are " + selectedMediaTypes + " based on Accept header types " + - "and producible media types " + producibleMediaTypes + ")"); - } - return selectedMediaTypes; - } - catch (IllegalArgumentException ex) { - if (logger.isDebugEnabled()) { - logger.debug("Could not parse accept header [" + acceptHeader + "]: " + ex.getMessage()); - } - return null; - } - } - } - if (this.defaultContentType != null) { + List selectedMediaTypes = new ArrayList(compatibleMediaTypes); + MediaType.sortBySpecificityAndQuality(selectedMediaTypes); if (logger.isDebugEnabled()) { - logger.debug("Requested media types is " + this.defaultContentType + - " (based on defaultContentType property)"); + logger.debug("Requested media types are " + selectedMediaTypes + " based on Accept header types " + + "and producible media types " + producibleMediaTypes + ")"); } - return Collections.singletonList(this.defaultContentType); + return selectedMediaTypes; } - else { - return Collections.emptyList(); + catch (HttpMediaTypeNotAcceptableException ex) { + return null; } } @@ -404,52 +379,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport return MediaType.SPECIFICITY_COMPARATOR.compare(acceptType, produceType) < 0 ? acceptType : produceType; } - /** - * Determines the {@link MediaType} for the given filename. - *

    The default implementation will check the {@linkplain #setMediaTypes(Map) media types} - * property first for a defined mapping. If not present, and if the Java Activation Framework - * can be found on the classpath, it will call {@link FileTypeMap#getContentType(String)} - *

    This method can be overridden to provide a different algorithm. - * @param filename the current request file name (i.e. {@code hotels.html}) - * @return the media type, if any - */ - protected MediaType getMediaTypeFromFilename(String filename) { - String extension = StringUtils.getFilenameExtension(filename); - if (!StringUtils.hasText(extension)) { - return null; - } - extension = extension.toLowerCase(Locale.ENGLISH); - MediaType mediaType = this.mediaTypes.get(extension); - if (mediaType == null) { - String mimeType = getServletContext().getMimeType(filename); - if (StringUtils.hasText(mimeType)) { - mediaType = MediaType.parseMediaType(mimeType); - } - if (this.useJaf && (mediaType == null || MediaType.APPLICATION_OCTET_STREAM.equals(mediaType))) { - MediaType jafMediaType = ActivationMediaTypeFactory.getMediaType(filename); - if (jafMediaType != null && !MediaType.APPLICATION_OCTET_STREAM.equals(jafMediaType)) { - mediaType = jafMediaType; - } - } - if (mediaType != null) { - this.mediaTypes.putIfAbsent(extension, mediaType); - } - } - return mediaType; - } - - /** - * Determines the {@link MediaType} for the given parameter value. - *

    The default implementation will check the {@linkplain #setMediaTypes(Map) media types} - * property for a defined mapping. - *

    This method can be overriden to provide a different algorithm. - * @param parameterValue the parameter value (i.e. {@code pdf}). - * @return the media type, if any - */ - protected MediaType getMediaTypeFromParameter(String parameterValue) { - return this.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH)); - } - private List getCandidateViews(String viewName, Locale locale, List requestedMediaTypes) throws Exception { @@ -460,7 +389,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport candidateViews.add(view); } for (MediaType requestedMediaType : requestedMediaTypes) { - List extensions = getExtensionsForMediaType(requestedMediaType); + List extensions = this.contentNegotiationManager.resolveExtensions(requestedMediaType); for (String extension : extensions) { String viewNameWithExtension = viewName + "." + extension; view = viewResolver.resolveViewName(viewNameWithExtension, locale); @@ -468,7 +397,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport candidateViews.add(view); } } - } } if (!CollectionUtils.isEmpty(this.defaultViews)) { @@ -477,16 +405,6 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport return candidateViews; } - private List getExtensionsForMediaType(MediaType requestedMediaType) { - List result = new ArrayList(); - for (Entry entry : this.mediaTypes.entrySet()) { - if (requestedMediaType.includes(entry.getValue())) { - result.add(entry.getKey()); - } - } - return result; - } - private View getBestView(List candidateViews, List requestedMediaTypes) { for (View candidateView : candidateViews) { if (candidateView instanceof SmartView) { @@ -528,54 +446,4 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport } }; - - /** - * Inner class to avoid hard-coded JAF dependency. - */ - private static class ActivationMediaTypeFactory { - - private static final FileTypeMap fileTypeMap; - - static { - fileTypeMap = loadFileTypeMapFromContextSupportModule(); - } - - private static FileTypeMap loadFileTypeMapFromContextSupportModule() { - // see if we can find the extended mime.types from the context-support module - Resource mappingLocation = new ClassPathResource("org/springframework/mail/javamail/mime.types"); - if (mappingLocation.exists()) { - if (logger.isTraceEnabled()) { - logger.trace("Loading Java Activation Framework FileTypeMap from " + mappingLocation); - } - InputStream inputStream = null; - try { - inputStream = mappingLocation.getInputStream(); - return new MimetypesFileTypeMap(inputStream); - } - catch (IOException ex) { - // ignore - } - finally { - if (inputStream != null) { - try { - inputStream.close(); - } - catch (IOException ex) { - // ignore - } - } - } - } - if (logger.isTraceEnabled()) { - logger.trace("Loading default Java Activation Framework FileTypeMap"); - } - return FileTypeMap.getDefaultFileTypeMap(); - } - - public static MediaType getMediaType(String filename) { - String mediaType = fileTypeMap.getContentType(filename); - return (StringUtils.hasText(mediaType) ? MediaType.parseMediaType(mediaType) : null); - } - } - } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ParamsRequestConditionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ParamsRequestConditionTests.java index 8f2035e592b..9d4c81140cf 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ParamsRequestConditionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ParamsRequestConditionTests.java @@ -44,7 +44,7 @@ public class ParamsRequestConditionTests { new ParamsRequestCondition("foo=bar").equals(new ParamsRequestCondition("FOO=bar"))); } - @Test + @Test public void paramPresent() { ParamsRequestCondition condition = new ParamsRequestCondition("foo"); @@ -96,7 +96,7 @@ public class ParamsRequestConditionTests { @Test public void compareTo() { MockHttpServletRequest request = new MockHttpServletRequest(); - + ParamsRequestCondition condition1 = new ParamsRequestCondition("foo", "bar", "baz"); ParamsRequestCondition condition2 = new ParamsRequestCondition("foo", "bar"); diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java index 437abeb0fd1..e32b5fcd6ce 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/condition/ProducesRequestConditionTests.java @@ -36,7 +36,7 @@ import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition.Pr public class ProducesRequestConditionTests { @Test - public void producesMatch() { + public void match() { ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -46,7 +46,7 @@ public class ProducesRequestConditionTests { } @Test - public void negatedProducesMatch() { + public void matchNegated() { ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -56,13 +56,13 @@ public class ProducesRequestConditionTests { } @Test - public void getProducibleMediaTypesNegatedExpression() { + public void getProducibleMediaTypes() { ProducesRequestCondition condition = new ProducesRequestCondition("!application/xml"); assertEquals(Collections.emptySet(), condition.getProducibleMediaTypes()); } @Test - public void producesWildcardMatch() { + public void matchWildcard() { ProducesRequestCondition condition = new ProducesRequestCondition("text/*"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -72,7 +72,7 @@ public class ProducesRequestConditionTests { } @Test - public void producesMultipleMatch() { + public void matchMultiple() { ProducesRequestCondition condition = new ProducesRequestCondition("text/plain", "application/xml"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -82,7 +82,7 @@ public class ProducesRequestConditionTests { } @Test - public void producesSingleNoMatch() { + public void matchSingle() { ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -92,7 +92,7 @@ public class ProducesRequestConditionTests { } @Test - public void producesParseError() { + public void matchParseError() { ProducesRequestCondition condition = new ProducesRequestCondition("text/plain"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -102,7 +102,7 @@ public class ProducesRequestConditionTests { } @Test - public void producesParseErrorWithNegation() { + public void matchParseErrorWithNegation() { ProducesRequestCondition condition = new ProducesRequestCondition("!text/plain"); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -111,6 +111,15 @@ public class ProducesRequestConditionTests { assertNull(condition.getMatchingCondition(request)); } + @Test + public void matchByRequestParameter() { + ProducesRequestCondition condition = new ProducesRequestCondition(new String[] {"text/plain"}, new String[] {}); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/foo.txt"); + + assertNotNull(condition.getMatchingCondition(request)); + } + @Test public void compareTo() { ProducesRequestCondition html = new ProducesRequestCondition("text/html"); @@ -286,7 +295,7 @@ public class ProducesRequestConditionTests { } @Test - public void parseProducesAndHeaders() { + public void instantiateWithProducesAndHeaderConditions() { String[] produces = new String[] {"text/plain"}; String[] headers = new String[]{"foo=bar", "accept=application/xml,application/pdf"}; ProducesRequestCondition condition = new ProducesRequestCondition(produces, headers); @@ -312,7 +321,7 @@ public class ProducesRequestConditionTests { private void assertConditions(ProducesRequestCondition condition, String... expected) { Collection expressions = condition.getContent(); - assertEquals("Invalid amount of conditions", expressions.size(), expected.length); + assertEquals("Invalid number of conditions", expressions.size(), expected.length); for (String s : expected) { boolean found = false; for (ProduceMediaTypeExpression expr : expressions) { diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java index 5d20f59e79c..6f5ccf64961 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java @@ -24,12 +24,10 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; -import static org.junit.Assert.assertTrue; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -42,6 +40,12 @@ import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; +import org.springframework.web.accept.ContentNegotiationManager; +import org.springframework.web.accept.FixedContentNegotiationStrategy; +import org.springframework.web.accept.HeaderContentNegotiationStrategy; +import org.springframework.web.accept.MappingMediaTypeExtensionsResolver; +import org.springframework.web.accept.ParameterContentNegotiationStrategy; +import org.springframework.web.accept.PathExtensionContentNegotiationStrategy; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.support.StaticWebApplicationContext; @@ -75,104 +79,22 @@ public class ContentNegotiatingViewResolverTests { } @Test - public void getMediaTypeFromFilenameMediaTypes() { - viewResolver.setMediaTypes(Collections.singletonMap("HTML", "application/xhtml+xml")); - assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"), - viewResolver.getMediaTypeFromFilename("test.html")); - } - - @Test - public void getMediaTypeFromFilenameJaf() { - assertEquals("Invalid content type", new MediaType("application", "vnd.ms-excel"), - viewResolver.getMediaTypeFromFilename("test.xls")); - } - - @Test - public void getMediaTypeFromFilenameNoJaf() { - viewResolver.setUseJaf(false); - assertEquals("Invalid content type", MediaType.APPLICATION_OCTET_STREAM, - viewResolver.getMediaTypeFromFilename("test.xls")); - } - - @Test - public void getMediaTypeFilename() { - request.setRequestURI("/test.html?foo=bar"); - List result = viewResolver.getMediaTypes(request); - assertEquals("Invalid content type", Collections.singletonList(new MediaType("text", "html")), result); - viewResolver.setMediaTypes(Collections.singletonMap("html", "application/xhtml+xml")); - result = viewResolver.getMediaTypes(request); - assertEquals("Invalid content type", Collections.singletonList(new MediaType("application", "xhtml+xml")), - result); - } - - // SPR-8678 - - @Test - public void getMediaTypeFilenameWithContextPath() { - request.setContextPath("/project-1.0.0.M3"); - request.setRequestURI("/project-1.0.0.M3/"); - assertTrue("Context path should be excluded", viewResolver.getMediaTypes(request).isEmpty()); - request.setRequestURI("/project-1.0.0.M3"); - assertTrue("Context path should be excluded", viewResolver.getMediaTypes(request).isEmpty()); - } - - // SPR-9390 - - @Test - public void getMediaTypeFilenameWithEncodedURI() { - request.setRequestURI("/quo%20vadis%3f.html"); - List result = viewResolver.getMediaTypes(request); - assertEquals("Invalid content type", Collections.singletonList(new MediaType("text", "html")), result); - } - - @Test - public void getMediaTypeParameter() { - viewResolver.setFavorParameter(true); - viewResolver.setMediaTypes(Collections.singletonMap("html", "application/xhtml+xml")); - request.addParameter("format", "html"); - List result = viewResolver.getMediaTypes(request); - assertEquals("Invalid content type", Collections.singletonList(new MediaType("application", "xhtml+xml")), - result); - } - - @Test - public void getMediaTypeAcceptHeader() { - request.addHeader("Accept", "text/html,application/xml;q=0.9,application/xhtml+xml,*/*;q=0.8"); - List result = viewResolver.getMediaTypes(request); - assertEquals("Invalid amount of media types", 4, result.size()); - assertEquals("Invalid content type", new MediaType("text", "html"), result.get(0)); - assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"), result.get(1)); - assertEquals("Invalid content type", new MediaType("application", "xml", Collections.singletonMap("q", "0.9")), - result.get(2)); - assertEquals("Invalid content type", new MediaType("*", "*", Collections.singletonMap("q", "0.8")), - result.get(3)); - } - - @Test - public void getMediaTypeAcceptHeaderWithProduces() { + public void getMediaTypeAcceptHeaderWithProduces() throws Exception { Set producibleTypes = Collections.singleton(MediaType.APPLICATION_XHTML_XML); request.setAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE, producibleTypes); request.addHeader("Accept", "text/html,application/xml;q=0.9,application/xhtml+xml,*/*;q=0.8"); + viewResolver.afterPropertiesSet(); List result = viewResolver.getMediaTypes(request); assertEquals("Invalid content type", new MediaType("application", "xhtml+xml"), result.get(0)); } - @Test - public void getDefaultContentType() { - request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - viewResolver.setIgnoreAcceptHeader(true); - viewResolver.setDefaultContentType(new MediaType("application", "pdf")); - List result = viewResolver.getMediaTypes(request); - assertEquals("Invalid amount of media types", 1, result.size()); - assertEquals("Invalid content type", new MediaType("application", "pdf"), result.get(0)); - } - @Test public void resolveViewNameWithPathExtension() throws Exception { request.setRequestURI("/test.xls"); ViewResolver viewResolverMock = createMock(ViewResolver.class); viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); + viewResolver.afterPropertiesSet(); View viewMock = createMock("application_xls", View.class); @@ -195,7 +117,11 @@ public class ContentNegotiatingViewResolverTests { public void resolveViewNameWithAcceptHeader() throws Exception { request.addHeader("Accept", "application/vnd.ms-excel"); - viewResolver.setMediaTypes(Collections.singletonMap("xls", "application/vnd.ms-excel")); + Map mapping = Collections.singletonMap("xls", "application/vnd.ms-excel"); + MappingMediaTypeExtensionsResolver extensionsResolver = new MappingMediaTypeExtensionsResolver(mapping); + ContentNegotiationManager manager = new ContentNegotiationManager(new HeaderContentNegotiationStrategy()); + manager.addExtensionsResolver(extensionsResolver); + viewResolver.setContentNegotiationManager(manager); ViewResolver viewResolverMock = createMock(ViewResolver.class); viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); @@ -220,6 +146,7 @@ public class ContentNegotiatingViewResolverTests { public void resolveViewNameWithInvalidAcceptHeader() throws Exception { request.addHeader("Accept", "application"); + viewResolver.afterPropertiesSet(); View result = viewResolver.resolveViewName("test", Locale.ENGLISH); assertNull(result); } @@ -228,12 +155,15 @@ public class ContentNegotiatingViewResolverTests { public void resolveViewNameWithRequestParameter() throws Exception { request.addParameter("format", "xls"); - viewResolver.setFavorParameter(true); - viewResolver.setMediaTypes(Collections.singletonMap("xls", "application/vnd.ms-excel")); + Map mapping = Collections.singletonMap("xls", "application/vnd.ms-excel"); + ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mapping); + viewResolver.setContentNegotiationManager(new ContentNegotiationManager(paramStrategy)); ViewResolver viewResolverMock = createMock(ViewResolver.class); viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); + viewResolver.afterPropertiesSet(); + View viewMock = createMock("application_xls", View.class); String viewName = "view"; @@ -255,13 +185,16 @@ public class ContentNegotiatingViewResolverTests { public void resolveViewNameWithDefaultContentType() throws Exception { request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); - viewResolver.setIgnoreAcceptHeader(true); - viewResolver.setDefaultContentType(new MediaType("application", "xml")); + MediaType mediaType = new MediaType("application", "xml"); + FixedContentNegotiationStrategy fixedStrategy = new FixedContentNegotiationStrategy(mediaType); + viewResolver.setContentNegotiationManager(new ContentNegotiationManager(fixedStrategy)); ViewResolver viewResolverMock1 = createMock("viewResolver1", ViewResolver.class); ViewResolver viewResolverMock2 = createMock("viewResolver2", ViewResolver.class); viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2)); + viewResolver.afterPropertiesSet(); + View viewMock1 = createMock("application_xml", View.class); View viewMock2 = createMock("text_html", View.class); @@ -289,6 +222,8 @@ public class ContentNegotiatingViewResolverTests { ViewResolver viewResolverMock2 = createMock(ViewResolver.class); viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2)); + viewResolver.afterPropertiesSet(); + View viewMock1 = createMock("application_xml", View.class); View viewMock2 = createMock("text_html", View.class); @@ -314,6 +249,8 @@ public class ContentNegotiatingViewResolverTests { public void resolveViewNameAcceptHeaderSortByQuality() throws Exception { request.addHeader("Accept", "text/plain;q=0.5, application/json"); + viewResolver.setContentNegotiationManager(new ContentNegotiationManager(new HeaderContentNegotiationStrategy())); + ViewResolver htmlViewResolver = createMock(ViewResolver.class); ViewResolver jsonViewResolver = createMock(ViewResolver.class); viewResolver.setViewResolvers(Arrays.asList(htmlViewResolver, jsonViewResolver)); @@ -330,7 +267,6 @@ public class ContentNegotiatingViewResolverTests { expect(jsonViewMock.getContentType()).andReturn("application/json").anyTimes(); replay(htmlViewResolver, jsonViewResolver, htmlView, jsonViewMock); - viewResolver.setFavorPathExtension(false); View result = viewResolver.resolveViewName(viewName, locale); assertSame("Invalid view", jsonViewMock, result); @@ -353,6 +289,8 @@ public class ContentNegotiatingViewResolverTests { defaultViews.add(viewMock3); viewResolver.setDefaultViews(defaultViews); + viewResolver.afterPropertiesSet(); + String viewName = "view"; Locale locale = Locale.ENGLISH; @@ -378,6 +316,8 @@ public class ContentNegotiatingViewResolverTests { ViewResolver viewResolverMock2 = createMock("viewResolver2", ViewResolver.class); viewResolver.setViewResolvers(Arrays.asList(viewResolverMock1, viewResolverMock2)); + viewResolver.afterPropertiesSet(); + View viewMock1 = createMock("application_xml", View.class); View viewMock2 = createMock("text_html", View.class); @@ -403,9 +343,10 @@ public class ContentNegotiatingViewResolverTests { public void resolveViewNameFilenameDefaultView() throws Exception { request.setRequestURI("/test.json"); - Map mediaTypes = new HashMap(); - mediaTypes.put("json", "application/json"); - viewResolver.setMediaTypes(mediaTypes); + + Map mapping = Collections.singletonMap("json", "application/json"); + PathExtensionContentNegotiationStrategy pathStrategy = new PathExtensionContentNegotiationStrategy(mapping); + viewResolver.setContentNegotiationManager(new ContentNegotiationManager(pathStrategy)); ViewResolver viewResolverMock1 = createMock(ViewResolver.class); ViewResolver viewResolverMock2 = createMock(ViewResolver.class); @@ -419,6 +360,8 @@ public class ContentNegotiatingViewResolverTests { defaultViews.add(viewMock3); viewResolver.setDefaultViews(defaultViews); + viewResolver.afterPropertiesSet(); + String viewName = "view"; Locale locale = Locale.ENGLISH; @@ -445,6 +388,8 @@ public class ContentNegotiatingViewResolverTests { ViewResolver viewResolverMock = createMock(ViewResolver.class); viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); + viewResolver.afterPropertiesSet(); + View viewMock = createMock("application_xml", View.class); String viewName = "view"; @@ -479,6 +424,8 @@ public class ContentNegotiatingViewResolverTests { View jsonView = createMock("application_json", View.class); viewResolver.setDefaultViews(Arrays.asList(jsonView)); + viewResolver.afterPropertiesSet(); + String viewName = "redirect:anotherTest"; Locale locale = Locale.ENGLISH; @@ -500,6 +447,8 @@ public class ContentNegotiatingViewResolverTests { ViewResolver viewResolverMock = createMock(ViewResolver.class); viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); + viewResolver.afterPropertiesSet(); + View viewMock = createMock("application_xml", View.class); String viewName = "view"; @@ -524,6 +473,8 @@ public class ContentNegotiatingViewResolverTests { ViewResolver viewResolverMock = createMock(ViewResolver.class); viewResolver.setViewResolvers(Collections.singletonList(viewResolverMock)); + viewResolver.afterPropertiesSet(); + View viewMock = createMock("application_xml", View.class); String viewName = "view"; @@ -553,7 +504,11 @@ public class ContentNegotiatingViewResolverTests { nestedResolver.setApplicationContext(webAppContext); nestedResolver.setViewClass(InternalResourceView.class); viewResolver.setViewResolvers(new ArrayList(Arrays.asList(nestedResolver))); - viewResolver.setDefaultContentType(MediaType.TEXT_HTML); + + FixedContentNegotiationStrategy fixedStrategy = new FixedContentNegotiationStrategy(MediaType.TEXT_HTML); + viewResolver.setContentNegotiationManager(new ContentNegotiationManager(fixedStrategy)); + + viewResolver.afterPropertiesSet(); String viewName = "view"; Locale locale = Locale.ENGLISH; diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index 7a9f321619d..9093f76eb74 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -10,7 +10,7 @@ Changes in version 3.2 M2 * raise RestClientException instead of IllegalArgumentException for unknown status codes * add JacksonObjectMapperFactoryBean for configuring a Jackson ObjectMapper in XML * infer return type of parameterized factory methods (SPR-9493) - +* add ContentNegotiationManager/ContentNegotiationStrategy to resolve requested media types Changes in version 3.2 M1 (2012-05-28) --------------------------------------