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) --------------------------------------