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