提交 c7e7e80a 编写于 作者: R Rossen Stoyanchev

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.
上级 4f114a65
/*
* 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.
*
* <p>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.
* <p>Can be used to check the content type upfront,
......
/*
* 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.
* <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
* them to derive typed Object values. Such values are frequently needed in views for
* constructing links to the same and other URLs.
* <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
* but not attributes already present in the model.
* <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.
* Whether to add path variables in the model or not.
* <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
* them to derive typed Object values. Such values are frequently needed in views for
* constructing links to the same and other URLs.
* <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
* but not attributes already present in the model.
* <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.
*/
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<String, Object> 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 <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.
*/
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) {
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() {
......
......@@ -278,7 +278,7 @@ public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
List<View> 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<View> candidateViews, List<MediaType> requestedMediaTypes) {
private View getBestView(List<View> candidateViews, List<MediaType> 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;
}
}
......
......@@ -54,7 +54,7 @@ public abstract class AbstractFeedView<T extends WireFeed> 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");
}
......
......@@ -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");
......
......@@ -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");
......
......@@ -96,8 +96,8 @@ public class MarshallingView extends AbstractView {
}
@Override
protected void renderMergedOutputModel(Map<String, Object> model,
HttpServletRequest request,
protected void renderMergedOutputModel(Map<String, Object> 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());
......
......@@ -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");
......
......@@ -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<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
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());
}
......
......@@ -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)
--------------------------------------
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册