From c7e7e80a3ae9f6b4ed7f2d99fe321c2e12550d7b Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 22 Oct 2012 17:18:14 -0400 Subject: [PATCH] Update AbstractView with method to set content type Before this change View implementations set the response content type to the fixed value they were configured with. This change makes it possible to configure a View implementation with a more general media type, e.g. "application/*+xml", and then set the response type to the more specific requested media type, e.g. "application/vnd.example-v1+xml". Issue: SPR-9807. --- .../org/springframework/web/servlet/View.java | 17 ++++++-- .../web/servlet/view/AbstractView.java | 43 +++++++++++++------ .../view/ContentNegotiatingViewResolver.java | 7 +-- .../servlet/view/feed/AbstractFeedView.java | 2 +- .../view/json/MappingJackson2JsonView.java | 2 +- .../view/json/MappingJacksonJsonView.java | 2 +- .../web/servlet/view/xml/MarshallingView.java | 6 +-- .../ContentNegotiatingViewResolverTests.java | 29 +++++++++++++ .../json/MappingJackson2JsonViewTests.java | 19 ++++++++ src/dist/changelog.txt | 2 + 10 files changed, 103 insertions(+), 26 deletions(-) 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 7ca5be4519..07c5a6e17e 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 5804d265ba..5df0792f24 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 ed796ac8bc..80be1d0fc1 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 b2a1563570..656bd32bbe 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 994213a334..7510ab304e 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 a1805e244a..5b97da49a8 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 fc2356aae3..15b9b50aae 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 d94ebf8d21..197536da32 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 a52273e370..9d1d8b6fd1 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 34a7983717..2999318b4e 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) --------------------------------------