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.
This commit is contained in:
Rossen Stoyanchev 2012-10-22 17:18:14 -04:00
parent 4f114a657f
commit c7e7e80a3a
10 changed files with 103 additions and 26 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.http.MediaType;
/** /**
* MVC View for a web interaction. Implementations are responsible for rendering * MVC View for a web interaction. Implementations are responsible for rendering
* content, and exposing the model. A single view exposes multiple model attributes. * 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. * 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 * 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. * Object-based values -- extracted from segments of the URL and type converted.
* *
* <p>Note: This attribute is not required to be supported by all View implementations. * <p>Note: This attribute is not required to be supported by all View implementations.
*/ */
String PATH_VARIABLES = View.class.getName() + ".pathVariables"; 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. * Return the content type of the view, if predetermined.
* <p>Can be used to check the content type upfront, * <p>Can be used to check the content type upfront,

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.Map;
import java.util.Properties; import java.util.Properties;
import java.util.StringTokenizer; import java.util.StringTokenizer;
import javax.servlet.ServletOutputStream; import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.BeanNameAware;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.web.context.support.WebApplicationObjectSupport; import org.springframework.web.context.support.WebApplicationObjectSupport;
import org.springframework.web.servlet.View; 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. * Whether to add path variables in the model or not.
* <p>Path variables are commonly bound to URI template variables through the {@code @PathVariable} * <p>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 * 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 * them to derive typed Object values. Such values are frequently needed in views for
* constructing links to the same and other URLs. * constructing links to the same and other URLs.
* <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)}) * <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
* but not attributes already present in the model. * but not attributes already present in the model.
* <p>By default this flag is set to {@code true}. Concrete view types can override this. * <p>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. * @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise.
*/ */
public void setExposePathVariables(boolean exposePathVariables) { public void setExposePathVariables(boolean exposePathVariables) {
this.exposePathVariables = 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 + logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
" and static attributes " + this.staticAttributes); " and static attributes " + this.staticAttributes);
} }
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response); prepareResponse(request, response);
@ -263,7 +265,7 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
} }
/** /**
* Creates a combined output Map (never <code>null</code>) that includes dynamic values and static attributes. * Creates a combined output Map (never <code>null</code>) that includes dynamic values and static attributes.
* Dynamic values take precedence over static attributes. * Dynamic values take precedence over static attributes.
*/ */
protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request, protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request,
@ -289,7 +291,7 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
if (this.requestContextAttribute != null) { if (this.requestContextAttribute != null) {
mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel)); mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
} }
return mergedModel; return mergedModel;
} }
@ -408,6 +410,21 @@ public abstract class AbstractView extends WebApplicationObjectSupport implement
out.flush(); 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 @Override
public String toString() { public String toString() {

View File

@ -278,7 +278,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest()); List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) { if (requestedMediaTypes != null) {
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes); List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = getBestView(candidateViews, requestedMediaTypes); View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) { if (bestView != null) {
return bestView; return bestView;
} }
@ -378,7 +378,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
return candidateViews; return candidateViews;
} }
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes) { private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
for (View candidateView : candidateViews) { for (View candidateView : candidateViews) {
if (candidateView instanceof SmartView) { if (candidateView instanceof SmartView) {
SmartView smartView = (SmartView) candidateView; SmartView smartView = (SmartView) candidateView;
@ -394,11 +394,12 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
for (View candidateView : candidateViews) { for (View candidateView : candidateViews) {
if (StringUtils.hasText(candidateView.getContentType())) { if (StringUtils.hasText(candidateView.getContentType())) {
MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType()); MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
if (mediaType.includes(candidateContentType)) { if (mediaType.isCompatibleWith(candidateContentType)) {
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Returning [" + candidateView + "] based on requested media type '" logger.debug("Returning [" + candidateView + "] based on requested media type '"
+ mediaType + "'"); + mediaType + "'");
} }
attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
return candidateView; return candidateView;
} }
} }

View File

@ -54,7 +54,7 @@ public abstract class AbstractFeedView<T extends WireFeed> extends AbstractView
buildFeedMetadata(model, wireFeed, request); buildFeedMetadata(model, wireFeed, request);
buildFeedEntries(model, wireFeed, request, response); buildFeedEntries(model, wireFeed, request, response);
response.setContentType(getContentType()); setResponseContentType(request, response);
if (!StringUtils.hasText(wireFeed.getEncoding())) { if (!StringUtils.hasText(wireFeed.getEncoding())) {
wireFeed.setEncoding("UTF-8"); wireFeed.setEncoding("UTF-8");
} }

View File

@ -214,7 +214,7 @@ public class MappingJackson2JsonView extends AbstractView {
@Override @Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
response.setContentType(getContentType()); setResponseContentType(request, response);
response.setCharacterEncoding(this.encoding.getJavaName()); response.setCharacterEncoding(this.encoding.getJavaName());
if (this.disableCaching) { if (this.disableCaching) {
response.addHeader("Pragma", "no-cache"); response.addHeader("Pragma", "no-cache");

View File

@ -217,7 +217,7 @@ public class MappingJacksonJsonView extends AbstractView {
@Override @Override
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
response.setContentType(getContentType()); setResponseContentType(request, response);
response.setCharacterEncoding(this.encoding.getJavaName()); response.setCharacterEncoding(this.encoding.getJavaName());
if (this.disableCaching) { if (this.disableCaching) {
response.addHeader("Pragma", "no-cache"); response.addHeader("Pragma", "no-cache");

View File

@ -96,8 +96,8 @@ public class MarshallingView extends AbstractView {
} }
@Override @Override
protected void renderMergedOutputModel(Map<String, Object> model, protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request, HttpServletRequest request,
HttpServletResponse response) throws Exception { HttpServletResponse response) throws Exception {
Object toBeMarshalled = locateToBeMarshalled(model); Object toBeMarshalled = locateToBeMarshalled(model);
if (toBeMarshalled == null) { if (toBeMarshalled == null) {
@ -106,7 +106,7 @@ public class MarshallingView extends AbstractView {
ByteArrayOutputStream bos = new ByteArrayOutputStream(2048); ByteArrayOutputStream bos = new ByteArrayOutputStream(2048);
marshaller.marshal(toBeMarshalled, new StreamResult(bos)); marshaller.marshal(toBeMarshalled, new StreamResult(bos));
response.setContentType(getContentType()); setResponseContentType(request, response);
response.setContentLength(bos.size()); response.setContentLength(bos.size());
FileCopyUtils.copy(bos.toByteArray(), response.getOutputStream()); FileCopyUtils.copy(bos.toByteArray(), response.getOutputStream());

View File

@ -273,6 +273,35 @@ public class ContentNegotiatingViewResolverTests {
verify(htmlViewResolver, jsonViewResolver, htmlView, jsonViewMock); 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 @Test
public void resolveViewNameAcceptHeaderDefaultView() throws Exception { public void resolveViewNameAcceptHeaderDefaultView() throws Exception {
request.addHeader("Accept", "application/json"); request.addHeader("Accept", "application/json");

View File

@ -35,10 +35,12 @@ import org.junit.Test;
import org.mozilla.javascript.Context; import org.mozilla.javascript.Context;
import org.mozilla.javascript.ContextFactory; import org.mozilla.javascript.ContextFactory;
import org.mozilla.javascript.ScriptableObject; import org.mozilla.javascript.ScriptableObject;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResult;
import org.springframework.web.servlet.View;
import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty; import com.fasterxml.jackson.databind.BeanProperty;
@ -110,6 +112,22 @@ public class MappingJackson2JsonViewTests {
validateResult(); validateResult();
} }
@Test
public void renderWithSelectedContentType() throws Exception {
Map<String, Object> model = new HashMap<String, Object>();
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 @Test
public void renderCaching() throws Exception { public void renderCaching() throws Exception {
view.setDisableCaching(false); view.setDisableCaching(false);
@ -265,6 +283,7 @@ public class MappingJackson2JsonViewTests {
Object jsResult = Object jsResult =
jsContext.evaluateString(jsScope, "(" + response.getContentAsString() + ")", "JSON Stream", 1, null); jsContext.evaluateString(jsScope, "(" + response.getContentAsString() + ")", "JSON Stream", 1, null);
assertNotNull("Json Result did not eval as valid JavaScript", jsResult); assertNotNull("Json Result did not eval as valid JavaScript", jsResult);
assertEquals("application/json", response.getContentType());
} }

View File

@ -30,6 +30,8 @@ Changes in version 3.2 RC1 (2012-10-29)
* added ObjectToStringHttpMessageConverter that delegates to a ConversionService (SPR-9738) * added ObjectToStringHttpMessageConverter that delegates to a ConversionService (SPR-9738)
* added Jackson2ObjectMapperBeanFactory (SPR-9739) * added Jackson2ObjectMapperBeanFactory (SPR-9739)
* added CallableProcessingInterceptor and DeferredResultProcessingInterceptor * 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) Changes in version 3.2 M2 (2012-09-11)
-------------------------------------- --------------------------------------