diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/View.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/View.java
index 7ca5be4519b53e4fc8de694788966fa6d328f8ec..07c5a6e17e3abfeecb47baaed449aa483fe4fc4b 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/View.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/View.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2008 the original author or authors.
+ * 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.
@@ -21,6 +21,8 @@ import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
+import org.springframework.http.MediaType;
+
/**
* MVC View for a web interaction. Implementations are responsible for rendering
* content, and exposing the model. A single view exposes multiple model attributes.
@@ -51,13 +53,20 @@ public interface View {
/**
* Name of the {@link HttpServletRequest} attribute that contains a Map with path variables.
- * The map consists of String-based URI template variable names as keys and their corresponding
- * Object-based values -- extracted from segments of the URL and type converted.
- *
+ * The map consists of String-based URI template variable names as keys and their corresponding
+ * Object-based values -- extracted from segments of the URL and type converted.
+ *
*
Note: This attribute is not required to be supported by all View implementations.
*/
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
+ /**
+ * The {@link MediaType} selected during content negotiation, which may be
+ * more specific than the one the View is configured with. For example:
+ * "application/vnd.example-v1+xml" vs "application/*+xml".
+ */
+ String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
+
/**
* Return the content type of the view, if predetermined.
*
Can be used to check the content type upfront,
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractView.java
index 5804d265bacda8b6ea3479f623a209e544453832..5df0792f249597e9c417e69aa22ad32d4e0202a4 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractView.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2009 the original author or authors.
+ * 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.
@@ -23,11 +23,13 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
+
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.servlet.View;
@@ -223,15 +225,15 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
}
/**
- * Whether to add path variables in the model or not.
- *
Path variables are commonly bound to URI template variables through the {@code @PathVariable}
- * annotation. They're are effectively URI template variables with type conversion applied to
- * them to derive typed Object values. Such values are frequently needed in views for
- * constructing links to the same and other URLs.
- *
Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
- * but not attributes already present in the model.
- *
By default this flag is set to {@code true}. Concrete view types can override this.
- * @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise.
+ * Whether to add path variables in the model or not.
+ *
Path variables are commonly bound to URI template variables through the {@code @PathVariable}
+ * annotation. They're are effectively URI template variables with type conversion applied to
+ * them to derive typed Object values. Such values are frequently needed in views for
+ * constructing links to the same and other URLs.
+ *
Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
+ * but not attributes already present in the model.
+ *
By default this flag is set to {@code true}. Concrete view types can override this.
+ * @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise.
*/
public void setExposePathVariables(boolean exposePathVariables) {
this.exposePathVariables = exposePathVariables;
@@ -255,7 +257,7 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
" and static attributes " + this.staticAttributes);
}
-
+
Map mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
@@ -263,7 +265,7 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
}
/**
- * Creates a combined output Map (never null
) that includes dynamic values and static attributes.
+ * Creates a combined output Map (never null
) that includes dynamic values and static attributes.
* Dynamic values take precedence over static attributes.
*/
protected Map createMergedOutputModel(Map model, HttpServletRequest request,
@@ -289,7 +291,7 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
if (this.requestContextAttribute != null) {
mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
}
-
+
return mergedModel;
}
@@ -408,6 +410,21 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
out.flush();
}
+ /**
+ * Set the content type of the response to the configured
+ * {@link #setContentType(String) content type} unless the
+ * {@link View#SELECTED_CONTENT_TYPE} request attribute is present and set
+ * to a concrete media type.
+ */
+ protected void setResponseContentType(HttpServletRequest request, HttpServletResponse response) {
+ MediaType mediaType = (MediaType) request.getAttribute(View.SELECTED_CONTENT_TYPE);
+ if (mediaType != null && mediaType.isConcrete()) {
+ response.setContentType(mediaType.toString());
+ }
+ else {
+ response.setContentType(getContentType());
+ }
+ }
@Override
public String toString() {
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 ed796ac8bc23bd91e556daaa28bfb174f0dac2e8..80be1d0fc119eb0624595acba03cb2c09e3b379a 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
@@ -278,7 +278,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
List requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
List candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
- View bestView = getBestView(candidateViews, requestedMediaTypes);
+ View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
@@ -378,7 +378,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
return candidateViews;
}
- private View getBestView(List candidateViews, List requestedMediaTypes) {
+ private View getBestView(List candidateViews, List requestedMediaTypes, RequestAttributes attrs) {
for (View candidateView : candidateViews) {
if (candidateView instanceof SmartView) {
SmartView smartView = (SmartView) candidateView;
@@ -394,11 +394,12 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
for (View candidateView : candidateViews) {
if (StringUtils.hasText(candidateView.getContentType())) {
MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
- if (mediaType.includes(candidateContentType)) {
+ if (mediaType.isCompatibleWith(candidateContentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Returning [" + candidateView + "] based on requested media type '"
+ mediaType + "'");
}
+ attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
return candidateView;
}
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/feed/AbstractFeedView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/feed/AbstractFeedView.java
index b2a1563570e720e852ea25f40cdc5d73e35ede23..656bd32bbe2a89743fe360c150440a7c8e4d614d 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/feed/AbstractFeedView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/feed/AbstractFeedView.java
@@ -54,7 +54,7 @@ public abstract class AbstractFeedView extends AbstractView
buildFeedMetadata(model, wireFeed, request);
buildFeedEntries(model, wireFeed, request, response);
- response.setContentType(getContentType());
+ setResponseContentType(request, response);
if (!StringUtils.hasText(wireFeed.getEncoding())) {
wireFeed.setEncoding("UTF-8");
}
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java
index 994213a33437d94548efa491ec4f91060d4e1ee1..7510ab304e69551c64b2b3c0b3c88f7dab27b66d 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java
@@ -214,7 +214,7 @@ public class MappingJackson2JsonView extends AbstractView {
@Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
- response.setContentType(getContentType());
+ setResponseContentType(request, response);
response.setCharacterEncoding(this.encoding.getJavaName());
if (this.disableCaching) {
response.addHeader("Pragma", "no-cache");
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java
index a1805e244ab964de3c44770cff89d5440d7f5043..5b97da49a82ae40a5a2b49966d31af2d711c6cbd 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java
@@ -217,7 +217,7 @@ public class MappingJacksonJsonView extends AbstractView {
@Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
- response.setContentType(getContentType());
+ setResponseContentType(request, response);
response.setCharacterEncoding(this.encoding.getJavaName());
if (this.disableCaching) {
response.addHeader("Pragma", "no-cache");
diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java
index fc2356aae367a4a381a21cc4eb00bb861e49c116..15b9b50aaea58dc83998ff49930b446e5d00ab91 100644
--- a/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java
+++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java
@@ -96,8 +96,8 @@ public class MarshallingView extends AbstractView {
}
@Override
- protected void renderMergedOutputModel(Map model,
- HttpServletRequest request,
+ protected void renderMergedOutputModel(Map model,
+ HttpServletRequest request,
HttpServletResponse response) throws Exception {
Object toBeMarshalled = locateToBeMarshalled(model);
if (toBeMarshalled == null) {
@@ -106,7 +106,7 @@ public class MarshallingView extends AbstractView {
ByteArrayOutputStream bos = new ByteArrayOutputStream(2048);
marshaller.marshal(toBeMarshalled, new StreamResult(bos));
- response.setContentType(getContentType());
+ setResponseContentType(request, response);
response.setContentLength(bos.size());
FileCopyUtils.copy(bos.toByteArray(), response.getOutputStream());
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 d94ebf8d217e36a226ff7f08067cac0ec1536f99..197536da32c8943d4418b1623cf74dcb29eadca7 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
@@ -273,6 +273,35 @@ public class ContentNegotiatingViewResolverTests {
verify(htmlViewResolver, jsonViewResolver, htmlView, jsonViewMock);
}
+ // SPR-9807
+
+ @Test
+ public void resolveViewNameAcceptHeaderWithSuffix() throws Exception {
+ request.addHeader("Accept", "application/vnd.example-v2+xml");
+
+ ViewResolver viewResolverMock = createMock(ViewResolver.class);
+ viewResolver.setViewResolvers(Arrays.asList(viewResolverMock));
+
+ viewResolver.afterPropertiesSet();
+
+ View viewMock = createMock("application_xml", View.class);
+
+ String viewName = "view";
+ Locale locale = Locale.ENGLISH;
+
+ expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock);
+ expect(viewMock.getContentType()).andReturn("application/*+xml").anyTimes();
+
+ replay(viewResolverMock, viewMock);
+
+ View result = viewResolver.resolveViewName(viewName, locale);
+
+ assertSame("Invalid view", viewMock, result);
+ assertEquals(new MediaType("application", "vnd.example-v2+xml"), request.getAttribute(View.SELECTED_CONTENT_TYPE));
+
+ verify(viewResolverMock, viewMock);
+ }
+
@Test
public void resolveViewNameAcceptHeaderDefaultView() throws Exception {
request.addHeader("Accept", "application/json");
diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java
index a52273e37032d36f94aac4592740349def9dde0b..9d1d8b6fd1133a4233445c15024ea020e3e1a76e 100644
--- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java
+++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java
@@ -35,10 +35,12 @@ import org.junit.Test;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ScriptableObject;
+import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
+import org.springframework.web.servlet.View;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
@@ -110,6 +112,22 @@ public class MappingJackson2JsonViewTests {
validateResult();
}
+ @Test
+ public void renderWithSelectedContentType() throws Exception {
+
+ Map model = new HashMap();
+ model.put("foo", "bar");
+
+ view.render(model, request, response);
+
+ assertEquals("application/json", response.getContentType());
+
+ request.setAttribute(View.SELECTED_CONTENT_TYPE, new MediaType("application", "vnd.example-v2+xml"));
+ view.render(model, request, response);
+
+ assertEquals("application/vnd.example-v2+xml", response.getContentType());
+ }
+
@Test
public void renderCaching() throws Exception {
view.setDisableCaching(false);
@@ -265,6 +283,7 @@ public class MappingJackson2JsonViewTests {
Object jsResult =
jsContext.evaluateString(jsScope, "(" + response.getContentAsString() + ")", "JSON Stream", 1, null);
assertNotNull("Json Result did not eval as valid JavaScript", jsResult);
+ assertEquals("application/json", response.getContentType());
}
diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt
index 34a7983717c299f40631cf2c7e6a16197b6ed6f1..2999318b4e24825f3be1ea47dc38671792f6e1dd 100644
--- a/src/dist/changelog.txt
+++ b/src/dist/changelog.txt
@@ -30,6 +30,8 @@ Changes in version 3.2 RC1 (2012-10-29)
* added ObjectToStringHttpMessageConverter that delegates to a ConversionService (SPR-9738)
* added Jackson2ObjectMapperBeanFactory (SPR-9739)
* added CallableProcessingInterceptor and DeferredResultProcessingInterceptor
+* added support for wildcard media types in AbstractView and ContentNegotiationViewResolver (SPR-9807)
+* the jackson message converters now include "application/*+json" in supported media types (SPR-7905)
Changes in version 3.2 M2 (2012-09-11)
--------------------------------------