/* * Copyright 2002-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.servlet.mvc.method.annotation; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.aop.interceptor.SimpleTraceInterceptor; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.StaticMethodMatcherPointcut; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.propertyeditors.CustomDateEditor; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.mock.web.test.MockHttpServletRequest; import org.springframework.mock.web.test.MockHttpServletResponse; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.servlet.HandlerExecutionChain; import org.springframework.web.servlet.ModelAndView; import static org.junit.Assert.*; /** * Test various scenarios for detecting method-level and method parameter annotations depending * on where they are located -- on interfaces, parent classes, in parameterized methods, or in * combination with proxies. * * @author Rossen Stoyanchev * @author Sam Brannen */ @RunWith(Parameterized.class) public class HandlerMethodAnnotationDetectionTests { @Parameters(name = "controller [{0}], auto-proxy [{1}]") public static Object[][] handlerTypes() { return new Object[][] { { SimpleController.class, true }, // CGLib proxy { SimpleController.class, false }, { AbstractClassController.class, true }, // CGLib proxy { AbstractClassController.class, false }, { ParameterizedAbstractClassController.class, true }, // CGLib proxy { ParameterizedAbstractClassController.class, false }, { ParameterizedSubclassOverridesDefaultMappings.class, true }, // CGLib proxy { ParameterizedSubclassOverridesDefaultMappings.class, false }, // TODO [SPR-9517] Enable ParameterizedSubclassDoesNotOverrideConcreteImplementationsFromGenericAbstractSuperclass test cases // { ParameterizedSubclassDoesNotOverrideConcreteImplementationsFromGenericAbstractSuperclass.class, true }, // CGLib proxy // { ParameterizedSubclassDoesNotOverrideConcreteImplementationsFromGenericAbstractSuperclass.class, false }, { InterfaceController.class, true }, // JDK dynamic proxy { InterfaceController.class, false }, { ParameterizedInterfaceController.class, false }, // no AOP { SupportClassController.class, true }, // CGLib proxy { SupportClassController.class, false } }; } private RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping(); private RequestMappingHandlerAdapter handlerAdapter = new RequestMappingHandlerAdapter(); private ExceptionHandlerExceptionResolver exceptionResolver = new ExceptionHandlerExceptionResolver(); public HandlerMethodAnnotationDetectionTests(final Class<?> controllerType, boolean useAutoProxy) { GenericWebApplicationContext context = new GenericWebApplicationContext(); context.registerBeanDefinition("controller", new RootBeanDefinition(controllerType)); context.registerBeanDefinition("handlerMapping", new RootBeanDefinition(RequestMappingHandlerMapping.class)); context.registerBeanDefinition("handlerAdapter", new RootBeanDefinition(RequestMappingHandlerAdapter.class)); context.registerBeanDefinition("exceptionResolver", new RootBeanDefinition(ExceptionHandlerExceptionResolver.class)); if (useAutoProxy) { DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setBeanFactory(context.getBeanFactory()); context.getBeanFactory().addBeanPostProcessor(autoProxyCreator); context.registerBeanDefinition("controllerAdvice", new RootBeanDefinition(ControllerAdvisor.class)); } context.refresh(); this.handlerMapping = context.getBean(RequestMappingHandlerMapping.class); this.handlerAdapter = context.getBean(RequestMappingHandlerAdapter.class); this.exceptionResolver = context.getBean(ExceptionHandlerExceptionResolver.class); context.close(); } class TestPointcut extends StaticMethodMatcherPointcut { @Override public boolean matches(Method method, Class<?> clazz) { return method.getName().equals("hashCode"); } } @Test public void testRequestMappingMethod() throws Exception { String datePattern = "MM:dd:yyyy"; SimpleDateFormat dateFormat = new SimpleDateFormat(datePattern); String dateA = "11:01:2011"; String dateB = "11:02:2011"; MockHttpServletRequest request = new MockHttpServletRequest("POST", "/path1/path2"); request.setParameter("datePattern", datePattern); request.addHeader("header1", dateA); request.addHeader("header2", dateB); HandlerExecutionChain chain = handlerMapping.getHandler(request); assertNotNull(chain); ModelAndView mav = handlerAdapter.handle(request, new MockHttpServletResponse(), chain.getHandler()); assertEquals("model attr1:", dateFormat.parse(dateA), mav.getModel().get("attr1")); assertEquals("model attr2:", dateFormat.parse(dateB), mav.getModel().get("attr2")); MockHttpServletResponse response = new MockHttpServletResponse(); exceptionResolver.resolveException(request, response, chain.getHandler(), new Exception("failure")); assertEquals("text/plain;charset=ISO-8859-1", response.getHeader("Content-Type")); assertEquals("failure", response.getContentAsString()); } /** * SIMPLE CASE */ @Controller static class SimpleController { @InitBinder public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String pattern) { CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(pattern), false); dataBinder.registerCustomEditor(Date.class, dateEditor); } @ModelAttribute public void initModel(@RequestHeader("header1") Date date, Model model) { model.addAttribute("attr1", date); } @RequestMapping(value="/path1/path2", method=RequestMethod.POST) @ModelAttribute("attr2") public Date handle(@RequestHeader("header2") Date date) throws Exception { return date; } @ExceptionHandler(Exception.class) @ResponseBody public String handleException(Exception exception) { return exception.getMessage(); } } @Controller static abstract class MappingAbstractClass { @InitBinder public abstract void initBinder(WebDataBinder dataBinder, String pattern); @ModelAttribute public abstract void initModel(Date date, Model model); @RequestMapping(value="/path1/path2", method=RequestMethod.POST) @ModelAttribute("attr2") public abstract Date handle(Date date, Model model) throws Exception; @ExceptionHandler(Exception.class) @ResponseBody public abstract String handleException(Exception exception); } /** * CONTROLLER WITH ABSTRACT CLASS * * <p>All annotations can be on methods in the abstract class except parameter annotations. */ static class AbstractClassController extends MappingAbstractClass { @Override public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String pattern) { CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(pattern), false); dataBinder.registerCustomEditor(Date.class, dateEditor); } @Override public void initModel(@RequestHeader("header1") Date date, Model model) { model.addAttribute("attr1", date); } @Override public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception { return date; } @Override public String handleException(Exception exception) { return exception.getMessage(); } } // SPR-9374 @RequestMapping static interface MappingInterface { @InitBinder void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern); @ModelAttribute void initModel(@RequestHeader("header1") Date date, Model model); @RequestMapping(value="/path1/path2", method=RequestMethod.POST) @ModelAttribute("attr2") Date handle(@RequestHeader("header2") Date date, Model model) throws Exception; @ExceptionHandler(Exception.class) @ResponseBody String handleException(Exception exception); } /** * CONTROLLER WITH INTERFACE * * JDK Dynamic proxy: * All annotations must be on the interface. * * Without AOP: * Annotations can be on interface methods except parameter annotations. */ static class InterfaceController implements MappingInterface { @Override public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) { CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false); dataBinder.registerCustomEditor(Date.class, dateEditor); } @Override public void initModel(@RequestHeader("header1") Date date, Model model) { model.addAttribute("attr1", date); } @Override public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception { return date; } @Override public String handleException(Exception exception) { return exception.getMessage(); } } @Controller static abstract class MappingGenericAbstractClass<A, B, C> { @InitBinder public abstract void initBinder(WebDataBinder dataBinder, A thePattern); @ModelAttribute public abstract void initModel(B date, Model model); @RequestMapping(value="/path1/path2", method=RequestMethod.POST) @ModelAttribute("attr2") public abstract Date handle(C date, Model model) throws Exception; @ExceptionHandler(Exception.class) @ResponseBody public abstract String handleException(Exception exception); } /** * CONTROLLER WITH PARAMETERIZED BASE CLASS * * <p>All annotations can be on methods in the abstract class except parameter annotations. */ static class ParameterizedAbstractClassController extends MappingGenericAbstractClass<String, Date, Date> { @Override public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) { CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false); dataBinder.registerCustomEditor(Date.class, dateEditor); } @Override public void initModel(@RequestHeader("header1") Date date, Model model) { model.addAttribute("attr1", date); } @Override public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception { return date; } @Override public String handleException(Exception exception) { return exception.getMessage(); } } @Controller static abstract class MappedGenericAbstractClassWithConcreteImplementations<A, B, C> { @InitBinder public abstract void initBinder(WebDataBinder dataBinder, A thePattern); @ModelAttribute public abstract void initModel(B date, Model model); @RequestMapping(value = "/path1/path2", method = RequestMethod.POST) @ModelAttribute("attr2") public Date handle(C date, Model model) throws Exception { return (Date) date; } @ExceptionHandler(Exception.class) @ResponseBody public abstract String handleException(Exception exception); } static class ParameterizedSubclassDoesNotOverrideConcreteImplementationsFromGenericAbstractSuperclass extends MappedGenericAbstractClassWithConcreteImplementations<String, Date, Date> { @Override public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) { CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false); dataBinder.registerCustomEditor(Date.class, dateEditor); } @Override public void initModel(@RequestHeader("header1") Date date, Model model) { model.addAttribute("attr1", date); } // does not override handle() @Override public String handleException(Exception exception) { return exception.getMessage(); } } @Controller static abstract class GenericAbstractClassDeclaresDefaultMappings<A, B, C> { @InitBinder public abstract void initBinder(WebDataBinder dataBinder, A thePattern); @ModelAttribute public abstract void initModel(B date, Model model); // /foo/bar should be overridden in concrete subclass @RequestMapping(value = "/foo/bar", method = RequestMethod.POST) // attrFoo should be overridden in concrete subclass @ModelAttribute("attrFoo") public abstract Date handle(C date, Model model) throws Exception; @ExceptionHandler(Exception.class) @ResponseBody public abstract String handleException(Exception exception); } static class ParameterizedSubclassOverridesDefaultMappings extends GenericAbstractClassDeclaresDefaultMappings<String, Date, Date> { @Override public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) { CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false); dataBinder.registerCustomEditor(Date.class, dateEditor); } @Override public void initModel(@RequestHeader("header1") Date date, Model model) { model.addAttribute("attr1", date); } @Override @RequestMapping(value = "/path1/path2", method = RequestMethod.POST) // NOTE: @ModelAttribute will NOT be found on the abstract superclass if // @RequestMapping is declared locally. Thus, we have to redeclare // @ModelAttribute locally as well. @ModelAttribute("attr2") public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception { return date; } @Override public String handleException(Exception exception) { return exception.getMessage(); } } @RequestMapping static interface MappingGenericInterface<A, B, C> { @InitBinder void initBinder(WebDataBinder dataBinder, A thePattern); @ModelAttribute void initModel(B date, Model model); @RequestMapping(value="/path1/path2", method=RequestMethod.POST) @ModelAttribute("attr2") Date handle(C date, Model model) throws Exception; @ExceptionHandler(Exception.class) @ResponseBody String handleException(Exception exception); } /** * CONTROLLER WITH PARAMETERIZED INTERFACE * * <p>All annotations can be on interface except parameter annotations. * * <p>Cannot be used as JDK dynamic proxy since parameterized interface does not contain type information. */ static class ParameterizedInterfaceController implements MappingGenericInterface<String, Date, Date> { @Override @InitBinder public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) { CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false); dataBinder.registerCustomEditor(Date.class, dateEditor); } @Override @ModelAttribute public void initModel(@RequestHeader("header1") Date date, Model model) { model.addAttribute("attr1", date); } @Override @RequestMapping(value="/path1/path2", method=RequestMethod.POST) @ModelAttribute("attr2") public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception { return date; } @Override @ExceptionHandler(Exception.class) @ResponseBody public String handleException(Exception exception) { return exception.getMessage(); } } /** * SPR-8248 * * <p>Support class contains all annotations. Subclass has type-level @{@link RequestMapping}. */ @Controller static class MappingSupportClass { @InitBinder public void initBinder(WebDataBinder dataBinder, @RequestParam("datePattern") String thePattern) { CustomDateEditor dateEditor = new CustomDateEditor(new SimpleDateFormat(thePattern), false); dataBinder.registerCustomEditor(Date.class, dateEditor); } @ModelAttribute public void initModel(@RequestHeader("header1") Date date, Model model) { model.addAttribute("attr1", date); } @RequestMapping(value="/path2", method=RequestMethod.POST) @ModelAttribute("attr2") public Date handle(@RequestHeader("header2") Date date, Model model) throws Exception { return date; } @ExceptionHandler(Exception.class) @ResponseBody public String handleException(Exception exception) { return exception.getMessage(); } } @Controller @RequestMapping("/path1") static class SupportClassController extends MappingSupportClass { } @SuppressWarnings("serial") static class ControllerAdvisor extends DefaultPointcutAdvisor { public ControllerAdvisor() { super(getControllerPointcut(), new SimpleTraceInterceptor()); } private static StaticMethodMatcherPointcut getControllerPointcut() { return new StaticMethodMatcherPointcut() { @Override public boolean matches(Method method, Class<?> targetClass) { return ((AnnotationUtils.findAnnotation(targetClass, Controller.class) != null) || (AnnotationUtils.findAnnotation(targetClass, RequestMapping.class) != null)); } }; } } }