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");
|
* 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.
|
||||||
|
|
@ -58,6 +60,13 @@ public interface View {
|
||||||
*/
|
*/
|
||||||
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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
--------------------------------------
|
--------------------------------------
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue