View Javadoc
1   /*
2    * Copyright 2002-2012 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.lang.reflect.Method;
20  import java.text.SimpleDateFormat;
21  import java.util.Arrays;
22  import java.util.Collection;
23  import java.util.Date;
24  
25  import org.junit.Test;
26  import org.junit.runner.RunWith;
27  import org.junit.runners.Parameterized;
28  import org.junit.runners.Parameterized.Parameters;
29  
30  import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
31  import org.springframework.aop.interceptor.SimpleTraceInterceptor;
32  import org.springframework.aop.support.DefaultPointcutAdvisor;
33  import org.springframework.aop.support.StaticMethodMatcherPointcut;
34  import org.springframework.beans.factory.support.RootBeanDefinition;
35  import org.springframework.beans.propertyeditors.CustomDateEditor;
36  import org.springframework.core.annotation.AnnotationUtils;
37  import org.springframework.mock.web.test.MockHttpServletRequest;
38  import org.springframework.mock.web.test.MockHttpServletResponse;
39  import org.springframework.stereotype.Controller;
40  import org.springframework.ui.Model;
41  import org.springframework.web.bind.WebDataBinder;
42  import org.springframework.web.bind.annotation.ExceptionHandler;
43  import org.springframework.web.bind.annotation.InitBinder;
44  import org.springframework.web.bind.annotation.ModelAttribute;
45  import org.springframework.web.bind.annotation.RequestHeader;
46  import org.springframework.web.bind.annotation.RequestMapping;
47  import org.springframework.web.bind.annotation.RequestMethod;
48  import org.springframework.web.bind.annotation.RequestParam;
49  import org.springframework.web.bind.annotation.ResponseBody;
50  import org.springframework.web.context.support.GenericWebApplicationContext;
51  import org.springframework.web.servlet.HandlerExecutionChain;
52  import org.springframework.web.servlet.ModelAndView;
53  
54  import static org.junit.Assert.*;
55  
56  /**
57   * Test various scenarios for detecting method-level and method parameter annotations depending
58   * on where they are located -- on interfaces, parent classes, in parameterized methods, or in
59   * combination with proxies.
60   *
61   * @author Rossen Stoyanchev
62   */
63  @RunWith(Parameterized.class)
64  public class HandlerMethodAnnotationDetectionTests {
65  
66  	@Parameters
67  	public static Collection<Object[]> handlerTypes() {
68  		Object[][] array = new Object[12][2];
69  
70  		array[0] = new Object[] { SimpleController.class, true};  // CGLib proxy
71  		array[1] = new Object[] { SimpleController.class, false};
72  
73  		array[2] = new Object[] { AbstractClassController.class, true };	// CGLib proxy
74  		array[3] = new Object[] { AbstractClassController.class, false };
75  
76  		array[4] = new Object[] { ParameterizedAbstractClassController.class, false}; // CGLib proxy
77  		array[5] = new Object[] { ParameterizedAbstractClassController.class, false};
78  
79  		array[6] = new Object[] { InterfaceController.class, true };	// JDK dynamic proxy
80  		array[7] = new Object[] { InterfaceController.class, false };
81  
82  		array[8] = new Object[] { ParameterizedInterfaceController.class, false}; // no AOP
83  		array[9] = new Object[] { ParameterizedInterfaceController.class, false};
84  
85  		array[10] = new Object[] { SupportClassController.class, true};  // CGLib proxy
86  		array[11] = new Object[] { SupportClassController.class, false};
87  
88  		return Arrays.asList(array);
89  	}
90  
91  	private RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping();
92  
93  	private RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter();
94  
95  	private ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver();
96  
97  	public HandlerMethodAnnotationDetectionTests(final Class<?> controllerType, boolean useAutoProxy) {
98  		GenericWebApplicationContext context = new GenericWebApplicationContext();
99  		context.registerBeanDefinition("controller", new RootBeanDefinition(controllerType));
100 		context.registerBeanDefinition("handlerMapping", new RootBeanDefinition(RequestMappingHandlerMapping.class));
101 		context.registerBeanDefinition("handlerAdapter", new RootBeanDefinition(RequestMappingHandlerAdapter.class));
102 		context.registerBeanDefinition("exceptionResolver", new RootBeanDefinition(ExceptionHandlerExceptionResolver.class));
103 		if (useAutoProxy) {
104 			DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
105 			autoProxyCreator.setBeanFactory(context.getBeanFactory());
106 			context.getBeanFactory().addBeanPostProcessor(autoProxyCreator);
107 			context.registerBeanDefinition("controllerAdvice", new RootBeanDefinition(ControllerAdvisor.class));
108 		}
109 		context.refresh();
110 
111 		this.handlerMapping = context.getBean(RequestMappingHandlerMapping.class);
112 		this.handlerAdapter = context.getBean(RequestMappingHandlerAdapter.class);
113 		this.exceptionResolver = context.getBean(ExceptionHandlerExceptionResolver.class);
114 	}
115 
116 	class TestPointcut extends StaticMethodMatcherPointcut {
117 		@Override
118 		public boolean matches(Method method, Class<?> clazz) {
119 			return method.getName().equals("hashCode");
120 		}
121 	}
122 
123 	@Test
124 	public void testRequestMappingMethod() throws Exception {
125 		String datePattern = "MM:dd:yyyy";
126 		SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern);
127 		String dateA = "11:01:2011";
128 		String dateB = "11:02:2011";
129 
130 		MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path1/path2");
131 		request.setParameter("datePattern", datePattern);
132 		request.addHeader("header1", dateA);
133 		request.addHeader("header2", dateB);
134 
135 		HandlerExecutionChain chain = handlerMapping.getHandler(request);
136 		assertNotNull(chain);
137 
138 		ModelAndView mav = handlerAdapter.handle(request, new MockHttpServletResponse(), chain.getHandler());
139 
140 		assertEquals(mav.getModel().get("attr1"), dateFormat.parse(dateA));
141 		assertEquals(mav.getModel().get("attr2"), dateFormat.parse(dateB));
142 
143 		MockHttpServletResponse response = new MockHttpServletResponse();
144 		exceptionResolver.resolveException(request, response, chain.getHandler(), new Exception("failure"));
145 		assertEquals("text/plain;charset=ISO-8859-1", response.getHeader("Content-Type"));
146 		assertEquals("failure", response.getContentAsString());
147 	}
148 
149 
150 	/**
151 	 * SIMPLE CASE
152 	 */
153 	@Controller
154 	static class SimpleController {
155 
156 		@InitBinder
157 		public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String pattern) {
158 			CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(pattern), false);
159 			dataBinder.registerCustomEditor(Date.class, dateEditor);
160 		}
161 
162 		@ModelAttribute
163 		public void initModel(@RequestHeader("header1") Date date, Model model) {
164 			model.addAttribute("attr1", date);
165 		}
166 
167 		@RequestMapping(value="/path1/path2", method=RequestMethod.POST)
168 		@ModelAttribute("attr2")
169 		public Date handle(@RequestHeader("header2") Date date) throws Exception {
170 			return date;
171 		}
172 
173 		@ExceptionHandler(Exception.class)
174 		@ResponseBody
175 		public String handleException(Exception exception) {
176 			return exception.getMessage();
177 		}
178 	}
179 
180 
181 	@Controller
182 	static abstract class MappingAbstractClass {
183 
184 		@InitBinder
185 		public abstract void initBinder(WebDataBinder dataBinder, String pattern);
186 
187 		@ModelAttribute
188 		public abstract void initModel(Date date, Model model);
189 
190 		@RequestMapping(value="/path1/path2", method=RequestMethod.POST)
191 		@ModelAttribute("attr2")
192 		public abstract Date handle(Date date, Model model) throws Exception;
193 
194 		@ExceptionHandler(Exception.class)
195 		@ResponseBody
196 		public abstract String handleException(Exception exception);
197 	}
198 
199 	/**
200 	 * CONTROLLER WITH ABSTRACT CLASS
201 	 *
202 	 * <p>All annotations can be on methods in the abstract class except parameter annotations.
203 	 */
204 	static class AbstractClassController extends MappingAbstractClass {
205 
206 		@Override
207 		public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String pattern) {
208 			CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(pattern), false);
209 			dataBinder.registerCustomEditor(Date.class, dateEditor);
210 		}
211 
212 		@Override
213 		public void initModel(@RequestHeader("header1") Date date, Model model) {
214 			model.addAttribute("attr1", date);
215 		}
216 
217 		@Override
218 		public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception {
219 			return date;
220 		}
221 
222 		@Override
223 		public String handleException(Exception exception) {
224 			return exception.getMessage();
225 		}
226 	}
227 
228 	// SPR-9374
229 
230 	@RequestMapping
231 	static interface MappingInterface {
232 
233 		@InitBinder
234 		void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern);
235 
236 		@ModelAttribute
237 		void initModel(@RequestHeader("header1") Date date, Model model);
238 
239 		@RequestMapping(value="/path1/path2", method=RequestMethod.POST)
240 		@ModelAttribute("attr2")
241 		Date handle(@RequestHeader("header2") Date date, Model model) throws Exception;
242 
243 		@ExceptionHandler(Exception.class)
244 		@ResponseBody
245 		String handleException(Exception exception);
246 	}
247 
248 	/**
249 	 * CONTROLLER WITH INTERFACE
250 	 *
251 	 * JDK Dynamic proxy:
252 	 * All annotations must be on the interface.
253 	 *
254 	 * Without AOP:
255 	 * Annotations can be on interface methods except parameter annotations.
256 	 */
257 	static class InterfaceController implements MappingInterface {
258 
259 		@Override
260 		public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) {
261 			CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false);
262 			dataBinder.registerCustomEditor(Date.class, dateEditor);
263 		}
264 
265 		@Override
266 		public void initModel(@RequestHeader("header1") Date date, Model model) {
267 			model.addAttribute("attr1", date);
268 		}
269 
270 		@Override
271 		public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception {
272 			return date;
273 		}
274 
275 		@Override
276 		public String handleException(Exception exception) {
277 			return exception.getMessage();
278 		}
279 	}
280 
281 
282 	@Controller
283 	static abstract class MappingParameterizedAbstractClass<A, B, C> {
284 
285 		@InitBinder
286 		public abstract void initBinder(WebDataBinder dataBinder, A thePattern);
287 
288 		@ModelAttribute
289 		public abstract void initModel(B date, Model model);
290 
291 		@RequestMapping(value="/path1/path2", method=RequestMethod.POST)
292 		@ModelAttribute("attr2")
293 		public abstract Date handle(C date, Model model) throws Exception;
294 
295 		@ExceptionHandler(Exception.class)
296 		@ResponseBody
297 		public abstract String handleException(Exception exception);
298 	}
299 
300 	/**
301 	 * CONTROLLER WITH PARAMETERIZED BASE CLASS
302 	 *
303 	 * <p>All annotations can be on methods in the abstract class except parameter annotations.
304 	 */
305 	static class ParameterizedAbstractClassController extends MappingParameterizedAbstractClass<String, Date, Date> {
306 
307 		@Override
308 		public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) {
309 			CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false);
310 			dataBinder.registerCustomEditor(Date.class, dateEditor);
311 		}
312 
313 		@Override
314 		public void initModel(@RequestHeader("header1") Date date, Model model) {
315 			model.addAttribute("attr1", date);
316 		}
317 
318 		@Override
319 		public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception {
320 			return date;
321 		}
322 
323 		@Override
324 		public String handleException(Exception exception) {
325 			return exception.getMessage();
326 		}
327 	}
328 
329 	@RequestMapping
330 	static interface MappingParameterizedInterface<A, B, C> {
331 
332 		@InitBinder
333 		void initBinder(WebDataBinder dataBinder, A thePattern);
334 
335 		@ModelAttribute
336 		void initModel(B date, Model model);
337 
338 		@RequestMapping(value="/path1/path2", method=RequestMethod.POST)
339 		@ModelAttribute("attr2")
340 		Date handle(C date, Model model) throws Exception;
341 
342 		@ExceptionHandler(Exception.class)
343 		@ResponseBody
344 		String handleException(Exception exception);
345 	}
346 
347 	/**
348 	 * CONTROLLER WITH PARAMETERIZED INTERFACE
349 	 *
350 	 * <p>All annotations can be on interface except parameter annotations.
351 	 *
352 	 * <p>Cannot be used as JDK dynamic proxy since parameterized interface does not contain type information.
353 	 */
354 	static class ParameterizedInterfaceController implements MappingParameterizedInterface<String, Date, Date> {
355 
356 		@Override
357 		@InitBinder
358 		public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) {
359 			CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false);
360 			dataBinder.registerCustomEditor(Date.class, dateEditor);
361 		}
362 
363 		@Override
364 		@ModelAttribute
365 		public void initModel(@RequestHeader("header1") Date date, Model model) {
366 			model.addAttribute("attr1", date);
367 		}
368 
369 		@Override
370 		@RequestMapping(value="/path1/path2", method=RequestMethod.POST)
371 		@ModelAttribute("attr2")
372 		public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception {
373 			return date;
374 		}
375 
376 		@Override
377 		@ExceptionHandler(Exception.class)
378 		@ResponseBody
379 		public String handleException(Exception exception) {
380 			return exception.getMessage();
381 		}
382 	}
383 
384 
385 	/**
386 	 * SPR-8248
387 	 *
388 	 * <p>Support class contains all annotations. Subclass has type-level @{@link RequestMapping}.
389 	 */
390 	@Controller
391 	static class MappingSupportClass {
392 
393 		@InitBinder
394 		public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) {
395 			CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false);
396 			dataBinder.registerCustomEditor(Date.class, dateEditor);
397 		}
398 
399 		@ModelAttribute
400 		public void initModel(@RequestHeader("header1") Date date, Model model) {
401 			model.addAttribute("attr1", date);
402 		}
403 
404 		@RequestMapping(value="/path2", method=RequestMethod.POST)
405 		@ModelAttribute("attr2")
406 		public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception {
407 			return date;
408 		}
409 
410 		@ExceptionHandler(Exception.class)
411 		@ResponseBody
412 		public String handleException(Exception exception) {
413 			return exception.getMessage();
414 		}
415 	}
416 
417 	@Controller
418 	@RequestMapping("/path1")
419 	static class SupportClassController extends MappingSupportClass {
420 	}
421 
422 
423 	@SuppressWarnings("serial")
424 	static class ControllerAdvisor extends DefaultPointcutAdvisor {
425 
426 		public ControllerAdvisor() {
427 			super(getControllerPointcut(), new SimpleTraceInterceptor());
428 		}
429 
430 		private static StaticMethodMatcherPointcut getControllerPointcut() {
431 			return new StaticMethodMatcherPointcut() {
432 				@Override
433 				public boolean matches(Method method, Class<?> targetClass) {
434 					return ((AnnotationUtils.findAnnotation(targetClass, Controller.class) != null) ||
435 							(AnnotationUtils.findAnnotation(targetClass, RequestMapping.class) != null));
436 				}
437 			};
438 		}
439 	}
440 
441 }