From d10d95e98d816ace715edd73b6adad402bed2ee8 Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Tue, 5 May 2009 11:40:36 +0000 Subject: [PATCH] SPR-5426 - Allow for custom processing or result objects returned from handler/controller methods git-svn-id: https://src.springframework.org/svn/spring-framework/trunk@1093 50f2f4bb-b051-0410-bef5-90022cba6387 --- .../AnnotationMethodHandlerAdapter.java | 46 +++++++-- .../PortletAnnotationControllerTests.java | 62 +++++++++++- .../AnnotationMethodHandlerAdapter.java | 32 ++++++- .../mvc/annotation/ModelAndViewResolver.java | 61 ++++++++++++ .../ServletAnnotationControllerTests.java | 95 +++++++++++++++---- 5 files changed, 270 insertions(+), 26 deletions(-) create mode 100644 org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ModelAndViewResolver.java diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index 67e71b3cdeb..4304cc88d93 100644 --- a/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.portlet/src/main/java/org/springframework/web/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -93,6 +93,7 @@ import org.springframework.web.portlet.handler.PortletContentGenerator; import org.springframework.web.portlet.handler.PortletSessionRequiredException; import org.springframework.web.portlet.util.PortletUtils; import org.springframework.web.servlet.View; +import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver; /** * Implementation of the {@link org.springframework.web.portlet.HandlerAdapter} @@ -131,6 +132,8 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl private WebArgumentResolver[] customArgumentResolvers; + private ModelAndViewResolver[] customModelAndViewResolvers; + private final Map, PortletHandlerMethodResolver> methodResolverCache = new ConcurrentHashMap, PortletHandlerMethodResolver>(); @@ -199,8 +202,8 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl } /** - * Set a custom ArgumentResolvers to use for special method parameter types. - * Such a custom ArgumentResolver will kick in first, having a chance to + * Set a custom WebArgumentResolvers to use for special method parameter types. + * Such a custom WebArgumentResolver will kick in first, having a chance to * resolve an argument value before the standard argument handling kicks in. */ public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { @@ -208,8 +211,8 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl } /** - * Set one or more custom ArgumentResolvers to use for special method - * parameter types. Any such custom ArgumentResolver will kick in first, + * Set one or more custom WebArgumentResolvers to use for special method + * parameter types. Any such custom WebArgumentResolver will kick in first, * having a chance to resolve an argument value before the standard * argument handling kicks in. */ @@ -217,6 +220,22 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl this.customArgumentResolvers = argumentResolvers; } + /** + * Set a custom ModelAndViewResolvers to use for special method return types. Such a custom ModelAndViewResolver will kick + * in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. + */ + public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) { + this.customModelAndViewResolvers = new ModelAndViewResolver[]{customModelAndViewResolver}; + } + + /** + * Set one or more custom ModelAndViewResolvers to use for special method return types. Any such custom ModelAndViewResolver + * will kick in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. + */ + public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) { + this.customModelAndViewResolvers = customModelAndViewResolvers; + } + public boolean supports(Object handler) { return getMethodResolver(handler).hasHandlerMethods(); @@ -296,7 +315,8 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl PortletHandlerMethodInvoker methodInvoker = new PortletHandlerMethodInvoker(methodResolver); Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); - ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel); + ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, + webRequest); methodInvoker.updateModelAttributes( handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest); @@ -660,8 +680,20 @@ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator impl } @SuppressWarnings("unchecked") - public ModelAndView getModelAndView( - Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel) { + public ModelAndView getModelAndView(Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel, + PortletWebRequest webRequest) { + // Invoke custom resolvers if present... + if (customModelAndViewResolvers != null) { + for (ModelAndViewResolver mavResolver : customModelAndViewResolvers) { + org.springframework.web.servlet.ModelAndView smav = mavResolver + .resolveModelAndView(handlerMethod, handlerType, returnValue, implicitModel, webRequest); + if (smav != ModelAndViewResolver.UNRESOLVED) { + return (smav.isReference() ? + new ModelAndView(smav.getViewName(), smav.getModelMap()) : + new ModelAndView(smav.getView(), smav.getModelMap())); + } + } + } if (returnValue instanceof ModelAndView) { ModelAndView mav = (ModelAndView) returnValue; diff --git a/org.springframework.web.portlet/src/test/java/org/springframework/web/portlet/mvc/annotation/PortletAnnotationControllerTests.java b/org.springframework.web.portlet/src/test/java/org/springframework/web/portlet/mvc/annotation/PortletAnnotationControllerTests.java index fe2ab448c53..2e39113ac00 100644 --- a/org.springframework.web.portlet/src/test/java/org/springframework/web/portlet/mvc/annotation/PortletAnnotationControllerTests.java +++ b/org.springframework.web.portlet/src/test/java/org/springframework/web/portlet/mvc/annotation/PortletAnnotationControllerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2008 the original author or authors. + * Copyright 2002-2009 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. @@ -17,6 +17,7 @@ package org.springframework.web.portlet.mvc.annotation; import java.io.IOException; +import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; import java.util.LinkedList; @@ -32,6 +33,8 @@ import javax.portlet.PortletSession; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.UnavailableException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import junit.framework.TestCase; @@ -72,6 +75,8 @@ import org.springframework.web.portlet.DispatcherPortlet; import org.springframework.web.portlet.ModelAndView; import org.springframework.web.portlet.context.StaticPortletApplicationContext; import org.springframework.web.portlet.mvc.AbstractController; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.mvc.annotation.ModelAndViewResolver; /** * @author Juergen Hoeller @@ -434,6 +439,28 @@ public class PortletAnnotationControllerTests extends TestCase { assertEquals("mySurpriseView", response.getContentAsString()); } + public void testMavResolver() throws Exception { + @SuppressWarnings("serial") DispatcherPortlet portlet = new DispatcherPortlet() { + @Override + protected ApplicationContext createPortletApplicationContext(ApplicationContext parent) throws BeansException { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", + new RootBeanDefinition(ModelAndViewResolverController.class)); + RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); + adapterDef.getPropertyValues() + .addPropertyValue("customModelAndViewResolver", new MyModelAndViewResolver()); + wac.registerBeanDefinition("handlerAdapter", adapterDef); + wac.refresh(); + return wac; + } + }; + portlet.init(new MockPortletConfig()); + + MockRenderRequest request = new MockRenderRequest(PortletMode.VIEW); + MockRenderResponse response = new MockRenderResponse(); + portlet.render(request, response); + } + @RequestMapping("VIEW") private static class MyController extends AbstractController { @@ -769,4 +796,37 @@ public class PortletAnnotationControllerTests extends TestCase { } } + @Controller + public static class ModelAndViewResolverController { + + @RequestMapping("VIEW") + public MySpecialArg handle() { + return new MySpecialArg("foo"); + } + } + + public static class MyModelAndViewResolver implements ModelAndViewResolver { + + public org.springframework.web.servlet.ModelAndView resolveModelAndView(Method handlerMethod, + Class handlerType, + Object returnValue, + ExtendedModelMap implicitModel, + NativeWebRequest webRequest) { + if (returnValue instanceof MySpecialArg) { + return new org.springframework.web.servlet.ModelAndView(new View() { + public String getContentType() { + return "text/html"; + } + + public void render(Map model, HttpServletRequest request, HttpServletResponse response) + throws Exception { + response.getWriter().write("myValue"); + } + + }); + } + return UNRESOLVED; + } + } + } diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java index e6b82218e90..a602bc3925d 100644 --- a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -150,6 +150,8 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen private WebArgumentResolver[] customArgumentResolvers; + private ModelAndViewResolver[] customModelAndViewResolvers; + private final Map, ServletHandlerMethodResolver> methodResolverCache = new ConcurrentHashMap, ServletHandlerMethodResolver>(); @@ -269,7 +271,7 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } /** - * Set a custom ArgumentResolvers to use for special method parameter types. Such a custom ArgumentResolver will kick + * Set a custom WebArgumentResolvers to use for special method parameter types. Such a custom WebArgumentResolver will kick * in first, having a chance to resolve an argument value before the standard argument handling kicks in. */ public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { @@ -277,13 +279,29 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen } /** - * Set one or more custom ArgumentResolvers to use for special method parameter types. Any such custom ArgumentResolver + * Set one or more custom WebArgumentResolvers to use for special method parameter types. Any such custom WebArgumentResolver * will kick in first, having a chance to resolve an argument value before the standard argument handling kicks in. */ public void setCustomArgumentResolvers(WebArgumentResolver[] argumentResolvers) { this.customArgumentResolvers = argumentResolvers; } + /** + * Set a custom ModelAndViewResolvers to use for special method return types. Such a custom ModelAndViewResolver will kick + * in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. + */ + public void setCustomModelAndViewResolver(ModelAndViewResolver customModelAndViewResolver) { + this.customModelAndViewResolvers = new ModelAndViewResolver[]{customModelAndViewResolver}; + } + + /** + * Set one or more custom ModelAndViewResolvers to use for special method return types. Any such custom ModelAndViewResolver + * will kick in first, having a chance to resolve an return value before the standard ModelAndView handling kicks in. + */ + public void setCustomModelAndViewResolvers(ModelAndViewResolver[] customModelAndViewResolvers) { + this.customModelAndViewResolvers = customModelAndViewResolvers; + } + /** * Set the message body converters to use. These converters are used to convert from and to HTTP requests and * responses. @@ -673,6 +691,16 @@ public class AnnotationMethodHandlerAdapter extends WebContentGenerator implemen ExtendedModelMap implicitModel, ServletWebRequest webRequest) { + // Invoke custom resolvers if present... + if (customModelAndViewResolvers != null) { + for (ModelAndViewResolver mavResolver : customModelAndViewResolvers) { + ModelAndView mav = mavResolver + .resolveModelAndView(handlerMethod, handlerType, returnValue, implicitModel, webRequest); + if (mav != ModelAndViewResolver.UNRESOLVED) { + return mav; + } + } + } if (handlerMethod.isAnnotationPresent(ResponseStatus.class)) { ResponseStatus responseStatus = handlerMethod.getAnnotation(ResponseStatus.class); HttpServletResponse response = webRequest.getResponse(); diff --git a/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ModelAndViewResolver.java b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ModelAndViewResolver.java new file mode 100644 index 00000000000..a6e25594b37 --- /dev/null +++ b/org.springframework.web.servlet/src/main/java/org/springframework/web/servlet/mvc/annotation/ModelAndViewResolver.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2009 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.annotation; + +import java.lang.reflect.Method; + +import org.springframework.ui.ExtendedModelMap; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.servlet.ModelAndView; + +/** + * SPI for resolving custom return values from a specific handler method. Typically implemented to detect special return + * types, resolving well-known result values for them. + * + *

A typical implementation could look like as follows: + * + *

+ * public class MyModelAndViewResolver implements ModelAndViewResolver {
+ *
+ *   public ModelAndView resolveModelAndView(Method handlerMethod,
+ *		   			Class handlerType,
+ *		   			Object returnValue,
+ *		 			ExtendedModelMap implicitModel,
+ *		 			NativeWebRequest webRequest) {
+ *     if (returnValue instanceof MySpecialRetVal.class)) {
+ *       return new MySpecialRetVal(returnValue);
+ *     }
+ *     return UNRESOLVED;
+ *   }
+ * }
+ * + * @author Arjen Poutsma + * @see org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter#setCustomModelAndViewResolvers + * @see org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter#setCustomModelAndViewResolvers + * @since 3.0 + */ +public interface ModelAndViewResolver { + + /** Marker to be returned when the resolver does not know how to handle the given method parameter. */ + ModelAndView UNRESOLVED = new ModelAndView(); + + ModelAndView resolveModelAndView(Method handlerMethod, + Class handlerType, + Object returnValue, + ExtendedModelMap implicitModel, + NativeWebRequest webRequest); +} diff --git a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java index 6689f77b9c6..266136ab665 100644 --- a/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java +++ b/org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/ServletAnnotationControllerTests.java @@ -18,6 +18,7 @@ package org.springframework.web.servlet.mvc.annotation; import java.io.IOException; import java.io.Writer; +import java.lang.reflect.Method; import java.security.Principal; import java.text.SimpleDateFormat; import java.util.Arrays; @@ -924,6 +925,32 @@ public class ServletAnnotationControllerTests { assertEquals(201, response.getStatus()); } + @Test + public void mavResolver() throws ServletException, IOException { + @SuppressWarnings("serial") DispatcherServlet servlet = new DispatcherServlet() { + @Override + protected WebApplicationContext createWebApplicationContext(WebApplicationContext parent) { + GenericWebApplicationContext wac = new GenericWebApplicationContext(); + wac.registerBeanDefinition("controller", + new RootBeanDefinition(ModelAndViewResolverController.class)); + RootBeanDefinition adapterDef = new RootBeanDefinition(AnnotationMethodHandlerAdapter.class); + adapterDef.getPropertyValues() + .addPropertyValue("customModelAndViewResolver", new MyModelAndViewResolver()); + wac.registerBeanDefinition("handlerAdapter", adapterDef); + wac.refresh(); + return wac; + } + }; + servlet.init(new MockServletConfig()); + + MockHttpServletRequest request = new MockHttpServletRequest("GET", "/"); + MockHttpServletResponse response = new MockHttpServletResponse(); + servlet.service(request, response); + assertEquals("myValue", response.getContentAsString()); + + } + + /* * Controllers */ @@ -1502,6 +1529,27 @@ public class ServletAnnotationControllerTests { } } + public static class MyMessageConverter implements HttpMessageConverter { + + public boolean supports(Class clazz) { + return true; + } + + public List getSupportedMediaTypes() { + return Collections.singletonList(new MediaType("application", "pdf")); + } + + public Object read(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + throw new HttpMessageNotReadableException("Could not read"); + } + + public void write(Object o, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + throw new UnsupportedOperationException("Not implemented"); + } + } + @Controller public static class HeadersController { @@ -1526,25 +1574,40 @@ public class ServletAnnotationControllerTests { } } - public static class MyMessageConverter implements HttpMessageConverter { - public boolean supports(Class clazz) { - return true; - } + @Controller + public static class ModelAndViewResolverController { - public List getSupportedMediaTypes() { - return Collections.singletonList(new MediaType("application", "pdf")); - } - - public Object read(Class clazz, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - throw new HttpMessageNotReadableException("Could not read"); - } - - public void write(Object o, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - throw new UnsupportedOperationException("Not implemented"); + @RequestMapping("/") + public MySpecialArg handle() { + return new MySpecialArg("foo"); } } + public static class MyModelAndViewResolver implements ModelAndViewResolver { + + public ModelAndView resolveModelAndView(Method handlerMethod, + Class handlerType, + Object returnValue, + ExtendedModelMap implicitModel, + NativeWebRequest webRequest) { + if (returnValue instanceof MySpecialArg) { + return new ModelAndView(new View() { + public String getContentType() { + return "text/html"; + } + + public void render(Map model, HttpServletRequest request, HttpServletResponse response) + throws Exception { + response.getWriter().write("myValue"); + } + + }); + } + return UNRESOLVED; + } + } + + + }