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:
parent
4f114a657f
commit
c7e7e80a3a
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
--------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue