/* * Copyright 2002-2008 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.portlet.mvc.annotation; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Method; import java.security.Principal; import java.util.Arrays; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.portlet.ActionRequest; import javax.portlet.ActionResponse; import javax.portlet.PortalContext; import javax.portlet.PortletException; import javax.portlet.PortletMode; import javax.portlet.PortletPreferences; import javax.portlet.PortletRequest; import javax.portlet.PortletResponse; import javax.portlet.PortletSession; import javax.portlet.RenderRequest; import javax.portlet.RenderResponse; import javax.portlet.UnavailableException; import javax.portlet.WindowState; import org.springframework.beans.BeanUtils; import org.springframework.core.Conventions; import org.springframework.core.GenericTypeResolver; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.style.StylerUtils; import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.Model; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.support.BindingAwareModelMap; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.InitBinder; import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.support.HandlerMethodInvoker; import org.springframework.web.bind.annotation.support.HandlerMethodResolver; import org.springframework.web.bind.support.DefaultSessionAttributeStore; import org.springframework.web.bind.support.SessionAttributeStore; import org.springframework.web.bind.support.WebArgumentResolver; import org.springframework.web.bind.support.WebBindingInitializer; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.portlet.HandlerAdapter; import org.springframework.web.portlet.ModelAndView; import org.springframework.web.portlet.bind.MissingPortletRequestParameterException; import org.springframework.web.portlet.bind.PortletRequestDataBinder; import org.springframework.web.portlet.context.PortletWebRequest; 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; /** * Implementation of the {@link org.springframework.web.portlet.HandlerAdapter} * interface that maps handler methods based on portlet modes, action/render phases * and request parameters expressed through the {@link RequestMapping} annotation. * * <p>Supports request parameter binding through the {@link RequestParam} annotation. * Also supports the {@link ModelAttribute} annotation for exposing model attribute * values to the view, as well as {@link InitBinder} for binder initialization methods * and {@link SessionAttributes} for automatic session management of specific attributes. * * <p>This adapter can be customized through various bean properties. * A common use case is to apply shared binder initialization logic through * a custom {@link #setWebBindingInitializer WebBindingInitializer}. * * @author Juergen Hoeller * @author Arjen Poutsma * @since 2.5 * @see #setWebBindingInitializer * @see #setSessionAttributeStore */ public class AnnotationMethodHandlerAdapter extends PortletContentGenerator implements HandlerAdapter { private static final String IMPLICIT_MODEL_ATTRIBUTE = "org.springframework.web.portlet.mvc.ImplicitModel"; private WebBindingInitializer webBindingInitializer; private SessionAttributeStore sessionAttributeStore = new DefaultSessionAttributeStore(); private int cacheSecondsForSessionAttributeHandlers = 0; private boolean synchronizeOnSession = false; private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); private WebArgumentResolver[] customArgumentResolvers; private final Map<Class<?>, PortletHandlerMethodResolver> methodResolverCache = new ConcurrentHashMap<Class<?>, PortletHandlerMethodResolver>(); /** * Specify a WebBindingInitializer which will apply pre-configured * configuration to every DataBinder that this controller uses. */ public void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { this.webBindingInitializer = webBindingInitializer; } /** * Specify the strategy to store session attributes with. * <p>Default is {@link org.springframework.web.bind.support.DefaultSessionAttributeStore}, * storing session attributes in the PortletSession, using the same * attribute name as in the model. */ public void setSessionAttributeStore(SessionAttributeStore sessionAttributeStore) { Assert.notNull(sessionAttributeStore, "SessionAttributeStore must not be null"); this.sessionAttributeStore = sessionAttributeStore; } /** * Cache content produced by <code>@SessionAttributes</code> annotated handlers * for the given number of seconds. Default is 0, preventing caching completely. * <p>In contrast to the "cacheSeconds" property which will apply to all general * handlers (but not to <code>@SessionAttributes</code> annotated handlers), this * setting will apply to <code>@SessionAttributes</code> annotated handlers only. * @see #setCacheSeconds * @see org.springframework.web.bind.annotation.SessionAttributes */ public void setCacheSecondsForSessionAttributeHandlers(int cacheSecondsForSessionAttributeHandlers) { this.cacheSecondsForSessionAttributeHandlers = cacheSecondsForSessionAttributeHandlers; } /** * Set if controller execution should be synchronized on the session, * to serialize parallel invocations from the same client. * <p>More specifically, the execution of each handler method will get * synchronized if this flag is "true". The best available session mutex * will be used for the synchronization; ideally, this will be a mutex * exposed by HttpSessionMutexListener. * <p>The session mutex is guaranteed to be the same object during * the entire lifetime of the session, available under the key defined * by the <code>SESSION_MUTEX_ATTRIBUTE</code> constant. It serves as a * safe reference to synchronize on for locking on the current session. * <p>In many cases, the PortletSession reference itself is a safe mutex * as well, since it will always be the same object reference for the * same active logical session. However, this is not guaranteed across * different servlet containers; the only 100% safe way is a session mutex. * @see org.springframework.web.util.HttpSessionMutexListener * @see org.springframework.web.portlet.util.PortletUtils#getSessionMutex(javax.portlet.PortletSession) */ public void setSynchronizeOnSession(boolean synchronizeOnSession) { this.synchronizeOnSession = synchronizeOnSession; } /** * Set the ParameterNameDiscoverer to use for resolving method parameter * names if needed (e.g. for default attribute names). * <p>Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer}. */ public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) { this.parameterNameDiscoverer = parameterNameDiscoverer; } /** * Set a custom ArgumentResolvers to use for special method parameter types. * Such a custom ArgumentResolver will kick in first, having a chance to * resolve an argument value before the standard argument handling kicks in. */ public void setCustomArgumentResolver(WebArgumentResolver argumentResolver) { this.customArgumentResolvers = new WebArgumentResolver[] {argumentResolver}; } /** * Set one or more custom ArgumentResolvers to use for special method * parameter types. Any such custom ArgumentResolver 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; } public boolean supports(Object handler) { return getMethodResolver(handler).hasHandlerMethods(); } public void handleAction(ActionRequest request, ActionResponse response, Object handler) throws Exception { Object returnValue = doHandle(request, response, handler); if (returnValue != null) { throw new IllegalStateException("Invalid action method return value: " + returnValue); } } public ModelAndView handleRender(RenderRequest request, RenderResponse response, Object handler) throws Exception { checkAndPrepare(request, response); return doHandle(request, response, handler); } protected ModelAndView doHandle(PortletRequest request, PortletResponse response, Object handler) throws Exception { ExtendedModelMap implicitModel = null; if (request instanceof RenderRequest && response instanceof RenderResponse) { RenderRequest renderRequest = (RenderRequest) request; RenderResponse renderResponse = (RenderResponse) response; // Detect implicit model from associated action phase. if (renderRequest.getParameter(IMPLICIT_MODEL_ATTRIBUTE) != null) { PortletSession session = request.getPortletSession(false); if (session != null) { implicitModel = (ExtendedModelMap) session.getAttribute(IMPLICIT_MODEL_ATTRIBUTE); } } if (handler.getClass().getAnnotation(SessionAttributes.class) != null) { // Always prevent caching in case of session attribute management. checkAndPrepare(renderRequest, renderResponse, this.cacheSecondsForSessionAttributeHandlers); } else { // Uses configured default cacheSeconds setting. checkAndPrepare(renderRequest, renderResponse); } } if (implicitModel == null) { implicitModel = new BindingAwareModelMap(); } // Execute invokeHandlerMethod in synchronized block if required. if (this.synchronizeOnSession) { PortletSession session = request.getPortletSession(false); if (session != null) { Object mutex = PortletUtils.getSessionMutex(session); synchronized (mutex) { return invokeHandlerMethod(request, response, handler, implicitModel); } } } return invokeHandlerMethod(request, response, handler, implicitModel); } private ModelAndView invokeHandlerMethod( PortletRequest request, PortletResponse response, Object handler, ExtendedModelMap implicitModel) throws Exception { PortletWebRequest webRequest = new PortletWebRequest(request, response); PortletHandlerMethodResolver methodResolver = getMethodResolver(handler); Method handlerMethod = methodResolver.resolveHandlerMethod(request, response); PortletHandlerMethodInvoker methodInvoker = new PortletHandlerMethodInvoker(methodResolver); Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel); ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel); methodInvoker.updateModelAttributes( handler, (mav != null ? mav.getModel() : null), implicitModel, webRequest); // Expose implicit model for subsequent render phase. if (response instanceof ActionResponse && !implicitModel.isEmpty()) { ActionResponse actionResponse = (ActionResponse) response; try { actionResponse.setRenderParameter(IMPLICIT_MODEL_ATTRIBUTE, Boolean.TRUE.toString()); request.getPortletSession().setAttribute(IMPLICIT_MODEL_ATTRIBUTE, implicitModel); } catch (IllegalStateException ex) { // Probably sendRedirect called... no need to expose model to render phase. } } return mav; } /** * Template method for creating a new PortletRequestDataBinder instance. * <p>The default implementation creates a standard PortletRequestDataBinder. * This can be overridden for custom PortletRequestDataBinder subclasses. * @param request current portlet request * @param target the target object to bind onto (or <code>null</code> * if the binder is just used to convert a plain parameter value) * @param objectName the objectName of the target object * @return the PortletRequestDataBinder instance to use * @throws Exception in case of invalid state or arguments * @see PortletRequestDataBinder#bind(javax.portlet.PortletRequest) * @see PortletRequestDataBinder#convertIfNecessary(Object, Class, MethodParameter) */ protected PortletRequestDataBinder createBinder( PortletRequest request, Object target, String objectName) throws Exception { return new PortletRequestDataBinder(target, objectName); } /** * Build a HandlerMethodResolver for the given handler type. */ private PortletHandlerMethodResolver getMethodResolver(Object handler) { Class handlerClass = ClassUtils.getUserClass(handler); PortletHandlerMethodResolver resolver = this.methodResolverCache.get(handlerClass); if (resolver == null) { resolver = new PortletHandlerMethodResolver(handlerClass); this.methodResolverCache.put(handlerClass, resolver); } return resolver; } private static class PortletHandlerMethodResolver extends HandlerMethodResolver { public PortletHandlerMethodResolver(Class<?> handlerType) { super(handlerType); } public Method resolveHandlerMethod(PortletRequest request, PortletResponse response) throws PortletException { String lookupMode = request.getPortletMode().toString(); Map<RequestMappingInfo, Method> targetHandlerMethods = new LinkedHashMap<RequestMappingInfo, Method>(); for (Method handlerMethod : getHandlerMethods()) { RequestMapping mapping = AnnotationUtils.findAnnotation(handlerMethod, RequestMapping.class); RequestMappingInfo mappingInfo = new RequestMappingInfo(); mappingInfo.modes = mapping.value(); mappingInfo.params = mapping.params(); mappingInfo.action = isActionMethod(handlerMethod); mappingInfo.render = isRenderMethod(handlerMethod); boolean match = false; if (mappingInfo.modes.length > 0) { for (String mappedMode : mappingInfo.modes) { if (mappedMode.equalsIgnoreCase(lookupMode)) { if (checkParameters(request, response, mappingInfo)) { match = true; } else { break; } } } } else { // No modes specified: parameter match sufficient. match = checkParameters(request, response, mappingInfo); } if (match) { Method oldMappedMethod = targetHandlerMethods.put(mappingInfo, handlerMethod); if (oldMappedMethod != null && oldMappedMethod != handlerMethod) { throw new IllegalStateException("Ambiguous handler methods mapped for portlet mode '" + lookupMode + "': {" + oldMappedMethod + ", " + handlerMethod + "}. If you intend to handle the same mode in multiple methods, then factor " + "them out into a dedicated handler class with that mode mapped at the type level!"); } } } if (!targetHandlerMethods.isEmpty()) { if (targetHandlerMethods.size() == 1) { return targetHandlerMethods.values().iterator().next(); } else { RequestMappingInfo bestMappingMatch = null; for (RequestMappingInfo mapping : targetHandlerMethods.keySet()) { if (bestMappingMatch == null) { bestMappingMatch = mapping; } else { if ((bestMappingMatch.modes.length == 0 && mapping.modes.length > 0) || bestMappingMatch.params.length < mapping.params.length) { bestMappingMatch = mapping; } } } return targetHandlerMethods.get(bestMappingMatch); } } else { throw new UnavailableException("No matching handler method found for portlet request: mode '" + request.getPortletMode() + "', type '" + (response instanceof ActionResponse ? "action" : "render") + "', parameters " + StylerUtils.style(request.getParameterMap())); } } private boolean checkParameters(PortletRequest request, PortletResponse response, RequestMappingInfo mapping) { if (response instanceof RenderResponse) { if (mapping.action) { return false; } } else if (response instanceof ActionResponse) { if (mapping.render) { return false; } } return PortletAnnotationMappingUtils.checkParameters(mapping.params, request); } private boolean isActionMethod(Method handlerMethod) { if (!void.class.equals(handlerMethod.getReturnType())) { return false; } for (Class<?> argType : handlerMethod.getParameterTypes()) { if (ActionRequest.class.isAssignableFrom(argType) || ActionResponse.class.isAssignableFrom(argType) || InputStream.class.isAssignableFrom(argType) || Reader.class.isAssignableFrom(argType)) { return true; } } return false; } private boolean isRenderMethod(Method handlerMethod) { if (!void.class.equals(handlerMethod.getReturnType())) { return true; } for (Class<?> argType : handlerMethod.getParameterTypes()) { if (RenderRequest.class.isAssignableFrom(argType) || RenderResponse.class.isAssignableFrom(argType) || OutputStream.class.isAssignableFrom(argType) || Writer.class.isAssignableFrom(argType)) { return true; } } return false; } } private class PortletHandlerMethodInvoker extends HandlerMethodInvoker { public PortletHandlerMethodInvoker(HandlerMethodResolver resolver) { super(resolver, webBindingInitializer, sessionAttributeStore, parameterNameDiscoverer, customArgumentResolvers); } @Override protected void raiseMissingParameterException(String paramName, Class paramType) throws Exception { throw new MissingPortletRequestParameterException(paramName, paramType.getName()); } @Override protected void raiseSessionRequiredException(String message) throws Exception { throw new PortletSessionRequiredException(message); } @Override protected WebDataBinder createBinder(NativeWebRequest webRequest, Object target, String objectName) throws Exception { return AnnotationMethodHandlerAdapter.this.createBinder( (PortletRequest) webRequest.getNativeRequest(), target, objectName); } @Override protected void doBind(NativeWebRequest webRequest, WebDataBinder binder, boolean failOnErrors) throws Exception { PortletRequestDataBinder servletBinder = (PortletRequestDataBinder) binder; servletBinder.bind((PortletRequest) webRequest.getNativeRequest()); if (failOnErrors) { servletBinder.closeNoCatch(); } } @Override protected Object resolveStandardArgument(Class parameterType, NativeWebRequest webRequest) throws Exception { PortletRequest request = (PortletRequest) webRequest.getNativeRequest(); PortletResponse response = (PortletResponse) webRequest.getNativeResponse(); if (PortletRequest.class.isAssignableFrom(parameterType)) { return request; } else if (PortletResponse.class.isAssignableFrom(parameterType)) { return response; } else if (PortletSession.class.isAssignableFrom(parameterType)) { return request.getPortletSession(); } else if (PortletPreferences.class.isAssignableFrom(parameterType)) { return request.getPreferences(); } else if (PortletMode.class.isAssignableFrom(parameterType)) { return request.getPortletMode(); } else if (WindowState.class.isAssignableFrom(parameterType)) { return request.getWindowState(); } else if (PortalContext.class.isAssignableFrom(parameterType)) { return request.getPortalContext(); } else if (Principal.class.isAssignableFrom(parameterType)) { return request.getUserPrincipal(); } else if (Locale.class.equals(parameterType)) { return request.getLocale(); } else if (InputStream.class.isAssignableFrom(parameterType)) { if (!(request instanceof ActionRequest)) { throw new IllegalStateException("InputStream can only get obtained for ActionRequest"); } return ((ActionRequest) request).getPortletInputStream(); } else if (Reader.class.isAssignableFrom(parameterType)) { if (!(request instanceof ActionRequest)) { throw new IllegalStateException("Reader can only get obtained for ActionRequest"); } return ((ActionRequest) request).getReader(); } else if (OutputStream.class.isAssignableFrom(parameterType)) { if (!(response instanceof RenderResponse)) { throw new IllegalStateException("OutputStream can only get obtained for RenderResponse"); } return ((RenderResponse) response).getPortletOutputStream(); } else if (Writer.class.isAssignableFrom(parameterType)) { if (!(response instanceof RenderResponse)) { throw new IllegalStateException("Writer can only get obtained for RenderResponse"); } return ((RenderResponse) response).getWriter(); } return super.resolveStandardArgument(parameterType, webRequest); } @SuppressWarnings("unchecked") public ModelAndView getModelAndView( Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel) { if (returnValue instanceof ModelAndView) { ModelAndView mav = (ModelAndView) returnValue; mav.getModelMap().mergeAttributes(implicitModel); return mav; } else if (returnValue instanceof org.springframework.web.servlet.ModelAndView) { org.springframework.web.servlet.ModelAndView smav = (org.springframework.web.servlet.ModelAndView) returnValue; ModelAndView mav = (smav.isReference() ? new ModelAndView(smav.getViewName(), smav.getModelMap()) : new ModelAndView(smav.getView(), smav.getModelMap())); mav.getModelMap().mergeAttributes(implicitModel); return mav; } else if (returnValue instanceof Model) { return new ModelAndView().addAllObjects(implicitModel).addAllObjects(((Model) returnValue).asMap()); } else if (returnValue instanceof Map) { return new ModelAndView().addAllObjects(implicitModel).addAllObjects((Map) returnValue); } else if (returnValue instanceof View) { return new ModelAndView(returnValue).addAllObjects(implicitModel); } else if (returnValue instanceof String) { return new ModelAndView((String) returnValue).addAllObjects(implicitModel); } else if (returnValue == null) { // Either returned null or was 'void' return. return null; } else if (!BeanUtils.isSimpleProperty(returnValue.getClass())) { // Assume a single model attribute... ModelAttribute attr = AnnotationUtils.findAnnotation(handlerMethod, ModelAttribute.class); String attrName = (attr != null ? attr.value() : ""); ModelAndView mav = new ModelAndView().addAllObjects(implicitModel); if ("".equals(attrName)) { Class resolvedType = GenericTypeResolver.resolveReturnType(handlerMethod, handlerType); attrName = Conventions.getVariableNameForReturnType(handlerMethod, resolvedType, returnValue); } return mav.addObject(attrName, returnValue); } else { throw new IllegalArgumentException("Invalid handler method return value: " + returnValue); } } } private static class RequestMappingInfo { public String[] modes = new String[0]; public String[] params = new String[0]; private boolean action = false; private boolean render = false; public boolean equals(Object obj) { RequestMappingInfo other = (RequestMappingInfo) obj; return (this.action == other.action && this.render == other.render && Arrays.equals(this.modes, other.modes) && Arrays.equals(this.params, other.params)); } public int hashCode() { return (Arrays.hashCode(this.modes) * 29 + Arrays.hashCode(this.params)); } } }