View Javadoc
1   /*
2    * Copyright 2002-2015 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.springframework.web.servlet.mvc.method.annotation;
18  
19  import java.io.Serializable;
20  import java.lang.reflect.Method;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.List;
24  
25  import com.fasterxml.jackson.annotation.JsonView;
26  import org.junit.Before;
27  import org.junit.Test;
28  
29  import org.springframework.aop.framework.ProxyFactory;
30  import org.springframework.aop.target.SingletonTargetSource;
31  import org.springframework.core.MethodParameter;
32  import org.springframework.http.HttpStatus;
33  import org.springframework.http.MediaType;
34  import org.springframework.http.ResponseEntity;
35  import org.springframework.http.converter.ByteArrayHttpMessageConverter;
36  import org.springframework.http.converter.HttpMessageConverter;
37  import org.springframework.http.converter.HttpMessageNotReadableException;
38  import org.springframework.http.converter.StringHttpMessageConverter;
39  import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
40  import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
41  import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
42  import org.springframework.mock.web.test.MockHttpServletRequest;
43  import org.springframework.mock.web.test.MockHttpServletResponse;
44  import org.springframework.util.MultiValueMap;
45  import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
46  import org.springframework.web.bind.WebDataBinder;
47  import org.springframework.web.bind.annotation.RequestBody;
48  import org.springframework.web.bind.annotation.RequestMapping;
49  import org.springframework.web.bind.annotation.ResponseBody;
50  import org.springframework.web.bind.annotation.RestController;
51  import org.springframework.web.bind.support.WebDataBinderFactory;
52  import org.springframework.web.context.request.NativeWebRequest;
53  import org.springframework.web.context.request.ServletWebRequest;
54  import org.springframework.web.method.HandlerMethod;
55  import org.springframework.web.method.support.ModelAndViewContainer;
56  import org.springframework.web.servlet.ModelAndView;
57  import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
58  
59  import static org.junit.Assert.*;
60  
61  /**
62   * Test fixture for a {@link RequestResponseBodyMethodProcessor} with
63   * actual delegation to {@link HttpMessageConverter} instances.
64   *
65   * <p>Also see {@link RequestResponseBodyMethodProcessorMockTests}.
66   *
67   * @author Rossen Stoyanchev
68   * @author Sebastien Deleuze
69   */
70  public class RequestResponseBodyMethodProcessorTests {
71  
72  	private MethodParameter paramGenericList;
73  	private MethodParameter paramSimpleBean;
74  	private MethodParameter paramMultiValueMap;
75  	private MethodParameter paramString;
76  	private MethodParameter returnTypeString;
77  
78  	private ModelAndViewContainer mavContainer;
79  
80  	private NativeWebRequest webRequest;
81  
82  	private MockHttpServletRequest servletRequest;
83  
84  	private MockHttpServletResponse servletResponse;
85  
86  	private ValidatingBinderFactory binderFactory;
87  
88  
89  	@Before
90  	public void setUp() throws Exception {
91  		Method method = getClass().getMethod("handle", List.class, SimpleBean.class, MultiValueMap.class, String.class);
92  
93  		paramGenericList = new MethodParameter(method, 0);
94  		paramSimpleBean = new MethodParameter(method, 1);
95  		paramMultiValueMap = new MethodParameter(method, 2);
96  		paramString = new MethodParameter(method, 3);
97  		returnTypeString = new MethodParameter(method, -1);
98  
99  		mavContainer = new ModelAndViewContainer();
100 
101 		servletRequest = new MockHttpServletRequest();
102 		servletResponse = new MockHttpServletResponse();
103 		webRequest = new ServletWebRequest(servletRequest, servletResponse);
104 
105 		this.binderFactory = new ValidatingBinderFactory();
106 	}
107 
108 	@Test
109 	public void resolveArgumentParameterizedType() throws Exception {
110 		String content = "[{\"name\" : \"Jad\"}, {\"name\" : \"Robert\"}]";
111 		this.servletRequest.setContent(content.getBytes("UTF-8"));
112 		this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
113 
114 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
115 		converters.add(new MappingJackson2HttpMessageConverter());
116 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
117 
118 		@SuppressWarnings("unchecked")
119 		List<SimpleBean> result = (List<SimpleBean>) processor.resolveArgument(
120 				paramGenericList, mavContainer, webRequest, binderFactory);
121 
122 		assertNotNull(result);
123 		assertEquals("Jad", result.get(0).getName());
124 		assertEquals("Robert", result.get(1).getName());
125 	}
126 
127 	@Test
128 	public void resolveArgumentRawTypeFromParameterizedType() throws Exception {
129 		String content = "fruit=apple&vegetable=kale";
130 		this.servletRequest.setContent(content.getBytes("UTF-8"));
131 		this.servletRequest.setContentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
132 
133 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
134 		converters.add(new AllEncompassingFormHttpMessageConverter());
135 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
136 
137 		@SuppressWarnings("unchecked")
138 		MultiValueMap<String, String> result = (MultiValueMap<String, String>) processor.resolveArgument(
139 				paramMultiValueMap, mavContainer, webRequest, binderFactory);
140 
141 		assertNotNull(result);
142 		assertEquals("apple", result.getFirst("fruit"));
143 		assertEquals("kale", result.getFirst("vegetable"));
144 	}
145 
146 	@Test
147 	public void resolveArgumentClassJson() throws Exception {
148 		String content = "{\"name\" : \"Jad\"}";
149 		this.servletRequest.setContent(content.getBytes("UTF-8"));
150 		this.servletRequest.setContentType("application/json");
151 
152 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
153 		converters.add(new MappingJackson2HttpMessageConverter());
154 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
155 
156 		SimpleBean result = (SimpleBean) processor.resolveArgument(
157 				paramSimpleBean, mavContainer, webRequest, binderFactory);
158 
159 		assertNotNull(result);
160 		assertEquals("Jad", result.getName());
161 	}
162 
163 	@Test
164 	public void resolveArgumentClassString() throws Exception {
165 		String content = "foobarbaz";
166 		this.servletRequest.setContent(content.getBytes("UTF-8"));
167 		this.servletRequest.setContentType("application/json");
168 
169 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
170 		converters.add(new StringHttpMessageConverter());
171 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
172 
173 		String result = (String) processor.resolveArgument(
174 				paramString, mavContainer, webRequest, binderFactory);
175 
176 		assertNotNull(result);
177 		assertEquals("foobarbaz", result);
178 	}
179 
180 	@Test(expected = HttpMessageNotReadableException.class)  // SPR-9942
181 	public void resolveArgumentRequiredNoContent() throws Exception {
182 		this.servletRequest.setContent(new byte[0]);
183 		this.servletRequest.setContentType("text/plain");
184 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
185 		converters.add(new StringHttpMessageConverter());
186 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
187 		processor.resolveArgument(paramString, mavContainer, webRequest, binderFactory);
188 	}
189 
190 	@Test  // SPR-9964
191 	public void resolveArgumentTypeVariable() throws Exception {
192 		Method method = MyParameterizedController.class.getMethod("handleDto", Identifiable.class);
193 		HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method);
194 		MethodParameter methodParam = handlerMethod.getMethodParameters()[0];
195 
196 		String content = "{\"name\" : \"Jad\"}";
197 		this.servletRequest.setContent(content.getBytes("UTF-8"));
198 		this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
199 
200 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
201 		converters.add(new MappingJackson2HttpMessageConverter());
202 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
203 
204 		SimpleBean result = (SimpleBean) processor.resolveArgument(methodParam, mavContainer, webRequest, binderFactory);
205 
206 		assertNotNull(result);
207 		assertEquals("Jad", result.getName());
208 	}
209 
210 	@Test  // SPR-11225
211 	public void resolveArgumentTypeVariableWithNonGenericConverter() throws Exception {
212 		Method method = MyParameterizedController.class.getMethod("handleDto", Identifiable.class);
213 		HandlerMethod handlerMethod = new HandlerMethod(new MySimpleParameterizedController(), method);
214 		MethodParameter methodParam = handlerMethod.getMethodParameters()[0];
215 
216 		String content = "{\"name\" : \"Jad\"}";
217 		this.servletRequest.setContent(content.getBytes("UTF-8"));
218 		this.servletRequest.setContentType(MediaType.APPLICATION_JSON_VALUE);
219 
220 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
221 		HttpMessageConverter target = new MappingJackson2HttpMessageConverter();
222 		HttpMessageConverter proxy = ProxyFactory.getProxy(HttpMessageConverter.class, new SingletonTargetSource(target));
223 		converters.add(proxy);
224 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
225 
226 		SimpleBean result = (SimpleBean) processor.resolveArgument(methodParam, mavContainer, webRequest, binderFactory);
227 
228 		assertNotNull(result);
229 		assertEquals("Jad", result.getName());
230 	}
231 
232 	@Test  // SPR-9160
233 	public void handleReturnValueSortByQuality() throws Exception {
234 		this.servletRequest.addHeader("Accept", "text/plain; q=0.5, application/json");
235 
236 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
237 		converters.add(new MappingJackson2HttpMessageConverter());
238 		converters.add(new StringHttpMessageConverter());
239 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
240 
241 		processor.writeWithMessageConverters("Foo", returnTypeString, webRequest);
242 
243 		assertEquals("application/json;charset=UTF-8", servletResponse.getHeader("Content-Type"));
244 	}
245 
246 	@Test
247 	public void handleReturnValueString() throws Exception {
248 		List<HttpMessageConverter<?>>converters = new ArrayList<HttpMessageConverter<?>>();
249 		converters.add(new ByteArrayHttpMessageConverter());
250 		converters.add(new StringHttpMessageConverter());
251 
252 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
253 		processor.handleReturnValue("Foo", returnTypeString, mavContainer, webRequest);
254 
255 		assertEquals("text/plain;charset=ISO-8859-1", servletResponse.getHeader("Content-Type"));
256 		assertEquals("Foo", servletResponse.getContentAsString());
257 	}
258 
259 	@Test
260 	public void handleReturnValueStringAcceptCharset() throws Exception {
261 		this.servletRequest.addHeader("Accept", "text/plain;charset=UTF-8");
262 
263 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
264 		converters.add(new ByteArrayHttpMessageConverter());
265 		converters.add(new StringHttpMessageConverter());
266 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
267 
268 		processor.writeWithMessageConverters("Foo", returnTypeString, webRequest);
269 
270 		assertEquals("text/plain;charset=UTF-8", servletResponse.getHeader("Content-Type"));
271 	}
272 
273 	@Test
274 	public void supportsReturnTypeResponseBodyOnType() throws Exception {
275 		Method method = ResponseBodyController.class.getMethod("handle");
276 		MethodParameter returnType = new MethodParameter(method, -1);
277 
278 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
279 		converters.add(new StringHttpMessageConverter());
280 
281 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
282 
283 		assertTrue("Failed to recognize type-level @ResponseBody", processor.supportsReturnType(returnType));
284 	}
285 
286 	@Test
287 	public void supportsReturnTypeRestController() throws Exception {
288 		Method method = TestRestController.class.getMethod("handle");
289 		MethodParameter returnType = new MethodParameter(method, -1);
290 
291 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
292 		converters.add(new StringHttpMessageConverter());
293 
294 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(converters);
295 
296 		assertTrue("Failed to recognize type-level @RestController", processor.supportsReturnType(returnType));
297 	}
298 
299 	@Test
300 	public void jacksonJsonViewWithResponseBodyAndJsonMessageConverter() throws Exception {
301 		Method method = JacksonViewController.class.getMethod("handleResponseBody");
302 		HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
303 		MethodParameter methodReturnType = handlerMethod.getReturnType();
304 
305 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
306 		converters.add(new MappingJackson2HttpMessageConverter());
307 
308 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
309 				converters, null, Arrays.asList(new JsonViewResponseBodyAdvice()));
310 
311 		Object returnValue = new JacksonViewController().handleResponseBody();
312 		processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
313 
314 		String content = this.servletResponse.getContentAsString();
315 		assertFalse(content.contains("\"withView1\":\"with\""));
316 		assertTrue(content.contains("\"withView2\":\"with\""));
317 		assertFalse(content.contains("\"withoutView\":\"without\""));
318 	}
319 
320 	@Test
321 	public void jacksonJsonViewWithResponseEntityAndJsonMessageConverter() throws Exception {
322 		Method method = JacksonViewController.class.getMethod("handleResponseEntity");
323 		HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
324 		MethodParameter methodReturnType = handlerMethod.getReturnType();
325 
326 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
327 		converters.add(new MappingJackson2HttpMessageConverter());
328 
329 		HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
330 				converters, null, Arrays.asList(new JsonViewResponseBodyAdvice()));
331 
332 		Object returnValue = new JacksonViewController().handleResponseEntity();
333 		processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
334 
335 		String content = this.servletResponse.getContentAsString();
336 		assertFalse(content.contains("\"withView1\":\"with\""));
337 		assertTrue(content.contains("\"withView2\":\"with\""));
338 		assertFalse(content.contains("\"withoutView\":\"without\""));
339 	}
340 
341 	@Test  // SPR-12149
342 	public void jacksonJsonViewWithResponseBodyAndXmlMessageConverter() throws Exception {
343 		Method method = JacksonViewController.class.getMethod("handleResponseBody");
344 		HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
345 		MethodParameter methodReturnType = handlerMethod.getReturnType();
346 
347 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
348 		converters.add(new MappingJackson2XmlHttpMessageConverter());
349 
350 		RequestResponseBodyMethodProcessor processor = new RequestResponseBodyMethodProcessor(
351 				converters, null, Arrays.asList(new JsonViewResponseBodyAdvice()));
352 
353 		Object returnValue = new JacksonViewController().handleResponseBody();
354 		processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
355 
356 		String content = this.servletResponse.getContentAsString();
357 		assertFalse(content.contains("<withView1>with</withView1>"));
358 		assertTrue(content.contains("<withView2>with</withView2>"));
359 		assertFalse(content.contains("<withoutView>without</withoutView>"));
360 	}
361 
362 	// SPR-12149
363 
364 	@Test
365 	public void jacksonJsonViewWithResponseEntityAndXmlMessageConverter() throws Exception {
366 		Method method = JacksonViewController.class.getMethod("handleResponseEntity");
367 		HandlerMethod handlerMethod = new HandlerMethod(new JacksonViewController(), method);
368 		MethodParameter methodReturnType = handlerMethod.getReturnType();
369 
370 		List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
371 		converters.add(new MappingJackson2XmlHttpMessageConverter());
372 
373 		HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(
374 				converters, null, Arrays.asList(new JsonViewResponseBodyAdvice()));
375 
376 		Object returnValue = new JacksonViewController().handleResponseEntity();
377 		processor.handleReturnValue(returnValue, methodReturnType, this.mavContainer, this.webRequest);
378 
379 		String content = this.servletResponse.getContentAsString();
380 		assertFalse(content.contains("<withView1>with</withView1>"));
381 		assertTrue(content.contains("<withView2>with</withView2>"));
382 		assertFalse(content.contains("<withoutView>without</withoutView>"));
383 	}
384 
385 
386 	public String handle(
387 			@RequestBody List<SimpleBean> list,
388 			@RequestBody SimpleBean simpleBean,
389 			@RequestBody MultiValueMap<String, String> multiValueMap,
390 			@RequestBody String string) {
391 
392 		return null;
393 	}
394 
395 
396 	private static abstract class MyParameterizedController<DTO extends Identifiable> {
397 
398 		@SuppressWarnings("unused")
399 		public void handleDto(@RequestBody DTO dto) {}
400 	}
401 
402 
403 	private static class MySimpleParameterizedController extends MyParameterizedController<SimpleBean> {
404 	}
405 
406 
407 	private interface Identifiable extends Serializable {
408 
409 		public Long getId();
410 
411 		public void setId(Long id);
412 	}
413 
414 
415 	@SuppressWarnings({ "serial" })
416 	private static class SimpleBean implements Identifiable {
417 
418 		private Long id;
419 
420 		private String name;
421 
422 		@Override
423 		public Long getId() {
424 			return id;
425 		}
426 
427 		@Override
428 		public void setId(Long id) {
429 			this.id = id;
430 		}
431 
432 		public String getName() {
433 			return name;
434 		}
435 
436 		@SuppressWarnings("unused")
437 		public void setName(String name) {
438 			this.name = name;
439 		}
440 	}
441 
442 
443 	private final class ValidatingBinderFactory implements WebDataBinderFactory {
444 
445 		@Override
446 		public WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception {
447 			LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
448 			validator.afterPropertiesSet();
449 			WebDataBinder dataBinder = new WebDataBinder(target, objectName);
450 			dataBinder.setValidator(validator);
451 			return dataBinder;
452 		}
453 	}
454 
455 
456 	@ResponseBody
457 	private static class ResponseBodyController {
458 
459 		@RequestMapping
460 		public String handle() {
461 			return "hello";
462 		}
463 	}
464 
465 
466 	@RestController
467 	private static class TestRestController {
468 
469 		@RequestMapping
470 		public String handle() {
471 			return "hello";
472 		}
473 	}
474 
475 	private interface MyJacksonView1 {};
476 	private interface MyJacksonView2 {};
477 
478 	private static class JacksonViewBean {
479 
480 		@JsonView(MyJacksonView1.class)
481 		private String withView1;
482 
483 		@JsonView(MyJacksonView2.class)
484 		private String withView2;
485 
486 		private String withoutView;
487 
488 		public String getWithView1() {
489 			return withView1;
490 		}
491 
492 		public void setWithView1(String withView1) {
493 			this.withView1 = withView1;
494 		}
495 
496 		public String getWithView2() {
497 			return withView2;
498 		}
499 
500 		public void setWithView2(String withView2) {
501 			this.withView2 = withView2;
502 		}
503 
504 		public String getWithoutView() {
505 			return withoutView;
506 		}
507 
508 		public void setWithoutView(String withoutView) {
509 			this.withoutView = withoutView;
510 		}
511 	}
512 
513 	private static class JacksonViewController {
514 
515 		@RequestMapping
516 		@ResponseBody
517 		@JsonView(MyJacksonView2.class)
518 		public JacksonViewBean handleResponseBody() {
519 			JacksonViewBean bean = new JacksonViewBean();
520 			bean.setWithView1("with");
521 			bean.setWithView2("with");
522 			bean.setWithoutView("without");
523 			return bean;
524 		}
525 
526 		@RequestMapping
527 		@JsonView(MyJacksonView2.class)
528 		public ResponseEntity<JacksonViewBean> handleResponseEntity() {
529 			JacksonViewBean bean = new JacksonViewBean();
530 			bean.setWithView1("with");
531 			bean.setWithView2("with");
532 			bean.setWithoutView("without");
533 			ModelAndView mav = new ModelAndView(new MappingJackson2JsonView());
534 			mav.addObject("bean", bean);
535 			return new ResponseEntity<JacksonViewBean>(bean, HttpStatus.OK);
536 		}
537 
538 	}
539 
540 }