Skip to content

Commit 0c47a01

Browse files
committed
fixed ContentNegotiatingViewResolver to work with the combination of ignoreAcceptHeader=true plus defaultContentType as well (SPR-6163)
1 parent 7a700ed commit 0c47a01

File tree

2 files changed

+102
-68
lines changed

2 files changed

+102
-68
lines changed

org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolver.java

Lines changed: 65 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -91,66 +91,66 @@
9191
* text/html} request {@code Accept} header has the same result.
9292
*
9393
* @author Arjen Poutsma
94-
* @author Rostislav Hristov
94+
* @author Juergen Hoeller
95+
* @since 3.0
9596
* @see ViewResolver
9697
* @see InternalResourceViewResolver
9798
* @see BeanNameViewResolver
98-
* @since 3.0
9999
*/
100100
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
101101

102+
private static final String ACCEPT_HEADER = "Accept";
103+
102104
private static final boolean jafPresent =
103105
ClassUtils.isPresent("javax.activation.FileTypeMap", ContentNegotiatingViewResolver.class.getClassLoader());
104106

105-
private static final String ACCEPT_HEADER = "Accept";
107+
private static final UrlPathHelper urlPathHelper = new UrlPathHelper();
108+
106109

107-
private UrlPathHelper urlPathHelper = new UrlPathHelper();
110+
private int order = Ordered.HIGHEST_PRECEDENCE;
108111

109112
private boolean favorPathExtension = true;
110113

111114
private boolean favorParameter = false;
112115

113-
private boolean ignoreAcceptHeader = false;
114-
115116
private String parameterName = "format";
116117

117-
private int order = Ordered.HIGHEST_PRECEDENCE;
118+
private boolean ignoreAcceptHeader = false;
118119

119120
private ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<String, MediaType>();
120121

121122
private List<View> defaultViews;
122123

124+
private MediaType defaultContentType;
125+
123126
private List<ViewResolver> viewResolvers;
124127

125-
private MediaType defaultContentType;
126128

127129
public void setOrder(int order) {
128130
this.order = order;
129131
}
130132

131133
public int getOrder() {
132-
return order;
134+
return this.order;
133135
}
134136

135137
/**
136-
* Indicates whether the extension of the request path should be used to determine the requested media type, in favor
137-
* of looking at the {@code Accept} header. The default value is {@code true}.
138-
*
139-
* <p>For instance, when this flag is <code>true</code> (the default), a request for {@code /hotels.pdf} will result in
140-
* an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined {@code
141-
* text/html,application/xhtml+xml}.
138+
* Indicates whether the extension of the request path should be used to determine the requested media type,
139+
* in favor of looking at the {@code Accept} header. The default value is {@code true}.
140+
* <p>For instance, when this flag is <code>true</code> (the default), a request for {@code /hotels.pdf}
141+
* will result in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the
142+
* browser-defined {@code text/html,application/xhtml+xml}.
142143
*/
143144
public void setFavorPathExtension(boolean favorPathExtension) {
144145
this.favorPathExtension = favorPathExtension;
145146
}
146147

147148
/**
148-
* Indicates whether a request parameter should be used to determine the requested media type, in favor of looking at
149-
* the {@code Accept} header. The default value is {@code false}.
150-
*
151-
* <p>For instance, when this flag is <code>true</code>, a request for {@code /hotels?format=pdf} will result in an
152-
* {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined {@code
153-
* text/html,application/xhtml+xml}.
149+
* Indicates whether a request parameter should be used to determine the requested media type,
150+
* in favor of looking at the {@code Accept} header. The default value is {@code false}.
151+
* <p>For instance, when this flag is <code>true</code>, a request for {@code /hotels?format=pdf} will result
152+
* in an {@code AbstractPdfView} being resolved, while the {@code Accept} header can be the browser-defined
153+
* {@code text/html,application/xhtml+xml}.
154154
*/
155155
public void setFavorParameter(boolean favorParameter) {
156156
this.favorParameter = favorParameter;
@@ -166,20 +166,18 @@ public void setParameterName(String parameterName) {
166166

167167
/**
168168
* Indicates whether the HTTP {@code Accept} header should be ignored. Default is {@code false}.
169-
*
170-
* If set to {@code true}, this view resolver will only refer to the file extension and/or paramter, as indicated by
171-
* the {@link #setFavorPathExtension(boolean) favorPathExtension} and {@link #setFavorParameter(boolean)
172-
* favorParameter} properties.
169+
* If set to {@code true}, this view resolver will only refer to the file extension and/or paramter,
170+
* as indicated by the {@link #setFavorPathExtension(boolean) favorPathExtension} and
171+
* {@link #setFavorParameter(boolean) favorParameter} properties.
173172
*/
174173
public void setIgnoreAcceptHeader(boolean ignoreAcceptHeader) {
175174
this.ignoreAcceptHeader = ignoreAcceptHeader;
176175
}
177176

178177
/**
179178
* Sets the mapping from file extensions to media types.
180-
*
181-
* <p>When this mapping is not set or when an extension is not present, this view resolver will fall back to using a
182-
* {@link FileTypeMap} when the Java Action Framework is available.
179+
* <p>When this mapping is not set or when an extension is not present, this view resolver
180+
* will fall back to using a {@link FileTypeMap} when the Java Action Framework is available.
183181
*/
184182
public void setMediaTypes(Map<String, String> mediaTypes) {
185183
Assert.notNull(mediaTypes, "'mediaTypes' must not be null");
@@ -190,13 +188,17 @@ public void setMediaTypes(Map<String, String> mediaTypes) {
190188
}
191189
}
192190

193-
/** Sets the default views to use when a more specific view can not be obtained from the {@link ViewResolver} chain. */
191+
/**
192+
* Sets the default views to use when a more specific view can not be obtained
193+
* from the {@link ViewResolver} chain.
194+
*/
194195
public void setDefaultViews(List<View> defaultViews) {
195196
this.defaultViews = defaultViews;
196197
}
197198

198199
/**
199-
* Sets the default content type. This content type will be used when file extension, parameter, nor {@code Accept}
200+
* Sets the default content type.
201+
* <p>This content type will be used when file extension, parameter, nor {@code Accept}
200202
* header define a content-type, either through being disabled or empty.
201203
*/
202204
public void setDefaultContentType(MediaType defaultContentType) {
@@ -205,7 +207,6 @@ public void setDefaultContentType(MediaType defaultContentType) {
205207

206208
/**
207209
* Sets the view resolvers to be wrapped by this view resolver.
208-
*
209210
* <p>If this property is not set, view resolvers will be detected automatically.
210211
*/
211212
public void setViewResolvers(List<ViewResolver> viewResolvers) {
@@ -214,9 +215,9 @@ public void setViewResolvers(List<ViewResolver> viewResolvers) {
214215

215216
@Override
216217
protected void initServletContext(ServletContext servletContext) {
217-
if (viewResolvers == null) {
218-
Map<String, ViewResolver> matchingBeans = BeanFactoryUtils
219-
.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class, true, false);
218+
if (this.viewResolvers == null) {
219+
Map<String, ViewResolver> matchingBeans =
220+
BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class);
220221
this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.size());
221222
for (ViewResolver viewResolver : matchingBeans.values()) {
222223
if (this != viewResolver) {
@@ -233,19 +234,16 @@ protected void initServletContext(ServletContext servletContext) {
233234

234235
/**
235236
* Determines the list of {@link MediaType} for the given {@link HttpServletRequest}.
236-
*
237237
* <p>The default implementation invokes {@link #getMediaTypeFromFilename(String)} if {@linkplain
238238
* #setFavorPathExtension(boolean) favorPathExtension} property is <code>true</code>. If the property is
239-
* <code>false</code>, or when a media type cannot be determined from the request path, this method will inspect the
240-
* {@code Accept} header of the request.
241-
*
239+
* <code>false</code>, or when a media type cannot be determined from the request path, this method will
240+
* inspect the {@code Accept} header of the request.
242241
* <p>This method can be overriden to provide a different algorithm.
243-
*
244242
* @param request the current servlet request
245243
* @return the list of media types requested, if any
246244
*/
247245
protected List<MediaType> getMediaTypes(HttpServletRequest request) {
248-
if (favorPathExtension) {
246+
if (this.favorPathExtension) {
249247
String requestUri = urlPathHelper.getRequestUri(request);
250248
String filename = WebUtils.extractFullFilenameFromUrlPath(requestUri);
251249
MediaType mediaType = getMediaTypeFromFilename(filename);
@@ -258,23 +256,22 @@ protected List<MediaType> getMediaTypes(HttpServletRequest request) {
258256
return mediaTypes;
259257
}
260258
}
261-
if (favorParameter) {
262-
if (request.getParameter(parameterName) != null) {
263-
String parameterValue = request.getParameter(parameterName);
259+
if (this.favorParameter) {
260+
if (request.getParameter(this.parameterName) != null) {
261+
String parameterValue = request.getParameter(this.parameterName);
264262
MediaType mediaType = getMediaTypeFromParameter(parameterValue);
265263
if (mediaType != null) {
266264
if (logger.isDebugEnabled()) {
267-
logger.debug(
268-
"Requested media type is '" + mediaType + "' (based on parameter '" + parameterName +
269-
"'='" + parameterValue + "')");
265+
logger.debug("Requested media type is '" + mediaType + "' (based on parameter '" +
266+
this.parameterName + "'='" + parameterValue + "')");
270267
}
271268
List<MediaType> mediaTypes = new ArrayList<MediaType>();
272269
mediaTypes.add(mediaType);
273270
return mediaTypes;
274271
}
275272
}
276273
}
277-
if (!ignoreAcceptHeader) {
274+
if (!this.ignoreAcceptHeader) {
278275
String acceptHeader = request.getHeader(ACCEPT_HEADER);
279276
if (StringUtils.hasText(acceptHeader)) {
280277
List<MediaType> mediaTypes = MediaType.parseMediaTypes(acceptHeader);
@@ -284,8 +281,8 @@ protected List<MediaType> getMediaTypes(HttpServletRequest request) {
284281
return mediaTypes;
285282
}
286283
}
287-
if (defaultContentType != null) {
288-
return Collections.singletonList(defaultContentType);
284+
if (this.defaultContentType != null) {
285+
return Collections.singletonList(this.defaultContentType);
289286
}
290287
else {
291288
return Collections.emptyList();
@@ -294,13 +291,10 @@ protected List<MediaType> getMediaTypes(HttpServletRequest request) {
294291

295292
/**
296293
* Determines the {@link MediaType} for the given filename.
297-
*
298294
* <p>The default implementation will check the {@linkplain #setMediaTypes(Map) media types} property first for a
299295
* defined mapping. If not present, and if the Java Activation Framework can be found on the class path, it will call
300296
* {@link FileTypeMap#getContentType(String)}
301-
*
302297
* <p>This method can be overriden to provide a different algorithm.
303-
*
304298
* @param filename the current request file name (i.e. {@code hotels.html})
305299
* @return the media type, if any
306300
*/
@@ -310,50 +304,51 @@ protected MediaType getMediaTypeFromFilename(String filename) {
310304
return null;
311305
}
312306
extension = extension.toLowerCase(Locale.ENGLISH);
313-
MediaType mediaType = mediaTypes.get(extension);
307+
MediaType mediaType = this.mediaTypes.get(extension);
314308
if (mediaType == null && jafPresent) {
315309
mediaType = ActivationMediaTypeFactory.getMediaType(filename);
316310
if (mediaType != null) {
317-
mediaTypes.putIfAbsent(extension, mediaType);
311+
this.mediaTypes.putIfAbsent(extension, mediaType);
318312
}
319313
}
320314
return mediaType;
321315
}
322316

323317
/**
324318
* Determines the {@link MediaType} for the given parameter value.
325-
*
326-
* <p>The default implementation will check the {@linkplain #setMediaTypes(Map) media types} property for a defined
327-
* mapping.
328-
*
319+
* <p>The default implementation will check the {@linkplain #setMediaTypes(Map) media types}
320+
* property for a defined mapping.
329321
* <p>This method can be overriden to provide a different algorithm.
330-
*
331322
* @param parameterValue the parameter value (i.e. {@code pdf}).
332323
* @return the media type, if any
333324
*/
334325
protected MediaType getMediaTypeFromParameter(String parameterValue) {
335-
parameterValue = parameterValue.toLowerCase(Locale.ENGLISH);
336-
return mediaTypes.get(parameterValue);
326+
return this.mediaTypes.get(parameterValue.toLowerCase(Locale.ENGLISH));
337327
}
338328

339329
public View resolveViewName(String viewName, Locale locale) throws Exception {
340330
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
341331
Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
342332
ServletRequestAttributes servletAttrs = (ServletRequestAttributes) attrs;
333+
343334
List<MediaType> requestedMediaTypes = getMediaTypes(servletAttrs.getRequest());
344-
Collections.sort(requestedMediaTypes);
335+
if (requestedMediaTypes.size() > 1) {
336+
// avoid sorting attempt for empty list and singleton list
337+
Collections.sort(requestedMediaTypes);
338+
}
345339

346340
SortedMap<MediaType, View> views = new TreeMap<MediaType, View>();
347341
List<View> candidateViews = new ArrayList<View>();
348-
for (ViewResolver viewResolver : viewResolvers) {
342+
for (ViewResolver viewResolver : this.viewResolvers) {
349343
View view = viewResolver.resolveViewName(viewName, locale);
350344
if (view != null) {
351345
candidateViews.add(view);
352346
}
353347
}
354-
if (!CollectionUtils.isEmpty(defaultViews)) {
355-
candidateViews.addAll(defaultViews);
348+
if (!CollectionUtils.isEmpty(this.defaultViews)) {
349+
candidateViews.addAll(this.defaultViews);
356350
}
351+
357352
for (View candidateView : candidateViews) {
358353
MediaType viewMediaType = MediaType.parseMediaType(candidateView.getContentType());
359354
for (MediaType requestedMediaType : requestedMediaTypes) {
@@ -365,6 +360,7 @@ public View resolveViewName(String viewName, Locale locale) throws Exception {
365360
}
366361
}
367362
}
363+
368364
if (!views.isEmpty()) {
369365
MediaType mediaType = views.firstKey();
370366
View view = views.get(mediaType);
@@ -378,7 +374,10 @@ public View resolveViewName(String viewName, Locale locale) throws Exception {
378374
}
379375
}
380376

381-
/** Inner class to avoid hard-coded JAF dependency. */
377+
378+
/**
379+
* Inner class to avoid hard-coded JAF dependency.
380+
*/
382381
private static class ActivationMediaTypeFactory {
383382

384383
private static final FileTypeMap fileTypeMap;

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/view/ContentNegotiatingViewResolverTests.java

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,16 +93,51 @@ public void getMediaTypeAcceptHeader() {
9393
}
9494

9595
@Test
96-
public void getDefaultMediaType() {
96+
public void getDefaultContentType() {
9797
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test");
9898
request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
99-
viewResolver.setDefaultContentType(new MediaType("application", "pdf"));
10099
viewResolver.setIgnoreAcceptHeader(true);
100+
viewResolver.setDefaultContentType(new MediaType("application", "pdf"));
101101
List<MediaType> result = viewResolver.getMediaTypes(request);
102102
assertEquals("Invalid amount of media types", 1, result.size());
103103
assertEquals("Invalid content type", new MediaType("application", "pdf"), result.get(0));
104104
}
105105

106+
@Test
107+
public void resolveViewNameWithDefaultContentType() throws Exception {
108+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test");
109+
request.addHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
110+
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
111+
112+
viewResolver.setIgnoreAcceptHeader(true);
113+
viewResolver.setDefaultContentType(new MediaType("application", "xml"));
114+
115+
ViewResolver viewResolverMock1 = createMock(ViewResolver.class);
116+
ViewResolver viewResolverMock2 = createMock(ViewResolver.class);
117+
List<ViewResolver> viewResolverMocks = new ArrayList<ViewResolver>();
118+
viewResolverMocks.add(viewResolverMock1);
119+
viewResolverMocks.add(viewResolverMock2);
120+
viewResolver.setViewResolvers(viewResolverMocks);
121+
122+
View viewMock1 = createMock("application_xml", View.class);
123+
View viewMock2 = createMock("text_html", View.class);
124+
125+
String viewName = "view";
126+
Locale locale = Locale.ENGLISH;
127+
128+
expect(viewResolverMock1.resolveViewName(viewName, locale)).andReturn(viewMock1);
129+
expect(viewResolverMock2.resolveViewName(viewName, locale)).andReturn(viewMock2);
130+
expect(viewMock1.getContentType()).andReturn("application/xml");
131+
expect(viewMock2.getContentType()).andReturn("text/html;charset=ISO-8859-1");
132+
133+
replay(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2);
134+
135+
View result = viewResolver.resolveViewName(viewName, locale);
136+
assertSame("Invalid view", viewMock1, result);
137+
138+
verify(viewResolverMock1, viewResolverMock2, viewMock1, viewMock2);
139+
}
140+
106141
@Test
107142
public void resolveViewNameAcceptHeader() throws Exception {
108143
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/test");

0 commit comments

Comments
 (0)