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");
* 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.
@ -58,6 +60,13 @@ public interface View {
*/
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,

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");
* 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;
@ -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() {

View File

@ -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;
}
}

View File

@ -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");
}

View File

@ -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");

View File

@ -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");

View File

@ -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());

View File

@ -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");

View File

@ -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());
}

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