Skip to content

Commit c7e7e80

Browse files
committed
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.
1 parent 4f114a6 commit c7e7e80

File tree

10 files changed

+103
-26
lines changed

10 files changed

+103
-26
lines changed

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/View.java

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2008 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@
2121
import javax.servlet.http.HttpServletRequest;
2222
import javax.servlet.http.HttpServletResponse;
2323

24+
import org.springframework.http.MediaType;
25+
2426
/**
2527
* MVC View for a web interaction. Implementations are responsible for rendering
2628
* content, and exposing the model. A single view exposes multiple model attributes.
@@ -51,13 +53,20 @@ public interface View {
5153

5254
/**
5355
* Name of the {@link HttpServletRequest} attribute that contains a Map with path variables.
54-
* The map consists of String-based URI template variable names as keys and their corresponding
55-
* Object-based values -- extracted from segments of the URL and type converted.
56-
*
56+
* The map consists of String-based URI template variable names as keys and their corresponding
57+
* Object-based values -- extracted from segments of the URL and type converted.
58+
*
5759
* <p>Note: This attribute is not required to be supported by all View implementations.
5860
*/
5961
String PATH_VARIABLES = View.class.getName() + ".pathVariables";
6062

63+
/**
64+
* The {@link MediaType} selected during content negotiation, which may be
65+
* more specific than the one the View is configured with. For example:
66+
* "application/vnd.example-v1+xml" vs "application/*+xml".
67+
*/
68+
String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";
69+
6170
/**
6271
* Return the content type of the view, if predetermined.
6372
* <p>Can be used to check the content type upfront,

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/view/AbstractView.java

+30-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2009 the original author or authors.
2+
* Copyright 2002-2012 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,11 +23,13 @@
2323
import java.util.Map;
2424
import java.util.Properties;
2525
import java.util.StringTokenizer;
26+
2627
import javax.servlet.ServletOutputStream;
2728
import javax.servlet.http.HttpServletRequest;
2829
import javax.servlet.http.HttpServletResponse;
2930

3031
import org.springframework.beans.factory.BeanNameAware;
32+
import org.springframework.http.MediaType;
3133
import org.springframework.util.CollectionUtils;
3234
import org.springframework.web.context.support.WebApplicationObjectSupport;
3335
import org.springframework.web.servlet.View;
@@ -223,15 +225,15 @@ public Map<String, Object> getStaticAttributes() {
223225
}
224226

225227
/**
226-
* Whether to add path variables in the model or not.
227-
* <p>Path variables are commonly bound to URI template variables through the {@code @PathVariable}
228-
* annotation. They're are effectively URI template variables with type conversion applied to
229-
* them to derive typed Object values. Such values are frequently needed in views for
230-
* constructing links to the same and other URLs.
231-
* <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
232-
* but not attributes already present in the model.
233-
* <p>By default this flag is set to {@code true}. Concrete view types can override this.
234-
* @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise.
228+
* Whether to add path variables in the model or not.
229+
* <p>Path variables are commonly bound to URI template variables through the {@code @PathVariable}
230+
* annotation. They're are effectively URI template variables with type conversion applied to
231+
* them to derive typed Object values. Such values are frequently needed in views for
232+
* constructing links to the same and other URLs.
233+
* <p>Path variables added to the model override static attributes (see {@link #setAttributes(Properties)})
234+
* but not attributes already present in the model.
235+
* <p>By default this flag is set to {@code true}. Concrete view types can override this.
236+
* @param exposePathVariables {@code true} to expose path variables, and {@code false} otherwise.
235237
*/
236238
public void setExposePathVariables(boolean exposePathVariables) {
237239
this.exposePathVariables = exposePathVariables;
@@ -255,15 +257,15 @@ public void render(Map<String, ?> model, HttpServletRequest request, HttpServlet
255257
logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
256258
" and static attributes " + this.staticAttributes);
257259
}
258-
260+
259261
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
260262

261263
prepareResponse(request, response);
262264
renderMergedOutputModel(mergedModel, request, response);
263265
}
264266

265267
/**
266-
* Creates a combined output Map (never <code>null</code>) that includes dynamic values and static attributes.
268+
* Creates a combined output Map (never <code>null</code>) that includes dynamic values and static attributes.
267269
* Dynamic values take precedence over static attributes.
268270
*/
269271
protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request,
@@ -289,7 +291,7 @@ protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, Http
289291
if (this.requestContextAttribute != null) {
290292
mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
291293
}
292-
294+
293295
return mergedModel;
294296
}
295297

@@ -408,6 +410,21 @@ protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStre
408410
out.flush();
409411
}
410412

413+
/**
414+
* Set the content type of the response to the configured
415+
* {@link #setContentType(String) content type} unless the
416+
* {@link View#SELECTED_CONTENT_TYPE} request attribute is present and set
417+
* to a concrete media type.
418+
*/
419+
protected void setResponseContentType(HttpServletRequest request, HttpServletResponse response) {
420+
MediaType mediaType = (MediaType) request.getAttribute(View.SELECTED_CONTENT_TYPE);
421+
if (mediaType != null && mediaType.isConcrete()) {
422+
response.setContentType(mediaType.toString());
423+
}
424+
else {
425+
response.setContentType(getContentType());
426+
}
427+
}
411428

412429
@Override
413430
public String toString() {

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java

+4-3
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ public View resolveViewName(String viewName, Locale locale) throws Exception {
278278
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
279279
if (requestedMediaTypes != null) {
280280
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
281-
View bestView = getBestView(candidateViews, requestedMediaTypes);
281+
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
282282
if (bestView != null) {
283283
return bestView;
284284
}
@@ -378,7 +378,7 @@ private List<View> getCandidateViews(String viewName, Locale locale, List<MediaT
378378
return candidateViews;
379379
}
380380

381-
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes) {
381+
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
382382
for (View candidateView : candidateViews) {
383383
if (candidateView instanceof SmartView) {
384384
SmartView smartView = (SmartView) candidateView;
@@ -394,11 +394,12 @@ private View getBestView(List<View> candidateViews, List<MediaType> requestedMed
394394
for (View candidateView : candidateViews) {
395395
if (StringUtils.hasText(candidateView.getContentType())) {
396396
MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
397-
if (mediaType.includes(candidateContentType)) {
397+
if (mediaType.isCompatibleWith(candidateContentType)) {
398398
if (logger.isDebugEnabled()) {
399399
logger.debug("Returning [" + candidateView + "] based on requested media type '"
400400
+ mediaType + "'");
401401
}
402+
attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
402403
return candidateView;
403404
}
404405
}

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/view/feed/AbstractFeedView.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ protected final void renderMergedOutputModel(
5454
buildFeedMetadata(model, wireFeed, request);
5555
buildFeedEntries(model, wireFeed, request, response);
5656

57-
response.setContentType(getContentType());
57+
setResponseContentType(request, response);
5858
if (!StringUtils.hasText(wireFeed.getEncoding())) {
5959
wireFeed.setEncoding("UTF-8");
6060
}

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJackson2JsonView.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ public void setUpdateContentLength(boolean updateContentLength) {
214214

215215
@Override
216216
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
217-
response.setContentType(getContentType());
217+
setResponseContentType(request, response);
218218
response.setCharacterEncoding(this.encoding.getJavaName());
219219
if (this.disableCaching) {
220220
response.addHeader("Pragma", "no-cache");

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/view/json/MappingJacksonJsonView.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ public void setUpdateContentLength(boolean updateContentLength) {
217217

218218
@Override
219219
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
220-
response.setContentType(getContentType());
220+
setResponseContentType(request, response);
221221
response.setCharacterEncoding(this.encoding.getJavaName());
222222
if (this.disableCaching) {
223223
response.addHeader("Pragma", "no-cache");

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/view/xml/MarshallingView.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,8 @@ protected void initApplicationContext() throws BeansException {
9696
}
9797

9898
@Override
99-
protected void renderMergedOutputModel(Map<String, Object> model,
100-
HttpServletRequest request,
99+
protected void renderMergedOutputModel(Map<String, Object> model,
100+
HttpServletRequest request,
101101
HttpServletResponse response) throws Exception {
102102
Object toBeMarshalled = locateToBeMarshalled(model);
103103
if (toBeMarshalled == null) {
@@ -106,7 +106,7 @@ protected void renderMergedOutputModel(Map<String, Object> model,
106106
ByteArrayOutputStream bos = new ByteArrayOutputStream(2048);
107107
marshaller.marshal(toBeMarshalled, new StreamResult(bos));
108108

109-
response.setContentType(getContentType());
109+
setResponseContentType(request, response);
110110
response.setContentLength(bos.size());
111111

112112
FileCopyUtils.copy(bos.toByteArray(), response.getOutputStream());

Diff for: spring-webmvc/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java

+29
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,35 @@ public void resolveViewNameAcceptHeaderSortByQuality() throws Exception {
273273
verify(htmlViewResolver, jsonViewResolver, htmlView, jsonViewMock);
274274
}
275275

276+
// SPR-9807
277+
278+
@Test
279+
public void resolveViewNameAcceptHeaderWithSuffix() throws Exception {
280+
request.addHeader("Accept", "application/vnd.example-v2+xml");
281+
282+
ViewResolver viewResolverMock = createMock(ViewResolver.class);
283+
viewResolver.setViewResolvers(Arrays.asList(viewResolverMock));
284+
285+
viewResolver.afterPropertiesSet();
286+
287+
View viewMock = createMock("application_xml", View.class);
288+
289+
String viewName = "view";
290+
Locale locale = Locale.ENGLISH;
291+
292+
expect(viewResolverMock.resolveViewName(viewName, locale)).andReturn(viewMock);
293+
expect(viewMock.getContentType()).andReturn("application/*+xml").anyTimes();
294+
295+
replay(viewResolverMock, viewMock);
296+
297+
View result = viewResolver.resolveViewName(viewName, locale);
298+
299+
assertSame("Invalid view", viewMock, result);
300+
assertEquals(new MediaType("application", "vnd.example-v2+xml"), request.getAttribute(View.SELECTED_CONTENT_TYPE));
301+
302+
verify(viewResolverMock, viewMock);
303+
}
304+
276305
@Test
277306
public void resolveViewNameAcceptHeaderDefaultView() throws Exception {
278307
request.addHeader("Accept", "application/json");

Diff for: spring-webmvc/src/test/java/org/springframework/web/servlet/view/json/MappingJackson2JsonViewTests.java

+19
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,12 @@
3535
import org.mozilla.javascript.Context;
3636
import org.mozilla.javascript.ContextFactory;
3737
import org.mozilla.javascript.ScriptableObject;
38+
import org.springframework.http.MediaType;
3839
import org.springframework.mock.web.MockHttpServletRequest;
3940
import org.springframework.mock.web.MockHttpServletResponse;
4041
import org.springframework.ui.ModelMap;
4142
import org.springframework.validation.BindingResult;
43+
import org.springframework.web.servlet.View;
4244

4345
import com.fasterxml.jackson.core.JsonGenerator;
4446
import com.fasterxml.jackson.databind.BeanProperty;
@@ -110,6 +112,22 @@ public void renderSimpleMap() throws Exception {
110112
validateResult();
111113
}
112114

115+
@Test
116+
public void renderWithSelectedContentType() throws Exception {
117+
118+
Map<String, Object> model = new HashMap<String, Object>();
119+
model.put("foo", "bar");
120+
121+
view.render(model, request, response);
122+
123+
assertEquals("application/json", response.getContentType());
124+
125+
request.setAttribute(View.SELECTED_CONTENT_TYPE, new MediaType("application", "vnd.example-v2+xml"));
126+
view.render(model, request, response);
127+
128+
assertEquals("application/vnd.example-v2+xml", response.getContentType());
129+
}
130+
113131
@Test
114132
public void renderCaching() throws Exception {
115133
view.setDisableCaching(false);
@@ -265,6 +283,7 @@ private void validateResult() throws Exception {
265283
Object jsResult =
266284
jsContext.evaluateString(jsScope, "(" + response.getContentAsString() + ")", "JSON Stream", 1, null);
267285
assertNotNull("Json Result did not eval as valid JavaScript", jsResult);
286+
assertEquals("application/json", response.getContentType());
268287
}
269288

270289

Diff for: src/dist/changelog.txt

+2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Changes in version 3.2 RC1 (2012-10-29)
3030
* added ObjectToStringHttpMessageConverter that delegates to a ConversionService (SPR-9738)
3131
* added Jackson2ObjectMapperBeanFactory (SPR-9739)
3232
* added CallableProcessingInterceptor and DeferredResultProcessingInterceptor
33+
* added support for wildcard media types in AbstractView and ContentNegotiationViewResolver (SPR-9807)
34+
* the jackson message converters now include "application/*+json" in supported media types (SPR-7905)
3335

3436
Changes in version 3.2 M2 (2012-09-11)
3537
--------------------------------------

0 commit comments

Comments
 (0)