diff --git a/org.springframework.web.portlet/build.xml b/org.springframework.web.portlet/build.xml new file mode 100644 index 00000000000..ce0ccd56387 --- /dev/null +++ b/org.springframework.web.portlet/build.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/org.springframework.web.portlet/ivy.xml b/org.springframework.web.portlet/ivy.xml new file mode 100644 index 00000000000..f34edae7f16 --- /dev/null +++ b/org.springframework.web.portlet/ivy.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.springframework.web.portlet/pom.xml b/org.springframework.web.portlet/pom.xml new file mode 100644 index 00000000000..a00a8275bf8 --- /dev/null +++ b/org.springframework.web.portlet/pom.xml @@ -0,0 +1,86 @@ + + + + org.springframework + org.springframework.parent + 3.0-M1-SNAPSHOT + + 4.0.0 + org.springframework.web + jar + Spring Framework: Web + + + org.springframework + org.springframework.core + + + org.springframework + org.springframework.beans + + + org.springframework + org.springframework.aop + + + org.springframework + org.springframework.context + + + javax.servlet + com.springsource.javax.servlet + true + + + javax.el + com.springsource.javax.el + true + + + javax.servlet + com.springsource.javax.servlet.jsp + true + + + javax.xml.rpc + com.springsource.javax.xml.rpc + true + + + org.aopalliance + com.springsource.org.aopalliance + true + + + org.apache.myfaces + com.springsource.org.apache.myfaces.javax.faces + true + + + com.caucho + com.springsource.com.caucho + true + + + org.apache.log4j + com.springsource.org.apache.log4j + compile + true + + + org.apache.commons + com.springsource.org.apache.commons.httpclient + true + + + org.apache.axis + com.springsource.org.apache.axis + true + + + org.apache.taglibs + com.springsource.org.apache.taglibs.standard + true + + + \ No newline at end of file diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/DispatcherPortlet.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/DispatcherPortlet.java new file mode 100644 index 00000000000..1140845cf27 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/DispatcherPortlet.java @@ -0,0 +1,1173 @@ +/* + * 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; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletException; +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 org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.context.i18n.SimpleLocaleContext; +import org.springframework.core.JdkVersion; +import org.springframework.core.OrderComparator; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.portlet.context.PortletRequestAttributes; +import org.springframework.web.portlet.multipart.MultipartActionRequest; +import org.springframework.web.portlet.multipart.PortletMultipartResolver; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.ViewRendererServlet; +import org.springframework.web.servlet.ViewResolver; + +/** + * Central dispatcher for use within the Portlet MVC framework, e.g. for web UI + * controllers. Dispatches to registered handlers for processing a portlet request. + * + *

This portlet is very flexible: It can be used with just about any workflow, + * with the installation of the appropriate adapter classes. It offers the + * following functionality that distinguishes it from other request-driven + * portlet MVC frameworks: + * + *

+ * + *

NOTE: The @RequestMapping annotation will only be processed + * if a corresponding HandlerMapping (for type level annotations) + * and/or HandlerAdapter (for method level annotations) + * is present in the dispatcher. This is the case by default. + * However, if you are defining custom HandlerMappings or + * HandlerAdapters, then you need to make sure that a + * corresponding custom DefaultAnnotationHandlerMapping + * and/or AnnotationMethodHandlerAdapter is defined as well + * - provided that you intend to use @RequestMapping. + * + *

A web application can define any number of DispatcherPortlets. + * Each portlet will operate in its own namespace, loading its own application + * context with mappings, handlers, etc. Only the root application context + * as loaded by {@link org.springframework.web.context.ContextLoaderListener}, + * if any, will be shared. + * + *

Thanks to Rainer Schmitz and Nick Lothian for their suggestions! + * + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.web.portlet.mvc.Controller + * @see org.springframework.web.servlet.ViewRendererServlet + * @see org.springframework.web.context.ContextLoaderListener + */ +public class DispatcherPortlet extends FrameworkPortlet { + + /** + * Well-known name for the PortletMultipartResolver object in the bean factory for this namespace. + */ + public static final String MULTIPART_RESOLVER_BEAN_NAME = "portletMultipartResolver"; + + /** + * Well-known name for the HandlerMapping object in the bean factory for this namespace. + * Only used when "detectAllHandlerMappings" is turned off. + * @see #setDetectAllViewResolvers + */ + public static final String HANDLER_MAPPING_BEAN_NAME = "handlerMapping"; + + /** + * Well-known name for the HandlerAdapter object in the bean factory for this namespace. + * Only used when "detectAllHandlerAdapters" is turned off. + * @see #setDetectAllHandlerAdapters + */ + public static final String HANDLER_ADAPTER_BEAN_NAME = "handlerAdapter"; + + /** + * Well-known name for the HandlerExceptionResolver object in the bean factory for this + * namespace. Only used when "detectAllHandlerExceptionResolvers" is turned off. + * @see #setDetectAllViewResolvers + */ + public static final String HANDLER_EXCEPTION_RESOLVER_BEAN_NAME = "handlerExceptionResolver"; + + /** + * Well-known name for the ViewResolver object in the bean factory for this namespace. + */ + public static final String VIEW_RESOLVER_BEAN_NAME = "viewResolver"; + + /** + * Default URL to ViewRendererServlet. This bridge servlet is used to convert + * portlet render requests to servlet requests in order to leverage the view support + * in the org.springframework.web.view package. + */ + public static final String DEFAULT_VIEW_RENDERER_URL = "/WEB-INF/servlet/view"; + + /** + * Request attribute to hold the currently chosen HandlerExecutionChain. + * Only used for internal optimizations. + */ + public static final String HANDLER_EXECUTION_CHAIN_ATTRIBUTE = + DispatcherPortlet.class.getName() + ".HANDLER"; + + /** + * Unlike the Servlet version of this class, we have to deal with the + * two-phase nature of the portlet request. To do this, we need to pass + * forward any exception that occurs during the action phase, so that + * it can be displayed in the render phase. The only direct way to pass + * things forward and preserve them for each render request is through + * render parameters, but these are limited to String objects and we need + * to pass the Exception itself. The only other way to do this is in the + * session. The bad thing about using the session is that we have no way + * of knowing when we are done re-rendering the request and so we don't + * know when we can remove the objects from the session. So we will end + * up polluting the session with an old exception when we finally leave + * the render phase of one request and move on to something else. + */ + public static final String ACTION_EXCEPTION_SESSION_ATTRIBUTE = + DispatcherPortlet.class.getName() + ".ACTION_EXCEPTION"; + + /** + * This render parameter is used to indicate forward to the render phase + * that an exception occurred during the action phase. + */ + public static final String ACTION_EXCEPTION_RENDER_PARAMETER = "actionException"; + + /** + * Log category to use when no mapped handler is found for a request. + */ + public static final String PAGE_NOT_FOUND_LOG_CATEGORY = "org.springframework.web.portlet.PageNotFound"; + + /** + * Name of the class path resource (relative to the DispatcherPortlet class) + * that defines DispatcherPortet's default strategy names. + */ + private static final String DEFAULT_STRATEGIES_PATH = "DispatcherPortlet.properties"; + + + /** + * Additional logger to use when no mapped handler is found for a request. + */ + protected static final Log pageNotFoundLogger = LogFactory.getLog(PAGE_NOT_FOUND_LOG_CATEGORY); + + private static final Properties defaultStrategies; + + static { + // Load default strategy implementations from properties file. + // This is currently strictly internal and not meant to be customized + // by application developers. + try { + ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherPortlet.class); + defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); + } + catch (IOException ex) { + throw new IllegalStateException("Could not load 'DispatcherPortlet.properties': " + ex.getMessage()); + } + } + + + /** Detect all HandlerMappings or just expect "handlerMapping" bean? */ + private boolean detectAllHandlerMappings = true; + + /** Detect all HandlerAdapters or just expect "handlerAdapter" bean? */ + private boolean detectAllHandlerAdapters = true; + + /** Detect all HandlerExceptionResolvers or just expect "handlerExceptionResolver" bean? */ + private boolean detectAllHandlerExceptionResolvers = true; + + /** Detect all ViewResolvers or just expect "viewResolver" bean? */ + private boolean detectAllViewResolvers = true; + + /** URL that points to the ViewRendererServlet */ + private String viewRendererUrl = DEFAULT_VIEW_RENDERER_URL; + + /** Expose LocaleContext and RequestAttributes as inheritable for child threads? */ + private boolean threadContextInheritable = false; + + + /** MultipartResolver used by this portlet */ + private PortletMultipartResolver multipartResolver; + + /** List of HandlerMappings used by this portlet */ + private List handlerMappings; + + /** List of HandlerAdapters used by this portlet */ + private List handlerAdapters; + + /** List of HandlerExceptionResolvers used by this portlet */ + private List handlerExceptionResolvers; + + /** List of ViewResolvers used by this portlet */ + private List viewResolvers; + + + /** + * Set whether to detect all HandlerMapping beans in this portlet's context. + * Else, just a single bean with name "handlerMapping" will be expected. + *

Default is true. Turn this off if you want this portlet to use a + * single HandlerMapping, despite multiple HandlerMapping beans being + * defined in the context. + */ + public void setDetectAllHandlerMappings(boolean detectAllHandlerMappings) { + this.detectAllHandlerMappings = detectAllHandlerMappings; + } + + /** + * Set whether to detect all HandlerAdapter beans in this portlet's context. + * Else, just a single bean with name "handlerAdapter" will be expected. + *

Default is "true". Turn this off if you want this portlet to use a + * single HandlerAdapter, despite multiple HandlerAdapter beans being + * defined in the context. + */ + public void setDetectAllHandlerAdapters(boolean detectAllHandlerAdapters) { + this.detectAllHandlerAdapters = detectAllHandlerAdapters; + } + + /** + * Set whether to detect all HandlerExceptionResolver beans in this portlet's context. + * Else, just a single bean with name "handlerExceptionResolver" will be expected. + *

Default is true. Turn this off if you want this portlet to use a + * single HandlerExceptionResolver, despite multiple HandlerExceptionResolver + * beans being defined in the context. + */ + public void setDetectAllHandlerExceptionResolvers(boolean detectAllHandlerExceptionResolvers) { + this.detectAllHandlerExceptionResolvers = detectAllHandlerExceptionResolvers; + } + + /** + * Set whether to detect all ViewResolver beans in this portlet's context. + * Else, just a single bean with name "viewResolver" will be expected. + *

Default is true. Turn this off if you want this portlet to use a + * single ViewResolver, despite multiple ViewResolver beans being + * defined in the context. + */ + public void setDetectAllViewResolvers(boolean detectAllViewResolvers) { + this.detectAllViewResolvers = detectAllViewResolvers; + } + + /** + * Set the URL to the ViewRendererServlet. That servlet is used to + * ultimately render all views in the portlet application. + */ + public void setViewRendererUrl(String viewRendererUrl) { + this.viewRendererUrl = viewRendererUrl; + } + + /** + * Set whether to expose the LocaleContext and RequestAttributes as inheritable + * for child threads (using an {@link java.lang.InheritableThreadLocal}). + *

Default is "false", to avoid side effects on spawned background threads. + * Switch this to "true" to enable inheritance for custom child threads which + * are spawned during request processing and only used for this request + * (that is, ending after their initial task, without reuse of the thread). + *

WARNING: Do not use inheritance for child threads if you are + * accessing a thread pool which is configured to potentially add new threads + * on demand (e.g. a JDK {@link java.util.concurrent.ThreadPoolExecutor}), + * since this will expose the inherited context to such a pooled thread. + */ + public void setThreadContextInheritable(boolean threadContextInheritable) { + this.threadContextInheritable = threadContextInheritable; + } + + + /** + * This implementation calls {@link #initStrategies}. + */ + public void onRefresh(ApplicationContext context) { + initStrategies(context); + } + + /** + * Refresh the strategy objects that this portlet uses. + *

May be overridden in subclasses in order to initialize + * further strategy objects. + */ + protected void initStrategies(ApplicationContext context) { + initMultipartResolver(context); + initHandlerMappings(context); + initHandlerAdapters(context); + initHandlerExceptionResolvers(context); + initViewResolvers(context); + } + + /** + * Initialize the PortletMultipartResolver used by this class. + *

If no valid bean is defined with the given name in the BeanFactory + * for this namespace, no multipart handling is provided. + */ + private void initMultipartResolver(ApplicationContext context) { + try { + this.multipartResolver = (PortletMultipartResolver) + context.getBean(MULTIPART_RESOLVER_BEAN_NAME, PortletMultipartResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("Using MultipartResolver [" + this.multipartResolver + "]"); + } + } + catch (NoSuchBeanDefinitionException ex) { + // Default is no multipart resolver. + this.multipartResolver = null; + if (logger.isDebugEnabled()) { + logger.debug("Unable to locate PortletMultipartResolver with name '" + MULTIPART_RESOLVER_BEAN_NAME + + "': no multipart request handling provided"); + } + } + } + + /** + * Initialize the HandlerMappings used by this class. + *

If no HandlerMapping beans are defined in the BeanFactory + * for this namespace, we default to PortletModeHandlerMapping. + */ + private void initHandlerMappings(ApplicationContext context) { + this.handlerMappings = null; + + if (this.detectAllHandlerMappings) { + // Find all HandlerMappings in the ApplicationContext, + // including ancestor contexts. + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerMapping.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerMappings = new ArrayList(matchingBeans.values()); + // We keep HandlerMappings in sorted order. + Collections.sort(this.handlerMappings, new OrderComparator()); + } + } + else { + try { + Object hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class); + this.handlerMappings = Collections.singletonList(hm); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default HandlerMapping later. + } + } + + // Ensure we have at least one HandlerMapping, by registering + // a default HandlerMapping if no other mappings are found. + if (this.handlerMappings == null) { + this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerMappings found in portlet '" + getPortletName() + "': using default"); + } + } + } + + /** + * Initialize the HandlerAdapters used by this class. + *

If no HandlerAdapter beans are defined in the BeanFactory + * for this namespace, we default to SimpleControllerHandlerAdapter. + */ + private void initHandlerAdapters(ApplicationContext context) { + this.handlerAdapters = null; + + if (this.detectAllHandlerAdapters) { + // Find all HandlerAdapters in the ApplicationContext, + // including ancestor contexts. + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerAdapter.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerAdapters = new ArrayList(matchingBeans.values()); + // We keep HandlerAdapters in sorted order. + Collections.sort(this.handlerAdapters, new OrderComparator()); + } + } + else { + try { + Object ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class); + this.handlerAdapters = Collections.singletonList(ha); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default HandlerAdapter later. + } + } + + // Ensure we have at least some HandlerAdapters, by registering + // default HandlerAdapters if no other adapters are found. + if (this.handlerAdapters == null) { + this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerAdapters found in portlet '" + getPortletName() + "': using default"); + } + } + } + + /** + * Initialize the HandlerExceptionResolver used by this class. + *

If no bean is defined with the given name in the BeanFactory + * for this namespace, we default to no exception resolver. + */ + private void initHandlerExceptionResolvers(ApplicationContext context) { + this.handlerExceptionResolvers = null; + + if (this.detectAllHandlerExceptionResolvers) { + // Find all HandlerExceptionResolvers in the ApplicationContext, + // including ancestor contexts. + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, HandlerExceptionResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + this.handlerExceptionResolvers = new ArrayList(matchingBeans.values()); + // We keep HandlerExceptionResolvers in sorted order. + Collections.sort(this.handlerExceptionResolvers, new OrderComparator()); + } + } + else { + try { + Object her = context.getBean( + HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class); + this.handlerExceptionResolvers = Collections.singletonList(her); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, no HandlerExceptionResolver is fine too. + } + } + + // Just for consistency, check for default HandlerExceptionResolvers... + // There aren't any in usual scenarios. + if (this.handlerExceptionResolvers == null) { + this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("No HandlerExceptionResolvers found in portlet '" + getPortletName() + "': using default"); + } + } + } + + /** + * Initialize the ViewResolvers used by this class. + *

If no ViewResolver beans are defined in the BeanFactory + * for this namespace, we default to InternalResourceViewResolver. + */ + private void initViewResolvers(ApplicationContext context) { + this.viewResolvers = null; + + if (this.detectAllViewResolvers) { + // Find all ViewResolvers in the ApplicationContext, + // including ancestor contexts. + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors( + context, ViewResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + this.viewResolvers = new ArrayList(matchingBeans.values()); + // We keep ViewResolvers in sorted order. + Collections.sort(this.viewResolvers, new OrderComparator()); + } + } + else { + try { + Object vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class); + this.viewResolvers = Collections.singletonList(vr); + } + catch (NoSuchBeanDefinitionException ex) { + // Ignore, we'll add a default ViewResolver later. + } + } + + // Ensure we have at least one ViewResolver, by registering + // a default ViewResolver if no other resolvers are found. + if (this.viewResolvers == null) { + this.viewResolvers = getDefaultStrategies(context, ViewResolver.class); + if (logger.isDebugEnabled()) { + logger.debug("No ViewResolvers found in portlet '" + getPortletName() + "': using default"); + } + } + } + + + /** + * Return the default strategy object for the given strategy interface. + *

The default implementation delegates to {@link #getDefaultStrategies}, + * expecting a single object in the list. + * @param context the current Portlet ApplicationContext + * @param strategyInterface the strategy interface + * @return the corresponding strategy object + * @throws BeansException if initialization failed + * @see #getDefaultStrategies + */ + protected Object getDefaultStrategy(ApplicationContext context, Class strategyInterface) throws BeansException { + List strategies = getDefaultStrategies(context, strategyInterface); + if (strategies.size() != 1) { + throw new BeanInitializationException( + "DispatcherPortlet needs exactly 1 strategy for interface [" + strategyInterface.getName() + "]"); + } + return strategies.get(0); + } + + /** + * Create a List of default strategy objects for the given strategy interface. + *

The default implementation uses the "DispatcherPortlet.properties" file + * (in the same package as the DispatcherPortlet class) to determine the class names. + * It instantiates the strategy objects and satisifies ApplicationContextAware + * if necessary. + * @param context the current Portlet ApplicationContext + * @param strategyInterface the strategy interface + * @return the List of corresponding strategy objects + * @throws BeansException if initialization failed + */ + protected List getDefaultStrategies(ApplicationContext context, Class strategyInterface) throws BeansException { + String key = strategyInterface.getName(); + List strategies = null; + String value = defaultStrategies.getProperty(key); + if (value != null) { + String[] classNames = StringUtils.commaDelimitedListToStringArray(value); + strategies = new ArrayList(classNames.length); + for (int i = 0; i < classNames.length; i++) { + String className = classNames[i]; + if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_15 && className.indexOf("Annotation") != -1) { + // Skip Java 5 specific strategies when running on JDK 1.4... + continue; + } + try { + Class clazz = ClassUtils.forName(className, DispatcherPortlet.class.getClassLoader()); + Object strategy = createDefaultStrategy(context, clazz); + strategies.add(strategy); + } + catch (ClassNotFoundException ex) { + throw new BeanInitializationException( + "Could not find DispatcherPortlet's default strategy class [" + className + + "] for interface [" + key + "]", ex); + } + catch (LinkageError err) { + throw new BeanInitializationException( + "Error loading DispatcherPortlet's default strategy class [" + className + + "] for interface [" + key + "]: problem with class file or dependent class", err); + } + } + } + else { + strategies = Collections.EMPTY_LIST; + } + return strategies; + } + + /** + * Create a default strategy. + *

The default implementation uses + * {@link org.springframework.beans.factory.config.AutowireCapableBeanFactory#createBean}. + * @param context the current Portlet ApplicationContext + * @param clazz the strategy implementation class to instantiate + * @return the fully configured strategy instance + * @throws BeansException if initialization failed + * @see org.springframework.context.ApplicationContext#getAutowireCapableBeanFactory() + */ + protected Object createDefaultStrategy(ApplicationContext context, Class clazz) throws BeansException { + return context.getAutowireCapableBeanFactory().createBean(clazz); + } + + + /** + * Obtain this portlet's PortletMultipartResolver, if any. + * @return the PortletMultipartResolver used by this portlet, or null + * if none (indicating that no multipart support is available) + */ + public PortletMultipartResolver getMultipartResolver() { + return this.multipartResolver; + } + + + /** + * Processes the actual dispatching to the handler for action requests. + *

The handler will be obtained by applying the portlet's HandlerMappings in order. + * The HandlerAdapter will be obtained by querying the portlet's installed + * HandlerAdapters to find the first that supports the handler class. + * @param request current portlet action request + * @param response current portlet Action response + * @throws Exception in case of any kind of processing failure + */ + protected void doActionService(ActionRequest request, ActionResponse response) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("DispatcherPortlet with name '" + getPortletName() + "' received action request"); + } + + // Expose current LocaleResolver and request as LocaleContext. + LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); + LocaleContextHolder.setLocaleContext(buildLocaleContext(request), this.threadContextInheritable); + + // Expose current RequestAttributes to current thread. + RequestAttributes previousRequestAttributes = RequestContextHolder.getRequestAttributes(); + PortletRequestAttributes requestAttributes = new PortletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable); + + if (logger.isDebugEnabled()) { + logger.debug("Bound action request context to thread: " + request); + } + + ActionRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + int interceptorIndex = -1; + + try { + processedRequest = checkMultipart(request); + + // Determine handler for the current request. + mappedHandler = getHandler(processedRequest, false); + if (mappedHandler == null || mappedHandler.getHandler() == null) { + noHandlerFound(processedRequest, response); + return; + } + + // Apply preHandle methods of registered interceptors. + HandlerInterceptor[] interceptors = mappedHandler.getInterceptors(); + if (interceptors != null) { + for (int i = 0; i < interceptors.length; i++) { + HandlerInterceptor interceptor = interceptors[i]; + if (!interceptor.preHandleAction(processedRequest, response, mappedHandler.getHandler())) { + triggerAfterActionCompletion(mappedHandler, interceptorIndex, processedRequest, response, null); + return; + } + interceptorIndex = i; + } + } + + // Actually invoke the handler. + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + ha.handleAction(processedRequest, response, mappedHandler.getHandler()); + + // Trigger after-completion for successful outcome. + triggerAfterActionCompletion(mappedHandler, interceptorIndex, processedRequest, response, null); + } + + catch (Exception ex) { + // Trigger after-completion for thrown exception. + triggerAfterActionCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex); + // Forward the exception to the render phase to be displayed. + try { + response.setRenderParameter(ACTION_EXCEPTION_RENDER_PARAMETER, ex.toString()); + request.getPortletSession().setAttribute(ACTION_EXCEPTION_SESSION_ATTRIBUTE, ex); + logger.debug("Caught exception during action phase - forwarding to render phase", ex); + } + catch (IllegalStateException ex2) { + // Probably sendRedirect called... need to rethrow exception immediately. + throw ex; + } + } + catch (Error err) { + PortletException ex = + new PortletException("Error occured during request processing: " + err.getMessage(), err); + // Trigger after-completion for thrown exception. + triggerAfterActionCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex); + throw ex; + } + + finally { + // Clean up any resources used by a multipart request. + if (processedRequest instanceof MultipartActionRequest && processedRequest != request) { + this.multipartResolver.cleanupMultipart((MultipartActionRequest) processedRequest); + } + + // Reset thread-bound context. + RequestContextHolder.setRequestAttributes(previousRequestAttributes, this.threadContextInheritable); + LocaleContextHolder.setLocaleContext(previousLocaleContext, this.threadContextInheritable); + + // Clear request attributes. + requestAttributes.requestCompleted(); + if (logger.isDebugEnabled()) { + logger.debug("Cleared thread-bound action request context: " + request); + } + } + } + + /** + * Processes the actual dispatching to the handler for render requests. + *

The handler will be obtained by applying the portlet's HandlerMappings in order. + * The HandlerAdapter will be obtained by querying the portlet's installed + * HandlerAdapters to find the first that supports the handler class. + * @param request current portlet render request + * @param response current portlet render response + * @throws Exception in case of any kind of processing failure + */ + protected void doRenderService(RenderRequest request, RenderResponse response) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("DispatcherPortlet with name '" + getPortletName() + "' received render request"); + } + + // Expose current LocaleResolver and request as LocaleContext. + LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext(); + LocaleContextHolder.setLocaleContext(buildLocaleContext(request), this.threadContextInheritable); + + // Expose current RequestAttributes to current thread. + RequestAttributes previousRequestAttributes = RequestContextHolder.getRequestAttributes(); + PortletRequestAttributes requestAttributes = new PortletRequestAttributes(request); + RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable); + + if (logger.isDebugEnabled()) { + logger.debug("Bound render request context to thread: " + request); + } + + RenderRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + int interceptorIndex = -1; + + try { + ModelAndView mv = null; + try { + // Check for forwarded exception from the action phase + PortletSession session = request.getPortletSession(false); + if (session != null) { + if (request.getParameter(ACTION_EXCEPTION_RENDER_PARAMETER) != null) { + Exception ex = (Exception) session.getAttribute(ACTION_EXCEPTION_SESSION_ATTRIBUTE); + if (ex != null) { + logger.debug("Render phase found exception caught during action phase - rethrowing it"); + throw ex; + } + } + else { + session.removeAttribute(ACTION_EXCEPTION_SESSION_ATTRIBUTE); + } + } + + // Determine handler for the current request. + mappedHandler = getHandler(processedRequest, false); + if (mappedHandler == null || mappedHandler.getHandler() == null) { + noHandlerFound(processedRequest, response); + return; + } + + // Apply preHandle methods of registered interceptors. + HandlerInterceptor[] interceptors = mappedHandler.getInterceptors(); + if (interceptors != null) { + for (int i = 0; i < interceptors.length; i++) { + HandlerInterceptor interceptor = interceptors[i]; + if (!interceptor.preHandleRender(processedRequest, response, mappedHandler.getHandler())) { + triggerAfterRenderCompletion(mappedHandler, interceptorIndex, processedRequest, response, null); + return; + } + interceptorIndex = i; + } + } + + // Actually invoke the handler. + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + mv = ha.handleRender(processedRequest, response, mappedHandler.getHandler()); + + // Apply postHandle methods of registered interceptors. + if (interceptors != null) { + for (int i = interceptors.length - 1; i >= 0; i--) { + HandlerInterceptor interceptor = interceptors[i]; + interceptor.postHandleRender(processedRequest, response, mappedHandler.getHandler(), mv); + } + } + } + catch (ModelAndViewDefiningException ex) { + logger.debug("ModelAndViewDefiningException encountered", ex); + mv = ex.getModelAndView(); + } + catch (Exception ex) { + Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); + mv = processHandlerException(request, response, handler, ex); + } + + // Did the handler return a view to render? + if (mv != null && !mv.isEmpty()) { + render(mv, processedRequest, response); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Null ModelAndView returned to DispatcherPortlet with name '" + + getPortletName() + "': assuming HandlerAdapter completed request handling"); + } + } + + // Trigger after-completion for successful outcome. + triggerAfterRenderCompletion(mappedHandler, interceptorIndex, processedRequest, response, null); + } + + catch (Exception ex) { + // Trigger after-completion for thrown exception. + triggerAfterRenderCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex); + throw ex; + } + catch (Error err) { + PortletException ex = + new PortletException("Error occured during request processing: " + err.getMessage(), err); + // Trigger after-completion for thrown exception. + triggerAfterRenderCompletion(mappedHandler, interceptorIndex, processedRequest, response, ex); + throw ex; + } + + finally { + // Reset thread-bound context. + RequestContextHolder.setRequestAttributes(previousRequestAttributes, this.threadContextInheritable); + LocaleContextHolder.setLocaleContext(previousLocaleContext, this.threadContextInheritable); + + // Clear request attributes. + requestAttributes.requestCompleted(); + if (logger.isDebugEnabled()) { + logger.debug("Cleared thread-bound render request context: " + request); + } + } + } + + + /** + * Build a LocaleContext for the given request, exposing the request's + * primary locale as current locale. + * @param request current HTTP request + * @return the corresponding LocaleContext + */ + protected LocaleContext buildLocaleContext(PortletRequest request) { + return new SimpleLocaleContext(request.getLocale()); + } + + /** + * Convert the request into a multipart request, and make multipart resolver available. + * If no multipart resolver is set, simply use the existing request. + * @param request current HTTP request + * @return the processed request (multipart wrapper if necessary) + */ + protected ActionRequest checkMultipart(ActionRequest request) throws MultipartException { + if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { + if (request instanceof MultipartActionRequest) { + logger.debug("Request is already a MultipartActionRequest - probably in a forward"); + } + else { + return this.multipartResolver.resolveMultipart(request); + } + } + // If not returned before: return original request. + return request; + } + + /** + * Return the HandlerExecutionChain for this request. + * Try all handler mappings in order. + * @param request current portlet request + * @param cache whether to cache the HandlerExecutionChain in a request attribute + * @return the HandlerExceutionChain, or null if no handler could be found + */ + protected HandlerExecutionChain getHandler(PortletRequest request, boolean cache) throws Exception { + HandlerExecutionChain handler = + (HandlerExecutionChain) request.getAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE); + if (handler != null) { + if (!cache) { + request.removeAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE); + } + return handler; + } + + Iterator it = this.handlerMappings.iterator(); + while (it.hasNext()) { + HandlerMapping hm = (HandlerMapping) it.next(); + if (logger.isDebugEnabled()) { + logger.debug("Testing handler map [" + hm + "] in DispatcherPortlet with name '" + + getPortletName() + "'"); + } + handler = hm.getHandler(request); + if (handler != null) { + if (cache) { + request.setAttribute(HANDLER_EXECUTION_CHAIN_ATTRIBUTE, handler); + } + return handler; + } + } + return null; + } + + /** + * No handler found -> throw appropriate exception. + * @param request current portlet request + * @param response current portlet response + * @throws Exception if preparing the response failed + */ + protected void noHandlerFound(PortletRequest request, PortletResponse response) throws Exception { + if (pageNotFoundLogger.isWarnEnabled()) { + pageNotFoundLogger.warn("No mapping found for current request " + + "in DispatcherPortlet with name '" + getPortletName() + "'" + + ", mode '" + request.getPortletMode() + "'" + + ", type '" + (response instanceof ActionResponse ? "action" : "render") + "'" + + ", session '" + request.getRequestedSessionId() + "'" + + ", user '" + getUsernameForRequest(request) + "'"); + } + throw new UnavailableException("No handler found for request"); + } + + /** + * Return the HandlerAdapter for this handler object. + * @param handler the handler object to find an adapter for + * @throws PortletException if no HandlerAdapter can be found for the handler. + * This is a fatal error. + */ + protected HandlerAdapter getHandlerAdapter(Object handler) throws PortletException { + Iterator it = this.handlerAdapters.iterator(); + while (it.hasNext()) { + HandlerAdapter ha = (HandlerAdapter) it.next(); + if (logger.isDebugEnabled()) { + logger.debug("Testing handler adapter [" + ha + "]"); + } + if (ha.supports(handler)) { + return ha; + } + } + throw new PortletException("No adapter for handler [" + handler + + "]: Does your handler implement a supported interface like Controller?"); + } + + /** + * Determine an error ModelAndView via the registered HandlerExceptionResolvers. + * @param request current portlet request + * @param response current portlet response + * @param handler the executed handler, or null if none chosen at the time of + * the exception (for example, if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to + * @throws Exception if no error ModelAndView found + */ + protected ModelAndView processHandlerException( + RenderRequest request, RenderResponse response, Object handler, Exception ex) + throws Exception { + + ModelAndView exMv = null; + for (Iterator it = this.handlerExceptionResolvers.iterator(); exMv == null && it.hasNext();) { + HandlerExceptionResolver resolver = (HandlerExceptionResolver) it.next(); + exMv = resolver.resolveException(request, response, handler, ex); + } + if (exMv != null) { + if (logger.isDebugEnabled()) { + logger.debug("HandlerExceptionResolver returned ModelAndView [" + exMv + "] for exception"); + } + logger.warn("Handler execution resulted in exception - forwarding to resolved error view", ex); + return exMv; + } + else { + throw ex; + } + } + + /** + * Trigger afterCompletion callbacks on the mapped HandlerInterceptors. + * Will just invoke afterCompletion for all interceptors whose preHandle + * invocation has successfully completed and returned true. + * @param mappedHandler the mapped HandlerExecutionChain + * @param interceptorIndex index of last interceptor that successfully completed + * @param ex Exception thrown on handler execution, or null if none + * @see HandlerInterceptor#afterRenderCompletion + */ + private void triggerAfterActionCompletion(HandlerExecutionChain mappedHandler, int interceptorIndex, + ActionRequest request, ActionResponse response, Exception ex) + throws Exception { + + // Apply afterCompletion methods of registered interceptors. + if (mappedHandler != null) { + HandlerInterceptor[] interceptors = mappedHandler.getInterceptors(); + if (interceptors != null) { + for (int i = interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = interceptors[i]; + try { + interceptor.afterActionCompletion(request, response, mappedHandler.getHandler(), ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } + } + } + } + + + /** + * Render the given ModelAndView. This is the last stage in handling a request. + * It may involve resolving the view by name. + * @param mv the ModelAndView to render + * @param request current portlet render request + * @param response current portlet render response + * @throws Exception if there's a problem rendering the view + */ + protected void render(ModelAndView mv, RenderRequest request, RenderResponse response) throws Exception { + View view = null; + if (mv.isReference()) { + // We need to resolve the view name. + view = resolveViewName(mv.getViewName(), mv.getModelInternal(), request); + if (view == null) { + throw new PortletException("Could not resolve view with name '" + mv.getViewName() + + "' in portlet with name '" + getPortletName() + "'"); + } + } + else { + // No need to lookup: the ModelAndView object contains the actual View object. + Object viewObject = mv.getView(); + if (viewObject == null) { + throw new PortletException("ModelAndView [" + mv + "] neither contains a view name nor a " + + "View object in portlet with name '" + getPortletName() + "'"); + } + if (!(viewObject instanceof View)) { + throw new PortletException( + "View object [" + viewObject + "] is not an instance of [org.springframework.web.servlet.View] - " + + "DispatcherPortlet does not support any other view types"); + } + view = (View) viewObject; + } + + if (view == null) { + throw new PortletException("Could not resolve view with name '" + mv.getViewName() + + "' in portlet with name '" + getPortletName() + "'"); + } + + // Set the content type on the response if needed and if possible. + // The Portlet spec requires the content type to be set on the RenderResponse; + // it's not sufficient to let the View set it on the ServletResponse. + if (response.getContentType() != null) { + if (logger.isDebugEnabled()) { + logger.debug("Portlet response content type already set to [" + response.getContentType() + "]"); + } + } + else { + // No Portlet content type specified yet -> use the view-determined type. + String contentType = view.getContentType(); + if (contentType != null) { + if (logger.isDebugEnabled()) { + logger.debug("Setting portlet response content type to view-determined type [" + contentType + "]"); + } + response.setContentType(contentType); + } + } + + doRender(view, mv.getModelInternal(), request, response); + } + + /** + * Resolve the given view name into a View object (to be rendered). + *

Default implementations asks all ViewResolvers of this dispatcher. + * Can be overridden for custom resolution strategies, potentially based + * on specific model attributes or request parameters. + * @param viewName the name of the view to resolve + * @param model the model to be passed to the view + * @param request current portlet render request + * @return the View object, or null if none found + * @throws Exception if the view cannot be resolved + * (typically in case of problems creating an actual View object) + * @see ViewResolver#resolveViewName + */ + protected View resolveViewName(String viewName, Map model, RenderRequest request) throws Exception { + for (Iterator it = this.viewResolvers.iterator(); it.hasNext();) { + ViewResolver viewResolver = (ViewResolver) it.next(); + View view = viewResolver.resolveViewName(viewName, request.getLocale()); + if (view != null) { + return view; + } + } + return null; + } + + /** + * Actually render the given view. + *

The default implementation delegates to + * {@link org.springframework.web.servlet.ViewRendererServlet}. + * @param view the View to render + * @param model the associated model + * @param request current portlet render request + * @param response current portlet render response + * @throws Exception if there's a problem rendering the view + */ + protected void doRender(View view, Map model, RenderRequest request, RenderResponse response) throws Exception { + // Expose Portlet ApplicationContext to view objects. + request.setAttribute(ViewRendererServlet.WEB_APPLICATION_CONTEXT_ATTRIBUTE, getPortletApplicationContext()); + + // These attributes are required by the ViewRendererServlet. + request.setAttribute(ViewRendererServlet.VIEW_ATTRIBUTE, view); + request.setAttribute(ViewRendererServlet.MODEL_ATTRIBUTE, model); + + // Include the content of the view in the render response. + getPortletContext().getRequestDispatcher(this.viewRendererUrl).include(request, response); + } + + /** + * Trigger afterCompletion callbacks on the mapped HandlerInterceptors. + * Will just invoke afterCompletion for all interceptors whose preHandle + * invocation has successfully completed and returned true. + * @param mappedHandler the mapped HandlerExecutionChain + * @param interceptorIndex index of last interceptor that successfully completed + * @param ex Exception thrown on handler execution, or null if none + * @see HandlerInterceptor#afterRenderCompletion + */ + private void triggerAfterRenderCompletion(HandlerExecutionChain mappedHandler, int interceptorIndex, + RenderRequest request, RenderResponse response, Exception ex) + throws Exception { + + // Apply afterCompletion methods of registered interceptors. + if (mappedHandler != null) { + HandlerInterceptor[] interceptors = mappedHandler.getInterceptors(); + if (interceptors != null) { + for (int i = interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = interceptors[i]; + try { + interceptor.afterRenderCompletion(request, response, mappedHandler.getHandler(), ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } + } + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/DispatcherPortlet.properties b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/DispatcherPortlet.properties new file mode 100644 index 00000000000..68c701b5536 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/DispatcherPortlet.properties @@ -0,0 +1,10 @@ +# Default implementation classes for DispatcherPortlet's strategy interfaces. +# Used as fallback when no matching beans are found in the DispatcherPortlet context. +# Not meant to be customized by application developers. + +org.springframework.web.portlet.HandlerMapping=org.springframework.web.portlet.mvc.annotation.DefaultAnnotationHandlerMapping + +org.springframework.web.portlet.HandlerAdapter=org.springframework.web.portlet.mvc.SimpleControllerHandlerAdapter,\ + org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter + +org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/FrameworkPortlet.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/FrameworkPortlet.java new file mode 100644 index 00000000000..577b3800a1b --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/FrameworkPortlet.java @@ -0,0 +1,599 @@ +/* + * 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; + +import java.io.IOException; +import java.security.Principal; +import java.util.Map; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextException; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.SourceFilteringListener; +import org.springframework.web.portlet.context.ConfigurablePortletApplicationContext; +import org.springframework.web.portlet.context.PortletApplicationContextUtils; +import org.springframework.web.portlet.context.PortletRequestHandledEvent; +import org.springframework.web.portlet.context.XmlPortletApplicationContext; + +/** + * Base portlet for Spring's portlet framework. Provides integration with + * a Spring application context, in a JavaBean-based overall solution. + * + *

This class offers the following functionality: + *

+ * + *

Subclasses must implement {@link #doActionService} and {@link #doRenderService} + * to handle action and render requests. Because this extends {@link GenericPortletBean} + * rather than Portlet directly, bean properties are mapped onto it. Subclasses can + * override {@link #initFrameworkPortlet()} for custom initialization. + * + *

Regards a "contextClass" parameter at the portlet init-param level, + * falling back to the default context class + * ({@link org.springframework.web.portlet.context.XmlPortletApplicationContext}) + * if not found. Note that, with the default FrameworkPortlet, + * a context class needs to implement the + * {@link org.springframework.web.portlet.context.ConfigurablePortletApplicationContext} SPI. + * + *

Passes a "contextConfigLocation" portlet init-param to the context instance, + * parsing it into potentially multiple file paths which can be separated by any + * number of commas and spaces, like "test-portlet.xml, myPortlet.xml". + * If not explicitly specified, the context implementation is supposed to build a + * default location from the namespace of the portlet. + * + *

Note: In case of multiple config locations, later bean definitions will + * override ones defined in earlier loaded files, at least when using one of + * Spring's default ApplicationContext implementations. This can be leveraged + * to deliberately override certain bean definitions via an extra XML file. + * + *

The default namespace is "'portlet-name'-portlet", e.g. "test-portlet" for a + * portlet-name "test" (leading to a "/WEB-INF/test-portlet.xml" default location + * with XmlPortletApplicationContext). The namespace can also be set explicitly via + * the "namespace" portlet init-param. + * + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + * @see #doActionService + * @see #doRenderService + * @see #setContextClass + * @see #setContextConfigLocation + * @see #setNamespace + */ +public abstract class FrameworkPortlet extends GenericPortletBean implements ApplicationListener { + + /** + * Default context class for FrameworkPortlet. + * @see org.springframework.web.portlet.context.XmlPortletApplicationContext + */ + public static final Class DEFAULT_CONTEXT_CLASS = XmlPortletApplicationContext.class; + + /** + * Suffix for Portlet ApplicationContext namespaces. If a portlet of this class is + * given the name "test" in a context, the namespace used by the portlet will + * resolve to "test-portlet". + */ + public static final String DEFAULT_NAMESPACE_SUFFIX = "-portlet"; + + /** + * Prefix for the PortletContext attribute for the Portlet ApplicationContext. + * The completion is the portlet name. + */ + public static final String PORTLET_CONTEXT_PREFIX = FrameworkPortlet.class.getName() + ".CONTEXT."; + + /** + * Default USER_INFO attribute names to search for the current username: + * "user.login.id", "user.name". + */ + public static final String[] DEFAULT_USERINFO_ATTRIBUTE_NAMES = {"user.login.id", "user.name"}; + + + /** Portlet ApplicationContext implementation class to use */ + private Class contextClass = DEFAULT_CONTEXT_CLASS; + + /** Namespace for this portlet */ + private String namespace; + + /** Explicit context config location */ + private String contextConfigLocation; + + /** Should we publish the context as a PortletContext attribute? */ + private boolean publishContext = true; + + /** Should we publish a PortletRequestHandledEvent at the end of each request? */ + private boolean publishEvents = true; + + /** USER_INFO attributes that may contain the username of the current user */ + private String[] userinfoUsernameAttributes = DEFAULT_USERINFO_ATTRIBUTE_NAMES; + + /** ApplicationContext for this portlet */ + private ApplicationContext portletApplicationContext; + + /** Flag used to detect whether onRefresh has already been called */ + private boolean refreshEventReceived = false; + + + /** + * Set a custom context class. This class must be of type ApplicationContext; + * when using the default FrameworkPortlet implementation, the context class + * must also implement ConfigurablePortletApplicationContext. + * @see #createPortletApplicationContext + */ + public void setContextClass(Class contextClass) { + this.contextClass = contextClass; + } + + /** + * Return the custom context class. + */ + public Class getContextClass() { + return this.contextClass; + } + + /** + * Set a custom namespace for this portlet, + * to be used for building a default context config location. + */ + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + /** + * Return the namespace for this portlet, falling back to default scheme if + * no custom namespace was set. (e.g. "test-portlet" for a portlet named "test") + */ + public String getNamespace() { + return (this.namespace != null) ? this.namespace : getPortletName() + DEFAULT_NAMESPACE_SUFFIX; + } + + /** + * Set the context config location explicitly, instead of relying on the default + * location built from the namespace. This location string can consist of + * multiple locations separated by any number of commas and spaces. + */ + public void setContextConfigLocation(String contextConfigLocation) { + this.contextConfigLocation = contextConfigLocation; + } + + /** + * Return the explicit context config location, if any. + */ + public String getContextConfigLocation() { + return this.contextConfigLocation; + } + + /** + * Set whether to publish this portlet's context as a PortletContext attribute, + * available to all objects in the web container. Default is true. + *

This is especially handy during testing, although it is debatable whether + * it's good practice to let other application objects access the context this way. + */ + public void setPublishContext(boolean publishContext) { + this.publishContext = publishContext; + } + + /** + * Return whether to publish this portlet's context as a PortletContext attribute. + */ + public boolean isPublishContext() { + return this.publishContext; + } + + /** + * Set whether this portlet should publish a PortletRequestHandledEvent at the end + * of each request. Default is true; can be turned off for a slight performance + * improvement, provided that no ApplicationListeners rely on such events. + * @see org.springframework.web.portlet.context.PortletRequestHandledEvent + */ + public void setPublishEvents(boolean publishEvents) { + this.publishEvents = publishEvents; + } + + /** + * Return whether this portlet should publish a PortletRequestHandledEvent at the end + * of each request. + */ + public boolean isPublishEvents() { + return this.publishEvents; + } + + /** + * Set the list of attributes to search in the USER_INFO map when trying + * to find the username of the current user. + * @see #getUsernameForRequest + */ + public void setUserinfoUsernameAttributes(String[] userinfoUsernameAttributes) { + this.userinfoUsernameAttributes = userinfoUsernameAttributes; + } + + /** + * Returns the list of attributes that will be searched in the USER_INFO map + * when trying to find the username of the current user + * @see #getUsernameForRequest + */ + public String[] getUserinfoUsernameAttributes() { + return this.userinfoUsernameAttributes; + } + + + /** + * Overridden method of GenericPortletBean, invoked after any bean properties + * have been set. Creates this portlet's ApplicationContext. + */ + protected final void initPortletBean() throws PortletException, BeansException { + getPortletContext().log("Initializing Spring FrameworkPortlet '" + getPortletName() + "'"); + if (logger.isInfoEnabled()) { + logger.info("FrameworkPortlet '" + getPortletName() + "': initialization started"); + } + long startTime = System.currentTimeMillis(); + + try { + this.portletApplicationContext = initPortletApplicationContext(); + initFrameworkPortlet(); + } + catch (PortletException ex) { + logger.error("Context initialization failed", ex); + throw ex; + } + catch (BeansException ex) { + logger.error("Context initialization failed", ex); + throw ex; + } + + if (logger.isInfoEnabled()) { + long elapsedTime = System.currentTimeMillis() - startTime; + logger.info("FrameworkPortlet '" + getPortletName() + "': initialization completed in " + elapsedTime + " ms"); + } + } + + /** + * Initialize and publish the Portlet ApplicationContext for this portlet. + *

Delegates to {@link #createPortletApplicationContext} for actual creation. + * Can be overridden in subclasses. + * @return the ApplicationContext for this portlet + * @throws BeansException if the context couldn't be initialized + */ + protected ApplicationContext initPortletApplicationContext() throws BeansException { + ApplicationContext parent = PortletApplicationContextUtils.getWebApplicationContext(getPortletContext()); + ApplicationContext pac = createPortletApplicationContext(parent); + + if (!this.refreshEventReceived) { + // Apparently not a ConfigurableApplicationContext with refresh support: + // triggering initial onRefresh manually here. + onRefresh(pac); + } + + if (isPublishContext()) { + // publish the context as a portlet context attribute + String attName = getPortletContextAttributeName(); + getPortletContext().setAttribute(attName, pac); + if (logger.isDebugEnabled()) { + logger.debug("Published ApplicationContext of portlet '" + getPortletName() + + "' as PortletContext attribute with name [" + attName + "]"); + } + } + return pac; + } + + /** + * Instantiate the Portlet ApplicationContext for this portlet, either a default + * XmlPortletApplicationContext or a custom context class if set. + *

This implementation expects custom contexts to implement + * ConfigurablePortletApplicationContext. Can be overridden in subclasses. + * @param parent the parent ApplicationContext to use, or null if none + * @return the Portlet ApplicationContext for this portlet + * @throws BeansException if the context couldn't be initialized + * @see #setContextClass + * @see org.springframework.web.portlet.context.XmlPortletApplicationContext + */ + protected ApplicationContext createPortletApplicationContext(ApplicationContext parent) + throws BeansException { + + if (logger.isDebugEnabled()) { + logger.debug("Portlet with name '" + getPortletName() + + "' will try to create custom ApplicationContext context of class '" + + getContextClass().getName() + "'" + ", using parent context [" + parent + "]"); + } + if (!ConfigurablePortletApplicationContext.class.isAssignableFrom(getContextClass())) { + throw new ApplicationContextException("Fatal initialization error in portlet with name '" + getPortletName() + + "': custom ApplicationContext class [" + getContextClass().getName() + + "] is not of type ConfigurablePortletApplicationContext"); + } + + ConfigurablePortletApplicationContext pac = + (ConfigurablePortletApplicationContext) BeanUtils.instantiateClass(getContextClass()); + pac.setParent(parent); + pac.setPortletContext(getPortletContext()); + pac.setPortletConfig(getPortletConfig()); + pac.setNamespace(getNamespace()); + pac.setConfigLocation(getContextConfigLocation()); + pac.addApplicationListener(new SourceFilteringListener(pac, this)); + + postProcessPortletApplicationContext(pac); + pac.refresh(); + + return pac; + } + + /** + * Post-process the given Portlet ApplicationContext before it is refreshed + * and activated as context for this portlet. + *

The default implementation is empty. refresh() will + * be called automatically after this method returns. + * @param pac the configured Portlet ApplicationContext (not refreshed yet) + * @see #createPortletApplicationContext + * @see ConfigurableApplicationContext#refresh() + */ + protected void postProcessPortletApplicationContext(ConfigurableApplicationContext pac) { + } + + /** + * Return the PortletContext attribute name for this portlets's ApplicationContext. + *

The default implementation returns PORTLET_CONTEXT_PREFIX + portlet name. + * @see #PORTLET_CONTEXT_PREFIX + * @see #getPortletName + */ + public String getPortletContextAttributeName() { + return PORTLET_CONTEXT_PREFIX + getPortletName(); + } + + /** + * Return this portlet's ApplicationContext. + */ + public final ApplicationContext getPortletApplicationContext() { + return this.portletApplicationContext; + } + + + /** + * This method will be invoked after any bean properties have been set and + * the ApplicationContext has been loaded. + *

The default implementation is empty; subclasses may override this method + * to perform any initialization they require. + * @throws PortletException in case of an initialization exception + * @throws BeansException if thrown by ApplicationContext methods + */ + protected void initFrameworkPortlet() throws PortletException, BeansException { + } + + /** + * Refresh this portlet's application context, as well as the + * dependent state of the portlet. + * @throws BeansException in case of errors + * @see #getPortletApplicationContext() + * @see org.springframework.context.ConfigurableApplicationContext#refresh() + */ + public void refresh() throws BeansException { + ApplicationContext pac = getPortletApplicationContext(); + if (!(pac instanceof ConfigurableApplicationContext)) { + throw new IllegalStateException("Portlet ApplicationContext does not support refresh: " + pac); + } + ((ConfigurableApplicationContext) pac).refresh(); + } + + /** + * ApplicationListener endpoint that receives events from this servlet's + * WebApplicationContext. + *

The default implementation calls {@link #onRefresh} in case of a + * {@link org.springframework.context.event.ContextRefreshedEvent}, + * triggering a refresh of this servlet's context-dependent state. + * @param event the incoming ApplicationContext event + */ + public void onApplicationEvent(ApplicationEvent event) { + if (event instanceof ContextRefreshedEvent) { + this.refreshEventReceived = true; + onRefresh(((ContextRefreshedEvent) event).getApplicationContext()); + } + } + + /** + * Template method which can be overridden to add portlet-specific refresh work. + * Called after successful context refresh. + *

This implementation is empty. + * @param context the current Portlet ApplicationContext + * @throws BeansException in case of errors + * @see #refresh() + */ + protected void onRefresh(ApplicationContext context) throws BeansException { + // For subclasses: do nothing by default. + } + + + /** + * Overridden for friendlier behavior in unit tests. + */ + protected String getTitle(RenderRequest renderRequest) { + try { + return super.getTitle(renderRequest); + } + catch (NullPointerException ex) { + return getPortletName(); + } + } + + /** + * Delegate render requests to processRequest/doRenderService. + */ + protected final void doDispatch(RenderRequest request, RenderResponse response) + throws PortletException, IOException { + + processRequest(request, response); + } + + /** + * Delegate action requests to processRequest/doActionService. + */ + public final void processAction(ActionRequest request, ActionResponse response) + throws PortletException, IOException { + + processRequest(request, response); + } + + /** + * Process this request, publishing an event regardless of the outcome. + * The actual event handling is performed by the abstract + * doActionService() and doRenderService() template methods. + * @see #doActionService + * @see #doRenderService + */ + protected final void processRequest(PortletRequest request, PortletResponse response) + throws PortletException, IOException { + + long startTime = System.currentTimeMillis(); + Throwable failureCause = null; + + try { + if (request instanceof ActionRequest) { + doActionService((ActionRequest) request, (ActionResponse) response); + } + else { + doRenderService((RenderRequest) request, (RenderResponse) response); + } + } + catch (PortletException ex) { + failureCause = ex; + throw ex; + } + catch (IOException ex) { + failureCause = ex; + throw ex; + } + catch (Throwable ex) { + failureCause = ex; + throw new PortletException("Request processing failed", ex); + } + + finally { + if (failureCause != null) { + logger.error("Could not complete request", failureCause); + } + else { + logger.debug("Successfully completed request"); + } + if (isPublishEvents()) { + // Whether or not we succeeded, publish an event. + long processingTime = System.currentTimeMillis() - startTime; + this.portletApplicationContext.publishEvent( + new PortletRequestHandledEvent(this, + getPortletConfig().getPortletName(), request.getPortletMode().toString(), + (request instanceof ActionRequest ? "action" : "render"), + request.getRequestedSessionId(), getUsernameForRequest(request), + processingTime, failureCause)); + } + } + } + + /** + * Determine the username for the given request. + *

The default implementation first tries the UserPrincipal. + * If that does not exist, then it checks the USER_INFO map. + * Can be overridden in subclasses. + * @param request current portlet request + * @return the username, or null if none found + * @see javax.portlet.PortletRequest#getUserPrincipal() + * @see javax.portlet.PortletRequest#getRemoteUser() + * @see javax.portlet.PortletRequest#USER_INFO + * @see #setUserinfoUsernameAttributes + */ + protected String getUsernameForRequest(PortletRequest request) { + // Try the principal. + Principal userPrincipal = request.getUserPrincipal(); + if (userPrincipal != null) { + return userPrincipal.getName(); + } + + // Try the remote user name. + String userName = request.getRemoteUser(); + if (userName != null) { + return userName; + } + + // Try the Portlet USER_INFO map. + Map userInfo = (Map) request.getAttribute(PortletRequest.USER_INFO); + if (userInfo != null) { + for (int i = 0, n = this.userinfoUsernameAttributes.length; i < n; i++) { + userName = (String) userInfo.get(this.userinfoUsernameAttributes[i]); + if (userName != null) { + return userName; + } + } + } + + // Nothing worked... + return null; + } + + /** + * Subclasses must implement this method to do the work of render request handling. + *

The contract is essentially the same as that for the doDispatch + * method of GenericPortlet. + *

This class intercepts calls to ensure that exception handling and + * event publication takes place. + * @param request current render request + * @param response current render response + * @throws Exception in case of any kind of processing failure + * @see javax.portlet.GenericPortlet#doDispatch + */ + protected abstract void doRenderService(RenderRequest request, RenderResponse response) + throws Exception; + + /** + * Subclasses must implement this method to do the work of action request handling. + *

The contract is essentially the same as that for the processAction + * method of GenericPortlet. + *

This class intercepts calls to ensure that exception handling and + * event publication takes place. + * @param request current action request + * @param response current action response + * @throws Exception in case of any kind of processing failure + * @see javax.portlet.GenericPortlet#processAction + */ + protected abstract void doActionService(ActionRequest request, ActionResponse response) + throws Exception; + + + /** + * Close the ApplicationContext of this portlet. + * @see org.springframework.context.ConfigurableApplicationContext#close() + */ + public void destroy() { + getPortletContext().log("Destroying Spring FrameworkPortlet '" + getPortletName() + "'"); + if (this.portletApplicationContext instanceof ConfigurableApplicationContext) { + ((ConfigurableApplicationContext) this.portletApplicationContext).close(); + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/GenericPortletBean.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/GenericPortletBean.java new file mode 100644 index 00000000000..c6061072873 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/GenericPortletBean.java @@ -0,0 +1,200 @@ +/* + * 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; + +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; + +import javax.portlet.GenericPortlet; +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; +import javax.portlet.PortletException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeansException; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyAccessorFactory; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceEditor; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.StringUtils; +import org.springframework.web.portlet.context.PortletContextResourceLoader; + +/** + * Simple extension of javax.portlet.GenericPortlet that treats + * its config parameters as bean properties. + * + *

A very handy superclass for any type of portlet. Type conversion is automatic. + * It is also possible for subclasses to specify required properties. + * + *

This portlet leaves request handling to subclasses, inheriting the default + * behaviour of GenericPortlet (doDispatch, processAction, etc). + * + *

This portlet superclass has no dependency on a Spring application context, + * in contrast to the FrameworkPortlet class which loads its own context. + * + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + * @see #addRequiredProperty + * @see #initPortletBean + * @see #doDispatch + * @see #processAction + * @see FrameworkPortlet + */ +public abstract class GenericPortletBean extends GenericPortlet { + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Set of required properties (Strings) that must be supplied as + * config parameters to this portlet. + */ + private final Set requiredProperties = new HashSet(); + + + /** + * Subclasses can invoke this method to specify that this property + * (which must match a JavaBean property they expose) is mandatory, + * and must be supplied as a config parameter. This method would + * normally be called from a subclass constructor. + * @param property name of the required property + */ + protected final void addRequiredProperty(String property) { + this.requiredProperties.add(property); + } + + /** + * Map config parameters onto bean properties of this portlet, and + * invoke subclass initialization. + * @throws PortletException if bean properties are invalid (or required + * properties are missing), or if subclass initialization fails. + */ + public final void init() throws PortletException { + if (logger.isInfoEnabled()) { + logger.info("Initializing portlet '" + getPortletName() + "'"); + } + + // Set bean properties from init parameters. + try { + PropertyValues pvs = new PortletConfigPropertyValues(getPortletConfig(), this.requiredProperties); + BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); + ResourceLoader resourceLoader = new PortletContextResourceLoader(getPortletContext()); + bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader)); + initBeanWrapper(bw); + bw.setPropertyValues(pvs, true); + } + catch (BeansException ex) { + logger.error("Failed to set bean properties on portlet '" + getPortletName() + "'", ex); + throw ex; + } + + // let subclasses do whatever initialization they like + initPortletBean(); + + if (logger.isInfoEnabled()) { + logger.info("Portlet '" + getPortletName() + "' configured successfully"); + } + } + + /** + * Initialize the BeanWrapper for this GenericPortletBean, + * possibly with custom editors. + * @param bw the BeanWrapper to initialize + * @throws BeansException if thrown by BeanWrapper methods + * @see org.springframework.beans.BeanWrapper#registerCustomEditor + */ + protected void initBeanWrapper(BeanWrapper bw) throws BeansException { + } + + + /** + * Overridden method that simply returns null when no + * PortletConfig set yet. + * @see #getPortletConfig() + */ + public final String getPortletName() { + return (getPortletConfig() != null ? getPortletConfig().getPortletName() : null); + } + + /** + * Overridden method that simply returns null when no + * PortletConfig set yet. + * @see #getPortletConfig() + */ + public final PortletContext getPortletContext() { + return (getPortletConfig() != null ? getPortletConfig().getPortletContext() : null); + } + + + /** + * Subclasses may override this to perform custom initialization. + * All bean properties of this portlet will have been set before this + * method is invoked. This default implementation does nothing. + * @throws PortletException if subclass initialization fails + */ + protected void initPortletBean() throws PortletException { + } + + + /** + * PropertyValues implementation created from PortletConfig init parameters. + */ + private static class PortletConfigPropertyValues extends MutablePropertyValues { + + /** + * Create new PortletConfigPropertyValues. + * @param config PortletConfig we'll use to take PropertyValues from + * @param requiredProperties set of property names we need, where + * we can't accept default values + * @throws PortletException if any required properties are missing + */ + private PortletConfigPropertyValues(PortletConfig config, Set requiredProperties) + throws PortletException { + + Set missingProps = (requiredProperties != null && !requiredProperties.isEmpty()) ? + new HashSet(requiredProperties) : null; + + Enumeration en = config.getInitParameterNames(); + while (en.hasMoreElements()) { + String property = (String) en.nextElement(); + Object value = config.getInitParameter(property); + addPropertyValue(new PropertyValue(property, value)); + if (missingProps != null) { + missingProps.remove(property); + } + } + + // fail if we are still missing properties + if (missingProps != null && missingProps.size() > 0) { + throw new PortletException( + "Initialization from PortletConfig for portlet '" + config.getPortletName() + + "' failed; the following required properties were missing: " + + StringUtils.collectionToDelimitedString(missingProps, ", ")); + } + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerAdapter.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerAdapter.java new file mode 100644 index 00000000000..ef771e02c37 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerAdapter.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2006 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; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +/** + * Portlet MVC framework SPI interface, allowing parameterization of core MVC workflow. + * + *

Interface that must be implemented for each handler type to handle a request. + * This interface is used to allow the DispatcherPortlet to be indefinitely + * extensible. The DispatcherPortlet accesses all installed handlers through this + * interface, meaning that it does not contain code specific to any handler type. + * + *

Note that a handler can be of type Object. This is to enable handlers from + * other frameworks to be integrated with this framework without custom coding. + * + *

This interface is not intended for application developers. It is available + * to handlers who want to develop their own web workflow. + * + *

Note: Implementations can implement the Ordered interface to be able to + * specify a sorting order and thus a priority for getting applied by + * DispatcherPortlet. Non-Ordered instances get treated as lowest priority. + * + * @author John A. Lewis + * @since 2.0 + * @see org.springframework.web.portlet.mvc.SimpleControllerHandlerAdapter + */ +public interface HandlerAdapter { + + /** + * Given a handler instance, return whether or not this HandlerAdapter can + * support it. Typical HandlerAdapters will base the decision on the handler + * type. HandlerAdapters will usually only support one handler type each. + *

A typical implementation: + *

+ * return (handler instanceof MyHandler); + * + * @param handler handler object to check + * @return whether or not this object can use the given handler + */ + boolean supports(Object handler); + + /** + * Use the given handler to handle this action request. + * The workflow that is required may vary widely. + * @param request current action request + * @param response current action response + * @param handler handler to use. This object must have previously been passed + * to the supports method of this interface, which must have + * returned true. + * @throws Exception in case of errors + */ + void handleAction(ActionRequest request, ActionResponse response, Object handler) throws Exception; + + /** + * Use the given handler to handle this render request. + * The workflow that is required may vary widely. + * @param request current render request + * @param response current render response + * @param handler handler to use. This object must have previously been passed + * to the supports method of this interface, which must have + * returned true. + * @throws Exception in case of errors + * @return ModelAndView object with the name of the view and the required + * model data, or null if the request has been handled directly + */ + ModelAndView handleRender(RenderRequest request, RenderResponse response, Object handler) throws Exception; + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerExceptionResolver.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerExceptionResolver.java new file mode 100644 index 00000000000..42695edd474 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerExceptionResolver.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2005 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; + +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +/** + * Interface to be implemented by objects than can resolve exceptions thrown + * during handler mapping or execution, in the typical case to error views. + * Implementors are typically registered as beans in the application context. + * + *

Error views are analogous to the error page JSPs, but can be used with + * any kind of exception including any checked exception, with potentially + * fine-granular mappings for specific handlers. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + */ +public interface HandlerExceptionResolver { + + /** + * Try to resolve the given exception that got thrown during on handler execution, + * returning a ModelAndView that represents a specific error page if appropriate. + * @param request current portlet request + * @param response current portlet response + * @param handler the executed handler, or null if none chosen at the time of + * the exception (for example, if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to, or null for default processing + */ + ModelAndView resolveException( + RenderRequest request, RenderResponse response, Object handler, Exception ex); + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerExecutionChain.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerExecutionChain.java new file mode 100644 index 00000000000..eb50e316009 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerExecutionChain.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2007 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; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.util.CollectionUtils; + +/** + * Handler execution chain, consisting of handler object and any handler interceptors. + * Returned by HandlerMapping's {@link HandlerMapping#getHandler} method. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see HandlerInterceptor + */ +public class HandlerExecutionChain { + + private final Object handler; + + private HandlerInterceptor[] interceptors; + + private List interceptorList; + + + /** + * Create a new HandlerExecutionChain. + * @param handler the handler object to execute + */ + public HandlerExecutionChain(Object handler) { + this(handler, null); + } + + /** + * Create a new HandlerExecutionChain. + * @param handler the handler object to execute + * @param interceptors the array of interceptors to apply + * (in the given order) before the handler itself executes + */ + public HandlerExecutionChain(Object handler, HandlerInterceptor[] interceptors) { + if (handler instanceof HandlerExecutionChain) { + HandlerExecutionChain originalChain = (HandlerExecutionChain) handler; + this.handler = originalChain.getHandler(); + this.interceptorList = new ArrayList(); + CollectionUtils.mergeArrayIntoCollection(originalChain.getInterceptors(), this.interceptorList); + CollectionUtils.mergeArrayIntoCollection(interceptors, this.interceptorList); + } + else { + this.handler = handler; + this.interceptors = interceptors; + } + } + + + /** + * Return the handler object to execute. + * @return the handler object + */ + public Object getHandler() { + return this.handler; + } + + public void addInterceptor(HandlerInterceptor interceptor) { + initInterceptorList(); + this.interceptorList.add(interceptor); + } + + public void addInterceptors(HandlerInterceptor[] interceptors) { + if (interceptors != null) { + initInterceptorList(); + for (int i = 0; i < interceptors.length; i++) { + this.interceptorList.add(interceptors[i]); + } + } + } + + private void initInterceptorList() { + if (this.interceptorList == null) { + this.interceptorList = new ArrayList(); + } + if (this.interceptors != null) { + for (int i = 0; i < this.interceptors.length; i++) { + this.interceptorList.add(this.interceptors[i]); + } + this.interceptors = null; + } + } + + /** + * Return the array of interceptors to apply (in the given order). + * @return the array of HandlerInterceptors instances (may be null) + */ + public HandlerInterceptor[] getInterceptors() { + if (this.interceptors == null && this.interceptorList != null) { + this.interceptors = (HandlerInterceptor[]) + this.interceptorList.toArray(new HandlerInterceptor[this.interceptorList.size()]); + } + return this.interceptors; + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerInterceptor.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerInterceptor.java new file mode 100644 index 00000000000..9ab888cd4c3 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerInterceptor.java @@ -0,0 +1,211 @@ +/* + * Copyright 2002-2007 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; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +/** + * Workflow interface that allows for customized handler execution chains. + * Applications can register any number of existing or custom interceptors + * for certain groups of handlers, to add common pre-processing behavior + * without needing to modify each handler implementation. + * + *

A HandlerInterceptor gets called before the appropriate + * {@link org.springframework.web.portlet.HandlerAdapter} triggers the + * execution of the handler itself. This mechanism can be used for a large + * field of preprocessing aspects, e.g. for authorization checks, + * or common handler behavior like locale or theme changes. Its main purpose + * is to permit the factoring out of otherwise repetitive handler code. + * + *

Typically an interceptor chain is defined per + * {@link org.springframework.web.portlet.HandlerMapping} bean, sharing its + * granularity. To be able to apply a certain interceptor chain to a group of + * handlers, one needs to map the desired handlers via one + * HandlerMapping bean. The interceptors themselves are defined as + * beans in the application context, referenced by the mapping bean definition + * via its + * {@link org.springframework.web.portlet.handler.AbstractHandlerMapping#setInterceptors "interceptors"} + * property (in XML: a <list> of <ref> elements). + * + *

A HandlerInterceptor is basically similar to a Servlet + * {@link javax.servlet.Filter}, but in contrast to the latter it allows + * custom pre-processing with the option to prohibit the execution of the handler + * itself, and custom post-processing. Filters are more powerful; + * for example they allow for exchanging the request and response objects that + * are handed down the chain. Note that a filter gets configured in + * web.xml, a HandlerInterceptor in the application context. + * + *

As a basic guideline, fine-grained handler-related pre-processing tasks are + * candidates for HandlerInterceptor implementations, especially + * factored-out common handler code and authorization checks. On the other hand, + * a Filter is well-suited for request content and view content + * handling, like multipart forms and GZIP compression. This typically shows when + * one needs to map the filter to certain content types (e.g. images), or to all + * requests. + * + *

Be aware that filters cannot be applied to portlet requests (they + * only operate on servlet requests), so for portlet requests interceptors are + * essential. + * + *

If we assume a "sunny day" request cycle (i.e. a request where nothing goes wrong + * and all is well), the workflow of a HandlerInterceptor will be as + * follows: + * + *

Action Request:

+ *

    + *
  1. DispatcherPortlet maps the action request to a particular handler + * and assembles a handler execution chain consisting of the handler that + * is to be invoked and all of the HandlerInterceptor + * instances that apply to the request.
  2. + *
  3. {@link org.springframework.web.portlet.HandlerInterceptor#preHandleAction(javax.portlet.ActionRequest, javax.portlet.ActionResponse, Object) preHandleAction(..)} + * is called; if the invocation of this method returns true then + * this workflow continues
  4. + *
  5. The target handler handles the action request (via + * {@link org.springframework.web.portlet.HandlerAdapter#handleAction(javax.portlet.ActionRequest, javax.portlet.ActionResponse, Object) HandlerAdapter.handleAction(..)})
  6. + *
  7. {@link org.springframework.web.portlet.HandlerInterceptor#afterActionCompletion(javax.portlet.ActionRequest, javax.portlet.ActionResponse, Object, Exception) afterActionCompletion(..)} + * is called
  8. + *
+ * + *

Render Request:

+ *

    + *
  1. DispatcherPortlet maps the render request to a particular handler + * and assembles a handler execution chain consisting of the handler that + * is to be invoked and all of the HandlerInterceptor + * instances that apply to the request.
  2. + *
  3. {@link org.springframework.web.portlet.HandlerInterceptor#preHandleRender(javax.portlet.RenderRequest, javax.portlet.RenderResponse, Object) preHandleRender(..)} + * is called; if the invocation of this method returns true then + * this workflow continues
  4. + *
  5. The target handler handles the render request (via + * {@link org.springframework.web.portlet.HandlerAdapter#handleRender(javax.portlet.RenderRequest, javax.portlet.RenderResponse, Object) HandlerAdapter.handleRender(..)})
  6. + *
  7. {@link org.springframework.web.portlet.HandlerInterceptor#postHandleRender(javax.portlet.RenderRequest, javax.portlet.RenderResponse, Object, ModelAndView) postHandleRender(..)} + * is called
  8. + *
  9. If the HandlerAdapter returned a ModelAndView, + * then DispatcherPortlet renders the view accordingly + *
  10. {@link org.springframework.web.portlet.HandlerInterceptor#afterRenderCompletion(javax.portlet.RenderRequest, javax.portlet.RenderResponse, Object, Exception) afterRenderCompletion(..)} + * is called
  11. + *
+ * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see HandlerExecutionChain#getInterceptors + * @see org.springframework.web.portlet.HandlerMapping + * @see org.springframework.web.portlet.handler.AbstractHandlerMapping#setInterceptors + * @see org.springframework.web.portlet.HandlerExecutionChain + */ +public interface HandlerInterceptor { + + /** + * Intercept the execution of a handler in the action phase. + *

Called after a HandlerMapping determines an appropriate handler object + * to handle an {@link ActionRequest}, but before said HandlerAdapter actually + * invokes the handler. + *

{@link DispatcherPortlet} processes a handler in an execution chain, + * consisting of any number of interceptors, with the handler itself at the end. + * With this method, each interceptor can decide to abort the execution chain, + * typically throwing an exception or writing a custom response. + * @param request current portlet action request + * @param response current portlet action response + * @param handler chosen handler to execute, for type and/or instance evaluation + * @return true if the execution chain should proceed with the + * next interceptor or the handler itself. Else, DispatcherPortlet + * assumes that this interceptor has already dealt with the response itself + * @throws Exception in case of errors + */ + boolean preHandleAction(ActionRequest request, ActionResponse response, Object handler) + throws Exception; + + /** + * Callback after completion of request processing in the action phase, that is, + * after rendering the view. Will be called on any outcome of handler execution, + * thus allowing for proper resource cleanup. + *

Note: Will only be called if this interceptor's + * {@link #preHandleAction(javax.portlet.ActionRequest, javax.portlet.ActionResponse, Object)} + * method has successfully completed and returned true! + * @param request current portlet action request + * @param response current portlet action response + * @param handler chosen handler to execute, for type and/or instance examination + * @param ex exception thrown on handler execution, if any (only included as + * additional context information for the case where a handler threw an exception; + * request execution may have failed even when this argument is null) + * @throws Exception in case of errors + */ + void afterActionCompletion( + ActionRequest request, ActionResponse response, Object handler, Exception ex) + throws Exception; + + /** + * Intercept the execution of a handler in the render phase. + *

Called after a HandlerMapping determines an appropriate handler object + * to handle a {@link RenderRequest}, but before said HandlerAdapter actually + * invokes the handler. + *

{@link DispatcherPortlet} processes a handler in an execution chain, + * consisting of any number of interceptors, with the handler itself at the end. + * With this method, each interceptor can decide to abort the execution chain, + * typically throwing an exception or writing a custom response. + * @param request current portlet render request + * @param response current portlet render response + * @param handler chosen handler to execute, for type and/or instance evaluation + * @return true if the execution chain should proceed with the + * next interceptor or the handler itself. Else, DispatcherPortlet + * assumes that this interceptor has already dealt with the response itself + * @throws Exception in case of errors + */ + boolean preHandleRender(RenderRequest request, RenderResponse response, Object handler) + throws Exception; + + /** + * Intercept the execution of a handler in the render phase. + *

Called after a {@link HandlerAdapter} actually invoked the handler, but + * before the DispatcherPortlet renders the view. Can thus expose + * additional model objects to the view via the given {@link ModelAndView}. + *

DispatcherPortlet processes a handler in an execution chain, + * consisting of any number of interceptors, with the handler itself at the end. + * With this method, each interceptor can post-process an execution, getting + * applied in inverse order of the execution chain. + * @param request current portlet render request + * @param response current portlet render response + * @param handler chosen handler to execute, for type and/or instance examination + * @param modelAndView the ModelAndView that the handler returned + * (can also be null) + * @throws Exception in case of errors + */ + void postHandleRender( + RenderRequest request, RenderResponse response, Object handler, ModelAndView modelAndView) + throws Exception; + + /** + * Callback after completion of request processing, that is, after rendering + * the view. Will be called on any outcome of handler execution, thus allowing + * for proper resource cleanup. + *

Note: Will only be called if this interceptor's + * {@link #preHandleRender(javax.portlet.RenderRequest, javax.portlet.RenderResponse, Object)} + * method has successfully completed and returned true! + * @param request current portlet render request + * @param response current portlet render response + * @param handler chosen handler to execute, for type and/or instance examination + * @param ex exception thrown on handler execution, if any + * @throws Exception in case of errors + */ + void afterRenderCompletion( + RenderRequest request, RenderResponse response, Object handler, Exception ex) + throws Exception; + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerMapping.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerMapping.java new file mode 100644 index 00000000000..7c79d8482ff --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/HandlerMapping.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2007 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; + +import javax.portlet.PortletRequest; + +/** + * Interface to be implemented by objects that define a mapping between + * requests and handler objects. + * + *

This class can be implemented by application developers, although this is not + * necessary, as {@link org.springframework.web.portlet.handler.PortletModeHandlerMapping}, + * {@link org.springframework.web.portlet.handler.ParameterHandlerMapping} and + * {@link org.springframework.web.portlet.handler.PortletModeParameterHandlerMapping} + * are included in the framework. The first is the default if no HandlerMapping + * bean is registered in the portlet application context. + * + *

HandlerMapping implementations can support mapped interceptors but do not + * have to. A handler will always be wrapped in a {@link HandlerExecutionChain} + * instance, optionally accompanied by some {@link HandlerInterceptor} instances. + * The DispatcherPortlet will first call each HandlerInterceptor's + * preHandle method in the given order, finally invoking the handler + * itself if all preHandle methods have returned true. + * + *

The ability to parameterize this mapping is a powerful and unusual + * capability of this Portlet MVC framework. For example, it is possible to + * write a custom mapping based on session state, cookie state or many other + * variables. No other MVC framework seems to be equally flexible. + * + *

Note: Implementations can implement the {@link org.springframework.core.Ordered} + * interface to be able to specify a sorting order and thus a priority for getting + * applied by DispatcherPortlet. Non-Ordered instances get treated as lowest priority. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @see org.springframework.core.Ordered + * @see org.springframework.web.portlet.handler.AbstractHandlerMapping + * @see org.springframework.web.portlet.handler.PortletModeHandlerMapping + * @see org.springframework.web.portlet.handler.ParameterHandlerMapping + * @see org.springframework.web.portlet.handler.PortletModeParameterHandlerMapping + */ +public interface HandlerMapping { + + /** + * Return a handler and any interceptors for this request. The choice may be made + * on portlet mode, session state, or any factor the implementing class chooses. + *

The returned HandlerExecutionChain contains a handler Object, rather than + * even a tag interface, so that handlers are not constrained in any way. + * For example, a HandlerAdapter could be written to allow another framework's + * handler objects to be used. + *

Returns null if no match was found. This is not an error. + * The DispatcherPortlet will query all registered HandlerMapping beans to find + * a match, and only decide there is an error if none can find a handler. + * @param request current portlet request + * @return a HandlerExecutionChain instance containing handler object and + * any interceptors, or null if no mapping found + * @throws Exception if there is an internal error + */ + HandlerExecutionChain getHandler(PortletRequest request) throws Exception; + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/ModelAndView.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/ModelAndView.java new file mode 100644 index 00000000000..c1c08a46eb3 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/ModelAndView.java @@ -0,0 +1,303 @@ +/* + * Copyright 2002-2006 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; + +import java.util.Map; + +import org.springframework.ui.ModelMap; + +/** + * Holder for both Model and View in the web MVC framework. + * Note that these are entirely distinct. This class merely holds + * both to make it possible for a controller to return both model + * and view in a single return value. + * + *

Represents a model and view returned by a handler, to be resolved + * by a DispatcherPortlet. The view can take the form of a String + * view name which will need to be resolved by a ViewResolver object; + * alternatively a view object can be specified directly. The model + * is a Map, allowing the use of multiple objects keyed by name. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.web.portlet.DispatcherPortlet + * @see org.springframework.web.servlet.ViewResolver + * @see org.springframework.web.portlet.HandlerAdapter + * @see org.springframework.web.portlet.mvc.Controller + */ +public class ModelAndView { + + /** View instance or view name String */ + private Object view; + + /** Model Map */ + private ModelMap model; + + /** + * Indicates whether or not this instance has been cleared with a call to {@link #clear()}. + */ + private boolean cleared; + + + /** + * Default constructor for bean-style usage: populating bean + * properties instead of passing in constructor arguments. + * @see #setView(Object) + * @see #setViewName(String) + */ + public ModelAndView() { + } + + /** + * Convenient constructor when there is no model data to expose. + * Can also be used in conjunction with addObject. + * @param viewName name of the View to render, to be resolved + * by the DispatcherPortlet's ViewResolver + * @see #addObject + */ + public ModelAndView(String viewName) { + this.view = viewName; + } + + /** + * Convenient constructor when there is no model data to expose. + * Can also be used in conjunction with addObject. + * @param view View object to render (usually a Servlet MVC View object) + * @see #addObject + */ + public ModelAndView(Object view) { + this.view = view; + } + + /** + * Create a new ModelAndView given a view name and a model. + * @param viewName name of the View to render, to be resolved + * by the DispatcherPortlet's ViewResolver + * @param model Map of model names (Strings) to model objects + * (Objects). Model entries may not be null, but the + * model Map may be null if there is no model data. + */ + public ModelAndView(String viewName, Map model) { + this.view = viewName; + if (model != null) { + getModelMap().addAllAttributes(model); + } + } + + /** + * Create a new ModelAndView given a View object and a model. + * @param view View object to render (usually a Servlet MVC View object) + * @param model Map of model names (Strings) to model objects + * (Objects). Model entries may not be null, but the + * model Map may be null if there is no model data. + */ + public ModelAndView(Object view, Map model) { + this.view = view; + if (model != null) { + getModelMap().addAllAttributes(model); + } + } + + /** + * Convenient constructor to take a single model object. + * @param viewName name of the View to render, to be resolved + * by the DispatcherPortlet's ViewResolver + * @param modelName name of the single entry in the model + * @param modelObject the single model object + */ + public ModelAndView(String viewName, String modelName, Object modelObject) { + this.view = viewName; + addObject(modelName, modelObject); + } + + /** + * Convenient constructor to take a single model object. + * @param view View object to render (usually a Servlet MVC View object) + * @param modelName name of the single entry in the model + * @param modelObject the single model object + */ + public ModelAndView(Object view, String modelName, Object modelObject) { + this.view = view; + addObject(modelName, modelObject); + } + + + /** + * Set a view name for this ModelAndView, to be resolved by the + * DispatcherPortlet via a ViewResolver. Will override any + * pre-existing view name or View. + */ + public void setViewName(String viewName) { + this.view = viewName; + } + + /** + * Return the view name to be resolved by the DispatcherPortlet + * via a ViewResolver, or null if we are using a view object. + */ + public String getViewName() { + return (this.view instanceof String ? (String) this.view : null); + } + + /** + * Set a View object for this ModelAndView. Will override any + * pre-existing view name or View. + *

The given View object will usually be a Servlet MVC View object. + * This is nevertheless typed as Object to avoid a Servlet API dependency + * in the Portlet ModelAndView class. + */ + public void setView(Object view) { + this.view = view; + } + + /** + * Return the View object, or null if we are using a view name + * to be resolved by the DispatcherPortlet via a ViewResolver. + */ + public Object getView() { + return (!(this.view instanceof String) ? this.view : null); + } + + /** + * Indicate whether or not this ModelAndView has a view, either + * as a view name or as a direct view instance. + */ + public boolean hasView() { + return (this.view != null); + } + + /** + * Return whether we use a view reference, i.e. true + * if the view has been specified via a name to be resolved by the + * DispatcherPortlet via a ViewResolver. + */ + public boolean isReference() { + return (this.view instanceof String); + } + + /** + * Return the model map. May return null. + * Called by DispatcherPortlet for evaluation of the model. + */ + protected Map getModelInternal() { + return this.model; + } + + /** + * Return the underlying ModelMap instance (never null). + */ + public ModelMap getModelMap() { + if (this.model == null) { + this.model = new ModelMap(); + } + return this.model; + } + + /** + * Return the model map. Never returns null. + * To be called by application code for modifying the model. + */ + public Map getModel() { + return getModelMap(); + } + + + /** + * Add an attribute to the model. + * @param attributeName name of the object to add to the model + * @param attributeValue object to add to the model (never null) + * @see ModelMap#addAttribute(String, Object) + * @see #getModelMap() + */ + public ModelAndView addObject(String attributeName, Object attributeValue) { + getModelMap().addAttribute(attributeName, attributeValue); + return this; + } + + /** + * Add an attribute to the model using parameter name generation. + * @param attributeValue the object to add to the model (never null) + * @see ModelMap#addAttribute(Object) + * @see #getModelMap() + */ + public ModelAndView addObject(Object attributeValue) { + getModelMap().addAttribute(attributeValue); + return this; + } + + /** + * Add all attributes contained in the provided Map to the model. + * @param modelMap a Map of attributeName -> attributeValue pairs + * @see ModelMap#addAllAttributes(Map) + * @see #getModelMap() + */ + public ModelAndView addAllObjects(Map modelMap) { + getModelMap().addAllAttributes(modelMap); + return this; + } + + + /** + * Clear the state of this ModelAndView object. + * The object will be empty afterwards. + *

Can be used to suppress rendering of a given ModelAndView object + * in the postHandleRender method of a HandlerInterceptor. + * @see #isEmpty() + * @see HandlerInterceptor#postHandleRender + */ + public void clear() { + this.view = null; + this.model = null; + this.cleared = true; + } + + /** + * Return whether this ModelAndView object is empty + * i.e. whether it does not hold any view and does not contain a model. + */ + public boolean isEmpty() { + return (this.view == null && this.model == null); + } + + /** + * Return whether this ModelAndView object is empty as a result of a call to {@link #clear} + * i.e. whether it does not hold any view and does not contain a model. + * Returns false if any additional state was added to the instance + * after the call to {@link #clear}. + * @see #clear() + */ + public boolean wasCleared() { + return (this.cleared && isEmpty()); + } + + + /** + * Return diagnostic information about this model and view. + */ + public String toString() { + StringBuffer buf = new StringBuffer("ModelAndView: "); + if (isReference()) { + buf.append("reference to view with name '").append(this.view).append("'"); + } + else { + buf.append("materialized View is [").append(this.view).append(']'); + } + buf.append("; model is ").append(this.model); + return buf.toString(); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/ModelAndViewDefiningException.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/ModelAndViewDefiningException.java new file mode 100644 index 00000000000..dcb2a64e4ea --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/ModelAndViewDefiningException.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2005 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; + +import javax.portlet.PortletException; + +/** + * Exception to be thrown on error conditions that should forward + * to a specific view with a specific model. + * + *

Can be thrown at any time during handler processing. + * This includes any template methods of pre-built controllers. + * For example, a form controller might abort to a specific error page + * if certain parameters do not allow to proceed with the normal workflow. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class ModelAndViewDefiningException extends PortletException { + + private ModelAndView modelAndView; + + + /** + * Create new ModelAndViewDefiningException with the given ModelAndView, + * typically representing a specific error page. + * @param modelAndView ModelAndView with view to forward to and model to expose + */ + public ModelAndViewDefiningException(ModelAndView modelAndView) { + if (modelAndView == null) { + throw new IllegalArgumentException("modelAndView must not be null in ModelAndViewDefiningException"); + } + this.modelAndView = modelAndView; + } + + /** + * Return the ModelAndView that this exception contains for forwarding to. + */ + public ModelAndView getModelAndView() { + return modelAndView; + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/MissingPortletRequestParameterException.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/MissingPortletRequestParameterException.java new file mode 100644 index 00000000000..d2c9cb5862a --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/MissingPortletRequestParameterException.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2007 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.bind; + +/** + * PortletRequestBindingException subclass that indicates a missing parameter. + * + * @author Juergen Hoeller + * @since 2.0.2 + */ +public class MissingPortletRequestParameterException extends PortletRequestBindingException { + + private String parameterName; + + private String parameterType; + + + /** + * Constructor for MissingPortletRequestParameterException. + * @param parameterName the name of the missing parameter + * @param parameterType the expected type of the missing parameter + */ + public MissingPortletRequestParameterException(String parameterName, String parameterType) { + super(""); + this.parameterName = parameterName; + this.parameterType = parameterType; + } + + + public String getMessage() { + return "Required " + this.parameterType + " parameter '" + parameterName + "' is not present"; + } + + /** + * Return the name of the offending parameter. + */ + public String getParameterName() { + return this.parameterName; + } + + /** + * Return the expected type of the offending parameter. + */ + public String getParameterType() { + return this.parameterType; + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestBindingException.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestBindingException.java new file mode 100644 index 00000000000..e460ce577c1 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestBindingException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2006 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.bind; + +import javax.portlet.PortletException; + +/** + * Fatal binding exception, thrown when we want to + * treat binding exceptions as unrecoverable. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + */ +public class PortletRequestBindingException extends PortletException { + + /** + * Constructor for PortletRequestBindingException. + * @param msg the detail message + */ + public PortletRequestBindingException(String msg) { + super(msg); + } + + /** + * Constructor for PortletRequestBindingException. + * @param msg the detail message + * @param cause the root cause + */ + public PortletRequestBindingException(String msg, Throwable cause) { + super(msg, cause); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestDataBinder.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestDataBinder.java new file mode 100644 index 00000000000..f3b51ad7165 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestDataBinder.java @@ -0,0 +1,129 @@ +/* + * 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.bind; + +import javax.portlet.PortletRequest; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.validation.BindException; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.portlet.multipart.MultipartActionRequest; + +/** + * Special {@link org.springframework.validation.DataBinder} to perform data binding + * from portlet request parameters to JavaBeans, including support for multipart files. + * + *

See the DataBinder/WebDataBinder superclasses for customization options, + * which include specifying allowed/required fields, and registering custom + * property editors. + * + *

Used by Spring Portlet MVC's BaseCommandController. + * Note that BaseCommandController and its subclasses allow for easy customization + * of the binder instances that they use through overriding initBinder. + * + *

Can also be used for manual data binding in custom web controllers: + * for example, in a plain Portlet Controller implementation. Simply instantiate + * a PortletRequestDataBinder for each binding process, and invoke bind + * with the current PortletRequest as argument: + * + *

+ * MyBean myBean = new MyBean();
+ * // apply binder to custom target object
+ * PortletRequestDataBinder binder = new PortletRequestDataBinder(myBean);
+ * // register custom editors, if desired
+ * binder.registerCustomEditor(...);
+ * // trigger actual binding of request parameters
+ * binder.bind(request);
+ * // optionally evaluate binding errors
+ * Errors errors = binder.getErrors();
+ * ...
+ * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see #bind(javax.portlet.PortletRequest) + * @see #registerCustomEditor + * @see #setAllowedFields + * @see #setRequiredFields + * @see #setFieldMarkerPrefix + * @see org.springframework.web.portlet.mvc.BaseCommandController#initBinder + */ +public class PortletRequestDataBinder extends WebDataBinder { + + /** + * Create a new PortletRequestDataBinder instance, with default object name. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @see #DEFAULT_OBJECT_NAME + */ + public PortletRequestDataBinder(Object target) { + super(target); + } + + /** + * Create a new PortletRequestDataBinder instance. + * @param target the target object to bind onto (or null + * if the binder is just used to convert a plain parameter value) + * @param objectName the name of the target object + */ + public PortletRequestDataBinder(Object target, String objectName) { + super(target, objectName); + } + + + /** + * Bind the parameters of the given request to this binder's target, + * also binding multipart files in case of a multipart request. + *

This call can create field errors, representing basic binding + * errors like a required field (code "required"), or type mismatch + * between value and bean property (code "typeMismatch"). + *

Multipart files are bound via their parameter name, just like normal + * HTTP parameters: i.e. "uploadedFile" to an "uploadedFile" bean property, + * invoking a "setUploadedFile" setter method. + *

The type of the target property for a multipart file can be MultipartFile, + * byte[], or String. The latter two receive the contents of the uploaded file; + * all metadata like original file name, content type, etc are lost in those cases. + * @param request request with parameters to bind (can be multipart) + * @see org.springframework.web.portlet.multipart.MultipartActionRequest + * @see org.springframework.web.multipart.MultipartFile + * @see #bindMultipartFiles + * @see #bind(org.springframework.beans.PropertyValues) + */ + public void bind(PortletRequest request) { + MutablePropertyValues mpvs = new PortletRequestParameterPropertyValues(request); + if (request instanceof MultipartActionRequest) { + MultipartActionRequest multipartRequest = (MultipartActionRequest) request; + bindMultipartFiles(multipartRequest.getFileMap(), mpvs); + } + doBind(mpvs); + } + + /** + * Treats errors as fatal. + *

Use this method only if it's an error if the input isn't valid. + * This might be appropriate if all input is from dropdowns, for example. + * @throws PortletRequestBindingException subclass of PortletException on any binding problem + */ + public void closeNoCatch() throws PortletRequestBindingException { + if (getBindingResult().hasErrors()) { + throw new PortletRequestBindingException( + "Errors binding onto object '" + getBindingResult().getObjectName() + "'", + new BindException(getBindingResult())); + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestParameterPropertyValues.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestParameterPropertyValues.java new file mode 100644 index 00000000000..b4369c7f7d4 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestParameterPropertyValues.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2005 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.bind; + +import javax.portlet.PortletRequest; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.web.portlet.util.PortletUtils; + +/** + * PropertyValues implementation created from parameters in a PortletRequest. + * Can look for all property values beginning with a certain prefix and + * prefix separator (default is "_"). + * + *

For example, with a prefix of "spring", "spring_param1" and + * "spring_param2" result in a Map with "param1" and "param2" as keys. + * + *

This class is not immutable to be able to efficiently remove property + * values that should be ignored for binding. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see org.springframework.web.portlet.util.PortletUtils#getParametersStartingWith + */ +public class PortletRequestParameterPropertyValues extends MutablePropertyValues { + + /** Default prefix separator */ + public static final String DEFAULT_PREFIX_SEPARATOR = "_"; + + + /** + * Create new PortletRequestPropertyValues using no prefix + * (and hence, no prefix separator). + * @param request portlet request + */ + public PortletRequestParameterPropertyValues(PortletRequest request) { + this(request, null, null); + } + + /** + * Create new PortletRequestPropertyValues using the given prefix and + * the default prefix separator (the underscore character "_"). + * @param request portlet request + * @param prefix the prefix for parameters (the full prefix will + * consist of this plus the separator) + * @see #DEFAULT_PREFIX_SEPARATOR + */ + public PortletRequestParameterPropertyValues(PortletRequest request, String prefix) { + this(request, prefix, DEFAULT_PREFIX_SEPARATOR); + } + + /** + * Create new PortletRequestPropertyValues supplying both prefix and + * prefix separator. + * @param request portlet request + * @param prefix the prefix for parameters (the full prefix will + * consist of this plus the separator) + * @param prefixSeparator separator delimiting prefix (e.g. "spring") + * and the rest of the parameter name ("param1", "param2") + */ + public PortletRequestParameterPropertyValues(PortletRequest request, String prefix, String prefixSeparator) { + super(PortletUtils.getParametersStartingWith( + request, (prefix != null) ? prefix + prefixSeparator : null)); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestUtils.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestUtils.java new file mode 100644 index 00000000000..6e13cbded79 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/PortletRequestUtils.java @@ -0,0 +1,699 @@ +/* + * Copyright 2002-2007 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.bind; + +import javax.portlet.PortletRequest; + +/** + * Parameter extraction methods, for an approach distinct from data binding, + * in which parameters of specific types are required. + * + *

This approach is very useful for simple submissions, where binding + * request parameters to a command object would be overkill. + * + * @author Juergen Hoeller + * @author Keith Donald + * @author John A. Lewis + * @since 2.0 + */ +public abstract class PortletRequestUtils { + + private static final IntParser INT_PARSER = new IntParser(); + + private static final LongParser LONG_PARSER = new LongParser(); + + private static final FloatParser FLOAT_PARSER = new FloatParser(); + + private static final DoubleParser DOUBLE_PARSER = new DoubleParser(); + + private static final BooleanParser BOOLEAN_PARSER = new BooleanParser(); + + private static final StringParser STRING_PARSER = new StringParser(); + + + /** + * Get an Integer parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current portlet request + * @param name the name of the parameter + * @return the Integer value, or null if not present + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static Integer getIntParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return new Integer(getRequiredIntParameter(request, name)); + } + + /** + * Get an int parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current portlet request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static int getIntParameter(PortletRequest request, String name, int defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredIntParameter(request, name); + } + catch (PortletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of int parameters, return an empty array if not found. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + */ + public static int[] getIntParameters(PortletRequest request, String name) { + try { + return getRequiredIntParameters(request, name); + } + catch (PortletRequestBindingException ex) { + return new int[0]; + } + } + + /** + * Get an int parameter, throwing an exception if it isn't found or isn't a number. + * @param request current portlet request + * @param name the name of the parameter + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static int getRequiredIntParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + return INT_PARSER.parseInt(name, request.getParameter(name)); + } + + /** + * Get an array of int parameters, throwing an exception if not found or one is not a number.. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static int[] getRequiredIntParameters(PortletRequest request, String name) + throws PortletRequestBindingException { + + return INT_PARSER.parseInts(name, request.getParameterValues(name)); + } + + + /** + * Get a Long parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current portlet request + * @param name the name of the parameter + * @return the Long value, or null if not present + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static Long getLongParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return new Long(getRequiredLongParameter(request, name)); + } + + /** + * Get a long parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current portlet request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static long getLongParameter(PortletRequest request, String name, long defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredLongParameter(request, name); + } + catch (PortletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of long parameters, return an empty array if not found. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + */ + public static long[] getLongParameters(PortletRequest request, String name) { + try { + return getRequiredLongParameters(request, name); + } + catch (PortletRequestBindingException ex) { + return new long[0]; + } + } + + /** + * Get a long parameter, throwing an exception if it isn't found or isn't a number. + * @param request current portlet request + * @param name the name of the parameter + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static long getRequiredLongParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + return LONG_PARSER.parseLong(name, request.getParameter(name)); + } + + /** + * Get an array of long parameters, throwing an exception if not found or one is not a number. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static long[] getRequiredLongParameters(PortletRequest request, String name) + throws PortletRequestBindingException { + + return LONG_PARSER.parseLongs(name, request.getParameterValues(name)); + } + + + /** + * Get a Float parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current portlet request + * @param name the name of the parameter + * @return the Float value, or null if not present + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static Float getFloatParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return new Float(getRequiredFloatParameter(request, name)); + } + + /** + * Get a float parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current portlet request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static float getFloatParameter(PortletRequest request, String name, float defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredFloatParameter(request, name); + } + catch (PortletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of float parameters, return an empty array if not found. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + */ + public static float[] getFloatParameters(PortletRequest request, String name) { + try { + return getRequiredFloatParameters(request, name); + } + catch (PortletRequestBindingException ex) { + return new float[0]; + } + } + + /** + * Get a float parameter, throwing an exception if it isn't found or isn't a number. + * @param request current portlet request + * @param name the name of the parameter + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static float getRequiredFloatParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + return FLOAT_PARSER.parseFloat(name, request.getParameter(name)); + } + + /** + * Get an array of float parameters, throwing an exception if not found or one is not a number. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static float[] getRequiredFloatParameters(PortletRequest request, String name) + throws PortletRequestBindingException { + + return FLOAT_PARSER.parseFloats(name, request.getParameterValues(name)); + } + + + /** + * Get a Double parameter, or null if not present. + * Throws an exception if it the parameter value isn't a number. + * @param request current portlet request + * @param name the name of the parameter + * @return the Double value, or null if not present + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static Double getDoubleParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return new Double(getRequiredDoubleParameter(request, name)); + } + + /** + * Get a double parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + * @param request current portlet request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static double getDoubleParameter(PortletRequest request, String name, double defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredDoubleParameter(request, name); + } + catch (PortletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of double parameters, return an empty array if not found. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + */ + public static double[] getDoubleParameters(PortletRequest request, String name) { + try { + return getRequiredDoubleParameters(request, name); + } + catch (PortletRequestBindingException ex) { + return new double[0]; + } + } + + /** + * Get a double parameter, throwing an exception if it isn't found or isn't a number. + * @param request current portlet request + * @param name the name of the parameter + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static double getRequiredDoubleParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + return DOUBLE_PARSER.parseDouble(name, request.getParameter(name)); + } + + /** + * Get an array of double parameters, throwing an exception if not found or one is not a number. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static double[] getRequiredDoubleParameters(PortletRequest request, String name) + throws PortletRequestBindingException { + + return DOUBLE_PARSER.parseDoubles(name, request.getParameterValues(name)); + } + + + /** + * Get a Boolean parameter, or null if not present. + * Throws an exception if it the parameter value isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current portlet request + * @param name the name of the parameter + * @return the Boolean value, or null if not present + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static Boolean getBooleanParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return (getRequiredBooleanParameter(request, name) ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Get a boolean parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value as default to enable checks of whether it was supplied. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current portlet request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static boolean getBooleanParameter(PortletRequest request, String name, boolean defaultVal) { + if (request.getParameter(name) == null) { + return defaultVal; + } + try { + return getRequiredBooleanParameter(request, name); + } + catch (PortletRequestBindingException ex) { + return defaultVal; + } + } + + /** + * Get an array of boolean parameters, return an empty array if not found. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + */ + public static boolean[] getBooleanParameters(PortletRequest request, String name) { + try { + return getRequiredBooleanParameters(request, name); + } + catch (PortletRequestBindingException ex) { + return new boolean[0]; + } + } + + /** + * Get a boolean parameter, throwing an exception if it isn't found + * or isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current portlet request + * @param name the name of the parameter + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static boolean getRequiredBooleanParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + return BOOLEAN_PARSER.parseBoolean(name, request.getParameter(name)); + } + + /** + * Get an array of boolean parameters, throwing an exception if not found + * or one isn't a boolean. + *

Accepts "true", "on", "yes" (any case) and "1" as values for true; + * treats every other non-empty value as false (i.e. parses leniently). + * @param request current portlet request + * @param name the name of the parameter + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static boolean[] getRequiredBooleanParameters(PortletRequest request, String name) + throws PortletRequestBindingException { + + return BOOLEAN_PARSER.parseBooleans(name, request.getParameterValues(name)); + } + + + /** + * Get a String parameter, or null if not present. + * Throws an exception if it the parameter value is empty. + * @param request current portlet request + * @param name the name of the parameter + * @return the String value, or null if not present + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static String getStringParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + if (request.getParameter(name) == null) { + return null; + } + return getRequiredStringParameter(request, name); + } + + /** + * Get a String parameter, with a fallback value. Never throws an exception. + * Can pass a distinguished value to default to enable checks of whether it was supplied. + * @param request current portlet request + * @param name the name of the parameter + * @param defaultVal the default value to use as fallback + */ + public static String getStringParameter(PortletRequest request, String name, String defaultVal) { + String val = request.getParameter(name); + return (val != null ? val : defaultVal); + } + + /** + * Get an array of String parameters, return an empty array if not found. + * @param request current portlet request + * @param name the name of the parameter with multiple possible values + */ + public static String[] getStringParameters(PortletRequest request, String name) { + try { + return getRequiredStringParameters(request, name); + } + catch (PortletRequestBindingException ex) { + return new String[0]; + } + } + + /** + * Get a String parameter, throwing an exception if it isn't found or is empty. + * @param request current portlet request + * @param name the name of the parameter + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static String getRequiredStringParameter(PortletRequest request, String name) + throws PortletRequestBindingException { + + return STRING_PARSER.validateRequiredString(name, request.getParameter(name)); + } + + /** + * Get an array of String parameters, throwing an exception if not found or one is empty. + * @param request current portlet request + * @param name the name of the parameter + * @throws PortletRequestBindingException a subclass of PortletException, + * so it doesn't need to be caught + */ + public static String[] getRequiredStringParameters(PortletRequest request, String name) + throws PortletRequestBindingException { + + return STRING_PARSER.validateRequiredStrings(name, request.getParameterValues(name)); + } + + + private abstract static class ParameterParser { + + protected final Object parse(String name, String parameter) throws PortletRequestBindingException { + validateRequiredParameter(name, parameter); + try { + return doParse(parameter); + } + catch (NumberFormatException ex) { + throw new PortletRequestBindingException( + "Required " + getType() + " parameter '" + name + "' with value of '" + + parameter + "' is not a valid number", ex); + } + } + + protected final void validateRequiredParameter(String name, Object parameter) + throws PortletRequestBindingException { + + if (parameter == null) { + throw new MissingPortletRequestParameterException(name, getType()); + } + } + + protected abstract String getType(); + + protected abstract Object doParse(String parameter) throws NumberFormatException; + } + + + private static class IntParser extends ParameterParser { + + protected String getType() { + return "int"; + } + + protected Object doParse(String s) throws NumberFormatException { + return Integer.valueOf(s); + } + + public int parseInt(String name, String parameter) throws PortletRequestBindingException { + return ((Number) parse(name, parameter)).intValue(); + } + + public int[] parseInts(String name, String[] values) throws PortletRequestBindingException { + validateRequiredParameter(name, values); + int[] parameters = new int[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseInt(name, values[i]); + } + return parameters; + } + } + + + private static class LongParser extends ParameterParser { + + protected String getType() { + return "long"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return Long.valueOf(parameter); + } + + public long parseLong(String name, String parameter) throws PortletRequestBindingException { + return ((Number) parse(name, parameter)).longValue(); + } + + public long[] parseLongs(String name, String[] values) throws PortletRequestBindingException { + validateRequiredParameter(name, values); + long[] parameters = new long[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseLong(name, values[i]); + } + return parameters; + } + } + + + private static class FloatParser extends ParameterParser { + + protected String getType() { + return "float"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return Float.valueOf(parameter); + } + + public float parseFloat(String name, String parameter) throws PortletRequestBindingException { + return ((Number) parse(name, parameter)).floatValue(); + } + + public float[] parseFloats(String name, String[] values) throws PortletRequestBindingException { + validateRequiredParameter(name, values); + float[] parameters = new float[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseFloat(name, values[i]); + } + return parameters; + } + } + + + private static class DoubleParser extends ParameterParser { + + protected String getType() { + return "double"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return Double.valueOf(parameter); + } + + public double parseDouble(String name, String parameter) throws PortletRequestBindingException { + return ((Number) parse(name, parameter)).doubleValue(); + } + + public double[] parseDoubles(String name, String[] values) throws PortletRequestBindingException { + validateRequiredParameter(name, values); + double[] parameters = new double[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseDouble(name, values[i]); + } + return parameters; + } + } + + + private static class BooleanParser extends ParameterParser { + + protected String getType() { + return "boolean"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return (parameter.equalsIgnoreCase("true") || parameter.equalsIgnoreCase("on") || + parameter.equalsIgnoreCase("yes") || parameter.equals("1") ? Boolean.TRUE : Boolean.FALSE); + } + + public boolean parseBoolean(String name, String parameter) throws PortletRequestBindingException { + return ((Boolean) parse(name, parameter)).booleanValue(); + } + + public boolean[] parseBooleans(String name, String[] values) throws PortletRequestBindingException { + validateRequiredParameter(name, values); + boolean[] parameters = new boolean[values.length]; + for (int i = 0; i < values.length; i++) { + parameters[i] = parseBoolean(name, values[i]); + } + return parameters; + } + } + + + private static class StringParser extends ParameterParser { + + protected String getType() { + return "string"; + } + + protected Object doParse(String parameter) throws NumberFormatException { + return parameter; + } + + public String validateRequiredString(String name, String value) throws PortletRequestBindingException { + validateRequiredParameter(name, value); + return value; + } + + public String[] validateRequiredStrings(String name, String[] values) throws PortletRequestBindingException { + validateRequiredParameter(name, values); + for (int i = 0; i < values.length; i++) { + validateRequiredParameter(name, values[i]); + } + return values; + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/package.html b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/package.html new file mode 100644 index 00000000000..399aced35b3 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/bind/package.html @@ -0,0 +1,7 @@ + + + +Provides portlet-specific data binding functionality. + + + diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/AbstractRefreshablePortletApplicationContext.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/AbstractRefreshablePortletApplicationContext.java new file mode 100644 index 00000000000..06b509dd375 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/AbstractRefreshablePortletApplicationContext.java @@ -0,0 +1,166 @@ +/* + * 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.context; + +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; +import javax.servlet.ServletContext; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.AbstractRefreshableConfigApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.web.context.ServletContextAware; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.ServletContextAwareProcessor; + +/** + * {@link org.springframework.context.support.AbstractRefreshableApplicationContext} + * subclass which implements the {@link ConfigurablePortletApplicationContext} + * interface for portlet environments. Provides a "configLocations" property, + * to be populated through the ConfigurablePortletApplicationContext interface + * on portlet application startup. + * + *

This class is as easy to subclass as AbstractRefreshableApplicationContext: + * All you need to implements is the {@link #loadBeanDefinitions} method; + * see the superclass javadoc for details. Note that implementations are supposed + * to load bean definitions from the files specified by the locations returned + * by the {@link #getConfigLocations} method. + * + *

Interprets resource paths as servlet context resources, i.e. as paths beneath + * the web application root. Absolute paths, e.g. for files outside the web app root, + * can be accessed via "file:" URLs, as implemented by + * {@link org.springframework.core.io.DefaultResourceLoader}. + * + *

This is the portlet context to be subclassed for a different bean definition format. + * Such a context implementation can be specified as "contextClass" init-param + * for FrameworkPortlet, replacing the default {@link XmlPortletApplicationContext}. + * It will then automatically receive the "contextConfigLocation" init-param. + * + *

Note that Portlet-based context implementations are generally supposed + * to configure themselves based on the configuration received through the + * {@link ConfigurablePortletApplicationContext} interface. In contrast, a standalone + * application context might allow for configuration in custom startup code + * (for example, {@link org.springframework.context.support.GenericApplicationContext}). + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see #loadBeanDefinitions + * @see org.springframework.web.portlet.context.ConfigurablePortletApplicationContext#setConfigLocations + * @see XmlPortletApplicationContext + */ +public abstract class AbstractRefreshablePortletApplicationContext extends AbstractRefreshableConfigApplicationContext + implements WebApplicationContext, ConfigurablePortletApplicationContext { + + /** Servlet context that this context runs in */ + private ServletContext servletContext; + + /** Portlet context that this context runs in */ + private PortletContext portletContext; + + /** Portlet config that this context runs in */ + private PortletConfig portletConfig; + + /** Namespace of this context, or null if root */ + private String namespace; + + + public AbstractRefreshablePortletApplicationContext() { + setDisplayName("Root PortletApplicationContext"); + } + + public void setParent(ApplicationContext parent) { + super.setParent(parent); + if (parent instanceof WebApplicationContext) { + this.servletContext = ((WebApplicationContext) parent).getServletContext(); + } + } + + public ServletContext getServletContext() { + return this.servletContext; + } + + public void setPortletContext(PortletContext portletContext) { + this.portletContext = portletContext; + } + + public PortletContext getPortletContext() { + return this.portletContext; + } + + public void setPortletConfig(PortletConfig portletConfig) { + this.portletConfig = portletConfig; + if (portletConfig != null && this.portletContext == null) { + this.portletContext = portletConfig.getPortletContext(); + } + } + + public PortletConfig getPortletConfig() { + return this.portletConfig; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + if (namespace != null) { + setDisplayName("PortletApplicationContext for namespace '" + namespace + "'"); + } + } + + public String getNamespace() { + return this.namespace; + } + + public String[] getConfigLocations() { + return super.getConfigLocations(); + } + + + /** + * Register request/session scopes, a {@link PortletContextAwareProcessor}, etc. + */ + protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext)); + beanFactory.addBeanPostProcessor(new PortletContextAwareProcessor(this.portletContext, this.portletConfig)); + beanFactory.ignoreDependencyInterface(ServletContextAware.class); + beanFactory.ignoreDependencyInterface(PortletContextAware.class); + beanFactory.ignoreDependencyInterface(PortletConfigAware.class); + beanFactory.registerResolvableDependency(ServletContext.class, this.servletContext); + beanFactory.registerResolvableDependency(PortletContext.class, this.portletContext); + beanFactory.registerResolvableDependency(PortletConfig.class, this.portletConfig); + + PortletApplicationContextUtils.registerPortletApplicationScopes(beanFactory); + } + + /** + * This implementation supports file paths beneath the root of the PortletContext. + * @see PortletContextResource + */ + protected Resource getResourceByPath(String path) { + return new PortletContextResource(this.portletContext, path); + } + + /** + * This implementation supports pattern matching in unexpanded WARs too. + * @see PortletContextResourcePatternResolver + */ + protected ResourcePatternResolver getResourcePatternResolver() { + return new PortletContextResourcePatternResolver(this); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/ConfigurablePortletApplicationContext.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/ConfigurablePortletApplicationContext.java new file mode 100644 index 00000000000..857d5718172 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/ConfigurablePortletApplicationContext.java @@ -0,0 +1,102 @@ +/* + * 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.context; + +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.web.context.WebApplicationContext; + +/** + * Interface to be implemented by configurable portlet application contexts. + * Supported by {@link org.springframework.web.portlet.FrameworkPortlet}. + * + *

Note: The setters of this interface need to be called before an + * invocation of the {@link #refresh} method inherited from + * {@link org.springframework.context.ConfigurableApplicationContext}. + * They do not cause an initialization of the context on their own. + * + * @author Juergen Hoeller + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @since 2.0 + * @see #refresh + * @see org.springframework.web.context.ContextLoader#createWebApplicationContext + * @see org.springframework.web.portlet.FrameworkPortlet#createPortletApplicationContext + * @see org.springframework.web.context.ConfigurableWebApplicationContext + */ +public interface ConfigurablePortletApplicationContext + extends WebApplicationContext, ConfigurableApplicationContext { + + /** + * Set the PortletContext for this portlet application context. + *

Does not cause an initialization of the context: refresh needs to be + * called after the setting of all configuration properties. + * @see #refresh() + */ + void setPortletContext(PortletContext portletContext); + + /** + * Return the standard Portlet API PortletContext for this application. + */ + PortletContext getPortletContext(); + + /** + * Set the PortletConfig for this portlet application context. + * @see #refresh() + */ + void setPortletConfig(PortletConfig portletConfig); + + /** + * Return the PortletConfig for this portlet application context, if any. + */ + PortletConfig getPortletConfig(); + + /** + * Set the namespace for this portlet application context, + * to be used for building a default context config location. + */ + void setNamespace(String namespace); + + /** + * Return the namespace for this web application context, if any. + */ + String getNamespace(); + + /** + * Set the config locations for this portlet application context in init-param style, + * i.e. with distinct locations separated by commas, semicolons or whitespace. + *

If not set, the implementation is supposed to use a default for the + * given namespace. + */ + void setConfigLocation(String configLocation); + + /** + * Set the config locations for this portlet application context. + *

If not set, the implementation is supposed to use a default for the + * given namespace. + */ + void setConfigLocations(String[] configLocations); + + /** + * Return the config locations for this web application context, + * or null if none specified. + */ + String[] getConfigLocations(); + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletApplicationContextUtils.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletApplicationContextUtils.java new file mode 100644 index 00000000000..11c676c3e77 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletApplicationContextUtils.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2007 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.context; + +import javax.portlet.PortletContext; +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; + +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.RequestScope; +import org.springframework.web.context.request.SessionScope; + +/** + * Convenience methods for retrieving the root WebApplicationContext for a given + * PortletContext. This is e.g. useful for accessing a Spring context from + * within custom Portlet implementations. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see org.springframework.web.context.ContextLoader + * @see org.springframework.web.context.support.WebApplicationContextUtils + * @see org.springframework.web.portlet.FrameworkPortlet + * @see org.springframework.web.portlet.DispatcherPortlet + */ +public abstract class PortletApplicationContextUtils { + + /** + * Find the root WebApplicationContext for this portlet application, which is + * typically loaded via ContextLoaderListener or ContextLoaderServlet. + *

Will rethrow an exception that happened on root context startup, + * to differentiate between a failed context startup and no context at all. + * @param pc PortletContext to find the web application context for + * @return the root WebApplicationContext for this web app, or null if none + * (typed to ApplicationContext to avoid a Servlet API dependency; can usually + * be casted to WebApplicationContext, but there shouldn't be a need to) + * @see org.springframework.web.context.WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + */ + public static ApplicationContext getWebApplicationContext(PortletContext pc) { + Assert.notNull(pc, "PortletContext must not be null"); + Object attr = pc.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); + if (attr == null) { + return null; + } + if (attr instanceof RuntimeException) { + throw (RuntimeException) attr; + } + if (attr instanceof Error) { + throw (Error) attr; + } + if (!(attr instanceof ApplicationContext)) { + throw new IllegalStateException("Root context attribute is not of type WebApplicationContext: " + attr); + } + return (ApplicationContext) attr; + } + + /** + * Find the root WebApplicationContext for this portlet application, which is + * typically loaded via ContextLoaderListener or ContextLoaderServlet. + *

Will rethrow an exception that happened on root context startup, + * to differentiate between a failed context startup and no context at all. + * @param pc PortletContext to find the web application context for + * @return the root WebApplicationContext for this web app + * (typed to ApplicationContext to avoid a Servlet API dependency; can usually + * be casted to WebApplicationContext, but there shouldn't be a need to) + * @throws IllegalStateException if the root WebApplicationContext could not be found + * @see org.springframework.web.context.WebApplicationContext#ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + */ + public static ApplicationContext getRequiredWebApplicationContext(PortletContext pc) + throws IllegalStateException { + + ApplicationContext wac = getWebApplicationContext(pc); + if (wac == null) { + throw new IllegalStateException("No WebApplicationContext found: no ContextLoaderListener registered?"); + } + return wac; + } + + + /** + * Register portlet-specific scopes with the given BeanFactory, + * as used by the Portlet ApplicationContext. + * @param beanFactory the BeanFactory to configure + */ + static void registerPortletApplicationScopes(ConfigurableListableBeanFactory beanFactory) { + beanFactory.registerScope(WebApplicationContext.SCOPE_REQUEST, new RequestScope()); + beanFactory.registerScope(WebApplicationContext.SCOPE_SESSION, new SessionScope(false)); + beanFactory.registerScope(WebApplicationContext.SCOPE_GLOBAL_SESSION, new SessionScope(true)); + + beanFactory.registerResolvableDependency(PortletRequest.class, new ObjectFactory() { + public Object getObject() { + RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes(); + if (!(requestAttr instanceof PortletRequestAttributes)) { + throw new IllegalStateException("Current request is not a portlet request"); + } + return ((PortletRequestAttributes) requestAttr).getRequest(); + } + }); + beanFactory.registerResolvableDependency(PortletSession.class, new ObjectFactory() { + public Object getObject() { + RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes(); + if (!(requestAttr instanceof PortletRequestAttributes)) { + throw new IllegalStateException("Current request is not a portlet request"); + } + return ((PortletRequestAttributes) requestAttr).getRequest().getPortletSession(); + } + }); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletApplicationObjectSupport.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletApplicationObjectSupport.java new file mode 100644 index 00000000000..58305c74b3e --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletApplicationObjectSupport.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2005 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.context; + +import java.io.File; + +import javax.portlet.PortletContext; + +import org.springframework.context.support.ApplicationObjectSupport; +import org.springframework.web.portlet.util.PortletUtils; + +/** + * Convenient superclass for application objects running in a Portlet ApplicationContext. + * Provides getApplicationContext, getServletContext, and getTempDir methods. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class PortletApplicationObjectSupport extends ApplicationObjectSupport + implements PortletContextAware { + + private PortletContext portletContext; + + + public void setPortletContext(PortletContext portletContext) { + this.portletContext = portletContext; + } + + + /** + * Overrides the base class behavior to enforce running in an ApplicationContext. + * All accessors will throw IllegalStateException if not running in a context. + * @see #getApplicationContext() + * @see #getMessageSourceAccessor() + * @see #getPortletContext() + * @see #getTempDir() + */ + protected boolean isContextRequired() { + return true; + } + + /** + * Return the current PortletContext. + * @throws IllegalStateException if not running within a PortletContext + */ + protected final PortletContext getPortletContext() throws IllegalStateException { + if (this.portletContext == null) { + throw new IllegalStateException( + "PortletApplicationObjectSupport instance [" + this + "] does not run within a PortletContext"); + } + return this.portletContext; + } + + /** + * Return the temporary directory for the current web application, + * as provided by the servlet container. + * @return the File representing the temporary directory + * @throws IllegalStateException if not running within a PortletContext + * @see org.springframework.web.portlet.util.PortletUtils#getTempDir(javax.portlet.PortletContext) + */ + protected final File getTempDir() throws IllegalStateException { + return PortletUtils.getTempDir(getPortletContext()); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletConfigAware.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletConfigAware.java new file mode 100644 index 00000000000..8b99a993e2e --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletConfigAware.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2005 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.context; + +import javax.portlet.PortletConfig; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the PortletConfig (typically determined by the PortletApplicationContext) + * that it runs in. + * + * @author Juergen Hoeller + * @since 2.0 + * @see PortletContextAware + */ +public interface PortletConfigAware { + + /** + * Set the PortletConfigthat this object runs in. + *

Invoked after population of normal bean properties but before an init + * callback like InitializingBean's afterPropertiesSet or a custom init-method. + * Invoked after ApplicationContextAware's setApplicationContext. + * @param portletConfig PortletConfig object to be used by this object + */ + void setPortletConfig(PortletConfig portletConfig); + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextAware.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextAware.java new file mode 100644 index 00000000000..8e7a3445581 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextAware.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2005 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.context; + +import javax.portlet.PortletContext; + +/** + * Interface to be implemented by any object that wishes to be notified + * of the PortletContext (typically determined by the PortletApplicationContext) + * that it runs in. + * + * @author Juergen Hoeller + * @author William G. Thompson, Jr. + * @since 2.0 + * @see PortletConfigAware + */ +public interface PortletContextAware { + + /** + * Set the PortletContext that this object runs in. + *

Invoked after population of normal bean properties but before an init + * callback like InitializingBean's afterPropertiesSet or a custom init-method. + * Invoked after ApplicationContextAware's setApplicationContext. + * @param portletContext PortletContext object to be used by this object + */ + void setPortletContext(PortletContext portletContext); + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextAwareProcessor.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextAwareProcessor.java new file mode 100644 index 00000000000..1daacb553ec --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextAwareProcessor.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2007 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.context; + +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; + +/** + * {@link org.springframework.beans.factory.config.BeanPostProcessor} + * implementation that passes the PortletContext to beans that implement + * the {@link PortletContextAware} interface. + * + *

Portlet application contexts will automatically register this with their + * underlying bean factory. Applications do not use this directly. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see org.springframework.web.portlet.context.PortletContextAware + * @see org.springframework.web.portlet.context.XmlPortletApplicationContext#postProcessBeanFactory + */ +public class PortletContextAwareProcessor implements BeanPostProcessor { + + private PortletContext portletContext; + + private PortletConfig portletConfig; + + + /** + * Create a new PortletContextAwareProcessor for the given context. + */ + public PortletContextAwareProcessor(PortletContext portletContext) { + this(portletContext, null); + } + + /** + * Create a new PortletContextAwareProcessor for the given config. + */ + public PortletContextAwareProcessor(PortletConfig portletConfig) { + this(null, portletConfig); + } + + /** + * Create a new PortletContextAwareProcessor for the given context and config. + */ + public PortletContextAwareProcessor(PortletContext portletContext, PortletConfig portletConfig) { + this.portletContext = portletContext; + this.portletConfig = portletConfig; + if (portletContext == null && portletConfig != null) { + this.portletContext = portletConfig.getPortletContext(); + } + } + + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (this.portletContext != null && bean instanceof PortletContextAware) { + ((PortletContextAware) bean).setPortletContext(this.portletContext); + } + if (this.portletConfig != null && bean instanceof PortletConfigAware) { + ((PortletConfigAware) bean).setPortletConfig(this.portletConfig); + } + return bean; + } + + public Object postProcessAfterInitialization(Object bean, String beanName) { + return bean; + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResource.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResource.java new file mode 100644 index 00000000000..7fc7dcb0c21 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResource.java @@ -0,0 +1,179 @@ +/* + * 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.context; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +import javax.portlet.PortletContext; + +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.ContextResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.portlet.util.PortletUtils; + +/** + * {@link org.springframework.core.io.Resource} implementation for + * {@link javax.portlet.PortletContext} resources, interpreting + * relative paths within the portlet application root directory. + * + *

Always supports stream access and URL access, but only allows + * java.io.File access when the portlet application archive + * is expanded. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see javax.portlet.PortletContext#getResourceAsStream + * @see javax.portlet.PortletContext#getRealPath + */ +public class PortletContextResource extends AbstractResource implements ContextResource { + + private final PortletContext portletContext; + + private final String path; + + + /** + * Create a new PortletContextResource. + *

The Portlet spec requires that resource paths start with a slash, + * even if many containers accept paths without leading slash too. + * Consequently, the given path will be prepended with a slash if it + * doesn't already start with one. + * @param portletContext the PortletContext to load from + * @param path the path of the resource + */ + public PortletContextResource(PortletContext portletContext, String path) { + // check PortletContext + Assert.notNull(portletContext, "Cannot resolve PortletContextResource without PortletContext"); + this.portletContext = portletContext; + + // check path + Assert.notNull(path, "Path is required"); + String pathToUse = StringUtils.cleanPath(path); + if (!pathToUse.startsWith("/")) { + pathToUse = "/" + pathToUse; + } + this.path = pathToUse; + } + + /** + * Return the PortletContext for this resource. + */ + public final PortletContext getPortletContext() { + return this.portletContext; + } + + /** + * Return the path for this resource. + */ + public final String getPath() { + return this.path; + } + + + /** + * This implementation checks PortletContext.getResource. + * @see javax.portlet.PortletContext#getResource(String) + */ + public boolean exists() { + try { + URL url = this.portletContext.getResource(this.path); + return (url != null); + } + catch (MalformedURLException ex) { + return false; + } + } + + /** + * This implementation delegates to PortletContext.getResourceAsStream, + * but throws a FileNotFoundException if not found. + * @see javax.portlet.PortletContext#getResourceAsStream(String) + */ + public InputStream getInputStream() throws IOException { + InputStream is = this.portletContext.getResourceAsStream(this.path); + if (is == null) { + throw new FileNotFoundException("Could not open " + getDescription()); + } + return is; + } + + /** + * This implementation delegates to PortletContext.getResource, + * but throws a FileNotFoundException if no resource found. + * @see javax.portlet.PortletContext#getResource(String) + */ + public URL getURL() throws IOException { + URL url = this.portletContext.getResource(this.path); + if (url == null) { + throw new FileNotFoundException( + getDescription() + " cannot be resolved to URL because it does not exist"); + } + return url; + } + + /** + * This implementation delegates to PortletContext.getRealPath, + * but throws a FileNotFoundException if not found or not resolvable. + * @see javax.portlet.PortletContext#getRealPath(String) + */ + public File getFile() throws IOException { + String realPath = PortletUtils.getRealPath(this.portletContext, this.path); + return new File(realPath); + } + + public Resource createRelative(String relativePath) { + String pathToUse = StringUtils.applyRelativePath(this.path, relativePath); + return new PortletContextResource(this.portletContext, pathToUse); + } + + public String getFilename() { + return StringUtils.getFilename(this.path); + } + + public String getDescription() { + return "PortletContext resource [" + this.path + "]"; + } + + public String getPathWithinContext() { + return this.path; + } + + + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj instanceof PortletContextResource) { + PortletContextResource otherRes = (PortletContextResource) obj; + return (this.portletContext.equals(otherRes.portletContext) && this.path.equals(otherRes.path)); + } + return false; + } + + public int hashCode() { + return this.path.hashCode(); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResourceLoader.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResourceLoader.java new file mode 100644 index 00000000000..d4a7c50b13b --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResourceLoader.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 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.context; + +import javax.portlet.PortletContext; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; + +/** + * ResourceLoader implementation that resolves paths as PortletContext + * resources, for use outside a Portlet ApplicationContext (for example, + * in a GenericPortletBean subclass). + * + *

Within a WebApplicationContext, resource paths are automatically + * resolved as PortletContext resources by the context implementation. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see #getResourceByPath + * @see PortletContextResource + * @see org.springframework.web.portlet.GenericPortletBean + */ +public class PortletContextResourceLoader extends DefaultResourceLoader { + + private final PortletContext portletContext; + + + /** + * Create a new PortletContextResourceLoader. + * @param portletContext the PortletContext to load resources with + */ + public PortletContextResourceLoader(PortletContext portletContext) { + this.portletContext = portletContext; + } + + /** + * This implementation supports file paths beneath the root of the web application. + * @see PortletContextResource + */ + protected Resource getResourceByPath(String path) { + return new PortletContextResource(this.portletContext, path); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResourcePatternResolver.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResourcePatternResolver.java new file mode 100644 index 00000000000..5f86aa767ee --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletContextResourcePatternResolver.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2007 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.context; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import javax.portlet.PortletContext; + +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.core.io.support.PathMatchingResourcePatternResolver; +import org.springframework.util.StringUtils; + +/** + * PortletContext-aware subclass of {@link PathMatchingResourcePatternResolver}, + * able to find matching resources below the web application root directory + * via Portlet API's PortletContext.getResourcePaths. + * Falls back to the superclass' file system checking for other resources. + * + *

The advantage of using PortletContext.getResourcePaths to + * find matching files is that it will work in a WAR file which has not been + * expanded too. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + */ +public class PortletContextResourcePatternResolver extends PathMatchingResourcePatternResolver { + + /** + * Create a new PortletContextResourcePatternResolver. + * @param portletContext the PortletContext to load resources with + * @see PortletContextResourceLoader#PortletContextResourceLoader(javax.portlet.PortletContext) + */ + public PortletContextResourcePatternResolver(PortletContext portletContext) { + super(new PortletContextResourceLoader(portletContext)); + } + + /** + * Create a new PortletContextResourcePatternResolver. + * @param resourceLoader the ResourceLoader to load root directories and + * actual resources with + */ + public PortletContextResourcePatternResolver(ResourceLoader resourceLoader) { + super(resourceLoader); + } + + + /** + * Overridden version which checks for PortletContextResource + * and uses PortletContext.getResourcePaths to find + * matching resources below the web application root directory. + * In case of other resources, delegates to the superclass version. + * @see #doRetrieveMatchingPortletContextResources + * @see org.springframework.web.portlet.context.PortletContextResource + * @see javax.portlet.PortletContext#getResourcePaths + */ + protected Set doFindPathMatchingFileResources(Resource rootDirResource, String subPattern) throws IOException { + if (rootDirResource instanceof PortletContextResource) { + PortletContextResource pcResource = (PortletContextResource) rootDirResource; + PortletContext pc = pcResource.getPortletContext(); + String fullPattern = pcResource.getPath() + subPattern; + Set result = new HashSet(); + doRetrieveMatchingPortletContextResources(pc, fullPattern, pcResource.getPath(), result); + return result; + } + return super.doFindPathMatchingFileResources(rootDirResource, subPattern); + } + + /** + * Recursively retrieve PortletContextResources that match the given pattern, + * adding them to the given result set. + * @param portletContext the PortletContext to work on + * @param fullPattern the pattern to match against, + * with preprended root directory path + * @param dir the current directory + * @param result the Set of matching Resources to add to + * @throws IOException if directory contents could not be retrieved + * @see org.springframework.web.portlet.context.PortletContextResource + * @see javax.portlet.PortletContext#getResourcePaths + */ + protected void doRetrieveMatchingPortletContextResources( + PortletContext portletContext, String fullPattern, String dir, Set result) throws IOException { + + Set candidates = portletContext.getResourcePaths(dir); + if (candidates != null) { + boolean dirDepthNotFixed = (fullPattern.indexOf("**") != -1); + for (Iterator it = candidates.iterator(); it.hasNext();) { + String currPath = (String) it.next(); + if (currPath.endsWith("/") && + (dirDepthNotFixed || + StringUtils.countOccurrencesOf(currPath, "/") <= StringUtils.countOccurrencesOf(fullPattern, "/"))) { + doRetrieveMatchingPortletContextResources(portletContext, fullPattern, currPath, result); + } + if (getPathMatcher().match(fullPattern, currPath)) { + result.add(new PortletContextResource(portletContext, currPath)); + } + } + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletRequestAttributes.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletRequestAttributes.java new file mode 100644 index 00000000000..68f3d70c8e5 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletRequestAttributes.java @@ -0,0 +1,287 @@ +/* + * 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.context; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.AbstractRequestAttributes; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.portlet.util.PortletUtils; + +/** + * Portlet-based implementation of the + * {@link org.springframework.web.context.request.RequestAttributes} interface. + * + *

Accesses objects from portlet request and portlet session scope, + * with a distinction between "session" (the PortletSession's "portlet scope") + * and "global session" (the PortletSession's "application scope"). + * + * @author Juergen Hoeller + * @since 2.0 + * @see javax.portlet.PortletRequest#getAttribute + * @see javax.portlet.PortletSession#getAttribute + * @see javax.portlet.PortletSession#PORTLET_SCOPE + * @see javax.portlet.PortletSession#APPLICATION_SCOPE + * @see RequestAttributes#SCOPE_SESSION + * @see RequestAttributes#SCOPE_GLOBAL_SESSION + */ +public class PortletRequestAttributes extends AbstractRequestAttributes { + + /** + * We'll create a lot of these objects, so we don't want a new logger every time. + */ + private static final Log logger = LogFactory.getLog(PortletRequestAttributes.class); + + + private final PortletRequest request; + + private volatile PortletSession session; + + private final Map sessionAttributesToUpdate = new HashMap(); + + private final Map globalSessionAttributesToUpdate = new HashMap(); + + + /** + * Create a new PortletRequestAttributes instance for the given request. + * @param request current portlet request + */ + public PortletRequestAttributes(PortletRequest request) { + Assert.notNull(request, "Request must not be null"); + this.request = request; + } + + + /** + * Exposes the native {@link PortletRequest} that we're wrapping. + */ + public final PortletRequest getRequest() { + return this.request; + } + + /** + * Exposes the {@link PortletSession} that we're wrapping. + * @param allowCreate whether to allow creation of a new session if none exists yet + */ + protected final PortletSession getSession(boolean allowCreate) { + if (isRequestActive()) { + return this.request.getPortletSession(allowCreate); + } + else { + // Access through stored session reference, if any... + if (this.session == null && allowCreate) { + throw new IllegalStateException( + "No session found and request already completed - cannot create new session!"); + } + return this.session; + } + } + + + public Object getAttribute(String name, int scope) { + if (scope == SCOPE_REQUEST) { + if (!isRequestActive()) { + throw new IllegalStateException( + "Cannot ask for request attribute - request is not active anymore!"); + } + return this.request.getAttribute(name); + } + else { + PortletSession session = getSession(false); + if (session != null) { + if (scope == SCOPE_GLOBAL_SESSION) { + Object value = session.getAttribute(name, PortletSession.APPLICATION_SCOPE); + if (value != null) { + synchronized (this.globalSessionAttributesToUpdate) { + this.globalSessionAttributesToUpdate.put(name, value); + } + } + return value; + } + else { + Object value = session.getAttribute(name); + if (value != null) { + synchronized (this.sessionAttributesToUpdate) { + this.sessionAttributesToUpdate.put(name, value); + } + } + return value; + } + } + else { + return null; + } + } + } + + public void setAttribute(String name, Object value, int scope) { + if (scope == SCOPE_REQUEST) { + if (!isRequestActive()) { + throw new IllegalStateException( + "Cannot set request attribute - request is not active anymore!"); + } + this.request.setAttribute(name, value); + } + else { + PortletSession session = getSession(true); + if (scope == SCOPE_GLOBAL_SESSION) { + session.setAttribute(name, value, PortletSession.APPLICATION_SCOPE); + synchronized (this.globalSessionAttributesToUpdate) { + this.globalSessionAttributesToUpdate.remove(name); + } + } + else { + session.setAttribute(name, value); + synchronized (this.sessionAttributesToUpdate) { + this.sessionAttributesToUpdate.remove(name); + } + } + } + } + + public void removeAttribute(String name, int scope) { + if (scope == SCOPE_REQUEST) { + if (isRequestActive()) { + this.request.removeAttribute(name); + removeRequestDestructionCallback(name); + } + } + else { + PortletSession session = getSession(false); + if (session != null) { + if (scope == SCOPE_GLOBAL_SESSION) { + session.removeAttribute(name, PortletSession.APPLICATION_SCOPE); + synchronized (this.globalSessionAttributesToUpdate) { + this.globalSessionAttributesToUpdate.remove(name); + } + } + else { + session.removeAttribute(name); + synchronized (this.sessionAttributesToUpdate) { + this.sessionAttributesToUpdate.remove(name); + } + } + } + } + } + + public String[] getAttributeNames(int scope) { + if (scope == SCOPE_REQUEST) { + if (!isRequestActive()) { + throw new IllegalStateException( + "Cannot ask for request attributes - request is not active anymore!"); + } + return StringUtils.toStringArray(this.request.getAttributeNames()); + } + else { + PortletSession session = getSession(false); + if (session != null) { + if (scope == SCOPE_GLOBAL_SESSION) { + return StringUtils.toStringArray(session.getAttributeNames(PortletSession.APPLICATION_SCOPE)); + } + else { + return StringUtils.toStringArray(session.getAttributeNames()); + } + } + else { + return new String[0]; + } + } + } + + public void registerDestructionCallback(String name, Runnable callback, int scope) { + if (scope == SCOPE_REQUEST) { + registerRequestDestructionCallback(name, callback); + } + else { + registerSessionDestructionCallback(name, callback); + } + } + + public String getSessionId() { + return getSession(true).getId(); + } + + public Object getSessionMutex() { + return PortletUtils.getSessionMutex(getSession(true)); + } + + + /** + * Update all accessed session attributes through session.setAttribute + * calls, explicitly indicating to the container that they might have been modified. + */ + protected void updateAccessedSessionAttributes() { + this.session = this.request.getPortletSession(false); + synchronized (this.sessionAttributesToUpdate) { + if (this.session != null) { + for (Iterator it = this.sessionAttributesToUpdate.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + String name = (String) entry.getKey(); + Object newValue = entry.getValue(); + Object oldValue = this.session.getAttribute(name); + if (oldValue == newValue) { + this.session.setAttribute(name, newValue); + } + } + } + this.sessionAttributesToUpdate.clear(); + } + synchronized (this.globalSessionAttributesToUpdate) { + if (this.session != null) { + for (Iterator it = this.globalSessionAttributesToUpdate.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + String name = (String) entry.getKey(); + Object newValue = entry.getValue(); + Object oldValue = this.session.getAttribute(name, PortletSession.APPLICATION_SCOPE); + if (oldValue == newValue) { + this.session.setAttribute(name, newValue, PortletSession.APPLICATION_SCOPE); + } + } + } + this.globalSessionAttributesToUpdate.clear(); + } + } + + /** + * Register the given callback as to be executed after session termination. + * @param name the name of the attribute to register the callback for + * @param callback the callback to be executed for destruction + */ + private void registerSessionDestructionCallback(String name, Runnable callback) { + if (logger.isWarnEnabled()) { + logger.warn("Could not register destruction callback [" + callback + "] for attribute '" + name + + "' for session scope because Portlet API 1.0 does not support session attribute callbacks"); + } + } + + + public String toString() { + return this.request.toString(); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletRequestHandledEvent.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletRequestHandledEvent.java new file mode 100644 index 00000000000..6e3c595f414 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletRequestHandledEvent.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2007 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.context; + +import org.springframework.web.context.support.RequestHandledEvent; + +/** + * Portlet-specific subclass of RequestHandledEvent, + * adding portlet-specific context information. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see org.springframework.web.portlet.FrameworkPortlet + * @see org.springframework.context.ApplicationContext#publishEvent + */ +public class PortletRequestHandledEvent extends RequestHandledEvent { + + /** Name of the portlet that handled the request */ + private final String portletName; + + /** PortletMode of the request */ + private final String portletMode; + + /** Type of Portlet Request */ + private final String requestType; + + + /** + * Create a new PortletRequestHandledEvent. + * @param source the component that published the event + * @param portletName the name of the portlet that handled the request + * @param portletMode the PortletMode of the request (usually 'view', 'edit', or 'help') + * @param requestType the type of Portlet request ('action' or 'render') + * @param sessionId the id of the HTTP session, if any + * @param userName the name of the user that was associated with the + * request, if any (usually the UserPrincipal) + * @param processingTimeMillis the processing time of the request in milliseconds + */ + public PortletRequestHandledEvent(Object source, String portletName, + String portletMode, String requestType, String sessionId, + String userName, long processingTimeMillis) { + + super(source, sessionId, userName, processingTimeMillis); + this.portletName = portletName; + this.portletMode = portletMode; + this.requestType = requestType; + } + + /** + * Create a new PortletRequestHandledEvent. + * @param source the component that published the event + * @param portletName the name of the portlet that handled the request + * @param portletMode the PortletMode of the request (usually 'view', 'edit', or 'help') + * @param requestType the type of Portlet request ('action' or 'render') + * @param sessionId the id of the HTTP session, if any + * @param userName the name of the user that was associated with the + * request, if any (usually the UserPrincipal) + * @param processingTimeMillis the processing time of the request in milliseconds + * @param failureCause the cause of failure, if any + */ + public PortletRequestHandledEvent(Object source, String portletName, + String portletMode, String requestType, String sessionId, + String userName, long processingTimeMillis, Throwable failureCause) { + + super(source, sessionId, userName, processingTimeMillis, failureCause); + this.portletName = portletName; + this.portletMode = portletMode; + this.requestType = requestType; + } + + + /** + * Return the name of the portlet that handled the request. + */ + public String getPortletName() { + return this.portletName; + } + + /** + * Return the mode of the portlet request (usually 'view', 'edit', or 'help'). + */ + public String getPortletMode() { + return this.portletMode; + } + + /** + * Return the the type of Portlet Request ('action' or 'render'). + */ + public String getRequestType() { + return this.requestType; + } + + + public String getShortDescription() { + StringBuffer sb = new StringBuffer(); + sb.append("portlet=[").append(this.portletName).append("]; "); + sb.append(super.getShortDescription()); + return sb.toString(); + } + + public String getDescription() { + StringBuffer sb = new StringBuffer(); + sb.append("portlet=[").append(this.portletName).append("]; "); + sb.append("mode=[").append(this.portletMode).append("]; "); + sb.append("type=[").append(this.requestType).append("]; "); + sb.append(super.getDescription()); + return sb.toString(); + } + + public String toString() { + return "PortletRequestHandledEvent: " + getDescription(); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletWebRequest.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletWebRequest.java new file mode 100644 index 00000000000..78f43086d2b --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/PortletWebRequest.java @@ -0,0 +1,143 @@ +/* + * 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.context; + +import java.security.Principal; +import java.util.Locale; +import java.util.Map; + +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.PortletSession; + +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.NativeWebRequest; + +/** + * {@link org.springframework.web.context.request.WebRequest} adapter + * for a {@link javax.portlet.PortletRequest}. + * + * @author Juergen Hoeller + * @since 2.0 + */ +public class PortletWebRequest extends PortletRequestAttributes implements NativeWebRequest { + + private PortletResponse response; + + + /** + * Create a new PortletWebRequest instance for the given request. + * @param request current portlet request + */ + public PortletWebRequest(PortletRequest request) { + super(request); + } + + /** + * Create a new PortletWebRequest instance for the given request/response pair. + * @param request current portlet request + * @param response current portlet response + */ + public PortletWebRequest(PortletRequest request, PortletResponse response) { + super(request); + this.response = response; + } + + + /** + * Exposes the native {@link PortletResponse} that we're wrapping (if any). + */ + public final PortletResponse getResponse() { + return this.response; + } + + public Object getNativeRequest() { + return getRequest(); + } + + public Object getNativeResponse() { + return getResponse(); + } + + + public String getParameter(String paramName) { + return getRequest().getParameter(paramName); + } + + public String[] getParameterValues(String paramName) { + return getRequest().getParameterValues(paramName); + } + + public Map getParameterMap() { + return getRequest().getParameterMap(); + } + + public Locale getLocale() { + return getRequest().getLocale(); + } + + public String getContextPath() { + return getRequest().getContextPath(); + } + + public String getRemoteUser() { + return getRequest().getRemoteUser(); + } + + public Principal getUserPrincipal() { + return getRequest().getUserPrincipal(); + } + + public boolean isUserInRole(String role) { + return getRequest().isUserInRole(role); + } + + public boolean isSecure() { + return getRequest().isSecure(); + } + + /** + * Last-modified handling not supported for portlet requests: + * As a consequence, this method always returns false. + */ + public boolean checkNotModified(long lastModifiedTimestamp) { + return false; + } + + + public String getDescription(boolean includeClientInfo) { + PortletRequest request = getRequest(); + StringBuffer buffer = new StringBuffer(); + buffer.append("context=").append(request.getContextPath()); + if (includeClientInfo) { + PortletSession session = request.getPortletSession(false); + if (session != null) { + buffer.append(";session=").append(session.getId()); + } + String user = getRequest().getRemoteUser(); + if (StringUtils.hasLength(user)) { + buffer.append(";user=").append(user); + } + } + return buffer.toString(); + } + + public String toString() { + return "PortletWebRequest: " + getDescription(true); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/StaticPortletApplicationContext.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/StaticPortletApplicationContext.java new file mode 100644 index 00000000000..ac341f4ad23 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/StaticPortletApplicationContext.java @@ -0,0 +1,163 @@ +/* + * 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.context; + +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; +import javax.servlet.ServletContext; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.ServletContextAwareProcessor; + +/** + * Static Portlet-based {@link org.springframework.context.ApplicationContext} + * implementation for testing. Not intended for use in production applications. + * + *

Implements the + * {@link org.springframework.web.portlet.context.ConfigurablePortletApplicationContext} + * interface to allow for direct replacement of an {@link XmlPortletApplicationContext}, + * despite not actually supporting external configuration files. + * + *

Interprets resource paths as portlet context resources, that is, as paths + * beneath the portlet application root. Absolute paths, for example for files + * outside the portlet app root, can be accessed via "file:" URLs, as implemented + * by {@link org.springframework.core.io.DefaultResourceLoader}. + * + * @author Juergen Hoeller + * @author Mark Fisher + * @since 2.0 + */ +public class StaticPortletApplicationContext extends StaticApplicationContext + implements ConfigurablePortletApplicationContext { + + private ServletContext servletContext; + + private PortletContext portletContext; + + private PortletConfig portletConfig; + + private String namespace; + + + public StaticPortletApplicationContext() { + setDisplayName("Root Portlet ApplicationContext"); + } + + + public void setParent(ApplicationContext parent) { + super.setParent(parent); + if (parent instanceof WebApplicationContext) { + this.servletContext = ((WebApplicationContext) parent).getServletContext(); + } + } + + public ServletContext getServletContext() { + return this.servletContext; + } + + public void setPortletContext(PortletContext portletContext) { + this.portletContext = portletContext; + } + + public PortletContext getPortletContext() { + return this.portletContext; + } + + public void setPortletConfig(PortletConfig portletConfig) { + this.portletConfig = portletConfig; + if (portletConfig != null && this.portletContext == null) { + this.portletContext = portletConfig.getPortletContext(); + } + } + + public PortletConfig getPortletConfig() { + return this.portletConfig; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + if (namespace != null) { + setDisplayName("Portlet ApplicationContext for namespace '" + namespace + "'"); + } + } + + public String getNamespace() { + return this.namespace; + } + + /** + * The {@link StaticPortletApplicationContext} class does not support this method. + * @throws UnsupportedOperationException always + */ + public void setConfigLocation(String configLocation) { + if (configLocation != null) { + throw new UnsupportedOperationException("StaticPortletApplicationContext does not support config locations"); + } + } + + /** + * The {@link StaticPortletApplicationContext} class does not support this method. + * @throws UnsupportedOperationException always + */ + public void setConfigLocations(String[] configLocations) { + if (configLocations != null) { + throw new UnsupportedOperationException("StaticPortletApplicationContext does not support config locations"); + } + } + + public String[] getConfigLocations() { + return null; + } + + + /** + * Register request/session scopes, a {@link PortletContextAwareProcessor}, etc. + */ + protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) { + beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext)); + beanFactory.addBeanPostProcessor(new PortletContextAwareProcessor(this.portletContext, this.portletConfig)); + beanFactory.ignoreDependencyInterface(PortletContextAware.class); + beanFactory.ignoreDependencyInterface(PortletConfigAware.class); + beanFactory.registerResolvableDependency(ServletContext.class, this.servletContext); + beanFactory.registerResolvableDependency(PortletContext.class, this.portletContext); + beanFactory.registerResolvableDependency(PortletConfig.class, this.portletConfig); + + PortletApplicationContextUtils.registerPortletApplicationScopes(beanFactory); + } + + /** + * This implementation supports file paths beneath the root of the PortletContext. + * @see PortletContextResource + */ + protected Resource getResourceByPath(String path) { + return new PortletContextResource(this.portletContext, path); + } + + /** + * This implementation supports pattern matching in unexpanded WARs too. + * @see PortletContextResourcePatternResolver + */ + protected ResourcePatternResolver getResourcePatternResolver() { + return new PortletContextResourcePatternResolver(this); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/XmlPortletApplicationContext.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/XmlPortletApplicationContext.java new file mode 100644 index 00000000000..c97e50e1b15 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/XmlPortletApplicationContext.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2007 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.context; + +import java.io.IOException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.ResourceEntityResolver; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; + +/** + * Portlet-based {@link org.springframework.web.context.WebApplicationContext} + * implementation which takes its configuration from XML documents, understood + * by an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader}. + * This is essentially the equivalent of + * {@link org.springframework.context.support.AbstractXmlApplicationContext} + * for a portlet environment. + * + *

By default, the configuration will be taken from "/WEB-INF/applicationContext.xml" + * for the root context, and "/WEB-INF/test-portlet.xml" for a context with the namespace + * "test-portlet" (like for a DispatcherPortlet instance with the portlet-name "test"). + * + *

The config location defaults can be overridden via the "contextConfigLocation" + * portlet init-param of {@link org.springframework.web.portlet.FrameworkPortlet}. + * Config locations can either denote concrete files like "/WEB-INF/context.xml" + * or Ant-style patterns like "/WEB-INF/*-context.xml" (see + * {@link org.springframework.util.PathMatcher} javadoc for pattern details). + * + *

Note: In case of multiple config locations, later bean definitions will + * override ones defined in earlier loaded files. This can be leveraged to + * deliberately override certain bean definitions via an extra XML file. + * + *

For a Portlet-based context that reads in a different bean definition format, + * create an analogous subclass of {@link AbstractRefreshablePortletApplicationContext}. + * Such a context implementation can be specified as "contextClass" init-param + * for a FrameworkPortlet instance. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see #setNamespace + * @see #setConfigLocations + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + * @see org.springframework.web.portlet.FrameworkPortlet#initPortletApplicationContext + */ +public class XmlPortletApplicationContext extends AbstractRefreshablePortletApplicationContext { + + /** Default config location for the root context */ + public static final String DEFAULT_CONFIG_LOCATION = "/WEB-INF/applicationContext.xml"; + + /** Default prefix for building a config location for a namespace */ + public static final String DEFAULT_CONFIG_LOCATION_PREFIX = "/WEB-INF/"; + + /** Default suffix for building a config location for a namespace */ + public static final String DEFAULT_CONFIG_LOCATION_SUFFIX = ".xml"; + + + /** + * Loads the bean definitions via an XmlBeanDefinitionReader. + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader + * @see #initBeanDefinitionReader + * @see #loadBeanDefinitions + */ + protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws IOException { + // Create a new XmlBeanDefinitionReader for the given BeanFactory. + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + // Configure the bean definition reader with this context's + // resource loading environment. + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + // Allow a subclass to provide custom initialization of the reader, + // then proceed with actually loading the bean definitions. + initBeanDefinitionReader(beanDefinitionReader); + loadBeanDefinitions(beanDefinitionReader); + } + + /** + * Initialize the bean definition reader used for loading the bean + * definitions of this context. Default implementation is empty. + *

Can be overridden in subclasses, e.g. for turning off XML validation + * or using a different XmlBeanDefinitionParser implementation. + * @param beanDefinitionReader the bean definition reader used by this context + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader#setValidationMode + * @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader#setDocumentReaderClass + */ + protected void initBeanDefinitionReader(XmlBeanDefinitionReader beanDefinitionReader) { + } + + /** + * Load the bean definitions with the given XmlBeanDefinitionReader. + *

The lifecycle of the bean factory is handled by the refreshBeanFactory method; + * therefore this method is just supposed to load and/or register bean definitions. + *

Delegates to a ResourcePatternResolver for resolving location patterns + * into Resource instances. + * @throws org.springframework.beans.BeansException in case of bean registration errors + * @throws java.io.IOException if the required XML document isn't found + * @see #refreshBeanFactory + * @see #getConfigLocations + * @see #getResources + * @see #getResourcePatternResolver + */ + protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + for (int i = 0; i < configLocations.length; i++) { + reader.loadBeanDefinitions(configLocations[i]); + } + } + } + + /** + * The default location for the root context is "/WEB-INF/applicationContext.xml", + * and "/WEB-INF/test-portlet.xml" for a context with the namespace "test-portlet" + * (like for a DispatcherPortlet instance with the portlet-name "test"). + */ + protected String[] getDefaultConfigLocations() { + if (getNamespace() != null) { + return new String[] {DEFAULT_CONFIG_LOCATION_PREFIX + getNamespace() + DEFAULT_CONFIG_LOCATION_SUFFIX}; + } + else { + return new String[] {DEFAULT_CONFIG_LOCATION}; + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/package.html b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/package.html new file mode 100644 index 00000000000..1771af47457 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/context/package.html @@ -0,0 +1,8 @@ + + + +Support for Spring's application context concept in a portlet environment, +including ApplicationContext implementations and various utility classes. + + + diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/AbstractHandlerMapping.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/AbstractHandlerMapping.java new file mode 100644 index 00000000000..b0e2f3cd613 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/AbstractHandlerMapping.java @@ -0,0 +1,261 @@ +/* + * Copyright 2002-2007 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.handler; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.portlet.PortletRequest; + +import org.springframework.beans.BeansException; +import org.springframework.context.support.ApplicationObjectSupport; +import org.springframework.core.Ordered; +import org.springframework.web.context.request.WebRequestInterceptor; +import org.springframework.web.portlet.HandlerExecutionChain; +import org.springframework.web.portlet.HandlerInterceptor; +import org.springframework.web.portlet.HandlerMapping; + +/** + * Abstract base class for {@link org.springframework.web.portlet.HandlerMapping} + * implementations. Supports ordering, a default handler, and handler interceptors. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see #getHandlerInternal + * @see #setDefaultHandler + * @see #setInterceptors + * @see org.springframework.web.portlet.HandlerInterceptor + */ +public abstract class AbstractHandlerMapping extends ApplicationObjectSupport + implements HandlerMapping, Ordered { + + private int order = Integer.MAX_VALUE; // default: same as non-Ordered + + private Object defaultHandler; + + private final List interceptors = new ArrayList(); + + private boolean applyWebRequestInterceptorsToRenderPhaseOnly = true; + + private HandlerInterceptor[] adaptedInterceptors; + + + /** + * Specify the order value for this HandlerMapping bean. + *

Default value is Integer.MAX_VALUE, meaning that it's non-ordered. + * @see org.springframework.core.Ordered#getOrder() + */ + public final void setOrder(int order) { + this.order = order; + } + + public final int getOrder() { + return this.order; + } + + /** + * Set the default handler for this handler mapping. + * This handler will be returned if no specific mapping was found. + *

Default is null, indicating no default handler. + */ + public void setDefaultHandler(Object defaultHandler) { + this.defaultHandler = defaultHandler; + } + + /** + * Return the default handler for this handler mapping, + * or null if none. + */ + public Object getDefaultHandler() { + return this.defaultHandler; + } + + /** + * Set the interceptors to apply for all handlers mapped by this handler mapping. + *

Supported interceptor types are HandlerInterceptor and WebRequestInterceptor. + * Each given WebRequestInterceptor will be wrapped in a WebRequestHandlerInterceptorAdapter. + * @param interceptors array of handler interceptors, or null if none + * @see #adaptInterceptor + * @see org.springframework.web.portlet.HandlerInterceptor + * @see org.springframework.web.context.request.WebRequestInterceptor + */ + public void setInterceptors(Object[] interceptors) { + this.interceptors.addAll(Arrays.asList(interceptors)); + } + + /** + * Specify whether to apply WebRequestInterceptors to the Portlet render phase + * only ("true", or whether to apply them to the Portlet action phase as well + * ("false"). + *

Default is "true", since WebRequestInterceptors are usually built for + * MVC-style handler execution plus rendering process (which is, for example, + * the primary target scenario for "Open Session in View" interceptors, + * offering lazy loading of persistent objects during view rendering). + * Set this to "false" to have WebRequestInterceptors apply to the action + * phase as well (for example, in case of an "Open Session in View" interceptor, + * to allow for lazy loading outside of a transaction during the action phase). + * @see #setInterceptors + * @see org.springframework.web.context.request.WebRequestInterceptor + * @see WebRequestHandlerInterceptorAdapter#WebRequestHandlerInterceptorAdapter(WebRequestInterceptor, boolean) + * @see org.springframework.orm.hibernate3.support.OpenSessionInViewInterceptor + */ + public void setApplyWebRequestInterceptorsToRenderPhaseOnly(boolean applyWebRequestInterceptorsToRenderPhaseOnly) { + this.applyWebRequestInterceptorsToRenderPhaseOnly = applyWebRequestInterceptorsToRenderPhaseOnly; + } + + + /** + * Initializes the interceptors. + * @see #extendInterceptors(java.util.List) + * @see #initInterceptors() + */ + protected void initApplicationContext() throws BeansException { + extendInterceptors(this.interceptors); + initInterceptors(); + } + + /** + * Extension hook that subclasses can override to register additional interceptors, + * given the configured interceptors (see {@link #setInterceptors}). + *

Will be invoked before {@link #initInterceptors()} adapts the specified + * interceptors into {@link HandlerInterceptor} instances. + *

The default implementation is empty. + * @param interceptors the configured interceptor List (never null), + * allowing to add further interceptors before as well as after the existing + * interceptors + */ + protected void extendInterceptors(List interceptors) { + } + + /** + * Initialize the specified interceptors, adapting them where necessary. + * @see #setInterceptors + * @see #adaptInterceptor + */ + protected void initInterceptors() { + if (!this.interceptors.isEmpty()) { + this.adaptedInterceptors = new HandlerInterceptor[this.interceptors.size()]; + for (int i = 0; i < this.interceptors.size(); i++) { + Object interceptor = this.interceptors.get(i); + if (interceptor == null) { + throw new IllegalArgumentException("Entry number " + i + " in interceptors array is null"); + } + this.adaptedInterceptors[i] = adaptInterceptor(interceptor); + } + } + } + + /** + * Adapt the given interceptor object to the HandlerInterceptor interface. + *

Supported interceptor types are HandlerInterceptor and WebRequestInterceptor. + * Each given WebRequestInterceptor will be wrapped in a WebRequestHandlerInterceptorAdapter. + * Can be overridden in subclasses. + * @param interceptor the specified interceptor object + * @return the interceptor wrapped as HandlerInterceptor + * @see #setApplyWebRequestInterceptorsToRenderPhaseOnly + * @see org.springframework.web.portlet.HandlerInterceptor + * @see org.springframework.web.context.request.WebRequestInterceptor + * @see WebRequestHandlerInterceptorAdapter + */ + protected HandlerInterceptor adaptInterceptor(Object interceptor) { + if (interceptor instanceof HandlerInterceptor) { + return (HandlerInterceptor) interceptor; + } + else if (interceptor instanceof WebRequestInterceptor) { + return new WebRequestHandlerInterceptorAdapter( + (WebRequestInterceptor) interceptor, this.applyWebRequestInterceptorsToRenderPhaseOnly); + } + else { + throw new IllegalArgumentException("Interceptor type not supported: " + interceptor.getClass().getName()); + } + } + + /** + * Return the adapted interceptors as HandlerInterceptor array. + * @return the array of HandlerInterceptors, or null if none + */ + protected final HandlerInterceptor[] getAdaptedInterceptors() { + return this.adaptedInterceptors; + } + + + /** + * Look up a handler for the given request, falling back to the default + * handler if no specific one is found. + * @param request current portlet request + * @return the corresponding handler instance, or the default handler + * @see #getHandlerInternal + */ + public final HandlerExecutionChain getHandler(PortletRequest request) throws Exception { + Object handler = getHandlerInternal(request); + if (handler == null) { + handler = getDefaultHandler(); + } + if (handler == null) { + return null; + } + // Bean name or resolved handler? + if (handler instanceof String) { + String handlerName = (String) handler; + handler = getApplicationContext().getBean(handlerName); + } + return getHandlerExecutionChain(handler, request); + } + + /** + * Look up a handler for the given request, returning null if no + * specific one is found. This method is called by {@link #getHandler}; + * a null return value will lead to the default handler, if one is set. + *

Note: This method may also return a pre-built {@link HandlerExecutionChain}, + * combining a handler object with dynamically determined interceptors. + * Statically specified interceptors will get merged into such an existing chain. + * @param request current portlet request + * @return the corresponding handler instance, or null if none found + * @throws Exception if there is an internal error + * @see #getHandler + */ + protected abstract Object getHandlerInternal(PortletRequest request) throws Exception; + + /** + * Build a HandlerExecutionChain for the given handler, including applicable interceptors. + *

The default implementation simply builds a standard HandlerExecutionChain with + * the given handler and this handler mapping's common interceptors. Subclasses may + * override this in order to extend/rearrange the list of interceptors. + *

NOTE: The passed-in handler object may be a raw handler or a pre-built + * HandlerExecutionChain. This method should handle those two cases explicitly, + * either building a new HandlerExecutionChain or extending the existing chain. + *

For simply adding an interceptor, consider calling super.getHandlerExecutionChain + * and invoking {@link HandlerExecutionChain#addInterceptor} on the returned chain object. + * @param handler the resolved handler instance (never null) + * @param request current HTTP request + * @return the HandlerExecutionChain (never null) + * @see #getAdaptedInterceptors() + */ + protected HandlerExecutionChain getHandlerExecutionChain(Object handler, PortletRequest request) { + if (handler instanceof HandlerExecutionChain) { + HandlerExecutionChain chain = (HandlerExecutionChain) handler; + chain.addInterceptors(getAdaptedInterceptors()); + return chain; + } + else { + return new HandlerExecutionChain(handler, getAdaptedInterceptors()); + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/AbstractMapBasedHandlerMapping.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/AbstractMapBasedHandlerMapping.java new file mode 100644 index 00000000000..cc480be06fd --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/AbstractMapBasedHandlerMapping.java @@ -0,0 +1,190 @@ +/* + * 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.handler; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.portlet.PortletRequest; + +import org.springframework.beans.BeansException; +import org.springframework.util.Assert; + +/** + * Abstract base class for {@link org.springframework.web.portlet.HandlerMapping} + * implementations that rely on a map which caches handler objects per lookup key. + * Supports arbitrary lookup keys, and automatically resolves handler bean names + * into handler bean instances. + * + * @author Juergen Hoeller + * @since 2.0 + * @see #getLookupKey(javax.portlet.PortletRequest) + * @see #registerHandler(Object, Object) + */ +public abstract class AbstractMapBasedHandlerMapping extends AbstractHandlerMapping { + + private boolean lazyInitHandlers = false; + + private final Map handlerMap = new HashMap(); + + + /** + * Set whether to lazily initialize handlers. Only applicable to + * singleton handlers, as prototypes are always lazily initialized. + * Default is false, as eager initialization allows for more efficiency + * through referencing the handler objects directly. + *

If you want to allow your handlers to be lazily initialized, + * make them "lazy-init" and set this flag to true. Just making them + * "lazy-init" will not work, as they are initialized through the + * references from the handler mapping in this case. + */ + public void setLazyInitHandlers(boolean lazyInitHandlers) { + this.lazyInitHandlers = lazyInitHandlers; + } + + + /** + * Determines a handler for the computed lookup key for the given request. + * @see #getLookupKey + */ + protected Object getHandlerInternal(PortletRequest request) throws Exception { + Object lookupKey = getLookupKey(request); + Object handler = this.handlerMap.get(lookupKey); + if (handler != null && logger.isDebugEnabled()) { + logger.debug("Key [" + lookupKey + "] -> handler [" + handler + "]"); + } + if (handler instanceof Map) { + Map predicateMap = (Map) handler; + List predicates = new LinkedList(predicateMap.keySet()); + Collections.sort(predicates); + for (Iterator it = predicates.iterator(); it.hasNext();) { + PortletRequestMappingPredicate predicate = (PortletRequestMappingPredicate) it.next(); + if (predicate.match(request)) { + return predicateMap.get(predicate); + } + } + return null; + } + return handler; + } + + /** + * Build a lookup key for the given request. + * @param request current portlet request + * @return the lookup key (never null) + * @throws Exception if key computation failed + */ + protected abstract Object getLookupKey(PortletRequest request) throws Exception; + + + /** + * Register all handlers specified in the Portlet mode map for the corresponding modes. + * @param handlerMap Map with lookup keys as keys and handler beans or bean names as values + * @throws BeansException if the handler couldn't be registered + */ + protected void registerHandlers(Map handlerMap) throws BeansException { + Assert.notNull(handlerMap, "Handler Map must not be null"); + for (Iterator it = handlerMap.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + registerHandler(entry.getKey(), entry.getValue()); + } + } + + /** + * Register the given handler instance for the given parameter value. + * @param lookupKey the key to map the handler onto + * @param handler the handler instance or handler bean name String + * (a bean name will automatically be resolved into the corresponding handler bean) + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandler(Object lookupKey, Object handler) throws BeansException, IllegalStateException { + registerHandler(lookupKey, handler, null); + } + + /** + * Register the given handler instance for the given parameter value. + * @param lookupKey the key to map the handler onto + * @param handler the handler instance or handler bean name String + * (a bean name will automatically be resolved into the corresponding handler bean) + * @param predicate a predicate object for this handler (may be null), + * determining a match with the primary lookup key + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + */ + protected void registerHandler(Object lookupKey, Object handler, PortletRequestMappingPredicate predicate) + throws BeansException, IllegalStateException { + + Assert.notNull(lookupKey, "Lookup key must not be null"); + Assert.notNull(handler, "Handler object must not be null"); + Object resolvedHandler = handler; + + // Eagerly resolve handler if referencing singleton via name. + if (!this.lazyInitHandlers && handler instanceof String) { + String handlerName = (String) handler; + if (getApplicationContext().isSingleton(handlerName)) { + resolvedHandler = getApplicationContext().getBean(handlerName); + } + } + + // Check for duplicate mapping. + Object mappedHandler = this.handlerMap.get(lookupKey); + if (mappedHandler != null && !(mappedHandler instanceof Map)) { + if (mappedHandler != resolvedHandler) { + throw new IllegalStateException("Cannot map handler [" + handler + "] to key [" + lookupKey + + "]: There's already handler [" + mappedHandler + "] mapped."); + } + } + else { + if (predicate != null) { + // Add the handler to the predicate map. + Map predicateMap = (Map) mappedHandler; + if (predicateMap == null) { + predicateMap = new LinkedHashMap(); + this.handlerMap.put(lookupKey, predicateMap); + } + predicateMap.put(predicate, resolvedHandler); + } + else { + // Add the single handler to the map. + this.handlerMap.put(lookupKey, resolvedHandler); + } + if (logger.isDebugEnabled()) { + logger.debug("Mapped key [" + lookupKey + "] onto handler [" + resolvedHandler + "]"); + } + } + } + + + /** + * Predicate interface for determining a match with a given request. + */ + protected interface PortletRequestMappingPredicate extends Comparable { + + /** + * Determine whether the given request matches this predicate. + * @param request current portlet request + */ + boolean match(PortletRequest request); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/HandlerInterceptorAdapter.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/HandlerInterceptorAdapter.java new file mode 100644 index 00000000000..5a3557be743 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/HandlerInterceptorAdapter.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2005 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.handler; + +import java.io.IOException; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.PortletException; + +import org.springframework.web.portlet.HandlerInterceptor; +import org.springframework.web.portlet.ModelAndView; + +/** + * Abstract adapter class for the HandlerInterceptor interface, + * for simplified implementation of pre-only/post-only interceptors. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + */ +public abstract class HandlerInterceptorAdapter implements HandlerInterceptor { + + /** + * This implementation delegates to preHandle. + * @see #preHandle + */ + public boolean preHandleAction(ActionRequest request, ActionResponse response, Object handler) throws Exception { + return preHandle(request, response, handler); + } + + /** + * This implementation delegates to afterCompletion. + * @see #afterCompletion + */ + public void afterActionCompletion( + ActionRequest request, ActionResponse response, Object handler, Exception ex) throws Exception { + + afterCompletion(request, response, handler, ex); + } + + + /** + * This implementation delegates to preHandle. + * @see #preHandle + */ + public boolean preHandleRender(RenderRequest request, RenderResponse response, Object handler) throws Exception { + return preHandle(request, response, handler); + } + + /** + * This implementation is empty. + */ + public void postHandleRender( + RenderRequest request, RenderResponse response, Object handler, ModelAndView modelAndView) throws Exception { + } + + /** + * This implementation delegates to afterCompletion. + * @see #afterCompletion + */ + public void afterRenderCompletion( + RenderRequest request, RenderResponse response, Object handler, Exception ex) throws Exception { + + afterCompletion(request, response, handler, ex); + } + + + /** + * Default callback that both preHandleRender + * and preHandleAction delegate to. + *

This implementation always returns true. + * @see #preHandleRender + * @see #preHandleAction + */ + protected boolean preHandle(PortletRequest request, PortletResponse response, Object handler) + throws Exception { + + return true; + } + + /** + * Default callback that both preHandleRender + * and preHandleAction delegate to. + *

This implementation is empty. + * @see #afterRenderCompletion + * @see #afterActionCompletion + */ + protected void afterCompletion( + PortletRequest request, PortletResponse response, Object handler, Exception ex) throws Exception { + + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/ParameterHandlerMapping.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/ParameterHandlerMapping.java new file mode 100644 index 00000000000..72571dc93c6 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/ParameterHandlerMapping.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2007 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.handler; + +import java.util.Map; + +import javax.portlet.PortletRequest; + +import org.springframework.beans.BeansException; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +/** + * Implementation of the {@link org.springframework.web.portlet.HandlerMapping} + * to map from a request parameter to request handler beans. + * + *

The default name of the parameter is "action", but can be changed using + * {@link #setParameterName setParameterName()}. + * + *

The bean configuration for this mapping will look somthing like this: + * + *

+ * <bean id="parameterHandlerMapping" class="org.springframework.web.portlet.handler.ParameterHandlerMapping">
+ *   <property name="parameterMap">
+ *     <map>
+ * 	     <entry key="add"><ref bean="addItemHandler"/></entry>
+ *       <entry key="edit"><ref bean="editItemHandler"/></entry>
+ *       <entry key="delete"><ref bean="deleteItemHandler"/></entry>
+ *     </map>
+ *   </property>
+ * </bean>
+ * + * Thanks to Rainer Schmitz for suggesting this mapping strategy! + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + * @see ParameterMappingInterceptor + */ +public class ParameterHandlerMapping extends AbstractMapBasedHandlerMapping { + + /** + * Default request parameter name to use for mapping to handlers: "action". + */ + public final static String DEFAULT_PARAMETER_NAME = "action"; + + + private String parameterName = DEFAULT_PARAMETER_NAME; + + private Map parameterMap; + + + /** + * Set the name of the parameter used for mapping to handlers. + *

Default is "action". + */ + public void setParameterName(String parameterName) { + Assert.hasText(parameterName, "'parameterName' must not be empty"); + this.parameterName = parameterName; + } + + /** + * Set a Map with parameters as keys and handler beans or bean names as values. + * Convenient for population with bean references. + * @param parameterMap map with parameters as keys and beans as values + */ + public void setParameterMap(Map parameterMap) { + this.parameterMap = parameterMap; + } + + + /** + * Calls the registerHandlers method in addition + * to the superclass's initialization. + * @see #registerHandlers + */ + public void initApplicationContext() throws BeansException { + super.initApplicationContext(); + registerHandlers(this.parameterMap); + } + + /** + * Register all handlers specified in the Portlet mode map for the corresponding modes. + * @param parameterMap Map with parameter names as keys and handler beans or bean names as values + * @throws BeansException if the handler couldn't be registered + */ + protected void registerHandlers(Map parameterMap) throws BeansException { + if (CollectionUtils.isEmpty(parameterMap)) { + logger.warn("'parameterMap' is empty on ParameterHandlerMapping"); + } + else { + super.registerHandlers(parameterMap); + } + } + + + /** + * Uses the value of the specified parameter as lookup key. + * @see #setParameterName + */ + protected Object getLookupKey(PortletRequest request) throws Exception { + return request.getParameter(this.parameterName); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/ParameterMappingInterceptor.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/ParameterMappingInterceptor.java new file mode 100644 index 00000000000..153be429106 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/ParameterMappingInterceptor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2007 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.handler; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; + +/** + * Interceptor to forward a request parameter from the ActionRequest to the + * RenderRequest. + * + *

This can be useful when using {@link ParameterHandlerMapping ParameterHandlerMapping} + * or {@link PortletModeParameterHandlerMapping PortletModeParameterHandlerMapping}. + * It will ensure that the parameter that was used to map the ActionRequest + * to a handler will be forwarded to the RenderRequest so that it will also be + * mapped the same way. + * + *

When using this Interceptor, you can still change the value of the mapping parameter + * in your handler in order to change where the render request will get mapped. + * + *

Be aware that this Interceptor does call ActionResponse.setRenderParameter, + * which means that you will not be able to call ActionResponse.sendRedirect in + * your handler. If you may need to issue a redirect, then you should avoid this Interceptor + * and either write a different one that does this in a different way, or manually forward + * the parameter from within your handler(s). + * + *

Thanks to Rainer Schmitz for suggesting this mapping strategy! + * + * @author John A. Lewis + * @since 2.0 + * @see ParameterHandlerMapping + * @see PortletModeParameterHandlerMapping + */ +public class ParameterMappingInterceptor extends HandlerInterceptorAdapter { + + /** Request parameter name to use for mapping to handlers */ + public final static String DEFAULT_PARAMETER_NAME = "action"; + + private String parameterName = DEFAULT_PARAMETER_NAME; + + + /** + * Set the name of the parameter used for mapping. + */ + public void setParameterName(String parameterName) { + this.parameterName = (parameterName != null ? parameterName : DEFAULT_PARAMETER_NAME); + } + + + /** + * If request is an {@link javax.portlet.ActionRequest ActionRequest}, + * get handler mapping parameter and add it to the ActionResponse. + */ + public boolean preHandleAction(ActionRequest request, ActionResponse response, Object handler) { + String mappingParameter = request.getParameter(this.parameterName); + if (mappingParameter != null) { + response.setRenderParameter(parameterName, mappingParameter); + } + return true; + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletContentGenerator.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletContentGenerator.java new file mode 100644 index 00000000000..470e0e2aa09 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletContentGenerator.java @@ -0,0 +1,162 @@ +/* + * Copyright 2002-2007 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.handler; + +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.web.portlet.context.PortletApplicationObjectSupport; + +/** + * Convenient superclass for any kind of web content generator, + * like {@link org.springframework.web.portlet.mvc.AbstractController}. + * Can also be used for custom handlers that have their own + * {@link org.springframework.web.portlet.HandlerAdapter}. + * + *

Supports portlet cache control options. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see #setCacheSeconds + * @see #setRequireSession + */ +public abstract class PortletContentGenerator extends PortletApplicationObjectSupport { + + private boolean requireSession = false; + + private int cacheSeconds = -1; + + + /** + * Set whether a session should be required to handle requests. + */ + public final void setRequireSession(boolean requireSession) { + this.requireSession = requireSession; + } + + /** + * Return whether a session is required to handle requests. + */ + public final boolean isRequireSession() { + return this.requireSession; + } + + /** + * Cache content for the given number of seconds. Default is -1, + * indicating no override of portlet content caching. + *

Only if this is set to 0 (no cache) or a positive value (cache for + * this many seconds) will this class override the portlet settings. + *

The cache setting can be overwritten by subclasses, before content is generated. + */ + public final void setCacheSeconds(int seconds) { + this.cacheSeconds = seconds; + } + + /** + * Return the number of seconds that content is cached. + */ + public final int getCacheSeconds() { + return this.cacheSeconds; + } + + + /** + * Check and prepare the given request and response according to the settings + * of this generator. Checks for a required session, and applies the number of + * cache seconds configured for this generator (if it is a render request/response). + * @param request current portlet request + * @param response current portlet response + * @throws PortletException if the request cannot be handled because a check failed + */ + protected final void check(PortletRequest request, PortletResponse response) throws PortletException { + if (this.requireSession) { + if (request.getPortletSession(false) == null) { + throw new PortletSessionRequiredException("Pre-existing session required but none found"); + } + } + } + + /** + * Check and prepare the given request and response according to the settings + * of this generator. Checks for a required session, and applies the number of + * cache seconds configured for this generator (if it is a render request/response). + * @param request current portlet request + * @param response current portlet response + * @throws PortletException if the request cannot be handled because a check failed + */ + protected final void checkAndPrepare(RenderRequest request, RenderResponse response) + throws PortletException { + + checkAndPrepare(request, response, this.cacheSeconds); + } + + /** + * Check and prepare the given request and response according to the settings + * of this generator. Checks for a required session, and applies the given + * number of cache seconds (if it is a render request/response). + * @param request current portlet request + * @param response current portlet response + * @param cacheSeconds positive number of seconds into the future that the + * response should be cacheable for, 0 to prevent caching + * @throws PortletException if the request cannot be handled because a check failed + */ + protected final void checkAndPrepare( + RenderRequest request, RenderResponse response, int cacheSeconds) + throws PortletException { + + check(request, response); + applyCacheSeconds(response, cacheSeconds); + } + + /** + * Prevent the render response from being cached. + */ + protected final void preventCaching(RenderResponse response) { + cacheForSeconds(response, 0); + } + + /** + * Set portlet response to allow caching for the given number of seconds. + * @param response current portlet render response + * @param seconds number of seconds into the future that the response + * should be cacheable for + */ + protected final void cacheForSeconds(RenderResponse response, int seconds) { + response.setProperty(RenderResponse.EXPIRATION_CACHE, Integer.toString(seconds)); + } + + /** + * Apply the given cache seconds to the render response + * @param response current portlet render response + * @param seconds positive number of seconds into the future that the + * response should be cacheable for, 0 to prevent caching + */ + protected final void applyCacheSeconds(RenderResponse response, int seconds) { + if (seconds > 0) { + cacheForSeconds(response, seconds); + } + else if (seconds == 0) { + preventCaching(response); + } + // Leave caching to the portlet configuration otherwise. + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletModeHandlerMapping.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletModeHandlerMapping.java new file mode 100644 index 00000000000..2992517d88c --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletModeHandlerMapping.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2007 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.handler; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Properties; + +import javax.portlet.PortletMode; +import javax.portlet.PortletRequest; + +import org.springframework.beans.BeansException; +import org.springframework.util.CollectionUtils; + +/** + * Implementation of the {@link org.springframework.web.portlet.HandlerMapping} + * interface to map from the current PortletMode to request handler beans. + * + *

The bean configuration for this mapping will look something like this: + *

+ * 	<bean id="portletModeHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeHandlerMapping">
+ * 		<property name="portletModeMap">
+ * 			<map>
+ * 				<entry key="view"><ref bean="viewHandler"/></entry>
+ * 				<entry key="edit"><ref bean="editHandler"/></entry>
+ * 				<entry key="help"><ref bean="helpHandler"/></entry>
+ * 			</map>
+ * 		</property>
+ * 	</bean>
+ * 
+ * + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @since 2.0 + */ +public class PortletModeHandlerMapping extends AbstractMapBasedHandlerMapping { + + private final Map portletModeMap = new HashMap(); + + + /** + * Set PortletMode to handler bean name mappings from a Properties object. + * @param mappings properties with PortletMode names as keys and bean names as values + */ + public void setMappings(Properties mappings) { + this.portletModeMap.putAll(mappings); + } + + /** + * Set a Map with PortletModes as keys and handler beans as values. + * Convenient for population with bean references. + * @param portletModeMap map with PortletMode names as keys and beans or bean names as values + */ + public void setPortletModeMap(Map portletModeMap) { + this.portletModeMap.putAll(portletModeMap); + } + + + /** + * Calls the registerHandlers method in addition + * to the superclass's initialization. + * @see #registerHandlers + */ + public void initApplicationContext() throws BeansException { + super.initApplicationContext(); + registerHandlers(this.portletModeMap); + } + + /** + * Register all handlers specified in the Portlet mode map for the corresponding modes. + * @param portletModeMap Map with mode names as keys and handler beans or bean names as values + * @throws BeansException if the handler couldn't be registered + */ + protected void registerHandlers(Map portletModeMap) throws BeansException { + if (CollectionUtils.isEmpty(portletModeMap)) { + logger.warn("Neither 'portletModeMap' nor 'mappings' set on PortletModeHandlerMapping"); + } + else { + for (Iterator it = portletModeMap.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + String modeKey = (String) entry.getKey(); + PortletMode mode = new PortletMode(modeKey); + Object handler = entry.getValue(); + registerHandler(mode, handler); + } + } + } + + + /** + * Uses the current PortletMode as lookup key. + */ + protected Object getLookupKey(PortletRequest request) throws Exception { + return request.getPortletMode(); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletModeParameterHandlerMapping.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletModeParameterHandlerMapping.java new file mode 100644 index 00000000000..e6795245956 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletModeParameterHandlerMapping.java @@ -0,0 +1,255 @@ +/* + * 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.handler; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.portlet.PortletMode; +import javax.portlet.PortletRequest; + +import org.springframework.beans.BeansException; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; + +/** + * Implementation of the {@link org.springframework.web.portlet.HandlerMapping} + * interface to map from the current PortletMode and a request parameter to + * request handler beans. The mapping consists of two levels: first the + * PortletMode and then the parameter value. In order to be mapped, + * both elements must match the mapping definition. + * + *

This is a combination of the methods used in {@link PortletModeHandlerMapping PortletModeHandlerMapping} + * and {@link ParameterHandlerMapping ParameterHandlerMapping}. Unlike + * those two classes, this mapping cannot be initialized with properties since it + * requires a two-level map. + * + *

The default name of the parameter is "action", but can be changed using + * {@link #setParameterName setParameterName()}. + * + *

By default, the same parameter value may not be used in two different portlet + * modes. This is so that if the portal itself changes the portlet mode, the request + * will no longer be valid in the mapping. This behavior can be changed with + * {@link #setAllowDuplicateParameters setAllowDupParameters()}. + * + *

The bean configuration for this mapping will look somthing like this: + * + *

+ * <bean id="portletModeParameterHandlerMapping" class="org.springframework.web.portlet.handler.PortletModeParameterHandlerMapping">
+ *   <property name="portletModeParameterMap">
+ *     <map>
+ *       <entry key="view"> <!-- portlet mode: view -->
+ *         <map>
+ *           <entry key="add"><ref bean="addItemHandler"/></entry>
+ *           <entry key="edit"><ref bean="editItemHandler"/></entry>
+ *           <entry key="delete"><ref bean="deleteItemHandler"/></entry>
+ *         </map>
+ *       </entry>
+ *       <entry key="edit"> <!-- portlet mode: edit -->
+ *         <map>
+ *           <entry key="prefs"><ref bean="preferencesHandler"/></entry>
+ *           <entry key="resetPrefs"><ref bean="resetPreferencesHandler"/></entry>
+ *         </map>
+ *       </entry>
+ *     </map>
+ *   </property>
+ * </bean>
+ * + *

This mapping can be chained ahead of a {@link PortletModeHandlerMapping PortletModeHandlerMapping}, + * which can then provide defaults for each mode and an overall default as well. + * + *

Thanks to Rainer Schmitz and Yujin Kim for suggesting this mapping strategy! + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + * @see ParameterMappingInterceptor + */ +public class PortletModeParameterHandlerMapping extends AbstractMapBasedHandlerMapping { + + /** + * Default request parameter name to use for mapping to handlers: "action". + */ + public final static String DEFAULT_PARAMETER_NAME = "action"; + + + private String parameterName = DEFAULT_PARAMETER_NAME; + + private Map portletModeParameterMap; + + private boolean allowDuplicateParameters = false; + + private final Set parametersUsed = new HashSet(); + + + /** + * Set the name of the parameter used for mapping to handlers. + *

Default is "action". + */ + public void setParameterName(String parameterName) { + Assert.hasText(parameterName, "'parameterName' must not be empty"); + this.parameterName = parameterName; + } + + /** + * Set a Map with portlet mode names as keys and another Map as values. + * The sub-map has parameter names as keys and handler bean or bean names as values. + *

Convenient for population with bean references. + * @param portletModeParameterMap two-level map of portlet modes and parameters to handler beans + */ + public void setPortletModeParameterMap(Map portletModeParameterMap) { + this.portletModeParameterMap = portletModeParameterMap; + } + + /** + * Set whether to allow duplicate parameter values across different portlet modes. + * Default is "false". + *

Doing this is dangerous because the portlet mode can be changed by the + * portal itself and the only way to see that is a rerender of the portlet. + * If the same parameter value is legal in multiple modes, then a change in + * mode could result in a matched mapping that is not intended and the user + * could end up in a strange place in the application. + */ + public void setAllowDuplicateParameters(boolean allowDuplicateParameters) { + this.allowDuplicateParameters = allowDuplicateParameters; + } + + + /** + * Calls the registerHandlers method in addition + * to the superclass's initialization. + * @see #registerHandlers + */ + public void initApplicationContext() throws BeansException { + super.initApplicationContext(); + registerHandlers(this.portletModeParameterMap); + } + + /** + * Register all handlers specified in the Portlet mode map for the corresponding modes. + * @param portletModeParameterMap Map with mode names as keys and parameter Maps as values + * @throws BeansException if the handler couldn't be registered + */ + protected void registerHandlers(Map portletModeParameterMap) throws BeansException { + if (CollectionUtils.isEmpty(portletModeParameterMap)) { + logger.warn("'portletModeParameterMap' not set on PortletModeParameterHandlerMapping"); + } + else { + for (Iterator it = portletModeParameterMap.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + String modeKey = (String) entry.getKey(); + PortletMode mode = new PortletMode(modeKey); + Object parameterMap = entry.getValue(); + if (!(parameterMap instanceof Map)) { + throw new IllegalArgumentException( + "The value for the portlet mode must be a Map of parameter Strings to handler Objects"); + } + registerHandler(mode, (Map) parameterMap); + } + } + } + + /** + * Register all handlers specified in the given parameter map. + * @param parameterMap Map with parameter names as keys and handler beans or bean names as values + * @throws BeansException if the handler couldn't be registered + */ + protected void registerHandler(PortletMode mode, Map parameterMap) throws BeansException { + for (Iterator it = parameterMap.entrySet().iterator(); it.hasNext();) { + Map.Entry entry = (Map.Entry) it.next(); + String parameter = (String) entry.getKey(); + Object handler = entry.getValue(); + registerHandler(mode, parameter, handler); + } + } + + /** + * Register the given handler instance for the given PortletMode and parameter value, + * under an appropriate lookup key. + * @param mode the PortletMode for which this mapping is valid + * @param parameter the parameter value to which this handler is mapped + * @param handler the handler instance bean + * @throws BeansException if the handler couldn't be registered + * @throws IllegalStateException if there is a conflicting handler registered + * @see #registerHandler(Object, Object) + */ + protected void registerHandler(PortletMode mode, String parameter, Object handler) + throws BeansException, IllegalStateException { + + // Check for duplicate parameter values across all portlet modes. + if (!this.allowDuplicateParameters && this.parametersUsed.contains(parameter)) { + throw new IllegalStateException( + "Duplicate entries for parameter [" + parameter + "] in different Portlet modes"); + } + this.parametersUsed.add(parameter); + + registerHandler(new LookupKey(mode, parameter), handler); + } + + + /** + * Returns a lookup key that combines the current PortletMode and the current + * value of the specified parameter. + * @see javax.portlet.PortletRequest#getPortletMode() + * @see #setParameterName + */ + protected Object getLookupKey(PortletRequest request) throws Exception { + PortletMode mode = request.getPortletMode(); + String parameter = request.getParameter(this.parameterName); + return new LookupKey(mode, parameter); + } + + + /** + * Internal class used as lookup key, combining PortletMode and parameter value. + */ + private static class LookupKey { + + private final PortletMode mode; + + private final String parameter; + + public LookupKey(PortletMode portletMode, String parameter) { + this.mode = portletMode; + this.parameter = parameter; + } + + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof LookupKey)) { + return false; + } + LookupKey otherKey = (LookupKey) other; + return (this.mode.equals(otherKey.mode) && + ObjectUtils.nullSafeEquals(this.parameter, otherKey.parameter)); + } + + public int hashCode() { + return (this.mode.hashCode() * 29 + ObjectUtils.nullSafeHashCode(this.parameter)); + } + + public String toString() { + return "Portlet mode '" + this.mode + "', parameter '" + this.parameter + "'"; + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletSessionRequiredException.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletSessionRequiredException.java new file mode 100644 index 00000000000..0b0b5bab47b --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/PortletSessionRequiredException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2006 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.handler; + +import javax.portlet.PortletException; + +/** + * Exception thrown when a portlet content generator requires a pre-existing session. + * + * @author John A. Lewis + * @since 2.0 + * @see org.springframework.web.portlet.handler.PortletContentGenerator + */ +public class PortletSessionRequiredException extends PortletException { + + /** + * Create a new PortletSessionRequiredException. + * @param msg the detail message + */ + public PortletSessionRequiredException(String msg) { + super(msg); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimpleMappingExceptionResolver.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimpleMappingExceptionResolver.java new file mode 100644 index 00000000000..03071d1419b --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimpleMappingExceptionResolver.java @@ -0,0 +1,396 @@ +/* + * Copyright 2002-2007 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.handler; + +import java.util.Enumeration; +import java.util.Properties; +import java.util.Set; + +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.WindowState; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.Ordered; +import org.springframework.web.portlet.HandlerExceptionResolver; +import org.springframework.web.portlet.ModelAndView; + +/** + * {@link org.springframework.web.portlet.HandlerExceptionResolver} implementation + * that allows for mapping exception class names to view names, either for a + * set of given handlers or for all handlers in the DispatcherPortlet. + * + *

Error views are analogous to error page JSPs, but can be used with any + * kind of exception including any checked one, with fine-granular mappings for + * specific handlers. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + */ +public class SimpleMappingExceptionResolver implements HandlerExceptionResolver, Ordered { + + /** + * The default name of the exception attribute: "exception". + */ + public static final String DEFAULT_EXCEPTION_ATTRIBUTE = "exception"; + + + /** Logger available to subclasses */ + protected final Log logger = LogFactory.getLog(getClass()); + + private int order = Integer.MAX_VALUE; // default: same as non-Ordered + + private Set mappedHandlers; + + private Class[] mappedHandlerClasses; + + private boolean renderWhenMinimized = false; + + private Log warnLogger; + + private Properties exceptionMappings; + + private String defaultErrorView; + + private String exceptionAttribute = DEFAULT_EXCEPTION_ATTRIBUTE; + + + public void setOrder(int order) { + this.order = order; + } + + public int getOrder() { + return this.order; + } + + /** + * Specify the set of handlers that this exception resolver should map. + * The exception mappings and the default error view will only apply + * to the specified handlers. + *

If no handlers set, both the exception mappings and the default error + * view will apply to all handlers. This means that a specified default + * error view will be used as fallback for all exceptions; any further + * HandlerExceptionResolvers in the chain will be ignored in this case. + */ + public void setMappedHandlers(Set mappedHandlers) { + this.mappedHandlers = mappedHandlers; + } + + /** + * Specify the set of classes that this exception resolver should apply to. + * The exception mappings and the default error view will only apply + * to handlers of the specified type; the specified types may be interfaces + * and superclasses of handlers as well. + *

If no handlers and handler classes are set, the exception mappings + * and the default error view will apply to all handlers. This means that + * a specified default error view will be used as fallback for all exceptions; + * any further HandlerExceptionResolvers in the chain will be ignored in + * this case. + */ + public void setMappedHandlerClasses(Class[] mappedHandlerClasses) { + this.mappedHandlerClasses = mappedHandlerClasses; + } + + /** + * Set if the resolver should render a view when the portlet is in + * a minimized window. The default is "false". + * @see javax.portlet.RenderRequest#getWindowState() + * @see javax.portlet.WindowState#MINIMIZED + */ + public void setRenderWhenMinimized(boolean renderWhenMinimized) { + this.renderWhenMinimized = renderWhenMinimized; + } + + /** + * Set the log category for warn logging. The name will be passed to the + * underlying logger implementation through Commons Logging, getting + * interpreted as log category according to the logger's configuration. + *

Default is no warn logging. Specify this setting to activate + * warn logging into a specific category. Alternatively, override + * the {@link #logException} method for custom logging. + * @see org.apache.commons.logging.LogFactory#getLog(String) + * @see org.apache.log4j.Logger#getLogger(String) + * @see java.util.logging.Logger#getLogger(String) + */ + public void setWarnLogCategory(String loggerName) { + this.warnLogger = LogFactory.getLog(loggerName); + } + + /** + * Set the mappings between exception class names and error view names. + * The exception class name can be a substring, with no wildcard support + * at present. A value of "PortletException" would match + * javax.portet.PortletException and subclasses, for example. + *

NB: Consider carefully how specific the pattern is, and whether + * to include package information (which isn't mandatory). For example, + * "Exception" will match nearly anything, and will probably hide other rules. + * "java.lang.Exception" would be correct if "Exception" was meant to define + * a rule for all checked exceptions. With more unusual exception names such + * as "BaseBusinessException" there's no need to use a FQN. + *

Follows the same matching algorithm as RuleBasedTransactionAttribute + * and RollbackRuleAttribute. + * @param mappings exception patterns (can also be fully qualified class names) + * as keys, and error view names as values + * @see org.springframework.transaction.interceptor.RuleBasedTransactionAttribute + * @see org.springframework.transaction.interceptor.RollbackRuleAttribute + */ + public void setExceptionMappings(Properties mappings) { + this.exceptionMappings = mappings; + } + + /** + * Set the name of the default error view. + * This view will be returned if no specific mapping was found. + *

Default is none. + */ + public void setDefaultErrorView(String defaultErrorView) { + this.defaultErrorView = defaultErrorView; + } + + /** + * Set the name of the model attribute as which the exception should + * be exposed. Default is "exception". + * @see #DEFAULT_EXCEPTION_ATTRIBUTE + */ + public void setExceptionAttribute(String exceptionAttribute) { + this.exceptionAttribute = exceptionAttribute; + } + + + /** + * Checks whether this resolver is supposed to apply (i.e. the handler + * matches in case of "mappedHandlers" having been specified), then + * delegates to the {@link #doResolveException} template method. + */ + public ModelAndView resolveException( + RenderRequest request, RenderResponse response, Object handler, Exception ex) { + + if (shouldApplyTo(request, handler)) { + return doResolveException(request, response, handler, ex); + } + else { + return null; + } + } + + /** + * Check whether this resolver is supposed to apply to the given handler. + *

The default implementation checks against the specified mapped handlers + * and handler classes, if any, and alspo checks the window state (according + * to the "renderWhenMinimize" property). + * @param request current portlet request + * @param handler the executed handler, or null if none chosen at the + * time of the exception (for example, if multipart resolution failed) + * @return whether this resolved should proceed with resolving the exception + * for the given request and handler + * @see #setMappedHandlers + * @see #setMappedHandlerClasses + */ + protected boolean shouldApplyTo(RenderRequest request, Object handler) { + // If the portlet is minimized and we don't want to render then return null. + if (WindowState.MINIMIZED.equals(request.getWindowState()) && !this.renderWhenMinimized) { + return false; + } + + if (handler != null) { + if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) { + return true; + } + if (this.mappedHandlerClasses != null) { + for (int i = 0; i < this.mappedHandlerClasses.length; i++) { + if (this.mappedHandlerClasses[i].isInstance(handler)) { + return true; + } + } + } + } + // Else only apply if there are no explicit handler mappings. + return (this.mappedHandlers == null && this.mappedHandlerClasses == null); + } + + /** + * Actually resolve the given exception that got thrown during on handler execution, + * returning a ModelAndView that represents a specific error page if appropriate. + * @param request current portlet request + * @param response current portlet response + * @param handler the executed handler, or null if none chosen at the time of + * the exception (for example, if multipart resolution failed) + * @param ex the exception that got thrown during handler execution + * @return a corresponding ModelAndView to forward to, or null for default processing + */ + protected ModelAndView doResolveException( + RenderRequest request, RenderResponse response, Object handler, Exception ex) { + + // Log exception, both at debug log level and at warn level, if desired. + if (logger.isDebugEnabled()) { + logger.debug("Resolving exception from handler [" + handler + "]: " + ex); + } + logException(ex, request); + + // Expose ModelAndView for chosen error view. + String viewName = determineViewName(ex, request); + if (viewName != null) { + return getModelAndView(viewName, ex, request); + } + else { + return null; + } + } + + + /** + * Log the given exception at warn level, provided that warn logging has been + * activated through the {@link #setWarnLogCategory "warnLogCategory"} property. + *

Calls {@link #buildLogMessage} in order to determine the concrete message + * to log. Always passes the full exception to the logger. + * @param ex the exception that got thrown during handler execution + * @param request current portlet request (useful for obtaining metadata) + * @see #setWarnLogCategory + * @see #buildLogMessage + * @see org.apache.commons.logging.Log#warn(Object, Throwable) + */ + protected void logException(Exception ex, RenderRequest request) { + if (this.warnLogger != null && this.warnLogger.isWarnEnabled()) { + this.warnLogger.warn(buildLogMessage(ex, request), ex); + } + } + + /** + * Build a log message for the given exception, occured during processing + * the given request. + * @param ex the exception that got thrown during handler execution + * @param request current portlet request (useful for obtaining metadata) + * @return the log message to use + */ + protected String buildLogMessage(Exception ex, RenderRequest request) { + return "Handler execution resulted in exception"; + } + + + /** + * Determine the view name for the given exception, searching the + * {@link #setExceptionMappings "exceptionMappings"}, using the + * {@link #setDefaultErrorView "defaultErrorView"} as fallback. + * @param ex the exception that got thrown during handler execution + * @param request current portlet request (useful for obtaining metadata) + * @return the resolved view name, or null if none found + */ + protected String determineViewName(Exception ex, RenderRequest request) { + String viewName = null; + // Check for specific exception mappings. + if (this.exceptionMappings != null) { + viewName = findMatchingViewName(this.exceptionMappings, ex); + } + // Return default error view else, if defined. + if (viewName == null && this.defaultErrorView != null) { + if (logger.isDebugEnabled()) { + logger.debug("Resolving to default view '" + this.defaultErrorView + + "' for exception of type [" + ex.getClass().getName() + "]"); + } + viewName = this.defaultErrorView; + } + return viewName; + } + + /** + * Find a matching view name in the given exception mappings + * @param exceptionMappings mappings between exception class names and error view names + * @param ex the exception that got thrown during handler execution + * @return the view name, or null if none found + * @see #setExceptionMappings + */ + protected String findMatchingViewName(Properties exceptionMappings, Exception ex) { + String viewName = null; + String dominantMapping = null; + int deepest = Integer.MAX_VALUE; + for (Enumeration names = exceptionMappings.propertyNames(); names.hasMoreElements();) { + String exceptionMapping = (String) names.nextElement(); + int depth = getDepth(exceptionMapping, ex); + if (depth >= 0 && depth < deepest) { + deepest = depth; + dominantMapping = exceptionMapping; + viewName = exceptionMappings.getProperty(exceptionMapping); + } + } + if (viewName != null && logger.isDebugEnabled()) { + logger.debug("Resolving to view '" + viewName + "' for exception of type [" + ex.getClass().getName() + + "], based on exception mapping [" + dominantMapping + "]"); + } + return viewName; + } + + /** + * Return the depth to the superclass matching. + *

0 means ex matches exactly. Returns -1 if there's no match. + * Otherwise, returns depth. Lowest depth wins. + *

Follows the same algorithm as + * {@link org.springframework.transaction.interceptor.RollbackRuleAttribute}. + */ + protected int getDepth(String exceptionMapping, Exception ex) { + return getDepth(exceptionMapping, ex.getClass(), 0); + } + + private int getDepth(String exceptionMapping, Class exceptionClass, int depth) { + if (exceptionClass.getName().indexOf(exceptionMapping) != -1) { + // Found it! + return depth; + } + // If we've gone as far as we can go and haven't found it... + if (exceptionClass.equals(Throwable.class)) { + return -1; + } + return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1); + } + + + /** + * Return a ModelAndView for the given request, view name and exception. + * Default implementation delegates to getModelAndView(viewName, ex). + * @param viewName the name of the error view + * @param ex the exception that got thrown during handler execution + * @param request current portlet request (useful for obtaining metadata) + * @return the ModelAndView instance + * @see #getModelAndView(String, Exception) + */ + protected ModelAndView getModelAndView(String viewName, Exception ex, RenderRequest request) { + return getModelAndView(viewName, ex); + } + + /** + * Return a ModelAndView for the given view name and exception. + * Default implementation adds the specified exception attribute. + * Can be overridden in subclasses. + * @param viewName the name of the error view + * @param ex the exception that got thrown during handler execution + * @return the ModelAndView instance + * @see #setExceptionAttribute + */ + protected ModelAndView getModelAndView(String viewName, Exception ex) { + ModelAndView mv = new ModelAndView(viewName); + if (this.exceptionAttribute != null) { + if (logger.isDebugEnabled()) { + logger.debug("Exposing Exception as model attribute '" + this.exceptionAttribute + "'"); + } + mv.addObject(this.exceptionAttribute, ex); + } + return mv; + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimplePortletHandlerAdapter.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimplePortletHandlerAdapter.java new file mode 100644 index 00000000000..f2f2ad3142b --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimplePortletHandlerAdapter.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2006 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.handler; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.Portlet; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.web.portlet.HandlerAdapter; +import org.springframework.web.portlet.ModelAndView; + +/** + * Adapter to use the Portlet interface with the generic DispatcherPortlet. + * Calls the Portlet's render and processAction + * methods to handle a request. + * + *

This adapter is not activated by default; it needs to be defined as a + * bean in the DispatcherPortlet context. It will automatically apply to + * mapped handler beans that implement the Portlet interface then. + * + *

Note that Portlet instances defined as bean will not receive initialization + * and destruction callbacks, unless a special post-processor such as + * SimplePortletPostProcessor is defined in the DispatcherPortlet context. + * + *

Alternatively, consider wrapping a Portlet with Spring's + * PortletWrappingController. This is particularly appropriate for + * existing Portlet classes, allowing to specify Portlet initialization + * parameters, etc. + * + * @author John A. Lewis + * @since 2.0 + * @see javax.portlet.Portlet + * @see SimplePortletPostProcessor + * @see org.springframework.web.portlet.mvc.PortletWrappingController + */ +public class SimplePortletHandlerAdapter implements HandlerAdapter { + + public boolean supports(Object handler) { + return (handler instanceof Portlet); + } + + public void handleAction(ActionRequest request, ActionResponse response, Object handler) + throws Exception { + + ((Portlet) handler).processAction(request, response); + } + + public ModelAndView handleRender(RenderRequest request, RenderResponse response, Object handler) + throws Exception { + + ((Portlet) handler).render(request, response); + return null; + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimplePortletPostProcessor.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimplePortletPostProcessor.java new file mode 100644 index 00000000000..323ff7e50a4 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/SimplePortletPostProcessor.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2006 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.handler; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.Locale; +import java.util.ResourceBundle; + +import javax.portlet.Portlet; +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; +import javax.portlet.PortletException; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; +import org.springframework.web.portlet.context.PortletConfigAware; +import org.springframework.web.portlet.context.PortletContextAware; + +/** + * Bean post-processor that applies initialization and destruction callbacks + * to beans that implement the Portlet interface. + * + *

After initialization of the bean instance, the Portlet init + * method will be called with a PortletConfig that contains the bean name + * of the Portlet and the PortletContext that it is running in. + * + *

Before destruction of the bean instance, the Portlet destroy + * will be called. + * + *

Note that this post-processor does not support Portlet initialization + * parameters. Bean instances that implement the Portlet interface are + * supposed to be configured like any other Spring bean, that is, through + * constructor arguments or bean properties. + * + *

For reuse of a Portlet implementation in a plain Portlet container and as + * a bean in a Spring context, consider deriving from Spring's GenericPortletBean + * base class that applies Portlet initialization parameters as bean properties, + * supporting both initialization styles. + * + *

Alternatively, consider wrapping a Portlet with Spring's + * PortletWrappingController. This is particularly appropriate for + * existing Portlet classes, allowing to specify Portlet initialization + * parameters etc. + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see javax.portlet.Portlet + * @see javax.portlet.PortletConfig + * @see SimplePortletHandlerAdapter + * @see org.springframework.web.portlet.GenericPortletBean + * @see org.springframework.web.portlet.mvc.PortletWrappingController + */ +public class SimplePortletPostProcessor + implements DestructionAwareBeanPostProcessor, PortletContextAware, PortletConfigAware { + + private boolean useSharedPortletConfig = true; + + private PortletContext portletContext; + + private PortletConfig portletConfig; + + + /** + * Set whether to use the shared PortletConfig object passed in + * through setPortletConfig, if available. + *

Default is "true". Turn this setting to "false" to pass in + * a mock PortletConfig object with the bean name as portlet name, + * holding the current PortletContext. + * @see #setPortletConfig + */ + public void setUseSharedPortletConfig(boolean useSharedPortletConfig) { + this.useSharedPortletConfig = useSharedPortletConfig; + } + + public void setPortletContext(PortletContext portletContext) { + this.portletContext = portletContext; + } + + public void setPortletConfig(PortletConfig portletConfig) { + this.portletConfig = portletConfig; + } + + + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return bean; + } + + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof Portlet) { + PortletConfig config = this.portletConfig; + if (config == null || !this.useSharedPortletConfig) { + config = new DelegatingPortletConfig(beanName, this.portletContext, this.portletConfig); + } + try { + ((Portlet) bean).init(config); + } + catch (PortletException ex) { + throw new BeanInitializationException("Portlet.init threw exception", ex); + } + } + return bean; + } + + public void postProcessBeforeDestruction(Object bean, String beanName) throws BeansException { + if (bean instanceof Portlet) { + ((Portlet) bean).destroy(); + } + } + + + /** + * Internal implementation of the PortletConfig interface, to be passed + * to the wrapped servlet. + */ + private static class DelegatingPortletConfig implements PortletConfig { + + private final String portletName; + + private final PortletContext portletContext; + + private final PortletConfig portletConfig; + + public DelegatingPortletConfig(String portletName, PortletContext portletContext, PortletConfig portletConfig) { + this.portletName = portletName; + this.portletContext = portletContext; + this.portletConfig = portletConfig; + } + + public String getPortletName() { + return portletName; + } + + public PortletContext getPortletContext() { + return portletContext; + } + + public String getInitParameter(String paramName) { + return null; + } + + public Enumeration getInitParameterNames() { + return Collections.enumeration(Collections.EMPTY_SET); + } + + public ResourceBundle getResourceBundle(Locale locale) { + return portletConfig == null ? null : portletConfig.getResourceBundle(locale); + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/UserRoleAuthorizationInterceptor.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/UserRoleAuthorizationInterceptor.java new file mode 100644 index 00000000000..dc876fc1edb --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/UserRoleAuthorizationInterceptor.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2006 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.handler; + +import java.io.IOException; + +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.PortletSecurityException; + +/** + * Interceptor that checks the authorization of the current user via the + * user's roles, as evaluated by PortletRequest's isUserInRole method. + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + * @see javax.portlet.PortletRequest#isUserInRole + */ +public class UserRoleAuthorizationInterceptor extends HandlerInterceptorAdapter { + + private String[] authorizedRoles; + + + /** + * Set the roles that this interceptor should treat as authorized. + * @param authorizedRoles array of role names + */ + public final void setAuthorizedRoles(String[] authorizedRoles) { + this.authorizedRoles = authorizedRoles; + } + + + public final boolean preHandle(PortletRequest request, PortletResponse response, Object handler) + throws PortletException, IOException { + + if (this.authorizedRoles != null) { + for (int i = 0; i < this.authorizedRoles.length; i++) { + if (request.isUserInRole(this.authorizedRoles[i])) { + return true; + } + } + } + handleNotAuthorized(request, response, handler); + return false; + } + + /** + * Handle a request that is not authorized according to this interceptor. + * Default implementation throws a new PortletSecurityException. + *

This method can be overridden to write a custom message, forward or + * redirect to some error page or login page, or throw a PortletException. + * @param request current portlet request + * @param response current portlet response + * @param handler chosen handler to execute, for type and/or instance evaluation + * @throws javax.portlet.PortletException if there is an internal error + * @throws java.io.IOException in case of an I/O error when writing the response + */ + protected void handleNotAuthorized(PortletRequest request, PortletResponse response, Object handler) + throws PortletException, IOException { + + throw new PortletSecurityException("Request not authorized"); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/WebRequestHandlerInterceptorAdapter.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/WebRequestHandlerInterceptorAdapter.java new file mode 100644 index 00000000000..9389feabe44 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/WebRequestHandlerInterceptorAdapter.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2006 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.handler; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.util.Assert; +import org.springframework.web.context.request.WebRequestInterceptor; +import org.springframework.web.portlet.HandlerInterceptor; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.web.portlet.context.PortletWebRequest; + +/** + * Adapter that implements the Portlet HandlerInterceptor interface + * and wraps an underlying WebRequestInterceptor. + * + *

NOTE: The WebRequestInterceptor is by default only applied to the Portlet + * render phase, which is dealing with preparing and rendering a Portlet view. + * The Portlet action phase will only be intercepted with WebRequestInterceptor calls + * if the renderPhaseOnly flag is explicitly set to false. + * In general, it is recommended to use the Portlet-specific HandlerInterceptor + * mechanism for differentiating between action and render interception. + * + * @author Juergen Hoeller + * @since 2.0 + * @see org.springframework.web.context.request.WebRequestInterceptor + * @see org.springframework.web.portlet.HandlerInterceptor + */ +public class WebRequestHandlerInterceptorAdapter implements HandlerInterceptor { + + private final WebRequestInterceptor requestInterceptor; + + private final boolean renderPhaseOnly; + + + /** + * Create a new WebRequestHandlerInterceptorAdapter for the given WebRequestInterceptor, + * applying to the render phase only. + * @param requestInterceptor the WebRequestInterceptor to wrap + */ + public WebRequestHandlerInterceptorAdapter(WebRequestInterceptor requestInterceptor) { + this(requestInterceptor, true); + } + + /** + * Create a new WebRequestHandlerInterceptorAdapter for the given WebRequestInterceptor. + * @param requestInterceptor the WebRequestInterceptor to wrap + * @param renderPhaseOnly whether to apply to the render phase only (true) + * or to the action phase as well (false) + */ + public WebRequestHandlerInterceptorAdapter(WebRequestInterceptor requestInterceptor, boolean renderPhaseOnly) { + Assert.notNull(requestInterceptor, "WebRequestInterceptor must not be null"); + this.requestInterceptor = requestInterceptor; + this.renderPhaseOnly = renderPhaseOnly; + } + + + public boolean preHandleAction(ActionRequest request, ActionResponse response, Object handler) throws Exception { + if (!this.renderPhaseOnly) { + this.requestInterceptor.preHandle(new PortletWebRequest(request)); + } + return true; + } + + public void afterActionCompletion( + ActionRequest request, ActionResponse response, Object handler, Exception ex) throws Exception { + + if (!this.renderPhaseOnly) { + this.requestInterceptor.afterCompletion(new PortletWebRequest(request), ex); + } + } + + public boolean preHandleRender(RenderRequest request, RenderResponse response, Object handler) throws Exception { + this.requestInterceptor.preHandle(new PortletWebRequest(request)); + return true; + } + + public void postHandleRender( + RenderRequest request, RenderResponse response, Object handler, ModelAndView modelAndView) throws Exception { + + this.requestInterceptor.postHandle(new PortletWebRequest(request), + (modelAndView != null ? modelAndView.getModelMap() : null)); + } + + public void afterRenderCompletion( + RenderRequest request, RenderResponse response, Object handler, Exception ex) throws Exception { + + this.requestInterceptor.afterCompletion(new PortletWebRequest(request), ex); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/package.html b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/package.html new file mode 100644 index 00000000000..56ea63f53ed --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/handler/package.html @@ -0,0 +1,8 @@ + + + +Provides standard HandlerMapping implementations, +including abstract base classes for custom implementations. + + + diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/CommonsPortletMultipartResolver.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/CommonsPortletMultipartResolver.java new file mode 100644 index 00000000000..97faf039ba5 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/CommonsPortletMultipartResolver.java @@ -0,0 +1,200 @@ +/* + * 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.multipart; + +import java.util.List; + +import javax.portlet.ActionRequest; +import javax.portlet.PortletContext; + +import org.apache.commons.fileupload.FileItemFactory; +import org.apache.commons.fileupload.FileUpload; +import org.apache.commons.fileupload.FileUploadBase; +import org.apache.commons.fileupload.FileUploadException; +import org.apache.commons.fileupload.portlet.PortletFileUpload; +import org.apache.commons.fileupload.portlet.PortletRequestContext; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.web.multipart.MaxUploadSizeExceededException; +import org.springframework.web.multipart.MultipartException; +import org.springframework.web.multipart.commons.CommonsFileUploadSupport; +import org.springframework.web.portlet.context.PortletContextAware; +import org.springframework.web.portlet.util.PortletUtils; + +/** + * {@link PortletMultipartResolver} implementation for + * Jakarta Commons FileUpload + * 1.1 or above. Commons FileUpload 1.2 or above is recommended. + * + *

Provides "maxUploadSize", "maxInMemorySize" and "defaultEncoding" settings as + * bean properties (inherited from {@link CommonsFileUploadSupport}). See corresponding + * PortletFileUpload / DiskFileItemFactory properties ("sizeMax", "sizeThreshold", + * "headerEncoding") for details in terms of defaults and accepted values. + * + *

Saves temporary files to the portlet container's temporary directory. + * Needs to be initialized either by an application context or + * via the constructor that takes a PortletContext (for standalone usage). + * + * @author Juergen Hoeller + * @since 2.0 + * @see #CommonsPortletMultipartResolver(javax.portlet.PortletContext) + * @see #setResolveLazily + * @see org.springframework.web.multipart.commons.CommonsMultipartResolver + * @see org.apache.commons.fileupload.portlet.PortletFileUpload + * @see org.apache.commons.fileupload.disk.DiskFileItemFactory + */ +public class CommonsPortletMultipartResolver extends CommonsFileUploadSupport + implements PortletMultipartResolver, PortletContextAware { + + private final boolean commonsFileUpload12Present = + ClassUtils.hasMethod(PortletFileUpload.class, "isMultipartContent", new Class[] {ActionRequest.class}); + + private boolean resolveLazily = false; + + + /** + * Constructor for use as bean. Determines the portlet container's + * temporary directory via the PortletContext passed in as through the + * PortletContextAware interface (typically by an ApplicationContext). + * @see #setPortletContext + * @see org.springframework.web.portlet.context.PortletContextAware + */ + public CommonsPortletMultipartResolver() { + super(); + } + + /** + * Constructor for standalone usage. Determines the portlet container's + * temporary directory via the given PortletContext. + * @param portletContext the PortletContext to use + */ + public CommonsPortletMultipartResolver(PortletContext portletContext) { + this(); + setPortletContext(portletContext); + } + + + /** + * Set whether to resolve the multipart request lazily at the time of + * file or parameter access. + *

Default is "false", resolving the multipart elements immediately, throwing + * corresponding exceptions at the time of the {@link #resolveMultipart} call. + * Switch this to "true" for lazy multipart parsing, throwing parse exceptions + * once the application attempts to obtain multipart files or parameters. + */ + public void setResolveLazily(boolean resolveLazily) { + this.resolveLazily = resolveLazily; + } + + /** + * Initialize the underlying org.apache.commons.fileupload.portlet.PortletFileUpload + * instance. Can be overridden to use a custom subclass, e.g. for testing purposes. + * @return the new PortletFileUpload instance + */ + protected FileUpload newFileUpload(FileItemFactory fileItemFactory) { + return new PortletFileUpload(fileItemFactory); + } + + public void setPortletContext(PortletContext portletContext) { + if (!isUploadTempDirSpecified()) { + getFileItemFactory().setRepository(PortletUtils.getTempDir(portletContext)); + } + } + + + public boolean isMultipart(ActionRequest request) { + if (request == null) { + return false; + } + else if (commonsFileUpload12Present) { + return PortletFileUpload.isMultipartContent(request); + } + else { + return PortletFileUpload.isMultipartContent(new PortletRequestContext(request)); + } + } + + public MultipartActionRequest resolveMultipart(final ActionRequest request) throws MultipartException { + Assert.notNull(request, "Request must not be null"); + if (this.resolveLazily) { + return new DefaultMultipartActionRequest(request) { + protected void initializeMultipart() { + MultipartParsingResult parsingResult = parseRequest(request); + setMultipartFiles(parsingResult.getMultipartFiles()); + setMultipartParameters(parsingResult.getMultipartParameters()); + } + }; + } + else { + MultipartParsingResult parsingResult = parseRequest(request); + return new DefaultMultipartActionRequest( + request, parsingResult.getMultipartFiles(), parsingResult.getMultipartParameters()); + } + } + + /** + * Parse the given portlet request, resolving its multipart elements. + * @param request the request to parse + * @return the parsing result + * @throws MultipartException if multipart resolution failed. + */ + protected MultipartParsingResult parseRequest(ActionRequest request) throws MultipartException { + String encoding = determineEncoding(request); + FileUpload fileUpload = prepareFileUpload(encoding); + try { + List fileItems = ((PortletFileUpload) fileUpload).parseRequest(request); + return parseFileItems(fileItems, encoding); + } + catch (FileUploadBase.SizeLimitExceededException ex) { + throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex); + } + catch (FileUploadException ex) { + throw new MultipartException("Could not parse multipart portlet request", ex); + } + } + + /** + * Determine the encoding for the given request. + * Can be overridden in subclasses. + *

The default implementation checks the request encoding, + * falling back to the default encoding specified for this resolver. + * @param request current portlet request + * @return the encoding for the request (never null) + * @see javax.portlet.ActionRequest#getCharacterEncoding + * @see #setDefaultEncoding + */ + protected String determineEncoding(ActionRequest request) { + String encoding = request.getCharacterEncoding(); + if (encoding == null) { + encoding = getDefaultEncoding(); + } + return encoding; + } + + public void cleanupMultipart(MultipartActionRequest request) { + if (request != null) { + try { + cleanupFileItems(request.getFileMap().values()); + } + catch (Throwable ex) { + logger.warn("Failed to perform multipart cleanup for portlet request", ex); + } + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/DefaultMultipartActionRequest.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/DefaultMultipartActionRequest.java new file mode 100644 index 00000000000..a9bc27105b3 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/DefaultMultipartActionRequest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2007 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.multipart; + +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.portlet.ActionRequest; + +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.portlet.util.ActionRequestWrapper; + +/** + * Default implementation of the {@link MultipartActionRequest} interface. + * Provides management of pre-generated parameter values. + * + * @author Juergen Hoeller + * @since 2.0 + * @see PortletMultipartResolver + */ +public class DefaultMultipartActionRequest extends ActionRequestWrapper implements MultipartActionRequest { + + private Map multipartFiles; + + private Map multipartParameters; + + + /** + * Wrap the given Portlet ActionRequest in a MultipartActionRequest. + * @param request the request to wrap + * @param multipartFiles a map of the multipart files + * @param multipartParameters a map of the parameters to expose, + * with Strings as keys and String arrays as values + */ + public DefaultMultipartActionRequest(ActionRequest request, Map multipartFiles, Map multipartParameters) { + super(request); + setMultipartFiles(multipartFiles); + setMultipartParameters(multipartParameters); + } + + /** + * Wrap the given Portlet ActionRequest in a MultipartActionRequest. + * @param request the request to wrap + */ + protected DefaultMultipartActionRequest(ActionRequest request) { + super(request); + } + + + public Iterator getFileNames() { + return getMultipartFiles().keySet().iterator(); + } + + public MultipartFile getFile(String name) { + return (MultipartFile) getMultipartFiles().get(name); + } + + public Map getFileMap() { + return getMultipartFiles(); + } + + + public Enumeration getParameterNames() { + Set paramNames = new HashSet(); + Enumeration paramEnum = super.getParameterNames(); + while (paramEnum.hasMoreElements()) { + paramNames.add(paramEnum.nextElement()); + } + paramNames.addAll(getMultipartParameters().keySet()); + return Collections.enumeration(paramNames); + } + + public String getParameter(String name) { + String[] values = (String[]) getMultipartParameters().get(name); + if (values != null) { + return (values.length > 0 ? values[0] : null); + } + return super.getParameter(name); + } + + public String[] getParameterValues(String name) { + String[] values = (String[]) getMultipartParameters().get(name); + if (values != null) { + return values; + } + return super.getParameterValues(name); + } + + public Map getParameterMap() { + Map paramMap = new HashMap(); + paramMap.putAll(super.getParameterMap()); + paramMap.putAll(getMultipartParameters()); + return paramMap; + } + + + /** + * Set a Map with parameter names as keys and MultipartFile objects as values. + * To be invoked by subclasses on initialization. + */ + protected final void setMultipartFiles(Map multipartFiles) { + this.multipartFiles = Collections.unmodifiableMap(multipartFiles); + } + + /** + * Obtain the MultipartFile Map for retrieval, + * lazily initializing it if necessary. + * @see #initializeMultipart() + */ + protected Map getMultipartFiles() { + if (this.multipartFiles == null) { + initializeMultipart(); + } + return this.multipartFiles; + } + + /** + * Set a Map with parameter names as keys and String array objects as values. + * To be invoked by subclasses on initialization. + */ + protected final void setMultipartParameters(Map multipartParameters) { + this.multipartParameters = multipartParameters; + } + + /** + * Obtain the multipart parameter Map for retrieval, + * lazily initializing it if necessary. + * @see #initializeMultipart() + */ + protected Map getMultipartParameters() { + if (this.multipartParameters == null) { + initializeMultipart(); + } + return this.multipartParameters; + } + + /** + * Lazily initialize the multipart request, if possible. + * Only called if not already eagerly initialized. + */ + protected void initializeMultipart() { + throw new IllegalStateException("Multipart request not initialized"); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/MultipartActionRequest.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/MultipartActionRequest.java new file mode 100644 index 00000000000..4e44f015fb9 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/MultipartActionRequest.java @@ -0,0 +1,41 @@ +/* + * 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.multipart; + +import javax.portlet.ActionRequest; + +import org.springframework.web.multipart.MultipartRequest; + +/** + * Interface which provides additional methods for dealing with multipart + * content within a portlet request, allowing to access uploaded files. + * Implementations also need to override the standard ActionRequest + * methods for parameter access, making multipart parameters available. + * + *

A concrete implementation is {@link DefaultMultipartActionRequest}. + * + * @author Juergen Hoeller + * @since 2.0 + * @see PortletMultipartResolver + * @see org.springframework.web.multipart.MultipartFile + * @see javax.portlet.ActionRequest#getParameter + * @see javax.portlet.ActionRequest#getParameterNames + * @see javax.portlet.ActionRequest#getParameterMap + */ +public interface MultipartActionRequest extends ActionRequest, MultipartRequest { + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/PortletMultipartResolver.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/PortletMultipartResolver.java new file mode 100644 index 00000000000..0370c8b77b4 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/PortletMultipartResolver.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2007 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.multipart; + +import javax.portlet.ActionRequest; + +import org.springframework.web.multipart.MultipartException; + +/** + * Portlet version of Spring's multipart resolution strategy for file uploads + * as defined in RFC 1867. + * + *

Implementations are typically usable both within any application context + * and standalone. + * + *

There is one concrete implementation included in Spring: + *

+ * + *

There is no default resolver implementation used for Spring + * {@link org.springframework.web.portlet.DispatcherPortlet DispatcherPortlets}, + * as an application might choose to parse its multipart requests itself. To + * define an implementation, create a bean with the id + * {@link org.springframework.web.portlet.DispatcherPortlet#MULTIPART_RESOLVER_BEAN_NAME "portletMultipartResolver"} + * in a DispatcherPortlet's application context. Such a resolver + * gets applied to all requests handled by that DispatcherPortlet. + * + *

If a DispatcherPortlet detects a multipart request, it will + * resolve it via the configured + * {@link org.springframework.web.multipart.MultipartResolver} and pass on a + * wrapped Portlet {@link ActionRequest}. + * {@link org.springframework.web.portlet.mvc.Controller Controllers} can then + * cast their given request to the {@link MultipartActionRequest} interface, + * being able to access MultipartFiles. Note that this cast is + * only supported in case of an actual multipart request. + * + *

 public void handleActionRequest(ActionRequest request, ActionResponse response) {
+ *   MultipartActionRequest multipartRequest = (MultipartActionRequest) request;
+ *   MultipartFile multipartFile = multipartRequest.getFile("image");
+ *   ...
+ * }
+ * + * Instead of direct access, command or form controllers can register a + * {@link org.springframework.web.multipart.support.ByteArrayMultipartFileEditor} + * or {@link org.springframework.web.multipart.support.StringMultipartFileEditor} + * with their data binder, to automatically apply multipart content to command + * bean properties. + * + *

Note: There is hardly ever a need to access the + * MultipartResolver itself from application code. It will simply + * do its work behind the scenes, making MultipartActionRequests + * available to controllers. + * + * @author Juergen Hoeller + * @since 2.0 + * @see MultipartActionRequest + * @see org.springframework.web.multipart.MultipartFile + * @see CommonsPortletMultipartResolver + * @see org.springframework.web.multipart.support.ByteArrayMultipartFileEditor + * @see org.springframework.web.multipart.support.StringMultipartFileEditor + * @see org.springframework.web.portlet.DispatcherPortlet + */ +public interface PortletMultipartResolver { + + /** + * Determine if the given request contains multipart content. + *

Will typically check for content type + * "multipart/form-data", but the actually accepted requests + * might depend on the capabilities of the resolver implementation. + * @param request the portlet request to be evaluated + * @return whether the request contains multipart content + */ + boolean isMultipart(ActionRequest request); + + /** + * Parse the given portlet request into multipart files and parameters, + * and wrap the request inside a MultipartActionRequest object + * that provides access to file descriptors and makes contained + * parameters accessible via the standard PortletRequest methods. + * @param request the portlet request to wrap (must be of a multipart content type) + * @return the wrapped portlet request + * @throws org.springframework.web.multipart.MultipartException if the portlet request + * is not multipart, or if implementation-specific problems are encountered + * (such as exceeding file size limits) + * @see org.springframework.web.portlet.multipart.MultipartActionRequest#getFile + * @see org.springframework.web.portlet.multipart.MultipartActionRequest#getFileNames + * @see org.springframework.web.portlet.multipart.MultipartActionRequest#getFileMap + * @see javax.portlet.ActionRequest#getParameter + * @see javax.portlet.ActionRequest#getParameterNames + * @see javax.portlet.ActionRequest#getParameterMap + */ + MultipartActionRequest resolveMultipart(ActionRequest request) throws MultipartException; + + /** + * Cleanup any resources used for the multipart handling, + * such as storage for any uploaded file(s). + * @param request the request to cleanup resources for + */ + void cleanupMultipart(MultipartActionRequest request); + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/package.html b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/package.html new file mode 100644 index 00000000000..865819867cc --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/multipart/package.html @@ -0,0 +1,10 @@ + + + +Multipart resolution framework for handling file uploads. +Provides a PortletMultipartResolver strategy interface, +and a generic extension of the ActionRequest interface +for accessing multipart files in web application code. + + + diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractCommandController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractCommandController.java new file mode 100644 index 00000000000..d48cde1c99c --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractCommandController.java @@ -0,0 +1,209 @@ +/* + * 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; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.validation.BindException; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.web.portlet.bind.PortletRequestDataBinder; +import org.springframework.web.portlet.util.PortletUtils; + +/** + *

Abstract base class for custom command controllers. Autopopulates a + * command bean from the request. For command validation, a validator + * (property inherited from BaseCommandController) can be used.

+ * + *

This command controller should preferrable not be used to handle form + * submission, because functionality for forms is more offered in more + * detail by the {@link org.springframework.web.portlet.mvc.AbstractFormController + * AbstractFormController} and its corresponding implementations.

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * none (so only those available in superclass).

+ * + *

Workflow + * (and that defined by superclass):
+ * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + * @see #setCommandClass + * @see #setCommandName + * @see #setValidator + */ +public abstract class AbstractCommandController extends BaseCommandController { + + /** + * This render parameter is used to indicate forward to the render phase + * that a valid command (and errors) object is in the session. + */ + private static final String COMMAND_IN_SESSION_PARAMETER = "command-in-session"; + + private static final String TRUE = Boolean.TRUE.toString(); + + + /** + * Create a new AbstractCommandController. + */ + public AbstractCommandController() { + } + + /** + * Create a new AbstractCommandController. + * @param commandClass class of the command bean + */ + public AbstractCommandController(Class commandClass) { + setCommandClass(commandClass); + } + + /** + * Create a new AbstractCommandController. + * @param commandClass class of the command bean + * @param commandName name of the command bean + */ + public AbstractCommandController(Class commandClass, String commandName) { + setCommandClass(commandClass); + setCommandName(commandName); + } + + + protected final void handleActionRequestInternal(ActionRequest request, ActionResponse response) + throws Exception { + + // Create the command object. + Object command = getCommand(request); + + // Compute the errors object. + PortletRequestDataBinder binder = bindAndValidate(request, command); + BindException errors = new BindException(binder.getBindingResult()); + + // Actually handle the action. + handleAction(request, response, command, errors); + + // Pass the command and errors forward to the render phase. + setRenderCommandAndErrors(request, command, errors); + setCommandInSession(response); + } + + protected final ModelAndView handleRenderRequestInternal( + RenderRequest request, RenderResponse response) throws Exception { + + Object command = null; + BindException errors = null; + + // Get the command and errors objects from the session, if they exist. + if (isCommandInSession(request)) { + logger.debug("Render phase obtaining command and errors objects from session"); + command = getRenderCommand(request); + errors = getRenderErrors(request); + } + else { + logger.debug("Render phase creating new command and errors objects"); + command = getCommand(request); + PortletRequestDataBinder binder = bindAndValidate(request, command); + errors = new BindException(binder.getBindingResult()); + } + + return handleRender(request, response, command, errors); + } + + + /** + * Template method for request handling, providing a populated and validated instance + * of the command class, and an Errors object containing binding and validation errors. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param request current action request + * @param response current action response + * @param command the populated command object + * @param errors validation errors holder + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected abstract void handleAction( + ActionRequest request, ActionResponse response, Object command, BindException errors) + throws Exception; + + /** + * Template method for render request handling, providing a populated and validated instance + * of the command class, and an Errors object containing binding and validation errors. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param request current render request + * @param response current render response + * @param command the populated command object + * @param errors validation errors holder + * @return a ModelAndView to render, or null if handled directly + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected abstract ModelAndView handleRender( + RenderRequest request, RenderResponse response, Object command, BindException errors) + throws Exception; + + + /** + * Return the name of the render parameter that indicates there + * is a valid command (and errors) object in the session. + * @return the name of the render parameter + * @see javax.portlet.RenderRequest#getParameter + */ + protected String getCommandInSessionParameterName() { + return COMMAND_IN_SESSION_PARAMETER; + } + + /** + * Set the action response parameter that indicates there is a + * command (and errors) object in the session for the render phase. + * @param response the current action response + * @see #getCommandInSessionParameterName + * @see #isCommandInSession + */ + protected final void setCommandInSession(ActionResponse response) { + if (logger.isDebugEnabled()) { + logger.debug("Setting render parameter [" + getCommandInSessionParameterName() + + "] to indicate a valid command (and errors) object are in the session"); + } + try { + response.setRenderParameter(getCommandInSessionParameterName(), TRUE); + } + catch (IllegalStateException ex) { + // Ignore in case sendRedirect was already set. + } + } + + /** + * Determine if there is a valid command (and errors) object in the + * session for this render request. + * @param request current render request + * @return if there is a valid command object in the session + * @see #getCommandInSessionParameterName + * @see #setCommandInSession + */ + protected boolean isCommandInSession(RenderRequest request) { + return (TRUE.equals(request.getParameter(getCommandInSessionParameterName())) && + PortletUtils.getSessionAttribute(request, getRenderCommandSessionAttributeName()) != null); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractController.java new file mode 100644 index 00000000000..fe7a6eea081 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractController.java @@ -0,0 +1,251 @@ +/* + * 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; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletException; +import javax.portlet.PortletSession; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; +import javax.portlet.WindowState; + +import org.springframework.web.portlet.ModelAndView; +import org.springframework.web.portlet.handler.PortletContentGenerator; +import org.springframework.web.portlet.util.PortletUtils; + +/** + * Convenient superclass for controller implementations, using the Template + * Method design pattern. + * + *

As stated in the {@link Controller Controller} + * interface, a lot of functionality is already provided by certain abstract + * base controllers. The AbstractController is one of the most important + * abstract base controller providing basic features such controlling if a + * session is required and render caching. + * + *

Workflow + * (and that defined by interface):
+ *

    + *
  1. If this is an action request, {@link #handleActionRequest handleActionRequest} + * will be called by the DispatcherPortlet once to perform the action defined by this + * controller.
  2. + *
  3. If a session is required, try to get it (PortletException if not found).
  4. + *
  5. Call method {@link #handleActionRequestInternal handleActionRequestInternal}, + * (optionally synchronizing around the call on the PortletSession), + * which should be overridden by extending classes to provide actual functionality to + * perform the desired action of the controller. This will be executed only once.
  6. + *
  7. For a straight render request, or the render phase of an action request (assuming the + * same controller is called for the render phase -- see tip below), + * {@link #handleRenderRequest handleRenderRequest} will be called by the DispatcherPortlet + * repeatedly to render the display defined by this controller.
  8. + *
  9. If a session is required, try to get it (PortletException if none found).
  10. + *
  11. It will control caching as defined by the cacheSeconds property.
  12. + *
  13. Call method {@link #handleRenderRequestInternal handleRenderRequestInternal}, + * (optionally synchronizing around the call on the PortletSession), + * which should be overridden by extending classes to provide actual functionality to + * return {@link org.springframework.web.portlet.ModelAndView ModelAndView} objects. + * This will be executed repeatedly as the portal updates the current displayed page.
  14. + *
+ * + *

Exposed configuration properties + * (and those defined by interface):
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
name + * defaultdescription
requireSessionfalsewhether a session should be required for requests to be able to + * be handled by this controller. This ensures, derived controller + * can - without fear of Nullpointers - call request.getSession() to + * retrieve a session. If no session can be found while processing + * the request, a PortletException will be thrown
synchronizeOnSessionfalsewhether the calls to handleRenderRequestInternal and + * handleRenderRequestInternal should be + * synchronized around the PortletSession, to serialize invocations + * from the same client. No effect if there is no PortletSession. + *
cacheSeconds-1indicates the amount of seconds to specify caching is allowed in + * the render response generatedby this request. 0 (zero) will indicate + * no caching is allowed at all, -1 (the default) will not override the + * portlet configuration and any positive number will cause the render + * response to declare the amount indicated as seconds to cache the content
renderWhenMinimizedfalsewhether should be rendered when the portlet is in a minimized state -- + * will return null for the ModelandView when the portlet is minimized + * and this is false
+ * + *

TIP: The controller mapping will be run twice by the PortletDispatcher for + * action requests -- once for the action phase and again for the render phase. You can + * reach the render phase of a different controller by simply changing the values for the + * criteria your mapping is using, such as portlet mode or a request parameter, during the + * action phase of your controller. This is very handy since redirects within the portlet + * are apparently impossible. Before doing this, it is usually wise to call + * clearAllRenderParameters and then explicitly set all the parameters that + * you want the new controller to see. This avoids unexpected parameters from being passed + * to the render phase of the second controller, such as the parameter indicating a form + * submit ocurred in an AbstractFormController. + * + *

Thanks to Rainer Schmitz and Nick Lothian for their suggestions! + * + * @author John A. Lewis + * @author Juergen Hoeller + * @since 2.0 + */ +public abstract class AbstractController extends PortletContentGenerator implements Controller { + + private boolean synchronizeOnSession = false; + + private boolean renderWhenMinimized = false; + + + /** + * Set if controller execution should be synchronized on the session, + * to serialize parallel invocations from the same client. + *

More specifically, the execution of the handleActionRequestInternal + * 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. + *

The session mutex is guaranteed to be the same object during + * the entire lifetime of the session, available under the key defined + * by the SESSION_MUTEX_ATTRIBUTE constant. It serves as a + * safe reference to synchronize on for locking on the current session. + *

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 #handleActionRequestInternal + * @see org.springframework.web.util.HttpSessionMutexListener + * @see org.springframework.web.portlet.util.PortletUtils#getSessionMutex(javax.portlet.PortletSession) + */ + public final void setSynchronizeOnSession(boolean synchronizeOnSession) { + this.synchronizeOnSession = synchronizeOnSession; + } + + /** + * Return whether controller execution should be synchronized on the session. + */ + public final boolean isSynchronizeOnSession() { + return this.synchronizeOnSession; + } + + /** + * Set if the controller should render an view when the portlet is in + * a minimized window. The default is false. + * @see javax.portlet.RenderRequest#getWindowState + * @see javax.portlet.WindowState#MINIMIZED + */ + public final void setRenderWhenMinimized(boolean renderWhenMinimized) { + this.renderWhenMinimized = renderWhenMinimized; + } + + /** + * Return whether controller will render when portlet is minimized. + */ + public final boolean isRenderWhenMinimized() { + return this.renderWhenMinimized; + } + + + public void handleActionRequest(ActionRequest request, ActionResponse response) throws Exception { + // Delegate to PortletContentGenerator for checking and preparing. + check(request, response); + + // Execute in synchronized block if required. + if (this.synchronizeOnSession) { + PortletSession session = request.getPortletSession(false); + if (session != null) { + synchronized (session) { + handleActionRequestInternal(request, response); + return; + } + } + } + + handleActionRequestInternal(request, response); + } + + public ModelAndView handleRenderRequest(RenderRequest request, RenderResponse response) throws Exception { + // If the portlet is minimized and we don't want to render then return null. + if (WindowState.MINIMIZED.equals(request.getWindowState()) && !this.renderWhenMinimized) { + return null; + } + + // Delegate to PortletContentGenerator for checking and preparing. + checkAndPrepare(request, response); + + // Execute in synchronized block if required. + if (this.synchronizeOnSession) { + PortletSession session = request.getPortletSession(false); + if (session != null) { + Object mutex = PortletUtils.getSessionMutex(session); + synchronized (mutex) { + return handleRenderRequestInternal(request, response); + } + } + } + + return handleRenderRequestInternal(request, response); + } + + + /** + * Subclasses are meant to override this method if the controller + * is expected to handle action requests. The contract is the same as + * for handleActionRequest. + *

The default implementation throws a PortletException. + * @see #handleActionRequest + * @see #handleRenderRequestInternal + */ + protected void handleActionRequestInternal(ActionRequest request, ActionResponse response) + throws Exception { + + throw new PortletException("[" + getClass().getName() + "] does not handle action requests"); + } + + /** + * Subclasses are meant to override this method if the controller + * is expected to handle render requests. The contract is the same as + * for handleRenderRequest. + *

The default implementation throws a PortletException. + * @see #handleRenderRequest + * @see #handleActionRequestInternal + */ + protected ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response) + throws Exception { + + throw new PortletException("[" + getClass().getName() + "] does not handle render requests"); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractFormController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractFormController.java new file mode 100644 index 00000000000..a1ec01b9f23 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractFormController.java @@ -0,0 +1,969 @@ +/* + * 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; + +import java.util.Arrays; +import java.util.Map; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.web.portlet.bind.PortletRequestDataBinder; +import org.springframework.web.portlet.handler.PortletSessionRequiredException; + +/** + *

Form controller that auto-populates a form bean from the request. + * This, either using a new bean instance per request, or using the same bean + * when the sessionForm property has been set to + * true.

+ * + *

This class is the base class for both framework subclasses such as + * {@link SimpleFormController} and {@link AbstractWizardFormController} + * and custom form controllers that you may provide yourself.

+ * + *

A form-input view and an after-submission view have to be provided + * programmatically. To provide those views using configuration properties, + * use the {@link SimpleFormController}.

+ * + *

Subclasses need to override showForm to prepare the form view, + * processFormSubmission to handle submit requests, and + * renderFormSubmission to display the results of the submit. + * For the latter two methods, binding errors like type mismatches will be + * reported via the given "errors" holder. For additional custom form validation, + * a validator (property inherited from BaseCommandController) can be used, + * reporting via the same "errors" instance.

+ * + *

Comparing this Controller to the Struts notion of the Action + * shows us that with Spring, you can use any ordinary JavaBeans or database- + * backed JavaBeans without having to implement a framework-specific class + * (like Struts' ActionForm). More complex properties of JavaBeans + * (Dates, Locales, but also your own application-specific or compound types) + * can be represented and submitted to the controller, by using the notion of + * a java.beans.PropertyEditors. For more information on that + * subject, see the workflow of this controller and the explanation of the + * {@link BaseCommandController BaseCommandController}.

+ * + *

This controller is different from it's servlet counterpart in that it must take + * into account the two phases of a portlet request: the action phase and the render + * phase. See the JSR-168 spec for more details on these two phases. + * Be especially aware that the action phase is called only once, but that the + * render phase will be called repeatedly by the portal; it does this every time + * the page containing the portlet is updated, even if the activity is in some other + * portlet. (This is not quite true, the portal can also be told to cache the results of + * the render for a period of time, but assume it is true for programming purposes.)

+ * + *

Workflow + * (and that defined by superclass):
+ *

    + *
  1. The controller receives a request for a new form (typically a + * Render Request only). The render phase will proceed to display + * the form as follows.
  2. + *
  3. Call to {@link #formBackingObject formBackingObject()} which by + * default, returns an instance of the commandClass that has been + * configured (see the properties the superclass exposes), but can also be + * overridden to e.g. retrieve an object from the database (that needs to + * be modified using the form).
  4. + *
  5. Call to {@link #initBinder initBinder()} which allows you to + * register custom editors for certain fields (often properties of non- + * primitive or non-String types) of the command class. This will render + * appropriate Strings for those property values, e.g. locale-specific + * date strings.
  6. + *
  7. The {@link PortletRequestDataBinder PortletRequestDataBinder} + * gets applied to populate the new form object with initial request parameters and the + * {@link #onBindOnNewForm(RenderRequest, Object, BindException)} callback method is invoked. + * (only if bindOnNewForm is set to true) + * Make sure that the initial parameters do not include the parameter that indicates a + * form submission has occurred.
  8. + *
  9. Call to {@link #showForm(RenderRequest, RenderResponse, + * BindException) showForm} to return a View that should be rendered + * (typically the view that renders the form). This method has to be + * implemented in subclasses.
  10. + *
  11. The showForm() implementation will call {@link #referenceData referenceData}, + * which you can implement to provide any relevant reference data you might need + * when editing a form (e.g. a List of Locale objects you're going to let the + * user select one from).
  12. + *
  13. Model gets exposed and view gets rendered, to let the user fill in + * the form.
  14. + *
  15. The controller receives a form submission (typically an Action + * Request). To use a different way of detecting a form submission, + * override the {@link #isFormSubmission isFormSubmission} method. + * The action phase will proceed to process the form submission as follows.
  16. + *
  17. If sessionForm is not set, {@link #formBackingObject + * formBackingObject} is called to retrieve a form object. Otherwise, + * the controller tries to find the command object which is already bound + * in the session. If it cannot find the object, the action phase does a + * call to {@link #handleInvalidSubmit handleInvalidSubmit} which - by default - + * tries to create a new form object and resubmit the form. It then sets + * a render parameter that will indicate to the render phase that this was + * an invalid submit.
  18. + *
  19. Still in the action phase of a valid submit, the {@link + * PortletRequestDataBinder PortletRequestDataBinder} gets applied to populate + * the form object with current request parameters.
  20. + *
  21. Call to {@link #onBind onBind(PortletRequest, Object, Errors)} + * which allows you to do custom processing after binding but before + * validation (e.g. to manually bind request parameters to bean + * properties, to be seen by the Validator).
  22. + *
  23. If validateOnBinding is set, a registered Validator + * will be invoked. The Validator will check the form object properties, + * and register corresponding errors via the given {@link Errors Errors} + * object.
  24. + *
  25. Call to {@link #onBindAndValidate onBindAndValidate} which allows + * you to do custom processing after binding and validation (e.g. to + * manually bind request parameters, and to validate them outside a + * Validator).
  26. + *
  27. Call to {@link #processFormSubmission processFormSubmission} + * to process the submission, with or without binding errors. + * This method has to be implemented in subclasses and will be called + * only once per form submission.
  28. + *
  29. The portal will then call the render phase of processing the form + * submission. This phase will be called repeatedly by the portal every + * time the page is refreshed. All processing here should take this into + * account. Any one-time-only actions (such as modifying a database) must + * be done in the action phase.
  30. + *
  31. If the action phase indicated this is an invalid submit, the render + * phase calls {@link #renderInvalidSubmit renderInvalidSubmit} which – + * also by default – will render the results of the resubmitted + * form. Be sure to override both handleInvalidSubmit and + * renderInvalidSubmit if you want to change this overall + * behavior.
  32. + *
  33. Finally, call {@link #renderFormSubmission renderFormSubmission} to + * render the results of the submission, with or without binding errors. + * This method has to be implemented in subclasses and will be called + * repeatedly by the portal.
  34. + *
+ *

+ * + *

In session form mode, a submission without an existing form object in the + * session is considered invalid, like in the case of a resubmit/reload by the browser. + * The {@link #handleInvalidSubmit handleInvalidSubmit} / + * {@link #renderInvalidSubmit renderInvalidSubmit} methods are invoked then, + * by default trying to resubmit. This can be overridden in subclasses to show + * corresponding messages or to redirect to a new form, in order to avoid duplicate + * submissions. The form object in the session can be considered a transaction token + * in that case.

+ * + *

Make sure that any URLs that take you to your form controller are Render URLs, + * so that it will not try to treat the initial call as a form submission. + * If you use action URLs to link to your controller, you will need to override the + * {@link #isFormSubmission isFormSubmission} method to use a different mechanism for + * determining whether a form has been submitted. Make sure this method will work for + * both the ActionRequest and the RenderRequest objects.

+ * + *

Note that views should never retrieve form beans from the session but always + * from the request, as prepared by the form controller. Remember that some view + * technologies like Velocity cannot even access a HTTP session.

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
namedefaultdescription
bindOnNewFormfalseIndicates whether to bind portlet request parameters when + * creating a new form. Otherwise, the parameters will only be + * bound on form submission attempts.
sessionFormfalseIndicates whether the form object should be kept in the session + * when a user asks for a new form. This allows you e.g. to retrieve + * an object from the database, let the user edit it, and then persist + * it again. Otherwise, a new command object will be created for each + * request (even when showing the form again after validation errors).
redirectActionfalseSpecifies whether processFormSubmission is expected to call + * {@link ActionResponse#sendRedirect ActionResponse.sendRedirect}. + * This is important because some methods may not be called before + * {@link ActionResponse#sendRedirect ActionResponse.sendRedirect} (e.g. + * {@link ActionResponse#setRenderParameter ActionResponse.setRenderParameter}). + * Setting this flag will prevent AbstractFormController from setting render + * parameters that it normally needs for the render phase. + * If this is set true and sendRedirect is not called, then + * processFormSubmission must call + * {@link #setFormSubmit setFormSubmit}. + * Otherwise, the render phase will not realize the form was submitted + * and will simply display a new blank form.
renderParametersnullAn array of parameters that will be passed forward from the action + * phase to the render phase if the form needs to be displayed + * again. These can also be passed forward explicitly by calling + * the passRenderParameters method from any action + * phase method. Abstract descendants of this controller should follow + * similar behavior. If there are parameters you need in + * renderFormSubmission, then you need to pass those + * forward from processFormSubmission. If you override the + * default behavior of invalid submits and you set sessionForm to true, + * then you probably will not need to set this because your parameters + * are only going to be needed on the first request.
+ *

+ * + *

Thanks to Rainer Schmitz and Nick Lothian for their suggestions! + * + * @author John A. Lewis + * @author Juergen Hoeller + * @author Alef Arendsen + * @author Rob Harrop + * @since 2.0 + * @see #showForm(RenderRequest, RenderResponse, BindException) + * @see SimpleFormController + * @see AbstractWizardFormController + */ +public abstract class AbstractFormController extends BaseCommandController { + + /** + * These render parameters are used to indicate forward to the render phase + * if the form was submitted and if the submission was invalid. + */ + private static final String FORM_SUBMISSION_PARAMETER = "form-submit"; + + private static final String INVALID_SUBMISSION_PARAMETER = "invalid-submit"; + + private static final String TRUE = Boolean.TRUE.toString(); + + + private boolean bindOnNewForm = false; + + private boolean sessionForm = false; + + private boolean redirectAction = false; + + private String[] renderParameters = null; + + + /** + * Create a new AbstractFormController. + *

Subclasses should set the following properties, either in the constructor + * or via a BeanFactory: commandName, commandClass, bindOnNewForm, sessionForm. + * Note that commandClass doesn't need to be set when overriding + * formBackingObject, as the latter determines the class anyway. + *

"cacheSeconds" is by default set to 0 (-> no caching for all form controllers). + * @see #setCommandName + * @see #setCommandClass + * @see #setBindOnNewForm + * @see #setSessionForm + * @see #formBackingObject + */ + public AbstractFormController() { + setCacheSeconds(0); + } + + /** + * Set if request parameters should be bound to the form object + * in case of a non-submitting request, i.e. a new form. + */ + public final void setBindOnNewForm(boolean bindOnNewForm) { + this.bindOnNewForm = bindOnNewForm; + } + + /** + * Return if request parameters should be bound in case of a new form. + */ + public final boolean isBindOnNewForm() { + return this.bindOnNewForm; + } + + /** + * Activate/deactivate session form mode. In session form mode, + * the form is stored in the session to keep the form object instance + * between requests, instead of creating a new one on each request. + *

This is necessary for either wizard-style controllers that populate a + * single form object from multiple pages, or forms that populate a persistent + * object that needs to be identical to allow for tracking changes. + */ + public final void setSessionForm(boolean sessionForm) { + this.sessionForm = sessionForm; + } + + /** + * Return if session form mode is activated. + */ + public final boolean isSessionForm() { + return this.sessionForm; + } + + /** + * Specify whether the action phase is expected to call + * {@link ActionResponse#sendRedirect}. + * This information is important because some methods may not be called + * before {@link ActionResponse#sendRedirect}, e.g. + * {@link ActionResponse#setRenderParameter} and + * {@link ActionResponse#setRenderParameters}. + *

NOTE: Call this at initialization time of your controller: + * either in the constructor or in the bean definition for your controller. + * @see ActionResponse#sendRedirect + */ + public void setRedirectAction(boolean redirectAction) { + this.redirectAction = redirectAction; + } + + /** + * Return if {@link ActionResponse#sendRedirect} is + * expected to be called in the action phase. + */ + public boolean isRedirectAction() { + return this.redirectAction; + } + + /** + * Specify the list of parameters that should be passed forward + * from the action phase to the render phase whenever the form is + * re-rendered or when {@link #passRenderParameters} is called. + * @see #passRenderParameters + */ + public void setRenderParameters(String[] parameters) { + this.renderParameters = parameters; + } + + /** + * Returns the list of parameters that will be passed forward + * from the action phase to the render phase whenever the form is + * rerendered or when {@link #passRenderParameters} is called. + * @return the list of parameters + * @see #passRenderParameters + */ + public String[] getRenderParameters() { + return this.renderParameters; + } + + + /** + * Handles action phase of two cases: form submissions and showing a new form. + * Delegates the decision between the two to isFormSubmission, + * always treating requests without existing form session attribute + * as new form when using session form mode. + * @see #isFormSubmission + * @see #processFormSubmission + * @see #handleRenderRequestInternal + */ + protected void handleActionRequestInternal(ActionRequest request, ActionResponse response) + throws Exception { + + // Form submission or new form to show? + if (isFormSubmission(request)) { + // Fetch form object, bind, validate, process submission. + try { + Object command = getCommand(request); + if (logger.isDebugEnabled()) { + logger.debug("Processing valid submit (redirectAction = " + isRedirectAction() + ")"); + } + if (!isRedirectAction()) { + setFormSubmit(response); + } + PortletRequestDataBinder binder = bindAndValidate(request, command); + BindException errors = new BindException(binder.getBindingResult()); + processFormSubmission(request, response, command, errors); + setRenderCommandAndErrors(request, command, errors); + return; + } + catch (PortletSessionRequiredException ex) { + // Cannot submit a session form if no form object is in the session. + if (logger.isDebugEnabled()) { + logger.debug("Invalid submit detected: " + ex.getMessage()); + } + setFormSubmit(response); + setInvalidSubmit(response); + handleInvalidSubmit(request, response); + return; + } + } + + else { + logger.debug("Not a form submit - passing parameters to render phase"); + passRenderParameters(request, response); + return; + } + } + + /** + * Handles render phase of two cases: form submissions and showing a new form. + * Delegates the decision between the two to isFormSubmission, + * always treating requests without existing form session attribute + * as new form when using session form mode. + * @see #isFormSubmission + * @see #showNewForm + * @see #processFormSubmission + * @see #handleActionRequestInternal + */ + protected ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response) + throws Exception { + + // Form submission or new form to show? + if (isFormSubmission(request)) { + + // If it is an invalid submit then handle it. + if (isInvalidSubmission(request)) { + logger.debug("Invalid submit - calling renderInvalidSubmit"); + return renderInvalidSubmit(request, response); + } + + // Valid submit -> render. + logger.debug("Valid submit - calling renderFormSubmission"); + return renderFormSubmission(request, response, getRenderCommand(request), getRenderErrors(request)); + } + + else { + // New form to show: render form view. + return showNewForm(request, response); + } + } + + /** + * Determine if the given request represents a form submission. + *

The default implementation checks to see if this is an ActionRequest + * and treats all action requests as form submission. During the action + * phase it will pass forward a render parameter to indicate to the render + * phase that this is a form submission. This method can check both + * kinds of requests and indicate if this is a form submission. + *

Subclasses can override this to use a custom strategy, e.g. a specific + * request parameter (assumably a hidden field or submit button name). Make + * sure that the override can handle both ActionRequest and RenderRequest + * objects properly. + * @param request current request + * @return if the request represents a form submission + */ + protected boolean isFormSubmission(PortletRequest request) { + return (request instanceof ActionRequest || TRUE.equals(request.getParameter(getFormSubmitParameterName()))); + } + + /** + * Determine if the given request represents an invalid form submission. + */ + protected boolean isInvalidSubmission(PortletRequest request) { + return TRUE.equals(request.getParameter(getInvalidSubmitParameterName())); + } + + /** + * Return the name of the render parameter that indicates this + * was a form submission. + * @return the name of the render parameter + * @see javax.portlet.RenderRequest#getParameter + */ + protected String getFormSubmitParameterName() { + return FORM_SUBMISSION_PARAMETER; + } + + /** + * Return the name of the render parameter that indicates this + * was an invalid form submission. + * @return the name of the render parameter + * @see javax.portlet.RenderRequest#getParameter + */ + protected String getInvalidSubmitParameterName() { + return INVALID_SUBMISSION_PARAMETER; + } + + /** + * Set the action response parameter that indicates this in a form submission. + * @param response the current action response + * @see #getFormSubmitParameterName() + */ + protected final void setFormSubmit(ActionResponse response) { + if (logger.isDebugEnabled()) { + logger.debug("Setting render parameter [" + getFormSubmitParameterName() + + "] to indicate this is a form submission"); + } + try { + response.setRenderParameter(getFormSubmitParameterName(), TRUE); + } + catch (IllegalStateException ex) { + // Ignore in case sendRedirect was already set. + } + } + + /** + * Set the action response parameter that indicates this in an invalid submission. + * @param response the current action response + * @see #getInvalidSubmitParameterName() + */ + protected final void setInvalidSubmit(ActionResponse response) { + if (logger.isDebugEnabled()) { + logger.debug("Setting render parameter [" + getInvalidSubmitParameterName() + + "] to indicate this is an invalid submission"); + } + try { + response.setRenderParameter(getInvalidSubmitParameterName(), TRUE); + } + catch (IllegalStateException ex) { + // Ignore in case sendRedirect was already set. + } + } + + /** + * Return the name of the PortletSession attribute that holds the form object + * for this form controller. + *

The default implementation delegates to the + * getFormSessionAttributeName version without arguments. + * @param request current HTTP request + * @return the name of the form session attribute, + * or null if not in session form mode + * @see #getFormSessionAttributeName() + * @see javax.portlet.PortletSession#getAttribute + */ + protected String getFormSessionAttributeName(PortletRequest request) { + return getFormSessionAttributeName(); + } + + /** + * Return the name of the PortletSession attribute that holds the form object + * for this form controller. + *

Default is an internal name, of no relevance to applications, as the form + * session attribute is not usually accessed directly. Can be overridden to use + * an application-specific attribute name, which allows other code to access + * the session attribute directly. + * @return the name of the form session attribute + * @see javax.portlet.PortletSession#getAttribute + */ + protected String getFormSessionAttributeName() { + return getClass().getName() + ".FORM." + getCommandName(); + } + + /** + * Pass the specified list of action request parameters to the render phase + * by putting them into the action response object. This may not be called + * when the action will call will call + * {@link ActionResponse#sendRedirect sendRedirect}. + * @param request the current action request + * @param response the current action response + * @see ActionResponse#setRenderParameter + */ + protected void passRenderParameters(ActionRequest request, ActionResponse response) { + if (this.renderParameters == null) { + return; + } + try { + for (int i = 0; i < this.renderParameters.length; i++) { + String paramName = this.renderParameters[i]; + String paramValues[] = request.getParameterValues(paramName); + if (paramValues != null) { + if (logger.isDebugEnabled()) { + logger.debug("Passing parameter to render phase '" + paramName + "' = " + + (paramValues == null ? "NULL" : Arrays.asList(paramValues).toString())); + } + response.setRenderParameter(paramName, paramValues); + } + } + } + catch (IllegalStateException ex) { + // Ignore in case sendRedirect was already set. + } + } + + + /** + * Show a new form. Prepares a backing object for the current form + * and the given request, including checking its validity. + * @param request current render request + * @param response current render response + * @return the prepared form view + * @throws Exception in case of an invalid new form object + * @see #getErrorsForNewForm + */ + protected final ModelAndView showNewForm(RenderRequest request, RenderResponse response) + throws Exception { + + logger.debug("Displaying new form"); + return showForm(request, response, getErrorsForNewForm(request)); + } + + /** + * Create a BindException instance for a new form. + * Called by showNewForm. + *

Can be used directly when intending to show a new form but with + * special errors registered on it (for example, on invalid submit). + * Usually, the resulting BindException will be passed to + * showForm, after registering the errors on it. + * @param request current render request + * @return the BindException instance + * @throws Exception in case of an invalid new form object + */ + protected final BindException getErrorsForNewForm(RenderRequest request) throws Exception { + // Create form-backing object for new form + Object command = formBackingObject(request); + if (command == null) { + throw new PortletException("Form object returned by formBackingObject() must not be null"); + } + if (!checkCommand(command)) { + throw new PortletException("Form object returned by formBackingObject() must match commandClass"); + } + + // Bind without validation, to allow for prepopulating a form, and for + // convenient error evaluation in views (on both first attempt and resubmit). + PortletRequestDataBinder binder = createBinder(request, command); + BindException errors = new BindException(binder.getBindingResult()); + + if (isBindOnNewForm()) { + if (logger.isDebugEnabled()) { + logger.debug("Binding to new form"); + } + binder.bind(request); + onBindOnNewForm(request, command, errors); + } + + // Return BindException object that resulted from binding. + return errors; + } + + /** + * Callback for custom post-processing in terms of binding for a new form. + * Called when preparing a new form if bindOnNewForm is true. + *

Default implementation delegates to onBindOnNewForm(request, command). + * @param request current render request + * @param command the command object to perform further binding on + * @param errors validation errors holder, allowing for additional + * custom registration of binding errors + * @throws Exception in case of invalid state or arguments + * @see #onBindOnNewForm(RenderRequest, Object) + * @see #setBindOnNewForm + */ + protected void onBindOnNewForm(RenderRequest request, Object command, BindException errors) + throws Exception { + + onBindOnNewForm(request, command); + } + + /** + * Callback for custom post-processing in terms of binding for a new form. + * Called by the default implementation of the onBindOnNewForm version + * with all parameters, after standard binding when displaying the form view. + * Only called if bindOnNewForm is set to true. + *

The default implementation is empty. + * @param request current render request + * @param command the command object to perform further binding on + * @throws Exception in case of invalid state or arguments + * @see #onBindOnNewForm(RenderRequest, Object, BindException) + * @see #setBindOnNewForm(boolean) + */ + protected void onBindOnNewForm(RenderRequest request, Object command) throws Exception { + } + + + /** + * Return the form object for the given request. + *

Calls formBackingObject if the object is not in the session + * @param request current request + * @return object form to bind onto + * @see #formBackingObject + */ + protected final Object getCommand(PortletRequest request) throws Exception { + // If not in session-form mode, create a new form-backing object. + if (!isSessionForm()) { + return formBackingObject(request); + } + + // Session-form mode: retrieve form object from portlet session attribute. + PortletSession session = request.getPortletSession(false); + if (session == null) { + throw new PortletSessionRequiredException("Must have session when trying to bind (in session-form mode)"); + } + String formAttrName = getFormSessionAttributeName(request); + Object sessionFormObject = session.getAttribute(formAttrName); + if (sessionFormObject == null) { + throw new PortletSessionRequiredException("Form object not found in session (in session-form mode)"); + } + + // Remove form object from porlet session: we might finish the form workflow + // in this request. If it turns out that we need to show the form view again, + // we'll re-bind the form object to the portlet session. + if (logger.isDebugEnabled()) { + logger.debug("Removing form session attribute [" + formAttrName + "]"); + } + session.removeAttribute(formAttrName); + + // Check the command object to make sure its valid + if (!checkCommand(sessionFormObject)) { + throw new PortletSessionRequiredException("Object found in session does not match commandClass"); + } + + return sessionFormObject; + } + + /** + * Retrieve a backing object for the current form from the given request. + *

The properties of the form object will correspond to the form field values + * in your form view. This object will be exposed in the model under the specified + * command name, to be accessed under that name in the view: for example, with + * a "spring:bind" tag. The default command name is "command". + *

Note that you need to activate session form mode to reuse the form-backing + * object across the entire form workflow. Else, a new instance of the command + * class will be created for each submission attempt, just using this backing + * object as template for the initial form. + *

The default implementation calls BaseCommandController.createCommand, + * creating a new empty instance of the command class. + * Subclasses can override this to provide a preinitialized backing object. + * @param request current portlet request + * @return the backing object + * @throws Exception in case of invalid state or arguments + * @see #setCommandName + * @see #setCommandClass + * @see #createCommand + */ + protected Object formBackingObject(PortletRequest request) throws Exception { + return createCommand(); + } + + + /** + * Prepare the form model and view, including reference and error data. + * Can show a configured form page, or generate a form view programmatically. + *

A typical implementation will call + * showForm(request, errors, "myView") + * to prepare the form view for a specific view name, returning the + * ModelAndView provided there. + *

For building a custom ModelAndView, call errors.getModel() + * to populate the ModelAndView model with the command and the Errors instance, + * under the specified command name, as expected by the "spring:bind" tag. + * You also need to include the model returned by referenceData. + *

Note: If you decide to have a "formView" property specifying the + * view name, consider using SimpleFormController. + * @param request current render request + * @param response current render response + * @param errors validation errors holder + * @return the prepared form view, or null if handled directly + * @throws Exception in case of invalid state or arguments + * @see #showForm(RenderRequest, BindException, String) + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + * @see #referenceData(PortletRequest, Object, Errors) + * @see SimpleFormController#setFormView + */ + protected abstract ModelAndView showForm(RenderRequest request, RenderResponse response, BindException errors) throws Exception; + + /** + * Prepare model and view for the given form, including reference and errors. + *

In session form mode: Re-puts the form object in the session when + * returning to the form, as it has been removed by getCommand. + *

Can be used in subclasses to redirect back to a specific form page. + * @param request current render request + * @param errors validation errors holder + * @param viewName name of the form view + * @return the prepared form view + * @throws Exception in case of invalid state or arguments + * @see #showForm(RenderRequest, BindException, String, Map) + * @see #showForm(RenderRequest, RenderResponse, BindException) + */ + protected final ModelAndView showForm(RenderRequest request, BindException errors, String viewName) + throws Exception { + + return showForm(request, errors, viewName, null); + } + + /** + * Prepare model and view for the given form, including reference and errors, + * adding a controller-specific control model. + *

In session form mode: Re-puts the form object in the session when returning + * to the form, as it has been removed by getCommand. + *

Can be used in subclasses to redirect back to a specific form page. + * @param request current render request + * @param errors validation errors holder + * @param viewName name of the form view + * @param controlModel model map containing controller-specific control data + * (e.g. current page in wizard-style controllers or special error message) + * @return the prepared form view + * @throws Exception in case of invalid state or arguments + * @see #showForm(RenderRequest, BindException, String) + * @see #showForm(RenderRequest, RenderResponse, BindException) + */ + protected final ModelAndView showForm(RenderRequest request, BindException errors, String viewName, Map controlModel) + throws Exception { + + // In session form mode, re-expose form object as portlet session attribute. + // Re-binding is necessary for proper state handling in a cluster, + // to notify other nodes of changes in the form object. + if (isSessionForm()) { + String formAttrName = getFormSessionAttributeName(request); + if (logger.isDebugEnabled()) { + logger.debug("Setting form session attribute [" + formAttrName + "] to: " + errors.getTarget()); + } + request.getPortletSession().setAttribute(formAttrName, errors.getTarget()); + } + + // Fetch errors model as starting point, containing form object under + // "commandName", and corresponding Errors instance under internal key. + Map model = errors.getModel(); + + // Merge reference data into model, if any. + Map referenceData = referenceData(request, errors.getTarget(), errors); + if (referenceData != null) { + model.putAll(referenceData); + } + + // Merge control attributes into model, if any. + if (controlModel != null) { + model.putAll(controlModel); + } + + // Trigger rendering of the specified view, using the final model. + return new ModelAndView(viewName, model); + } + + /** + * Create a reference data map for the given request, consisting of + * bean name/bean instance pairs as expected by ModelAndView. + *

The default implementation returns null. + * Subclasses can override this to set reference data used in the view. + * @param request current render request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see ModelAndView + */ + protected Map referenceData(PortletRequest request, Object command, Errors errors) throws Exception { + return null; + } + + + /** + * Process render phase of form submission request. Called by handleRequestInternal + * in case of a form submission, with or without binding errors. Implementations + * need to proceed properly, typically showing a form view in case of binding + * errors or rendering the result of a submit action else. + *

For a success view, call errors.getModel() to populate the + * ModelAndView model with the command and the Errors instance, under the + * specified command name, as expected by the "spring:bind" tag. For a form view, + * simply return the ModelAndView object privded by showForm. + * @param request current render request + * @param response current render response + * @param command form object with request parameters bound onto it + * @param errors errors holder + * @return the prepared model and view, or null + * @throws Exception in case of errors + * @see #handleRenderRequestInternal + * @see #processFormSubmission + * @see #isFormSubmission + * @see #showForm(RenderRequest, RenderResponse, BindException) + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected abstract ModelAndView renderFormSubmission(RenderRequest request, RenderResponse response, Object command, BindException errors) + throws Exception; + + /** + * Process action phase of form submission request. Called by handleRequestInternal + * in case of a form submission, with or without binding errors. Implementations + * need to proceed properly, typically performing a submit action if there are no binding errors. + *

Subclasses can implement this to provide custom submission handling + * like triggering a custom action. They can also provide custom validation + * or proceed with the submission accordingly. + * @param request current action request + * @param response current action response + * @param command form object with request parameters bound onto it + * @param errors errors holder (subclass can add errors if it wants to) + * @throws Exception in case of errors + * @see #handleActionRequestInternal + * @see #renderFormSubmission + * @see #isFormSubmission + * @see org.springframework.validation.Errors + */ + protected abstract void processFormSubmission(ActionRequest request, ActionResponse response, Object command, BindException errors) + throws Exception; + + /** + * Handle an invalid submit request, e.g. when in session form mode but no form object + * was found in the session (like in case of an invalid resubmit by the browser). + *

The default implementation simply tries to resubmit the form with a new form object. + * This should also work if the user hit the back button, changed some form data, + * and resubmitted the form. + *

Note: To avoid duplicate submissions, you need to override this method. + * Either show some "invalid submit" message, or call showNewForm for + * resetting the form (prepopulating it with the current values if "bindOnNewForm" + * is true). In this case, the form object in the session serves as transaction token. + *

+	 * protected ModelAndView handleInvalidSubmit(RenderRequest request, RenderResponse response) throws Exception {
+	 *   return showNewForm(request, response);
+	 * }
+ * You can also show a new form but with special errors registered on it: + *
+	 * protected ModelAndView handleInvalidSubmit(RenderRequest request, RenderResponse response) throws Exception {
+	 *   BindException errors = getErrorsForNewForm(request);
+	 *   errors.reject("duplicateFormSubmission", "Duplicate form submission");
+	 *   return showForm(request, response, errors);
+	 * }
+ *

WARNING: If you override this method, be sure to also override the action + * phase version of this method so that it will not attempt to perform the resubmit + * action by default. + * @param request current render request + * @param response current render response + * @return a prepared view, or null if handled directly + * @throws Exception in case of errors + * @see #handleInvalidSubmit + */ + protected ModelAndView renderInvalidSubmit(RenderRequest request, RenderResponse response) + throws Exception { + + return renderFormSubmission(request, response, getRenderCommand(request), getRenderErrors(request)); + } + + /** + * Handle an invalid submit request, e.g. when in session form mode but no form object + * was found in the session (like in case of an invalid resubmit by the browser). + *

The default implementation simply tries to resubmit the form with a new form object. + * This should also work if the user hit the back button, changed some form data, + * and resubmitted the form. + *

Note: To avoid duplicate submissions, you need to override this method. + * Most likely you will simply want it to do nothing here in the action phase + * and diplay an appropriate error and a new form in the render phase. + *

+	 * protected void handleInvalidSubmit(ActionRequest request, ActionResponse response) throws Exception {
+	 * }
+ *

If you override this method but you do need a command object and bind errors + * in the render phase, be sure to call {@link #setRenderCommandAndErrors setRenderCommandAndErrors} + * from here. + * @param request current action request + * @param response current action response + * @throws Exception in case of errors + * @see #renderInvalidSubmit + * @see #setRenderCommandAndErrors + */ + protected void handleInvalidSubmit(ActionRequest request, ActionResponse response) throws Exception { + passRenderParameters(request, response); + Object command = formBackingObject(request); + if (command == null) { + throw new PortletException("Form object returned by formBackingObject() must not be null"); + } + if (!checkCommand(command)) { + throw new PortletException("Form object returned by formBackingObject() must match commandClass"); + } + PortletRequestDataBinder binder = bindAndValidate(request, command); + BindException errors = new BindException(binder.getBindingResult()); + processFormSubmission(request, response, command, errors); + setRenderCommandAndErrors(request, command, errors); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractWizardFormController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractWizardFormController.java new file mode 100644 index 00000000000..632985e0fea --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/AbstractWizardFormController.java @@ -0,0 +1,975 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.web.portlet.util.PortletUtils; + +/** + * Form controller for typical wizard-style workflows. + * + *

In contrast to classic forms, wizards have more than one form view page. + * Therefore, there are various actions instead of one single submit action: + *

+ * + *

Finish and cancel actions can be triggered by request parameters, named + * PARAM_FINISH ("_finish") and PARAM_CANCEL ("_cancel"), ignoring parameter + * values to allow for HTML buttons. The target page for page changes can be + * specified by PARAM_TARGET, appending the page number to the parameter name + * (e.g. "_target1"). The action parameters are recognized when triggered by + * image buttons too (via "_finish.x", "_abort.x", or "_target1.x"). + * + *

The current page number will be stored in the session. It can also be + * specified as request parameter PARAM_PAGE ("_page") in order to properly handle + * usage of the back button in a browser: In this case, a submission will always + * contain the correct page number, even if the user submitted from an old view. + * + *

The page can only be changed if it validates correctly, except if a + * "dirty back" or "dirty forward" is allowed. At finish, all pages get + * validated again to guarantee a consistent state. + * + *

Note that a validator's default validate method is not executed when using + * this class! Rather, the validatePage implementation should call + * special validateXXX methods that the validator needs to provide, + * validating certain pieces of the object. These can be combined to validate + * the elements of individual pages. + * + *

Note: Page numbering starts with 0, to be able to pass an array + * consisting of the corresponding view names to the "pages" bean property. + * + *

Parameters indicated with setPassRenderParameters will be present + * for each page. If there are render parameters you need in renderFinish + * or renderCancel, then you need to pass those forward from the + * processFinish or processCancel methods, respectively. + + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + * @see #setPages + * @see #validatePage + * @see #processFinish + * @see #processCancel + */ +public abstract class AbstractWizardFormController extends AbstractFormController { + + /** + * Parameter triggering the finish action. + * Can be called from any wizard page! + */ + public static final String PARAM_FINISH = "_finish"; + + /** + * Parameter triggering the cancel action. + * Can be called from any wizard page! + */ + public static final String PARAM_CANCEL = "_cancel"; + + /** + * Parameter specifying the target page, + * appending the page number to the name. + */ + public static final String PARAM_TARGET = "_target"; + + /** + * Parameter specifying the current page as value. Not necessary on + * form pages, but allows to properly handle usage of the back button. + * @see #setPageAttribute + */ + public static final String PARAM_PAGE = "_page"; + + + private String[] pages; + + private String pageAttribute; + + private boolean allowDirtyBack = true; + + private boolean allowDirtyForward = false; + + + /** + * Create a new AbstractWizardFormController. + *

"sessionForm" is automatically turned on, "validateOnBinding" + * turned off, and "cacheSeconds" set to 0 by the base class + * (-> no caching for all form controllers). + */ + public AbstractWizardFormController() { + // AbstractFormController sets default cache seconds to 0. + super(); + + // Always needs session to keep data from all pages. + setSessionForm(true); + + // Never validate everything on binding -> + // wizards validate individual pages. + setValidateOnBinding(false); + } + + /** + * Set the wizard pages, i.e. the view names for the pages. + * The array index is interpreted as page number. + * @param pages view names for the pages + */ + public final void setPages(String[] pages) { + if (pages == null || pages.length == 0) { + throw new IllegalArgumentException("No wizard pages defined"); + } + this.pages = pages; + } + + /** + * Return the wizard pages, i.e. the view names for the pages. + * The array index corresponds to the page number. + *

Note that a concrete wizard form controller might override + * getViewName(PortletRequest, Object, int) to + * determine the view name for each page dynamically. + * @see #getViewName(PortletRequest, Object, int) + */ + public final String[] getPages() { + return this.pages; + } + + /** + * Return the number of wizard pages. + * Useful to check whether the last page has been reached. + *

Note that a concrete wizard form controller might override + * getPageCount(PortletRequest, Object) to determine + * the page count dynamically. + * @see #getPageCount(PortletRequest, Object) + */ + protected final int getPageCount() { + return this.pages.length; + } + + /** + * Set the name of the page attribute in the model, containing + * an Integer with the current page number. + *

This will be necessary for single views rendering multiple view pages. + * It also allows for specifying the optional "_page" parameter. + * @param pageAttribute name of the page attribute + * @see #PARAM_PAGE + */ + public final void setPageAttribute(String pageAttribute) { + this.pageAttribute = pageAttribute; + } + + /** + * Return the name of the page attribute in the model. + */ + public final String getPageAttribute() { + return this.pageAttribute; + } + + /** + * Set if "dirty back" is allowed, i.e. if moving to a former wizard + * page is allowed in case of validation errors for the current page. + * @param allowDirtyBack if "dirty back" is allowed + */ + public final void setAllowDirtyBack(boolean allowDirtyBack) { + this.allowDirtyBack = allowDirtyBack; + } + + /** + * Return whether "dirty back" is allowed. + */ + public final boolean isAllowDirtyBack() { + return this.allowDirtyBack; + } + + /** + * Set if "dirty forward" is allowed, i.e. if moving to a later wizard + * page is allowed in case of validation errors for the current page. + * @param allowDirtyForward if "dirty forward" is allowed + */ + public final void setAllowDirtyForward(boolean allowDirtyForward) { + this.allowDirtyForward = allowDirtyForward; + } + + /** + * Return whether "dirty forward" is allowed. + */ + public final boolean isAllowDirtyForward() { + return this.allowDirtyForward; + } + + + /** + * Calls page-specific onBindAndValidate method. + */ + protected final void onBindAndValidate(PortletRequest request, Object command, BindException errors) + throws Exception { + + onBindAndValidate(request, command, errors, getCurrentPage(request)); + } + + /** + * Callback for custom post-processing in terms of binding and validation. + * Called on each submit, after standard binding but before page-specific + * validation of this wizard form controller. + *

Note: AbstractWizardFormController does not perform standard + * validation on binding but rather applies page-specific validation + * on processing the form submission. + * @param request current portlet request + * @param command bound command + * @param errors Errors instance for additional custom validation + * @param page current wizard page + * @throws Exception in case of invalid state or arguments + * @see #bindAndValidate + * @see #processFormSubmission + * @see org.springframework.validation.Errors + */ + protected void onBindAndValidate(PortletRequest request, Object command, BindException errors, int page) + throws Exception { + } + + /** + * Consider an explicit finish or cancel request as a form submission too. + * @see #isFinishRequest(PortletRequest) + * @see #isCancelRequest(PortletRequest) + */ + protected boolean isFormSubmission(PortletRequest request) { + return super.isFormSubmission(request) || isFinishRequest(request) || isCancelRequest(request); + } + + /** + * Calls page-specific referenceData method. + */ + protected final Map referenceData(PortletRequest request, Object command, Errors errors) + throws Exception { + + return referenceData(request, command, errors, getCurrentPage(request)); + } + + /** + * Create a reference data map for the given request, consisting of + * bean name/bean instance pairs as expected by ModelAndView. + *

The default implementation delegates to {@link #referenceData(PortletRequest, int)}. + * Subclasses can override this to set reference data used in the view. + * @param request current portlet request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @param page current wizard page + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see #referenceData(PortletRequest, int) + * @see org.springframework.web.portlet.ModelAndView + */ + protected Map referenceData(PortletRequest request, Object command, Errors errors, int page) + throws Exception { + + return referenceData(request, page); + } + + /** + * Create a reference data map for the given request, consisting of + * bean name/bean instance pairs as expected by ModelAndView. + *

The default implementation returns null. + * Subclasses can override this to set reference data used in the view. + * @param request current portlet request + * @param page current wizard page + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see org.springframework.web.portlet.ModelAndView + */ + protected Map referenceData(PortletRequest request, int page) throws Exception { + return null; + } + + + /** + * Show the first page as form view. + *

This can be overridden in subclasses, e.g. to prepare wizard-specific + * error views in case of an Exception. + */ + protected ModelAndView showForm( + RenderRequest request, RenderResponse response, BindException errors) throws Exception { + + return showPage(request, errors, getInitialPage(request, errors.getTarget())); + } + + /** + * Prepare the form model and view, including reference and error data, + * for the given page. Can be used in processFinish implementations, + * to show the corresponding page in case of validation errors. + * @param request current portlet render request + * @param errors validation errors holder + * @param page number of page to show + * @return the prepared form view + * @throws Exception in case of invalid state or arguments + */ + protected final ModelAndView showPage(RenderRequest request, BindException errors, int page) + throws Exception { + + if (page >= 0 && page < getPageCount(request, errors.getTarget())) { + if (logger.isDebugEnabled()) { + logger.debug("Showing wizard page " + page + " for form bean '" + getCommandName() + "'"); + } + + // Set page session attribute, expose overriding request attribute. + Integer pageInteger = new Integer(page); + String pageAttrName = getPageSessionAttributeName(request); + if (isSessionForm()) { + if (logger.isDebugEnabled()) { + logger.debug("Setting page session attribute [" + pageAttrName + "] to: " + pageInteger); + } + request.getPortletSession().setAttribute(pageAttrName, pageInteger); + } + request.setAttribute(pageAttrName, pageInteger); + + // Set page request attribute for evaluation by views. + Map controlModel = new HashMap(); + if (this.pageAttribute != null) { + controlModel.put(this.pageAttribute, new Integer(page)); + } + String viewName = getViewName(request, errors.getTarget(), page); + return showForm(request, errors, viewName, controlModel); + } + + else { + throw new PortletException("Invalid wizard page number: " + page); + } + } + + /** + * Return the page count for this wizard form controller. + *

The default implementation delegates to {@link #getPageCount()}. + * Can be overridden to dynamically adapt the page count. + * @param request current portlet request + * @param command the command object as returned by formBackingObject + * @return the current page count + * @see #getPageCount + */ + protected int getPageCount(PortletRequest request, Object command) { + return getPageCount(); + } + + /** + * Return the name of the view for the specified page of this wizard form controller. + *

The default implementation takes the view name from the {@link #getPages()} array. + * Can be overridden to dynamically switch the page view or to return view names + * for dynamically defined pages. + * @param request current portlet request + * @param command the command object as returned by formBackingObject + * @return the current page count + * @see #getPageCount + */ + protected String getViewName(PortletRequest request, Object command, int page) { + return getPages()[page]; + } + + /** + * Return the initial page of the wizard, i.e. the page shown at wizard startup. + *

The default implementation delegates to {@link #getInitialPage(PortletRequest)}. + * @param request current portlet request + * @param command the command object as returned by formBackingObject + * @return the initial page number + * @see #getInitialPage(PortletRequest) + * @see #formBackingObject + */ + protected int getInitialPage(PortletRequest request, Object command) { + return getInitialPage(request); + } + + /** + * Return the initial page of the wizard, i.e. the page shown at wizard startup. + *

The default implementation returns 0 for first page. + * @param request current portlet request + * @return the initial page number + */ + protected int getInitialPage(PortletRequest request) { + return 0; + } + + /** + * Return the name of the PortletSession attribute that holds the page object + * for this wizard form controller. + *

The default implementation delegates to the getPageSessionAttributeName + * version without arguments. + * @param request current portlet request + * @return the name of the form session attribute, or null if not in session form mode + * @see #getPageSessionAttributeName + * @see #getFormSessionAttributeName + * @see javax.portlet.PortletSession#getAttribute + */ + protected String getPageSessionAttributeName(PortletRequest request) { + return getPageSessionAttributeName(); + } + + /** + * Return the name of the PortletSession attribute that holds the page object + * for this wizard form controller. + *

Default is an internal name, of no relevance to applications, as the form + * session attribute is not usually accessed directly. Can be overridden to use + * an application-specific attribute name, which allows other code to access + * the session attribute directly. + * @return the name of the page session attribute + * @see #getFormSessionAttributeName + * @see javax.portlet.PortletSession#getAttribute + */ + protected String getPageSessionAttributeName() { + return getClass().getName() + ".PAGE." + getCommandName(); + } + + /** + * Pass the page number to the render phase by setting a render parameter. + * This method may not be called when the action calls + * {@link javax.portlet.ActionResponse#sendRedirect(String)}. + * @param response the current action response + * @param page the page number + * @see ActionResponse#setRenderParameter + */ + protected void setPageRenderParameter(ActionResponse response, int page) { + if (logger.isDebugEnabled()) + logger.debug("Setting page number render parameter [" + PARAM_PAGE + "] to [" + page + "]"); + try { + response.setRenderParameter(PARAM_PAGE, new Integer(page).toString()); + } + catch (IllegalStateException ex) { + // ignore in case sendRedirect was already set + } + } + + /** + * Pass the the parameter that indicates the target page of the request + * forward to the render phase. If the getTargetPage method + * was overridden, this may need to be overriden as well. + * @param request the current action request + * @param response the current action response + * @see #PARAM_TARGET + * @see #getTargetPage(PortletRequest, int) + * @see #getTargetPage(PortletRequest, Object, Errors, int) + * @see ActionResponse#setRenderParameter + */ + protected void setTargetRenderParameter(ActionRequest request, ActionResponse response) { + try { + Iterator it = PortletUtils.getParametersStartingWith(request, PARAM_TARGET).entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + String param = PARAM_TARGET + (String) entry.getKey(); + Object value = entry.getValue(); + if (logger.isDebugEnabled()) { + logger.debug("Setting target render parameter [" + param + "]"); + } + if (value instanceof String) { + response.setRenderParameter(param, (String) value); + } + else if (value instanceof String[]) { + response.setRenderParameter(param, (String[]) value); + } + } + } + catch (IllegalStateException ex) { + // ignore in case sendRedirect was already set + } + } + + /** + * Pass the the parameter that indicates a finish request forward to the + * render phase. If the isFinishRequest method + * was overridden, this may need to be overriden as well. + * @param request the current action request + * @param response the current action response + * @see #PARAM_FINISH + * @see #isFinishRequest + * @see ActionResponse#setRenderParameter + */ + protected void setFinishRenderParameter(ActionRequest request, ActionResponse response) { + if (logger.isDebugEnabled()) + logger.debug("Setting cancel render parameter [" + PARAM_FINISH + "]"); + try { + String name = PortletUtils.getSubmitParameter(request, PARAM_FINISH); + if (name != null) + response.setRenderParameter(name, request.getParameter(name)); + } + catch (IllegalStateException ex) { + // ignore in case sendRedirect was already set + } + } + + /** + * Pass the the parameter that indicates a cancel request forward to the + * render phase. If the isCancelRequest method + * was overridden, this may need to be overriden as well. + * @param request the current action request + * @param response the current action response + * @see #PARAM_CANCEL + * @see #isCancelRequest + * @see ActionResponse#setRenderParameter + */ + protected void setCancelRenderParameter(ActionRequest request, ActionResponse response) { + if (logger.isDebugEnabled()) + logger.debug("Setting cancel render parameter [" + PARAM_CANCEL + "]"); + try { + String name = PortletUtils.getSubmitParameter(request, PARAM_CANCEL); + if (name != null) + response.setRenderParameter(name, request.getParameter(name)); + } + catch (IllegalStateException ex) { + // ignore in case sendRedirect was already set + } + } + + /** + * Handle an invalid submit request, e.g. when in session form mode but no form object + * was found in the session (like in case of an invalid resubmit by the browser). + *

The default implementation for wizard form controllers simply shows the initial page + * of a new wizard form. If you want to show some "invalid submit" message, you need + * to override this method. + * @param request current portlet render request + * @param response current portlet render response + * @return a prepared view, or null if handled directly + * @throws Exception in case of errors + * @see #showNewForm + * @see #setBindOnNewForm + * @see #handleInvalidSubmit + */ + protected ModelAndView renderInvalidSubmit(RenderRequest request, RenderResponse response) + throws Exception { + + return showNewForm(request, response); + } + + /** + * Handle an invalid submit request, e.g. when in session form mode but no form object + * was found in the session (like in case of an invalid resubmit by the browser). + *

The default implementation for wizard form controllers simply shows the initial page + * of a new wizard form, so here in the action phase this method does nothing. If you + * want to take some action on an invalid submit, you need to override this method. + * @param request current portlet action request + * @param response current portlet action response + * @throws Exception in case of errors + * @see #renderInvalidSubmit + */ + protected void handleInvalidSubmit(ActionRequest request, ActionResponse response) throws Exception { + } + + /** + * Apply wizard workflow: finish, cancel, page change. + * @see #processFormSubmission + */ + protected final ModelAndView renderFormSubmission(RenderRequest request, RenderResponse response, Object command, BindException errors) + throws Exception { + + int currentPage = getCurrentPage(request); + String pageAttrName = getPageSessionAttributeName(request); + request.setAttribute(pageAttrName, new Integer(currentPage)); + + // cancel? + if (isCancelRequest(request)) { + if (logger.isDebugEnabled()) { + logger.debug("Cancelling wizard for form bean '" + getCommandName() + "'"); + } + return renderCancel(request, response, command, errors); + } + + // finish? + if (isFinishRequest(request)) { + if (logger.isDebugEnabled()) { + logger.debug("Finishing wizard for form bean '" + getCommandName() + "'"); + } + return renderValidatePagesAndFinish(request, response, command, errors, currentPage); + } + + // Normal submit: show specified target page. + int targetPage = getTargetPage(request, command, errors, currentPage); + if (logger.isDebugEnabled()) { + logger.debug("Target page " + targetPage + " requested"); + } + if (targetPage != currentPage) { + if (!errors.hasErrors() || (this.allowDirtyBack && targetPage < currentPage) || + (this.allowDirtyForward && targetPage > currentPage)) { + // Allowed to go to target page. + return showPage(request, errors, targetPage); + } + } + + // Show current page again + return showPage(request, errors, currentPage); + } + + + /** + * Apply wizard workflow: finish, cancel, page change. + * @see #renderFormSubmission + */ + protected final void processFormSubmission( + ActionRequest request, ActionResponse response, Object command, BindException errors) + throws Exception { + + int currentPage = getCurrentPage(request); + // Remove page session attribute, provide copy as request attribute. + String pageAttrName = getPageSessionAttributeName(request); + if (isSessionForm()) { + if (logger.isDebugEnabled()) { + logger.debug("Removing page session attribute [" + pageAttrName + "]"); + } + request.getPortletSession().removeAttribute(pageAttrName); + } + request.setAttribute(pageAttrName, new Integer(currentPage)); + + // cancel? + if (isCancelRequest(request)) { + if (logger.isDebugEnabled()) { + logger.debug("Cancelling wizard for form bean '" + getCommandName() + "'"); + } + setPageRenderParameter(response, currentPage); + setCancelRenderParameter(request, response); + processCancel(request, response, command, errors); + return; + } + + // finish? + if (isFinishRequest(request)) { + if (logger.isDebugEnabled()) { + logger.debug("Finishing wizard for form bean '" + getCommandName() + "'"); + } + if (!isRedirectAction()) { + setPageRenderParameter(response, currentPage); + setFinishRenderParameter(request, response); + } + validatePagesAndFinish(request, response, command, errors, currentPage); + return; + } + + // Normal submit: validate current page + if (!suppressValidation(request)) { + if (logger.isDebugEnabled()) { + logger.debug("Validating wizard page " + currentPage + " for form bean '" + getCommandName() + "'"); + } + validatePage(command, errors, currentPage, false); + } + + setPageRenderParameter(response, currentPage); + setTargetRenderParameter(request, response); + passRenderParameters(request, response); + + // Give subclasses a change to perform custom post-procession + // of the current page and its command object. + postProcessPage(request, command, errors, currentPage); + + } + + /** + * Return the current page number. Used by {@link #processFormSubmission}. + *

The default implementation checks the page session attribute. + * Subclasses can override this for customized page determination. + * @param request current portlet request + * @return the current page number + * @see #getPageSessionAttributeName() + */ + protected int getCurrentPage(PortletRequest request) { + // Check for overriding attribute in request. + String pageAttrName = getPageSessionAttributeName(request); + Integer pageAttr = (Integer) request.getAttribute(pageAttrName); + if (pageAttr != null) { + return pageAttr.intValue(); + } + // Check for explicit request parameter. + String pageParam = request.getParameter(PARAM_PAGE); + if (pageParam != null) { + return Integer.parseInt(pageParam); + } + // Check for original attribute in session. + if (isSessionForm()) { + pageAttr = (Integer) request.getPortletSession().getAttribute(pageAttrName); + if (pageAttr != null) { + return pageAttr.intValue(); + } + } + throw new IllegalStateException("Page attribute [" + pageAttrName + "] neither found in session nor in request"); + } + + /** + * Determine whether the incoming request is a request to finish the + * processing of the current form. + *

By default, this method returns true if a parameter + * matching the "_finish" key is present in the request, otherwise it + * returns false. Subclasses may override this method + * to provide custom logic to detect a finish request. + *

The parameter is recognized both when sent as a plain parameter + * ("_finish") or when triggered by an image button ("_finish.x"). + * @param request current portlet request + * @return whether the request indicates to finish form processing + * @see #PARAM_FINISH + */ + protected boolean isFinishRequest(PortletRequest request) { + return PortletUtils.hasSubmitParameter(request, PARAM_FINISH); + } + + /** + * Determine whether the incoming request is a request to cancel the + * processing of the current form. + *

By default, this method returns true if a parameter + * matching the "_cancel" key is present in the request, otherwise it + * returns false. Subclasses may override this method + * to provide custom logic to detect a cancel request. + *

The parameter is recognized both when sent as a plain parameter + * ("_cancel") or when triggered by an image button ("_cancel.x"). + * @param request current portlet request + * @return whether the request indicates to cancel form processing + * @see #PARAM_CANCEL + */ + protected boolean isCancelRequest(PortletRequest request) { + return PortletUtils.hasSubmitParameter(request, PARAM_CANCEL); + } + + /** + * Return the target page specified in the request. + *

The default implementation delegates to {@link #getTargetPage(PortletRequest, int)}. + * Subclasses can override this for customized target page determination. + * @param request current portlet request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @param currentPage the current page, to be returned as fallback + * if no target page specified + * @return the page specified in the request, or current page if not found + * @see #getTargetPage(PortletRequest, int) + */ + protected int getTargetPage(PortletRequest request, Object command, Errors errors, int currentPage) { + return getTargetPage(request, currentPage); + } + + /** + * Return the target page specified in the request. + *

The default implementation examines "_target" parameter (e.g. "_target1"). + * Subclasses can override this for customized target page determination. + * @param request current portlet request + * @param currentPage the current page, to be returned as fallback + * if no target page specified + * @return the page specified in the request, or current page if not found + * @see #PARAM_TARGET + */ + protected int getTargetPage(PortletRequest request, int currentPage) { + return PortletUtils.getTargetPage(request, PARAM_TARGET, currentPage); + } + + /** + * Validate all pages and process finish. + * If there are page validation errors, show the corresponding view page. + * @see #validatePagesAndFinish + */ + private ModelAndView renderValidatePagesAndFinish( + RenderRequest request, RenderResponse response, Object command, BindException errors, int currentPage) + throws Exception { + + // In case of any errors -> show current page. + if (errors.hasErrors()) + return showPage(request, errors, currentPage); + + // No remaining errors -> proceed with finish. + return renderFinish(request, response, command, errors); + } + + /** + * Validate all pages and process finish. + * If there are page validation errors, show the corresponding view page. + * @see #renderValidatePagesAndFinish + */ + private void validatePagesAndFinish( + ActionRequest request, ActionResponse response, Object command, BindException errors, int currentPage) + throws Exception { + + // In case of binding errors -> show current page. + if (errors.hasErrors()) { + setPageRenderParameter(response, currentPage); + passRenderParameters(request, response); + return; + } + + if (!suppressValidation(request)) { + // In case of remaining errors on a page -> show the page. + for (int page = 0; page < getPageCount(request, command); page++) { + validatePage(command, errors, page, true); + if (errors.hasErrors()) { + setPageRenderParameter(response, currentPage); + passRenderParameters(request, response); + return; + } + } + } + + // No remaining errors -> proceed with finish. + if (!isRedirectAction()) + setPageRenderParameter(response, currentPage); + processFinish(request, response, command, errors); + + } + + /** + * Template method for custom validation logic for individual pages. + * The default implementation calls validatePage(command, errors, page). + *

Implementations will typically call fine-granular validateXXX + * methods of this instance's Validator, combining them to validation of the + * corresponding pages. The Validator's default validate method + * will not be called by a wizard form controller! + * @param command form object with the current wizard state + * @param errors validation errors holder + * @param page number of page to validate + * @param finish whether this method is called during final revalidation on finish + * (else, it is called for validating the current page) + * @see #validatePage(Object, Errors, int) + * @see org.springframework.validation.Validator#validate + */ + protected void validatePage(Object command, Errors errors, int page, boolean finish) { + validatePage(command, errors, page); + } + + /** + * Template method for custom validation logic for individual pages. + * The default implementation is empty. + *

Implementations will typically call fine-granular validateXXX methods of this + * instance's validator, combining them to validation of the corresponding pages. + * The validator's default validate method will not be called by a + * wizard form controller! + * @param command form object with the current wizard state + * @param errors validation errors holder + * @param page number of page to validate + * @see org.springframework.validation.Validator#validate + */ + protected void validatePage(Object command, Errors errors, int page) { + } + + /** + * Post-process the given page after binding and validation, potentially + * updating its command object. The passed-in request might contain special + * parameters sent by the page. + *

Only invoked when displaying another page or the same page again, + * not when finishing or cancelling. + * @param request current action request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @param page number of page to post-process + * @throws Exception in case of invalid state or arguments + */ + protected void postProcessPage(ActionRequest request, Object command, Errors errors, int page) + throws Exception { + } + + /** + * Template method for the render phase of the finish action of this wizard. + *

The default implementation throws a PortletException, saying that a finish + * render request is not supported by this controller. Thus, you do not need to + * implement this template method if you do not need to render after a finish. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param request current portlet render request + * @param response current portlet render response + * @param command form object with the current wizard state + * @param errors validation errors holder + * @return the finish view + * @throws Exception in case of invalid state or arguments + * @see #processFinish + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected ModelAndView renderFinish( + RenderRequest request, RenderResponse response, Object command, BindException errors) + throws Exception { + + throw new PortletException("Wizard form controller class [" + getClass().getName() + "] does not support a finish render request"); + } + + /** + * Template method for the action phase of the finish action of this wizard. + *

The default implementation throws a PortletException, saying that a finish + * action request is not supported by this controller. You will almost certainly + * need to override this method. + * @param request current portlet action request + * @param response current portlet action response + * @param command form object with the current wizard state + * @param errors validation errors holder + * @throws Exception in case of invalid state or arguments + * @see #renderFinish + * @see org.springframework.validation.Errors + */ + protected void processFinish( + ActionRequest request, ActionResponse response, Object command, BindException errors) + throws Exception { + + throw new PortletException( + "Wizard form controller class [" + getClass().getName() + "] does not support a finish action request"); + } + + /** + * Template method for the render phase of the cancel action of this wizard. + *

The default implementation throws a PortletException, saying that a cancel + * render request is not supported by this controller. Thus, you do not need to + * implement this template method if you do not support a cancel operation. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param request current portlet render request + * @param response current portlet render response + * @param command form object with the current wizard state + * @param errors Errors instance containing errors + * @return the cancellation view + * @throws Exception in case of invalid state or arguments + * @see #processCancel + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected ModelAndView renderCancel( + RenderRequest request, RenderResponse response, Object command, BindException errors) + throws Exception { + + throw new PortletException( + "Wizard form controller class [" + getClass().getName() + "] does not support a cancel render request"); + } + + /** + * Template method for the action phase of the cancel action of this wizard. + *

The default implementation throws a PortletException, saying that a cancel + * action request is not supported by this controller. Thus, you do not need to + * implement this template method if you do not support a cancel operation. + * @param request current portlet action request + * @param response current portlet action response + * @param command form object with the current wizard state + * @param errors Errors instance containing errors + * @throws Exception in case of invalid state or arguments + * @see #renderCancel + * @see org.springframework.validation.Errors + */ + protected void processCancel( + ActionRequest request, ActionResponse response, Object command, BindException errors) + throws Exception { + + throw new PortletException( + "Wizard form controller class [" + getClass().getName() + "] does not support a cancel action request"); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/BaseCommandController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/BaseCommandController.java new file mode 100644 index 00000000000..eaa296d20e2 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/BaseCommandController.java @@ -0,0 +1,663 @@ +/* + * 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; + +import javax.portlet.ActionRequest; +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; +import javax.portlet.RenderRequest; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.validation.BindException; +import org.springframework.validation.BindingErrorProcessor; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.web.bind.support.WebBindingInitializer; +import org.springframework.web.portlet.bind.PortletRequestDataBinder; +import org.springframework.web.portlet.context.PortletWebRequest; +import org.springframework.web.portlet.handler.PortletSessionRequiredException; + +/** + *

Controller implementation which creates an object (the command object) on + * receipt of a request and attempts to populate this object with request parameters.

+ * + *

This controller is the base for all controllers wishing to populate + * JavaBeans based on request parameters, validate the content of such + * JavaBeans using {@link Validator Validators} and use custom editors (in the form of + * {@link java.beans.PropertyEditor PropertyEditors}) to transform + * objects into strings and vice versa, for example. Three notions are mentioned here:

+ * + *

Command class:
+ * An instance of the command class will be created for each request and populated + * with request parameters. A command class can basically be any Java class; the only + * requirement is a no-arg constructor. The command class should preferably be a + * JavaBean in order to be able to populate bean properties with request parameters.

+ * + *

Populating using request parameters and PropertyEditors:
+ * Upon receiving a request, any BaseCommandController will attempt to fill the + * command object using the request parameters. This is done using the typical + * and well-known JavaBeans property notation. When a request parameter named + * 'firstName' exists, the framework will attempt to call + * setFirstName([value]) passing the value of the parameter. Nested properties + * are of course supported. For instance a parameter named 'address.city' + * will result in a getAddress().setCity([value]) call on the + * command class.

+ * + *

It's important to realize that you are not limited to String arguments in + * your JavaBeans. Using the PropertyEditor-notion as supplied by the + * java.beans package, you will be able to transform Strings to Objects and + * the other way around. For instance setLocale(Locale loc) is + * perfectly possible for a request parameter named locale having + * a value of en, as long as you register the appropriate + * PropertyEditor in the Controller (see {@link #initBinder initBinder()} + * for more information on that matter).

+ * + *

Validators: + * After the controller has successfully populated the command object with + * parameters from the request, it will use any configured validators to + * validate the object. Validation results will be put in a + * {@link org.springframework.validation.Errors Errors} object which can be + * used in a View to render any input problems.

+ * + *

Workflow + * (and that defined by superclass):
+ * Since this class is an abstract base class for more specific implementation, + * it does not override the handleRequestInternal() methods and also has no + * actual workflow. Implementing classes like + * {@link AbstractFormController AbstractFormController}, + * {@link AbstractCommandController AbstractCommandController}, + * {@link SimpleFormController SimpleFormController} and + * {@link AbstractWizardFormController AbstractWizardFormController} + * provide actual functionality and workflow. + * More information on workflow performed by superclasses can be found + * here.

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
name + * defaultdescription
commandNamecommandthe name to use when binding the instantiated command class + * to the request
commandClassnullthe class to use upon receiving a request and which to fill + * using the request parameters. What object is used and whether + * or not it should be created is defined by extending classes + * and their configuration properties and methods.
validatorsnullArray of Validator beans. The validator will be called at appropriate + * places in the workflow of subclasses (have a look at those for more info) + * to validate the command object.
validatornullShort-form property for setting only one Validator bean (usually passed in + * using a <ref bean="beanId"/> property.
validateOnBindingtrueIndicates whether or not to validate the command object after the + * object has been populated with request parameters.
+ *

+ * + *

Thanks to Rainer Schmitz and Nick Lothian for their suggestions! + * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + */ +public abstract class BaseCommandController extends AbstractController { + + /** + * Unlike the servlet version of these classes, we have to deal with the + * two-phase nature of the portlet request. To do this, we need to pass + * forward the command object and the bind/validation errors that occured + * on the command object from the action phase to the render phase. + * The only direct way to pass things forward and preserve them for each + * render request is through render parameters, but these are limited to + * String objects and we need to pass more complicated objects. The only + * other way to do this is in the session. The bad thing about using the + * session is that we have no way of knowing when we are done re-rendering + * the request and so we don't know when we can remove the objects from + * the session. So we will end up polluting the session with old objects + * when we finally leave the render of this controller and move on to + * somthing else. To minimize the pollution, we will use a static string + * value as the session attribute name. At least this way we are only ever + * leaving one orphaned set behind. The methods that return these names + * can be overridden if you want to use a different method, but be aware + * of the session pollution that may occur. + */ + private static final String RENDER_COMMAND_SESSION_ATTRIBUTE = + "org.springframework.web.portlet.mvc.RenderCommand"; + + private static final String RENDER_ERRORS_SESSION_ATTRIBUTE = + "org.springframework.web.portlet.mvc.RenderErrors"; + + public static final String DEFAULT_COMMAND_NAME = "command"; + + + private String commandName = DEFAULT_COMMAND_NAME; + + private Class commandClass; + + private Validator[] validators; + + private boolean validateOnBinding = true; + + private MessageCodesResolver messageCodesResolver; + + private BindingErrorProcessor bindingErrorProcessor; + + private PropertyEditorRegistrar[] propertyEditorRegistrars; + + private WebBindingInitializer webBindingInitializer; + + + /** + * Set the name of the command in the model. + * The command object will be included in the model under this name. + */ + public final void setCommandName(String commandName) { + this.commandName = commandName; + } + + /** + * Return the name of the command in the model. + */ + public final String getCommandName() { + return this.commandName; + } + + /** + * Set the command class for this controller. + * An instance of this class gets populated and validated on each request. + */ + public final void setCommandClass(Class commandClass) { + this.commandClass = commandClass; + } + + /** + * Return the command class for this controller. + */ + public final Class getCommandClass() { + return this.commandClass; + } + + /** + * Set the primary Validator for this controller. The Validator + * must support the specified command class. If there are one + * or more existing validators set already when this method is + * called, only the specified validator will be kept. Use + * {@link #setValidators(Validator[])} to set multiple validators. + */ + public final void setValidator(Validator validator) { + this.validators = new Validator[] {validator}; + } + + /** + * @return the primary Validator for this controller. + */ + public final Validator getValidator() { + return (this.validators != null && this.validators.length > 0 ? this.validators[0] : null); + } + + /** + * Set the Validators for this controller. + * The Validator must support the specified command class. + */ + public final void setValidators(Validator[] validators) { + this.validators = validators; + } + + /** + * Return the Validators for this controller. + */ + public final Validator[] getValidators() { + return this.validators; + } + + /** + * Set if the Validator should get applied when binding. + */ + public final void setValidateOnBinding(boolean validateOnBinding) { + this.validateOnBinding = validateOnBinding; + } + + /** + * Return if the Validator should get applied when binding. + */ + public final boolean isValidateOnBinding() { + return this.validateOnBinding; + } + + /** + * Set the strategy to use for resolving errors into message codes. + * Applies the given strategy to all data binders used by this controller. + *

Default is null, i.e. using the default strategy of the data binder. + * @see #createBinder + * @see org.springframework.validation.DataBinder#setMessageCodesResolver + */ + public final void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + } + + /** + * Return the strategy to use for resolving errors into message codes (if any). + */ + public final MessageCodesResolver getMessageCodesResolver() { + return this.messageCodesResolver; + } + + /** + * Set the strategy to use for processing binding errors, that is, + * required field errors and PropertyAccessExceptions. + *

Default is null, i.e. using the default strategy of + * the data binder. + * @see #createBinder + * @see org.springframework.validation.DataBinder#setBindingErrorProcessor + */ + public final void setBindingErrorProcessor(BindingErrorProcessor bindingErrorProcessor) { + this.bindingErrorProcessor = bindingErrorProcessor; + } + + /** + * Return the strategy to use for processing binding errors (if any). + */ + public final BindingErrorProcessor getBindingErrorProcessor() { + return this.bindingErrorProcessor; + } + + /** + * Specify a single PropertyEditorRegistrar to be applied + * to every DataBinder that this controller uses. + *

Allows for factoring out the registration of PropertyEditors + * to separate objects, as an alternative to initBinder. + * @see #initBinder + */ + public final void setPropertyEditorRegistrar(PropertyEditorRegistrar propertyEditorRegistrar) { + this.propertyEditorRegistrars = new PropertyEditorRegistrar[] {propertyEditorRegistrar}; + } + + /** + * Specify one or more PropertyEditorRegistrars to be applied + * to every DataBinder that this controller uses. + *

Allows for factoring out the registration of PropertyEditors + * to separate objects, as alternative to initBinder. + * @see #initBinder + */ + public final void setPropertyEditorRegistrars(PropertyEditorRegistrar[] propertyEditorRegistrars) { + this.propertyEditorRegistrars = propertyEditorRegistrars; + } + + /** + * Return the PropertyEditorRegistrars (if any) to be applied + * to every DataBinder that this controller uses. + */ + public final PropertyEditorRegistrar[] getPropertyEditorRegistrars() { + return this.propertyEditorRegistrars; + } + + /** + * Specify a WebBindingInitializer which will apply pre-configured + * configuration to every DataBinder that this controller uses. + *

Allows for factoring out the entire binder configuration + * to separate objects, as an alternative to {@link #initBinder}. + */ + public final void setWebBindingInitializer(WebBindingInitializer webBindingInitializer) { + this.webBindingInitializer = webBindingInitializer; + } + + /** + * Return the WebBindingInitializer (if any) which will apply pre-configured + * configuration to every DataBinder that this controller uses. + */ + public final WebBindingInitializer getWebBindingInitializer() { + return this.webBindingInitializer; + } + + + protected void initApplicationContext() { + if (this.validators != null) { + for (int i = 0; i < this.validators.length; i++) { + if (this.commandClass != null && !this.validators[i].supports(this.commandClass)) + throw new IllegalArgumentException("Validator [" + this.validators[i] + + "] does not support command class [" + + this.commandClass.getName() + "]"); + } + } + } + + + /** + * Retrieve a command object for the given request. + *

The default implementation calls {@link #createCommand()}. + * Subclasses can override this. + * @param request current portlet request + * @return object command to bind onto + * @see #createCommand + */ + protected Object getCommand(PortletRequest request) throws Exception { + return createCommand(); + } + + /** + * Create a new command instance for the command class of this controller. + *

This implementation uses BeanUtils.instantiateClass, + * so the command needs to have a no-arg constructor (supposed to be + * public, but not required to). + * @return the new command instance + * @throws Exception if the command object could not be instantiated + * @see org.springframework.beans.BeanUtils#instantiateClass(Class) + */ + protected final Object createCommand() throws Exception { + if (this.commandClass == null) { + throw new IllegalStateException("Cannot create command without commandClass being set - " + + "either set commandClass or (in a form controller) override formBackingObject"); + } + if (logger.isDebugEnabled()) { + logger.debug("Creating new command of class [" + this.commandClass.getName() + "]"); + } + return BeanUtils.instantiateClass(this.commandClass); + } + + /** + * Check if the given command object is a valid for this controller, + * i.e. its command class. + * @param command the command object to check + * @return if the command object is valid for this controller + */ + protected final boolean checkCommand(Object command) { + return (this.commandClass == null || this.commandClass.isInstance(command)); + } + + + /** + * Bind the parameters of the given request to the given command object. + * @param request current portlet request + * @param command the command to bind onto + * @return the PortletRequestDataBinder instance for additional custom validation + * @throws Exception in case of invalid state or arguments + */ + protected final PortletRequestDataBinder bindAndValidate(PortletRequest request, Object command) + throws Exception { + + PortletRequestDataBinder binder = createBinder(request, command); + if (!suppressBinding(request)) { + binder.bind(request); + BindException errors = new BindException(binder.getBindingResult()); + onBind(request, command, errors); + if (this.validators != null && isValidateOnBinding() && !suppressValidation(request)) { + for (int i = 0; i < this.validators.length; i++) { + ValidationUtils.invokeValidator(this.validators[i], command, errors); + } + } + onBindAndValidate(request, command, errors); + } + return binder; + } + + /** + * Return whether to suppress binding for the given request. + *

The default implementation always returns false. + * Can be overridden in subclasses to suppress validation: + * for example, if a special request parameter is set. + * @param request current portlet request + * @return whether to suppress binding for the given request + * @see #suppressValidation + */ + protected boolean suppressBinding(PortletRequest request) { + return false; + } + + /** + * Create a new binder instance for the given command and request. + *

Called by bindAndValidate. Can be overridden to plug in + * custom PortletRequestDataBinder instances. + *

The default implementation creates a standard PortletRequestDataBinder and + * invokes prepareBinder and initBinder. + *

Note that neither prepareBinder nor initBinder + * will be invoked automatically if you override this method! Call those methods + * at appropriate points of your overridden method. + * @param request current portlet request + * @param command the command to bind onto + * @return the new binder instance + * @throws Exception in case of invalid state or arguments + * @see #bindAndValidate + * @see #prepareBinder + * @see #initBinder + */ + protected PortletRequestDataBinder createBinder(PortletRequest request, Object command) + throws Exception { + + PortletRequestDataBinder binder = new PortletRequestDataBinder(command, getCommandName()); + prepareBinder(binder); + initBinder(request, binder); + return binder; + } + + /** + * Prepare the given binder, applying the specified MessageCodesResolver, + * BindingErrorProcessor and PropertyEditorRegistrars (if any). + * Called by createBinder. + * @param binder the new binder instance + * @see #createBinder + * @see #setMessageCodesResolver + * @see #setBindingErrorProcessor + */ + protected final void prepareBinder(PortletRequestDataBinder binder) { + if (useDirectFieldAccess()) { + binder.initDirectFieldAccess(); + } + if (this.messageCodesResolver != null) { + binder.setMessageCodesResolver(this.messageCodesResolver); + } + if (this.bindingErrorProcessor != null) { + binder.setBindingErrorProcessor(this.bindingErrorProcessor); + } + if (this.propertyEditorRegistrars != null) { + for (int i = 0; i < this.propertyEditorRegistrars.length; i++) { + this.propertyEditorRegistrars[i].registerCustomEditors(binder); + } + } + } + + /** + * Determine whether to use direct field access instead of bean property access. + * Applied by prepareBinder. + *

The default is false. Can be overridden in subclasses. + * @see #prepareBinder + * @see org.springframework.validation.DataBinder#initDirectFieldAccess() + */ + protected boolean useDirectFieldAccess() { + return false; + } + + /** + * Initialize the given binder instance, for example with custom editors. + * Called by createBinder. + *

This method allows you to register custom editors for certain fields of your + * command class. For instance, you will be able to transform Date objects into a + * String pattern and back, in order to allow your JavaBeans to have Date properties + * and still be able to set and display them in an HTML interface. + *

The default implementation is empty. + * @param request current portlet request + * @param binder new binder instance + * @throws Exception in case of invalid state or arguments + * @see #createBinder + * @see org.springframework.validation.DataBinder#registerCustomEditor + * @see org.springframework.beans.propertyeditors.CustomDateEditor + */ + protected void initBinder(PortletRequest request, PortletRequestDataBinder binder) throws Exception { + if (this.webBindingInitializer != null) { + this.webBindingInitializer.initBinder(binder, new PortletWebRequest(request)); + } + } + + /** + * Callback for custom post-processing in terms of binding. + * Called on each submit, after standard binding but before validation. + *

The default implementation delegates to onBind(request, command). + * @param request current portlet request + * @param command the command object to perform further binding on + * @param errors validation errors holder, allowing for additional + * custom registration of binding errors + * @throws Exception in case of invalid state or arguments + * @see #bindAndValidate + * @see #onBind(PortletRequest, Object) + */ + protected void onBind(PortletRequest request, Object command, BindException errors) throws Exception { + onBind(request, command); + } + + /** + * Callback for custom post-processing in terms of binding. + * Called by the default implementation of the onBind version with + * all parameters, after standard binding but before validation. + *

The default implementation is empty. + * @param request current portlet request + * @param command the command object to perform further binding on + * @throws Exception in case of invalid state or arguments + * @see #onBind(PortletRequest, Object, BindException) + */ + protected void onBind(PortletRequest request, Object command) throws Exception { + } + + /** + * Return whether to suppress validation for the given request. + *

The default implementation always returns false. + * Can be overridden in subclasses to suppress validation: + * for example, if a special request parameter is set. + * @param request current portlet request + * @return whether to suppress validation for the given request + */ + protected boolean suppressValidation(PortletRequest request) { + return false; + } + + /** + * Callback for custom post-processing in terms of binding and validation. + * Called on each submit, after standard binding and validation, + * but before error evaluation. + *

The default implementation is empty. + * @param request current portlet request + * @param command the command object, still allowing for further binding + * @param errors validation errors holder, allowing for additional + * custom validation + * @throws Exception in case of invalid state or arguments + * @see #bindAndValidate + * @see org.springframework.validation.Errors + */ + protected void onBindAndValidate(PortletRequest request, Object command, BindException errors) + throws Exception { + } + + + /** + * Return the name of the session attribute that holds + * the render phase command object for this form controller. + * @return the name of the render phase command object session attribute + * @see javax.portlet.PortletSession#getAttribute + */ + protected String getRenderCommandSessionAttributeName() { + return RENDER_COMMAND_SESSION_ATTRIBUTE; + } + + /** + * Return the name of the session attribute that holds + * the render phase command object for this form controller. + * @return the name of the render phase command object session attribute + * @see javax.portlet.PortletSession#getAttribute + */ + protected String getRenderErrorsSessionAttributeName() { + return RENDER_ERRORS_SESSION_ATTRIBUTE; + } + + /** + * Get the command object cached for the render phase. + * @see #getRenderErrors + * @see #getRenderCommandSessionAttributeName + * @see #setRenderCommandAndErrors + */ + protected final Object getRenderCommand(RenderRequest request) throws PortletException { + PortletSession session = request.getPortletSession(false); + if (session == null) { + throw new PortletSessionRequiredException("Could not obtain portlet session"); + } + Object command = session.getAttribute(getRenderCommandSessionAttributeName()); + if (command == null) { + throw new PortletSessionRequiredException("Could not obtain command object from portlet session"); + } + return command; + } + + /** + * Get the bind and validation errors cached for the render phase. + * @see #getRenderCommand + * @see #getRenderErrorsSessionAttributeName + * @see #setRenderCommandAndErrors + */ + protected final BindException getRenderErrors(RenderRequest request) throws PortletException { + PortletSession session = request.getPortletSession(false); + if (session == null) { + throw new PortletSessionRequiredException("Could not obtain portlet session"); + } + BindException errors = (BindException) session.getAttribute(getRenderErrorsSessionAttributeName()); + if (errors == null) { + throw new PortletSessionRequiredException("Could not obtain errors object from portlet session"); + } + return errors; + } + + /** + * Set the command object and errors object for the render phase. + * @param request the current action request + * @param command the command object to preserve for the render phase + * @param errors the errors from binding and validation to preserve for the render phase + * @see #getRenderCommand + * @see #getRenderErrors + * @see #getRenderCommandSessionAttributeName + * @see #getRenderErrorsSessionAttributeName + */ + protected final void setRenderCommandAndErrors( + ActionRequest request, Object command, BindException errors) throws Exception { + + logger.debug("Storing command and error objects in session for render phase"); + PortletSession session = request.getPortletSession(); + session.setAttribute(getRenderCommandSessionAttributeName(), command); + session.setAttribute(getRenderErrorsSessionAttributeName(), errors); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/Controller.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/Controller.java new file mode 100644 index 00000000000..bfb49294be0 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/Controller.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2006 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; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.web.portlet.ModelAndView; + +/** + * Base portlet Controller interface, representing a component that receives + * RenderRequest/RenderResponse and ActionRequest/ActionResponse like a + * Portlet but is able to participate in an MVC workflow. + * + *

Any implementation of the portlet Controller interface should be a + * reusable, threadsafe class, capable of handling multiple + * portlet requests throughout the lifecycle of an application. To be able to + * configure Controller(s) in an easy way, Controllers are usually JavaBeans.

+ * + *

Workflow:

+ * + *

After the DispatcherPortlet has received a request and has done its work + * to resolve locales, themes and suchlike, it tries to resolve a + * Controller to handle that request, using a + * {@link org.springframework.web.portlet.HandlerMapping HandlerMapping}. + * When a Controller has been found, the + * {@link #handleRenderRequest handleRenderRequest} or {@link #handleActionRequest handleActionRequest} + * method will be invoked, which is responsible for handling the actual + * request and - if applicable - returning an appropriate ModelAndView. + * So actually, these method are the main entrypoint for the + * {@link org.springframework.web.portlet.DispatcherPortlet DispatcherPortlet} + * which delegates requests to controllers. These method - and also this interface - + * should preferrably not be implemented by custom controllers directly, since + * abstract controllers also provided by this package already provide a lot of + * functionality for typical use cases in portlet applications. A few examples of + * those controllers: + * {@link AbstractController AbstractController}, + * {@link AbstractCommandController AbstractCommandController}, + * {@link AbstractFormController AbstractFormController}, + * {@link SimpleFormController SimpleFormController}.

+ * + *

So basically any direct implementation of the Controller interface + * just handles RenderRequests/ActionRequests and should return a ModelAndView, to be + * further used by the DispatcherPortlet. Any additional functionality such as + * optional validation, form handling, etc should be obtained through extending + * one of the abstract controller classes mentioned above.

+ * + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @since 2.0 + * @see SimpleControllerHandlerAdapter + * @see AbstractController + * @see AbstractCommandController + * @see AbstractFormController + * @see SimpleFormController + * @see org.springframework.context.ApplicationContextAware + * @see org.springframework.context.ResourceLoaderAware + * @see org.springframework.web.portlet.context.PortletContextAware + */ +public interface Controller { + + /** + * Process the action request. There is nothing to return. + * @param request current portlet action request + * @param response current portlet action response + * @throws Exception in case of errors + */ + void handleActionRequest(ActionRequest request, ActionResponse response) throws Exception; + + /** + * Process the render request and return a ModelAndView object which the DispatcherPortlet + * will render. A null return value is not an error: It indicates that this + * object completed request processing itself, thus there is no ModelAndView to render. + * @param request current portlet render request + * @param response current portlet render response + * @return a ModelAndView to render, or null if handled directly + * @throws Exception in case of errors + */ + ModelAndView handleRenderRequest(RenderRequest request, RenderResponse response) throws Exception; + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/ParameterizableViewController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/ParameterizableViewController.java new file mode 100644 index 00000000000..7c93b6ec733 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/ParameterizableViewController.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2007 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; + +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.web.portlet.ModelAndView; + +/** + *

Trivial controller that always returns a named view. The view + * can be configured using an exposed configuration property. This + * controller offers an alternative to sending a request straight to a view + * such as a JSP. The advantage here is that the client is not exposed to + * the concrete view technology but rather just to the controller URL; + * the concrete view will be determined by the ViewResolver.

+ * + *

Workflow + * (and that defined by superclass):
+ *

    + *
  1. Render request is received by the controller
  2. + *
  3. call to {@link #handleRenderRequestInternal handleRenderRequestInternal} which + * just returns the view, named by the configuration property + * viewName. Nothing more, nothing less
  4. + *
+ *

+ * + *

This controller does not handle action requests.

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * + * + * + * + * + * + * + * + * + * + * + *
namedefaultdescription
viewNamenullthe name of the view the viewResolver will use to forward to + * (if this property is not set, an exception will be thrown during + * initialization)
+ *

+ * + * @author John A. Lewis + * @since 2.0 + */ +public class ParameterizableViewController extends AbstractController { + + private String viewName; + + + /** + * Set the name of the view to delegate to. + */ + public void setViewName(String viewName) { + this.viewName = viewName; + } + + /** + * Return the name of the view to delegate to. + */ + public String getViewName() { + return this.viewName; + } + + protected void initApplicationContext() { + if (this.viewName == null) { + throw new IllegalArgumentException("Property 'viewName' is required"); + } + } + + + /** + * Return a ModelAndView object with the specified view name. + */ + protected ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response) + throws Exception { + + return new ModelAndView(getViewName()); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/PortletModeNameViewController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/PortletModeNameViewController.java new file mode 100644 index 00000000000..b6e9217d64e --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/PortletModeNameViewController.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2006 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; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletException; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.web.portlet.ModelAndView; + +/** + *

Trivial controller that transforms the PortletMode to a view name. + * The advantage here is that the client is not exposed to + * the concrete view technology but rather just to the controller URL; + * the concrete view will be determined by the ViewResolver.

+ * + *

Example: PortletMode.VIEW -> "view"

+ * + *

This controller does not handle action requests.

+ * + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @since 2.0 + */ +public class PortletModeNameViewController implements Controller { + + public void handleActionRequest(ActionRequest request, ActionResponse response) throws Exception { + throw new PortletException("PortletModeNameViewController does not handle action requests"); + } + + public ModelAndView handleRenderRequest(RenderRequest request, RenderResponse response) { + return new ModelAndView(request.getPortletMode().toString()); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/PortletWrappingController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/PortletWrappingController.java new file mode 100644 index 00000000000..470bdfb8339 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/PortletWrappingController.java @@ -0,0 +1,208 @@ +/* + * Copyright 2002-2006 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; + +import java.util.Enumeration; +import java.util.Locale; +import java.util.Properties; +import java.util.ResourceBundle; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.Portlet; +import javax.portlet.PortletConfig; +import javax.portlet.PortletContext; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.web.portlet.context.PortletConfigAware; +import org.springframework.web.portlet.context.PortletContextAware; + +/** + * {@link Controller} implementation that wraps a portlet instance which it manages + * internally. Such a wrapped portlet is not known outside of this controller; + * its entire lifecycle is covered here. + * + *

Useful to invoke an existing portlet via Spring's dispatching infrastructure, + * for example to apply Spring + * {@link org.springframework.web.portlet.HandlerInterceptor HandlerInterceptors} + * to its requests. + * + *

Example: + * + *

<bean id="wrappingController" class="org.springframework.web.portlet.mvc.PortletWrappingController">
+ *   <property name="portletClass">
+ *     <value>org.springframework.web.portlet.sample.HelloWorldPortlet</value>
+ *   </property>
+ *   <property name="portletName">
+ *     <value>hello-world</value>
+ *   </property>
+ *   <property name="initParameters">
+ *     <props>
+ *       <prop key="config">/WEB-INF/hello-world-portlet-config.xml</prop>
+ *     </props>
+ *   </property>
+ * </bean>
+ * + * @author Juergen Hoeller + * @author John A. Lewis + * @since 2.0 + */ +public class PortletWrappingController extends AbstractController + implements BeanNameAware, InitializingBean, DisposableBean, PortletContextAware, PortletConfigAware { + + private boolean useSharedPortletConfig = true; + + private PortletContext portletContext; + + private PortletConfig portletConfig; + + private Class portletClass; + + private String portletName; + + private Properties initParameters = new Properties(); + + private String beanName; + + private Portlet portletInstance; + + + /** + * Set whether to use the shared PortletConfig object passed in + * through setPortletConfig, if available. + *

Default is "true". Turn this setting to "false" to pass in + * a mock PortletConfig object with the bean name as portlet name, + * holding the current PortletContext. + * @see #setPortletConfig + */ + public void setUseSharedPortletConfig(boolean useSharedPortletConfig) { + this.useSharedPortletConfig = useSharedPortletConfig; + } + + public void setPortletContext(PortletContext portletContext) { + this.portletContext = portletContext; + } + + public void setPortletConfig(PortletConfig portletConfig) { + this.portletConfig = portletConfig; + } + + /** + * Set the class of the Portlet to wrap. + * Needs to implement javax.portlet.Portlet. + * @see javax.portlet.Portlet + */ + public void setPortletClass(Class portletClass) { + this.portletClass = portletClass; + } + + /** + * Set the name of the Portlet to wrap. + * Default is the bean name of this controller. + */ + public void setPortletName(String portletName) { + this.portletName = portletName; + } + + /** + * Specify init parameters for the portlet to wrap, + * as name-value pairs. + */ + public void setInitParameters(Properties initParameters) { + this.initParameters = initParameters; + } + + public void setBeanName(String name) { + this.beanName = name; + } + + + public void afterPropertiesSet() throws Exception { + if (this.portletClass == null) { + throw new IllegalArgumentException("portletClass is required"); + } + if (!Portlet.class.isAssignableFrom(this.portletClass)) { + throw new IllegalArgumentException("portletClass [" + this.portletClass.getName() + + "] needs to implement interface [javax.portlet.Portlet]"); + } + if (this.portletName == null) { + this.portletName = this.beanName; + } + PortletConfig config = this.portletConfig; + if (config == null || !this.useSharedPortletConfig) { + config = new DelegatingPortletConfig(); + } + this.portletInstance = (Portlet) this.portletClass.newInstance(); + this.portletInstance.init(config); + } + + + protected void handleActionRequestInternal( + ActionRequest request, ActionResponse response) throws Exception { + + this.portletInstance.processAction(request, response); + } + + protected ModelAndView handleRenderRequestInternal( + RenderRequest request, RenderResponse response) throws Exception { + + this.portletInstance.render(request, response); + return null; + } + + + public void destroy() { + this.portletInstance.destroy(); + } + + + /** + * Internal implementation of the PortletConfig interface, to be passed + * to the wrapped portlet. + *

Delegates to {@link PortletWrappingController} fields + * and methods to provide init parameters and other environment info. + */ + private class DelegatingPortletConfig implements PortletConfig { + + public String getPortletName() { + return portletName; + } + + public PortletContext getPortletContext() { + return portletContext; + } + + public String getInitParameter(String paramName) { + return initParameters.getProperty(paramName); + } + + public Enumeration getInitParameterNames() { + return initParameters.keys(); + } + + public ResourceBundle getResourceBundle(Locale locale) { + return (portletConfig != null ? portletConfig.getResourceBundle(locale) : null); + } + + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/SimpleControllerHandlerAdapter.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/SimpleControllerHandlerAdapter.java new file mode 100644 index 00000000000..5eac5c4ae18 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/SimpleControllerHandlerAdapter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2006 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; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.web.portlet.HandlerAdapter; +import org.springframework.web.portlet.ModelAndView; + +/** + * Adapter to use the Controller workflow interface with the generic DispatcherPortlet. + * + *

This is an SPI class, not used directly by application code. + * + * @author John A. Lewis + * @since 2.0 + * @see org.springframework.web.portlet.DispatcherPortlet + * @see Controller + */ +public class SimpleControllerHandlerAdapter implements HandlerAdapter { + + public boolean supports(Object handler) { + return (handler instanceof Controller); + } + + public void handleAction(ActionRequest request, ActionResponse response, Object handler) + throws Exception { + + ((Controller) handler).handleActionRequest(request, response); + } + + public ModelAndView handleRender(RenderRequest request, RenderResponse response, Object handler) + throws Exception { + + return ((Controller) handler).handleRenderRequest(request, response); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/SimpleFormController.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/SimpleFormController.java new file mode 100644 index 00000000000..d290253b73a --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/SimpleFormController.java @@ -0,0 +1,555 @@ +/* + * 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; + +import java.util.Map; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletException; +import javax.portlet.PortletRequest; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.web.portlet.ModelAndView; + +/** + *

Concrete FormController implementation that provides configurable + * form and success views, and an onSubmit chain for convenient overriding. + * Automatically resubmits to the form view in case of validation errors, + * and renders the success view in case of a valid submission.

+ * + *

The workflow of this Controller does not differ much from the one described + * in the {@link AbstractFormController AbstractFormController}. The difference + * is that you do not need to implement {@link #showForm showForm}, + * {@link #processFormSubmission processFormSubmission}, and + * {@link #renderFormSubmission renderFormSubmission}: A form view and a + * success view can be configured declaratively.

+ * + *

This controller is different from it's servlet counterpart in that it must take + * into account the two phases of a portlet request: the action phase and the render + * phase. See the JSR-168 spec for more details on these two phases. + * Be especially aware that the action phase is called only once, but that the + * render phase will be called repeatedly by the portal -- it does this every time + * the page containing the portlet is updated, even if the activity is in some other + * portlet. The main difference in the methods in this class is that the + * onSubmit methods have all been split into onSubmitAction + * and onSubmitRender to account for the two phases.

+ * + *

Workflow + * (in addition to the superclass):
+ *

    + *
  1. Call to {@link #processFormSubmission processFormSubmission} which inspects + * the {@link org.springframework.validation.Errors Errors} object to see if + * any errors have occurred during binding and validation.
  2. + *
  3. If errors occured, the controller will return the configured formView, + * showing the form again (possibly rendering according error messages).
  4. + *
  5. If {@link #isFormChangeRequest isFormChangeRequest} is overridden and returns + * true for the given request, the controller will return the formView too. + * In that case, the controller will also suppress validation. Before returning the formView, + * the controller will invoke {@link #onFormChange}, giving sub-classes a chance + * to make modification to the command object. + * This is intended for requests that change the structure of the form, + * which should not cause validation and show the form in any case.
  6. + *
  7. If no errors occurred, the controller will call + * {@link #onSubmitAction(ActionRequest, ActionResponse, Object, BindException) onSubmitAction} + * during the action phase and then {@link #onSubmitRender(RenderRequest, RenderResponse, + * Object, BindException) onSubmitRender} during the render phase, which in case of the + * default implementation delegate to {@link #onSubmitAction(Object, BindException) + * onSubmitAction} and {@link #onSubmitRender(Object, BindException) onSubmitRender} + * with just the command object. + * The default implementation of the latter method will return the configured + * successView. Consider just implementing {@link #doSubmitAction doSubmitAction} + * for simply performing a submit action during the action phase and then rendering + * the success view during the render phase.
  8. + *
+ *

+ * + *

The submit behavior can be customized by overriding one of the + * {@link #onSubmitAction onSubmitAction} or {@link #onSubmitRender onSubmitRender} + * methods. Submit actions can also perform custom validation if necessary + * (typically database-driven checks), calling {@link #showForm(RenderRequest, + * RenderResponse, BindException) showForm} in case of validation errors to show + * the form view again. You do not have to override both the onSubmitAction and + * onSubmitRender methods at a given level unless you truly have custom logic to + * perform in both.

+ * + *

WARNING: Make sure that any one-time system updates (such as database + * updates or file writes) are performed in either an {@link #onSubmitAction onSubmitAction} + * method or the {@link #doSubmitAction doSubmitAction} method. Logic in the + * {@link #onSubmitRender onSubmitRender} methods may be executed repeatedly by + * the portal whenever the page containing the portlet is updated.

+ * + *

Exposed configuration properties + * (and those defined by superclass):
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
namedefaultdescription
formViewnullIndicates what view to use when the user asks for a new form + * or when validation errors have occurred on form submission.
successViewnullIndicates what view to use when successful form submissions have + * occurred. Such a success view could e.g. display a submission summary. + * More sophisticated actions can be implemented by overriding one of + * the {@link #onSubmitRender(Object) onSubmitRender()} methods.
+ *

+ * + *

Parameters indicated with setPassRenderParameters will be + * preserved if the form has errors or if a form change request occurs. + * If there are render parameters you need in onSubmitRender, + * then you need to pass those forward from onSubmitAction. + * + *

Thanks to Rainer Schmitz and Nick Lothian for their suggestions! + * + * @author John A. Lewis + * @author Juergen Hoeller + * @author Rob Harrop + * @since 2.0 + */ +public class SimpleFormController extends AbstractFormController { + + private String formView; + + private String successView; + + + /** + * Create a new SimpleFormController. + *

Subclasses should set the following properties, either in the constructor + * or via a BeanFactory: commandName, commandClass, sessionForm, formView, + * successView. Note that commandClass doesn't need to be set when overriding + * formBackingObject, as this determines the class anyway. + * @see #setCommandClass(Class) + * @see #setCommandName(String) + * @see #setSessionForm(boolean) + * @see #setFormView + * @see #setSuccessView + * @see #formBackingObject(PortletRequest) + */ + public SimpleFormController() { + // AbstractFormController sets default cache seconds to 0. + super(); + } + + /** + * Set the name of the view that should be used for form display. + */ + public final void setFormView(String formView) { + this.formView = formView; + } + + /** + * Return the name of the view that should be used for form display. + */ + public final String getFormView() { + return this.formView; + } + + /** + * Set the name of the view that should be shown on successful submit. + */ + public final void setSuccessView(String successView) { + this.successView = successView; + } + + /** + * Return the name of the view that should be shown on successful submit. + */ + public final String getSuccessView() { + return this.successView; + } + + + /** + * This implementation shows the configured form view, delegating to the + * analogous showForm version with a controlModel argument. + *

Can be called within onSubmit implementations, to redirect back to the form + * in case of custom validation errors (i.e. not determined by the validator). + *

Can be overridden in subclasses to show a custom view, writing directly + * to the response or preparing the response before rendering a view. + *

If calling showForm with a custom control model in subclasses, it's preferable + * to override the analogous showForm version with a controlModel argument + * (which will handle both standard form showing and custom form showing then). + * @see #setFormView + * @see #showForm(RenderRequest, RenderResponse, BindException, Map) + */ + protected ModelAndView showForm(RenderRequest request, RenderResponse response, BindException errors) + throws Exception { + + return showForm(request, response, errors, null); + } + + /** + * This implementation shows the configured form view. + *

Can be called within onSubmit implementations, to redirect back to the form + * in case of custom validation errors (i.e. not determined by the validator). + *

Can be overridden in subclasses to show a custom view, writing directly + * to the response or preparing the response before rendering a view. + * @param request current render request + * @param errors validation errors holder + * @param controlModel model map containing controller-specific control data + * (e.g. current page in wizard-style controllers or special error message) + * @return the prepared form view + * @throws Exception in case of invalid state or arguments + * @see #setFormView + */ + protected ModelAndView showForm(RenderRequest request, RenderResponse response, BindException errors, Map controlModel) + throws Exception { + + return showForm(request, errors, getFormView(), controlModel); + } + + /** + * Create a reference data map for the given request and command, + * consisting of bean name/bean instance pairs as expected by ModelAndView. + *

The default implementation delegates to {@link #referenceData(PortletRequest)}. + * Subclasses can override this to set reference data used in the view. + * @param request current portlet request + * @param command form object with request parameters bound onto it + * @param errors validation errors holder + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see ModelAndView + */ + protected Map referenceData(PortletRequest request, Object command, Errors errors) throws Exception { + return referenceData(request); + } + + /** + * Create a reference data map for the given request. + * Called by referenceData version with all parameters. + *

The default implementation returns null. + * Subclasses can override this to set reference data used in the view. + * @param request current portlet request + * @return a Map with reference data entries, or null if none + * @throws Exception in case of invalid state or arguments + * @see #referenceData(PortletRequest, Object, Errors) + * @see ModelAndView + */ + protected Map referenceData(PortletRequest request) throws Exception { + return null; + } + + + /** + * This implementation calls showForm in case of errors, + * and delegates to onSubmitRender's full version else. + *

This can only be overridden to check for an action that should be executed + * without respect to binding errors, like a cancel action. To just handle successful + * submissions without binding errors, override one of the onSubmitRender + * methods. + * @see #showForm(RenderRequest, RenderResponse, BindException) + * @see #onSubmitRender(RenderRequest, RenderResponse, Object, BindException) + * @see #onSubmitRender(Object, BindException) + * @see #onSubmitRender(Object) + * @see #processFormSubmission(ActionRequest, ActionResponse, Object, BindException) + */ + protected ModelAndView renderFormSubmission(RenderRequest request, RenderResponse response, Object command, BindException errors) + throws Exception { + + if (errors.hasErrors() || isFormChangeRequest(request)) { + return showForm(request, response, errors); + } + else { + return onSubmitRender(request, response, command, errors); + } + } + + /** + * This implementation does nothing in case of errors, + * and delegates to onSubmitAction's full version else. + *

This can only be overridden to check for an action that should be executed + * without respect to binding errors, like a cancel action. To just handle successful + * submissions without binding errors, override one of the onSubmitAction + * methods or doSubmitAction. + * @see #showForm + * @see #onSubmitAction(ActionRequest, ActionResponse, Object, BindException) + * @see #onSubmitAction(Object, BindException) + * @see #onSubmitAction(Object) + * @see #doSubmitAction(Object) + * @see #renderFormSubmission(RenderRequest, RenderResponse, Object, BindException) + */ + protected void processFormSubmission( + ActionRequest request, ActionResponse response, Object command, BindException errors) + throws Exception { + + if (errors.hasErrors()) { + if (logger.isDebugEnabled()) { + logger.debug("Data binding errors: " + errors.getErrorCount()); + } + if (isRedirectAction()) { + setFormSubmit(response); + } + passRenderParameters(request, response); + } + else if (isFormChangeRequest(request)) { + logger.debug("Detected form change request -> routing request to onFormChange"); + if (isRedirectAction()) { + setFormSubmit(response); + } + passRenderParameters(request, response); + onFormChange(request, response, command, errors); + } + else { + logger.debug("No errors - processing submit"); + onSubmitAction(request, response, command, errors); + } + } + + /** + * This implementation delegates to {@link #isFormChangeRequest}: + * A form change request changes the appearance of the form + * and should not get validated but just show the new form. + * @see #isFormChangeRequest + */ + protected boolean suppressValidation(PortletRequest request) { + return isFormChangeRequest(request); + } + + /** + * Determine whether the given request is a form change request. + * A form change request changes the appearance of the form + * and should always show the new form, without validation. + *

Gets called by {@link #suppressValidation} and {@link #processFormSubmission}. + * Consequently, this single method determines to suppress validation + * and to show the form view in any case. + *

The default implementation returns falsetrue. Allows subclasses to implement custom logic + * to modify the command object to directly modify data in the form. + *

The default implementation delegates to + * onFormChange(request, response, command). + * @param request current action request + * @param response current action response + * @param command form object with request parameters bound onto it + * @param errors validation errors holder, allowing for additional + * custom validation + * @throws Exception in case of errors + * @see #isFormChangeRequest(PortletRequest) + * @see #onFormChange(ActionRequest, ActionResponse, Object) + */ + protected void onFormChange(ActionRequest request, ActionResponse response, Object command, BindException errors) + throws Exception { + + onFormChange(request, response, command); + } + + /** + * Simpler onFormChange variant, called by the full version + * onFormChange(request, response, command, errors). + *

The default implementation is empty. + * @param request current action request + * @param response current action response + * @param command form object with request parameters bound onto it + * @throws Exception in case of errors + * @see #onFormChange(ActionRequest, ActionResponse, Object, BindException) + */ + protected void onFormChange(ActionRequest request, ActionResponse response, Object command) + throws Exception { + } + + + /** + * Submit render phase callback with all parameters. Called in case of submit without errors + * reported by the registered validator, or on every submit if no validator. + *

The default implementation delegates to {@link #onSubmitRender(Object, BindException)}. + * For simply performing a submit action and rendering the specified success view, + * do not implement an onSubmitRender at all. + *

Subclasses can override this to provide custom rendering to display results of + * the action phase. Implementations can also call showForm to return to the form + * if the onSubmitAction failed custom validation. Do not implement multiple + * onSubmitRender methods: In that case, + * just this method will be called by the controller. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param request current render request + * @param response current render response + * @param command form object with request parameters bound onto it + * @param errors Errors instance without errors (subclass can add errors if it wants to) + * @return the prepared model and view + * @throws Exception in case of errors + * @see #onSubmitAction(ActionRequest, ActionResponse, Object, BindException) + * @see #onSubmitRender(Object, BindException) + * @see #doSubmitAction + * @see #showForm + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected ModelAndView onSubmitRender(RenderRequest request, RenderResponse response, Object command, BindException errors) + throws Exception { + + return onSubmitRender(command, errors); + } + + /** + * Submit action phase callback with all parameters. Called in case of submit without errors + * reported by the registered validator respectively on every submit if no validator. + *

The default implementation delegates to {@link #onSubmitAction(Object, BindException)}. + * For simply performing a submit action consider implementing doSubmitAction + * rather than an onSubmitAction version. + *

Subclasses can override this to provide custom submission handling like storing + * the object to the database. Implementations can also perform custom validation and + * signal the render phase to call showForm to return to the form. Do not + * implement multiple onSubmitAction methods: In that case, + * just this method will be called by the controller. + * @param request current action request + * @param response current action response + * @param command form object with request parameters bound onto it + * @param errors Errors instance without errors (subclass can add errors if it wants to) + * @throws Exception in case of errors + * @see #onSubmitRender(RenderRequest, RenderResponse, Object, BindException) + * @see #onSubmitAction(Object, BindException) + * @see #doSubmitAction + * @see org.springframework.validation.Errors + */ + protected void onSubmitAction(ActionRequest request, ActionResponse response, Object command, BindException errors) + throws Exception { + + onSubmitAction(command, errors); + } + + /** + * Simpler onSubmitRender version. Called by the default implementation + * of the onSubmitRender version with all parameters. + *

The default implementation calls {@link #onSubmitRender(Object)}, using the + * returned ModelAndView if actually implemented in a subclass. Else, the + * default behavior will apply: rendering the success view with the command + * and Errors instance as model. + *

Subclasses can override this to provide custom submission handling that + * does not need request and response. + *

Call errors.getModel() to populate the ModelAndView model + * with the command and the Errors instance, under the specified command name, + * as expected by the "spring:bind" tag. + * @param command form object with request parameters bound onto it + * @param errors Errors instance without errors + * @return the prepared model and view, or null + * @throws Exception in case of errors + * @see #onSubmitRender(RenderRequest, RenderResponse, Object, BindException) + * @see #onSubmitRender(Object) + * @see #onSubmitAction(Object, BindException) + * @see #setSuccessView + * @see org.springframework.validation.Errors + * @see org.springframework.validation.BindException#getModel + */ + protected ModelAndView onSubmitRender(Object command, BindException errors) throws Exception { + ModelAndView mv = onSubmitRender(command); + if (mv != null) { + // simplest onSubmit version implemented in custom subclass + return mv; + } + else { + // default behavior: render success view + if (getSuccessView() == null) { + throw new PortletException("successView isn't set"); + } + return new ModelAndView(getSuccessView(), errors.getModel()); + } + } + + /** + * Simpler onSubmitAction version. Called by the default implementation + * of the onSubmitAction version with all parameters. + *

The default implementation calls {@link #onSubmitAction(Object)}. + *

Subclasses can override this to provide custom submission handling that + * does not need request and response. + * @param command form object with request parameters bound onto it + * @param errors Errors instance without errors + * @throws Exception in case of errors + * @see #onSubmitAction(ActionRequest, ActionResponse, Object, BindException) + * @see #onSubmitAction(Object) + * @see #onSubmitRender(Object, BindException) + * @see org.springframework.validation.Errors + */ + protected void onSubmitAction(Object command, BindException errors) throws Exception { + onSubmitAction(command); + } + + /** + * Simplest onSubmitRender version. Called by the default implementation + * of the onSubmitRender version with command and BindException parameters. + *

This implementation returns null as ModelAndView, making the calling + * onSubmitRender method perform its default rendering of the success view. + *

Subclasses can override this to provide custom submission handling + * that just depends on the command object. + * @param command form object with request parameters bound onto it + * @return the prepared model and view, or null for default (i.e. successView) + * @throws Exception in case of errors + * @see #onSubmitRender(Object, BindException) + * @see #onSubmitAction(Object) + * @see #doSubmitAction + * @see #setSuccessView + */ + protected ModelAndView onSubmitRender(Object command) throws Exception { + return null; + } + + /** + * Simplest onSubmitAction version. Called by the default implementation + * of the onSubmitAction version with command and BindException parameters. + *

This implementation calls doSubmitAction. + *

Subclasses can override this to provide custom submission handling + * that just depends on the command object. + * @param command form object with request parameters bound onto it + * @throws Exception in case of errors + * @see #onSubmitAction(Object, BindException) + * @see #onSubmitRender(Object) + * @see #doSubmitAction + */ + protected void onSubmitAction(Object command) throws Exception { + doSubmitAction(command); + } + + /** + * Template method for submit actions. Called by the default implementation + * of the simplest onSubmitAction version. + *

This is the preferred submit callback to implement if you want to + * perform an action (like storing changes to the database) and then render + * the success view with the command and Errors instance as model. + * @param command form object with request parameters bound onto it + * @throws Exception in case of errors + * @see #onSubmitAction(Object) + * @see #onSubmitRender(Object) + * @see #setSuccessView + */ + protected void doSubmitAction(Object command) throws Exception { + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java new file mode 100644 index 00000000000..545df19d3f8 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/AnnotationMethodHandlerAdapter.java @@ -0,0 +1,606 @@ +/* + * 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. + * + *

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. + * + *

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, PortletHandlerMethodResolver> methodResolverCache = + new ConcurrentHashMap, 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. + *

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 @SessionAttributes annotated handlers + * for the given number of seconds. Default is 0, preventing caching completely. + *

In contrast to the "cacheSeconds" property which will apply to all general + * handlers (but not to @SessionAttributes annotated handlers), this + * setting will apply to @SessionAttributes 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. + *

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. + *

The session mutex is guaranteed to be the same object during + * the entire lifetime of the session, available under the key defined + * by the SESSION_MUTEX_ATTRIBUTE constant. It serves as a + * safe reference to synchronize on for locking on the current session. + *

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). + *

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. + *

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 null + * 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 targetHandlerMethods = new LinkedHashMap(); + 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)); + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/DefaultAnnotationHandlerMapping.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/DefaultAnnotationHandlerMapping.java new file mode 100644 index 00000000000..4c3db085504 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/DefaultAnnotationHandlerMapping.java @@ -0,0 +1,202 @@ +/* + * 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.lang.reflect.Method; +import java.util.HashSet; +import java.util.Set; + +import javax.portlet.PortletMode; +import javax.portlet.PortletRequest; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.generic.GenericBeanFactoryAccessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Controller; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.portlet.handler.AbstractMapBasedHandlerMapping; + +/** + * Implementation of the {@link org.springframework.web.portlet.HandlerMapping} + * interface that maps handlers based on portlet modes expressed through the + * {@link RequestMapping} annotation at the type or method level. + * + *

Registered by default in {@link org.springframework.web.portlet.DispatcherPortlet} + * on Java 5+. NOTE: If you define custom HandlerMapping beans in your + * DispatcherPortlet context, you need to add a DefaultAnnotationHandlerMapping bean + * explicitly, since custom HandlerMapping beans replace the default mapping strategies. + * Defining a DefaultAnnotationHandlerMapping also allows for registering custom + * interceptors: + * + *

+ * <bean class="org.springframework.web.portlet.mvc.annotation.DefaultAnnotationHandlerMapping">
+ *   <property name="interceptors">
+ *     ...
+ *   </property>
+ * </bean>
+ * + * Annotated controllers are usually marked with the {@link Controller} stereotype + * at the type level. This is not strictly necessary when {@link RequestMapping} is + * applied at the type level (since such a handler usually implements the + * {@link org.springframework.web.portlet.mvc.Controller} interface). However, + * {@link Controller} is required for detecting {@link RequestMapping} annotations + * at the method level. + * + *

NOTE: Method-level mappings are only allowed to narrow the mapping + * expressed at the class level (if any). Portlet modes need to uniquely map onto + * specific handler beans, with any given portlet mode only allowed to be mapped + * onto one specific handler bean (not spread across multiple handler beans). + * It is strongly recommended to co-locate related handler methods into the same bean. + * + *

The {@link AnnotationMethodHandlerAdapter} is responsible for processing + * annotated handler methods, as mapped by this HandlerMapping. For + * {@link RequestMapping} at the type level, specific HandlerAdapters such as + * {@link org.springframework.web.portlet.mvc.SimpleControllerHandlerAdapter} apply. + * + * @author Juergen Hoeller + * @since 2.5 + * @see RequestMapping + * @see AnnotationMethodHandlerAdapter + */ +public class DefaultAnnotationHandlerMapping extends AbstractMapBasedHandlerMapping { + + /** + * Calls the registerHandlers method in addition + * to the superclass's initialization. + * @see #detectHandlers + */ + public void initApplicationContext() throws BeansException { + super.initApplicationContext(); + detectHandlers(); + } + + /** + * Register all handlers specified in the Portlet mode map for the corresponding modes. + * @throws org.springframework.beans.BeansException if the handler couldn't be registered + */ + protected void detectHandlers() throws BeansException { + ApplicationContext context = getApplicationContext(); + String[] beanNames = context.getBeanNamesForType(Object.class); + for (String beanName : beanNames) { + Class handlerType = context.getType(beanName); + ListableBeanFactory bf = (context instanceof ConfigurableApplicationContext ? + ((ConfigurableApplicationContext) context).getBeanFactory() : context); + GenericBeanFactoryAccessor bfa = new GenericBeanFactoryAccessor(bf); + RequestMapping mapping = bfa.findAnnotationOnBean(beanName, RequestMapping.class); + if (mapping != null) { + String[] modeKeys = mapping.value(); + String[] params = mapping.params(); + boolean registerHandlerType = true; + if (modeKeys.length == 0 || params.length == 0) { + registerHandlerType = !detectHandlerMethods(handlerType, beanName, mapping); + } + if (registerHandlerType) { + ParameterMappingPredicate predicate = new ParameterMappingPredicate(params); + for (String modeKey : modeKeys) { + registerHandler(new PortletMode(modeKey), beanName, predicate); + } + } + } + else if (AnnotationUtils.findAnnotation(handlerType, Controller.class) != null) { + detectHandlerMethods(handlerType, beanName, mapping); + } + } + } + + /** + * Derive portlet mode mappings from the handler's method-level mappings. + * @param handlerType the handler type to introspect + * @param beanName the name of the bean introspected + * @param typeMapping the type level mapping (if any) + * @return true if at least 1 handler method has been registered; + * false otherwise + */ + protected boolean detectHandlerMethods(Class handlerType, final String beanName, final RequestMapping typeMapping) { + final Set handlersRegistered = new HashSet(1); + ReflectionUtils.doWithMethods(handlerType, new ReflectionUtils.MethodCallback() { + public void doWith(Method method) { + RequestMapping mapping = method.getAnnotation(RequestMapping.class); + if (mapping != null) { + String[] modeKeys = mapping.value(); + if (modeKeys.length == 0) { + if (typeMapping != null) { + modeKeys = typeMapping.value(); + } + else { + throw new IllegalStateException( + "No portlet mode mappings specified - neither at type nor method level"); + } + } + String[] params = mapping.params(); + if (typeMapping != null) { + PortletAnnotationMappingUtils.validateModeMapping(modeKeys, typeMapping.value()); + params = StringUtils.mergeStringArrays(typeMapping.params(), params); + } + ParameterMappingPredicate predicate = new ParameterMappingPredicate(params); + for (String modeKey : modeKeys) { + registerHandler(new PortletMode(modeKey), beanName, predicate); + handlersRegistered.add(Boolean.TRUE); + } + } + } + }); + return !handlersRegistered.isEmpty(); + } + + /** + * Uses the current PortletMode as lookup key. + */ + protected Object getLookupKey(PortletRequest request) throws Exception { + return request.getPortletMode(); + } + + + /** + * Predicate that matches against parameter conditions. + */ + private static class ParameterMappingPredicate implements PortletRequestMappingPredicate { + + private final String[] params; + + private ParameterMappingPredicate(String[] params) { + this.params = params; + } + + public boolean match(PortletRequest request) { + return PortletAnnotationMappingUtils.checkParameters(this.params, request); + } + + public int compareTo(Object other) { + if (other instanceof PortletRequestMappingPredicate) { + return new Integer(((ParameterMappingPredicate) other).params.length).compareTo(this.params.length); + } + else { + return 0; + } + } + + public String toString() { + return StringUtils.arrayToCommaDelimitedString(this.params); + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/PortletAnnotationMappingUtils.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/PortletAnnotationMappingUtils.java new file mode 100644 index 00000000000..cb8edf7062a --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/PortletAnnotationMappingUtils.java @@ -0,0 +1,86 @@ +/* + * 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 javax.portlet.PortletRequest; + +import org.springframework.util.ObjectUtils; +import org.springframework.web.portlet.util.PortletUtils; + +/** + * Helper class for annotation-based request mapping. + * + * @author Juergen Hoeller + * @since 2.5.2 + */ +abstract class PortletAnnotationMappingUtils { + + /** + * Check whether the given request matches the specified request methods. + * @param modes the mapped portlet modes to check + * @param typeLevelModes the type-level mode mappings to check against + */ + public static boolean validateModeMapping(String[] modes, String[] typeLevelModes) { + if (!ObjectUtils.isEmpty(modes)) { + for (String mode : modes) { + boolean match = false; + for (String typeLevelMode : typeLevelModes) { + if (mode.equalsIgnoreCase(typeLevelMode)) { + match = true; + } + } + if (!match) { + return false; + } + } + } + return true; + } + + /** + * Check whether the given request matches the specified parameter conditions. + * @param params the parameter conditions, following + * {@link org.springframework.web.bind.annotation.RequestMapping#params()} + * @param request the current HTTP request to check + */ + public static boolean checkParameters(String[] params, PortletRequest request) { + if (!ObjectUtils.isEmpty(params)) { + for (String param : params) { + int separator = param.indexOf('='); + if (separator == -1) { + if (param.startsWith("!")) { + if (PortletUtils.hasSubmitParameter(request, param.substring(1))) { + return false; + } + } + else if (!PortletUtils.hasSubmitParameter(request, param)) { + return false; + } + } + else { + String key = param.substring(0, separator); + String value = param.substring(separator + 1); + if (!value.equals(request.getParameter(key))) { + return false; + } + } + } + } + return true; + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/package.html b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/package.html new file mode 100644 index 00000000000..d21f48f2eca --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/annotation/package.html @@ -0,0 +1,7 @@ + + + +Support package for annotation-based Portlet MVC controllers. + + + diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/package.html b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/package.html new file mode 100644 index 00000000000..0de22f0dd6b --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/mvc/package.html @@ -0,0 +1,33 @@ + + + +

+ Standard controller implementations for the portlet MVC framework that + comes with Spring. Provides both abstract base classes and concrete + implementations for often seen use cases. +

+ +

+ A Controller - as defined in this package - is analogous to a Struts + Action. Usually Controllers are JavaBeans + to allow easy configuration using the {@link org.springframework.beans org.springframework.beans} + package. Controllers define the C from so-called MVC paradigm + and can be used in conjunction with the {@link org.springframework.web.portlet.ModelAndView ModelAndView} + to achieve interactive applications. The view might be represented by a + HTML interface, but, because of model and the controller being completely + independent of the view, PDF views are possible, as well as for instance Excel + views. +

+ +

+ Especially useful to read, while getting into the Spring MVC framework + are the following: +

+

+ + + diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/package.html b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/package.html new file mode 100644 index 00000000000..3adc8e35a1e --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/package.html @@ -0,0 +1,9 @@ + + + +Provides JSR-168 portlets that integrate with the application context +infrastructure, and the core interfaces and classes for the Portlet +variant of Spring's web MVC framework. + + + diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/ActionRequestWrapper.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/ActionRequestWrapper.java new file mode 100644 index 00000000000..3993af1f9c0 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/ActionRequestWrapper.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2006 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.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +import javax.portlet.ActionRequest; + +/** + * Simple wrapper for a Portlet {@link javax.portlet.ActionRequest}, + * delegating all calls to the underlying request. + * + *

(In the style of the Servlet API's {@link javax.servlet.http.HttpServletRequestWrapper}.) + * + * @author Juergen Hoeller + * @since 2.0 + * @see ActionRequestWrapper + * @see javax.servlet.http.HttpServletRequestWrapper + */ +public class ActionRequestWrapper extends PortletRequestWrapper implements ActionRequest { + + /** Original request that we're delegating to */ + private final ActionRequest actionRequest; + + + /** + * Create a ActionRequestWrapper for the given request. + * @param request the original request to wrap + * @throws IllegalArgumentException if the supplied request is null + */ + public ActionRequestWrapper(ActionRequest request) { + super(request); + this.actionRequest = request; + } + + + public InputStream getPortletInputStream() throws IOException { + return this.actionRequest.getPortletInputStream(); + } + + public void setCharacterEncoding(String enc) throws UnsupportedEncodingException { + this.actionRequest.setCharacterEncoding(enc); + } + + public BufferedReader getReader() throws IOException { + return this.actionRequest.getReader(); + } + + public String getCharacterEncoding() { + return this.actionRequest.getCharacterEncoding(); + } + + public String getContentType() { + return this.actionRequest.getContentType(); + } + + public int getContentLength() { + return this.actionRequest.getContentLength(); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/PortletRequestWrapper.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/PortletRequestWrapper.java new file mode 100644 index 00000000000..9c65318e132 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/PortletRequestWrapper.java @@ -0,0 +1,197 @@ +/* + * Copyright 2002-2006 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.util; + +import java.util.Enumeration; +import java.util.Locale; +import java.util.Map; +import java.security.Principal; + +import javax.portlet.PortalContext; +import javax.portlet.PortletMode; +import javax.portlet.PortletPreferences; +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; +import javax.portlet.WindowState; + +import org.springframework.util.Assert; + +/** + * Simple wrapper for a {@link javax.portlet.PortletRequest}, delegating all + * calls to the underlying request. + * + *

(In the style of the Servlet API's {@link javax.servlet.ServletRequestWrapper}.) + * + * @author Juergen Hoeller + * @since 2.0 + * @see ActionRequestWrapper + * @see javax.servlet.ServletRequestWrapper + */ +public class PortletRequestWrapper implements PortletRequest { + + /** Original request that we're delegating to */ + private final PortletRequest portletRequest; + + + /** + * Create a PortletRequestWrapper for the given {@link javax.portlet.PortletRequest}. + * @param request the original {@link javax.portlet.PortletRequest} to wrap + * @throws IllegalArgumentException if the supplied request is null + */ + public PortletRequestWrapper(PortletRequest request) { + Assert.notNull(request, "Request is required"); + this.portletRequest = request; + } + + + public boolean isWindowStateAllowed(WindowState state) { + return this.portletRequest.isWindowStateAllowed(state); + } + + public boolean isPortletModeAllowed(PortletMode mode) { + return this.portletRequest.isPortletModeAllowed(mode); + } + + public PortletMode getPortletMode() { + return this.portletRequest.getPortletMode(); + } + + public WindowState getWindowState() { + return this.portletRequest.getWindowState(); + } + + public PortletPreferences getPreferences() { + return this.portletRequest.getPreferences(); + } + + public PortletSession getPortletSession() { + return this.portletRequest.getPortletSession(); + } + + public PortletSession getPortletSession(boolean create) { + return this.portletRequest.getPortletSession(create); + } + + public String getProperty(String name) { + return this.portletRequest.getProperty(name); + } + + public Enumeration getProperties(String name) { + return this.portletRequest.getProperties(name); + } + + public Enumeration getPropertyNames() { + return this.portletRequest.getPropertyNames(); + } + + public PortalContext getPortalContext() { + return this.portletRequest.getPortalContext(); + } + + public String getAuthType() { + return this.portletRequest.getAuthType(); + } + + public String getContextPath() { + return this.portletRequest.getContextPath(); + } + + public String getRemoteUser() { + return this.portletRequest.getRemoteUser(); + } + + public Principal getUserPrincipal() { + return this.portletRequest.getUserPrincipal(); + } + + public boolean isUserInRole(String role) { + return this.portletRequest.isUserInRole(role); + } + + public Object getAttribute(String name) { + return this.portletRequest.getAttribute(name); + } + + public Enumeration getAttributeNames() { + return this.portletRequest.getAttributeNames(); + } + + public String getParameter(String name) { + return this.portletRequest.getParameter(name); + } + + public Enumeration getParameterNames() { + return this.portletRequest.getParameterNames(); + } + + public String[] getParameterValues(String name) { + return this.portletRequest.getParameterValues(name); + } + + public Map getParameterMap() { + return this.portletRequest.getParameterMap(); + } + + public boolean isSecure() { + return this.portletRequest.isSecure(); + } + + public void setAttribute(String name, Object value) { + this.portletRequest.setAttribute(name, value); + } + + public void removeAttribute(String name) { + this.portletRequest.removeAttribute(name); + } + + public String getRequestedSessionId() { + return this.portletRequest.getRequestedSessionId(); + } + + public boolean isRequestedSessionIdValid() { + return this.portletRequest.isRequestedSessionIdValid(); + } + + public String getResponseContentType() { + return this.portletRequest.getResponseContentType(); + } + + public Enumeration getResponseContentTypes() { + return this.portletRequest.getResponseContentTypes(); + } + + public Locale getLocale() { + return this.portletRequest.getLocale(); + } + + public Enumeration getLocales() { + return this.portletRequest.getLocales(); + } + + public String getScheme() { + return this.portletRequest.getScheme(); + } + + public String getServerName() { + return this.portletRequest.getServerName(); + } + + public int getServerPort() { + return this.portletRequest.getServerPort(); + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/PortletUtils.java b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/PortletUtils.java new file mode 100644 index 00000000000..c76f45ffce8 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/PortletUtils.java @@ -0,0 +1,439 @@ +/* + * Copyright 2002-2007 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.util; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletContext; +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; + +import org.springframework.util.Assert; +import org.springframework.web.util.WebUtils; + +/** + * Miscellaneous utilities for portlet applications. + * Used by various framework classes. + * + * @author Juergen Hoeller + * @author William G. Thompson, Jr. + * @author John A. Lewis + * @since 2.0 + */ +public abstract class PortletUtils { + + /** + * Return the temporary directory for the current web application, + * as provided by the portlet container. + * @param portletContext the portlet context of the web application + * @return the File representing the temporary directory + */ + public static File getTempDir(PortletContext portletContext) { + Assert.notNull(portletContext, "PortletContext must not be null"); + return (File) portletContext.getAttribute(WebUtils.TEMP_DIR_CONTEXT_ATTRIBUTE); + } + + /** + * Return the real path of the given path within the web application, + * as provided by the portlet container. + *

Prepends a slash if the path does not already start with a slash, + * and throws a {@link java.io.FileNotFoundException} if the path cannot + * be resolved to a resource (in contrast to + * {@link javax.portlet.PortletContext#getRealPath PortletContext's getRealPath}, + * which simply returns null). + * @param portletContext the portlet context of the web application + * @param path the relative path within the web application + * @return the corresponding real path + * @throws FileNotFoundException if the path cannot be resolved to a resource + * @see javax.portlet.PortletContext#getRealPath + */ + public static String getRealPath(PortletContext portletContext, String path) throws FileNotFoundException { + Assert.notNull(portletContext, "PortletContext must not be null"); + // Interpret location as relative to the web application root directory. + if (!path.startsWith("/")) { + path = "/" + path; + } + String realPath = portletContext.getRealPath(path); + if (realPath == null) { + throw new FileNotFoundException( + "PortletContext resource [" + path + "] cannot be resolved to absolute file path - " + + "web application archive not expanded?"); + } + return realPath; + } + + + /** + * Check the given request for a session attribute of the given name under the + * {@link javax.portlet.PortletSession#PORTLET_SCOPE}. + * Returns null if there is no session or if the session has no such attribute in that scope. + * Does not create a new session if none has existed before! + * @param request current portlet request + * @param name the name of the session attribute + * @return the value of the session attribute, or null if not found + */ + public static Object getSessionAttribute(PortletRequest request, String name) { + return getSessionAttribute(request, name, PortletSession.PORTLET_SCOPE); + } + + /** + * Check the given request for a session attribute of the given name in the given scope. + * Returns null if there is no session or if the session has no such attribute in that scope. + * Does not create a new session if none has existed before! + * @param request current portlet request + * @param name the name of the session attribute + * @param scope session scope of this attribute + * @return the value of the session attribute, or null if not found + */ + public static Object getSessionAttribute(PortletRequest request, String name, int scope) { + Assert.notNull(request, "Request must not be null"); + PortletSession session = request.getPortletSession(false); + return (session != null ? session.getAttribute(name, scope) : null); + } + + /** + * Check the given request for a session attribute of the given name + * under the {@link javax.portlet.PortletSession#PORTLET_SCOPE}. + * Throws an exception if there is no session or if the session has + * no such attribute in that scope. + *

Does not create a new session if none has existed before! + * @param request current portlet request + * @param name the name of the session attribute + * @return the value of the session attribute + * @throws IllegalStateException if the session attribute could not be found + */ + public static Object getRequiredSessionAttribute(PortletRequest request, String name) + throws IllegalStateException { + + return getRequiredSessionAttribute(request, name, PortletSession.PORTLET_SCOPE); + } + + /** + * Check the given request for a session attribute of the given name in the given scope. + * Throws an exception if there is no session or if the session has no such attribute + * in that scope. + *

Does not create a new session if none has existed before! + * @param request current portlet request + * @param name the name of the session attribute + * @param scope session scope of this attribute + * @return the value of the session attribute + * @throws IllegalStateException if the session attribute could not be found + */ + public static Object getRequiredSessionAttribute(PortletRequest request, String name, int scope) + throws IllegalStateException { + Object attr = getSessionAttribute(request, name, scope); + if (attr == null) { + throw new IllegalStateException("No session attribute '" + name + "' found"); + } + return attr; + } + + /** + * Set the session attribute with the given name to the given value under the {@link javax.portlet.PortletSession#PORTLET_SCOPE}. + * Removes the session attribute if value is null, if a session existed at all. + * Does not create a new session if not necessary! + * @param request current portlet request + * @param name the name of the session attribute + * @param value the value of the session attribute + */ + public static void setSessionAttribute(PortletRequest request, String name, Object value) { + setSessionAttribute(request, name, value, PortletSession.PORTLET_SCOPE); + } + + /** + * Set the session attribute with the given name to the given value in the given scope. + * Removes the session attribute if value is null, if a session existed at all. + * Does not create a new session if not necessary! + * @param request current portlet request + * @param name the name of the session attribute + * @param value the value of the session attribute + * @param scope session scope of this attribute + */ + public static void setSessionAttribute(PortletRequest request, String name, Object value, int scope) { + Assert.notNull(request, "Request must not be null"); + if (value != null) { + request.getPortletSession().setAttribute(name, value, scope); + } + else { + PortletSession session = request.getPortletSession(false); + if (session != null) { + session.removeAttribute(name, scope); + } + } + } + + /** + * Get the specified session attribute under the {@link javax.portlet.PortletSession#PORTLET_SCOPE}, + * creating and setting a new attribute if no existing found. The given class + * needs to have a public no-arg constructor. + * Useful for on-demand state objects in a web tier, like shopping carts. + * @param session current portlet session + * @param name the name of the session attribute + * @param clazz the class to instantiate for a new attribute + * @return the value of the session attribute, newly created if not found + * @throws IllegalArgumentException if the session attribute could not be instantiated + */ + public static Object getOrCreateSessionAttribute(PortletSession session, String name, Class clazz) + throws IllegalArgumentException { + + return getOrCreateSessionAttribute(session, name, clazz, PortletSession.PORTLET_SCOPE); + } + + /** + * Get the specified session attribute in the given scope, + * creating and setting a new attribute if no existing found. The given class + * needs to have a public no-arg constructor. + * Useful for on-demand state objects in a web tier, like shopping carts. + * @param session current portlet session + * @param name the name of the session attribute + * @param clazz the class to instantiate for a new attribute + * @param scope the session scope of this attribute + * @return the value of the session attribute, newly created if not found + * @throws IllegalArgumentException if the session attribute could not be instantiated + */ + public static Object getOrCreateSessionAttribute(PortletSession session, String name, Class clazz, int scope) + throws IllegalArgumentException { + + Assert.notNull(session, "Session must not be null"); + Object sessionObject = session.getAttribute(name, scope); + if (sessionObject == null) { + Assert.notNull(clazz, "Class must not be null if attribute value is to be instantiated"); + try { + sessionObject = clazz.newInstance(); + } + catch (InstantiationException ex) { + throw new IllegalArgumentException( + "Could not instantiate class [" + clazz.getName() + + "] for session attribute '" + name + "': " + ex.getMessage()); + } + catch (IllegalAccessException ex) { + throw new IllegalArgumentException( + "Could not access default constructor of class [" + clazz.getName() + + "] for session attribute '" + name + "': " + ex.getMessage()); + } + session.setAttribute(name, sessionObject, scope); + } + return sessionObject; + } + + /** + * Return the best available mutex for the given session: + * that is, an object to synchronize on for the given session. + *

Returns the session mutex attribute if available; usually, + * this means that the + * {@link org.springframework.web.util.HttpSessionMutexListener} + * needs to be defined in web.xml. Falls back to the + * {@link javax.portlet.PortletSession} itself if no mutex attribute found. + *

The session mutex is guaranteed to be the same object during + * the entire lifetime of the session, available under the key defined + * by the {@link org.springframework.web.util.WebUtils#SESSION_MUTEX_ATTRIBUTE} + * constant. It serves as a safe reference to synchronize on for locking + * on the current session. + *

In many cases, the {@link javax.portlet.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. + * @param session the HttpSession to find a mutex for + * @return the mutex object (never null) + * @see org.springframework.web.util.WebUtils#SESSION_MUTEX_ATTRIBUTE + * @see org.springframework.web.util.HttpSessionMutexListener + */ + public static Object getSessionMutex(PortletSession session) { + Assert.notNull(session, "Session must not be null"); + Object mutex = session.getAttribute(WebUtils.SESSION_MUTEX_ATTRIBUTE); + if (mutex == null) { + mutex = session; + } + return mutex; + } + + /** + * Expose the given Map as request attributes, using the keys as attribute names + * and the values as corresponding attribute values. Keys must be Strings. + * @param request current portlet request + * @param attributes the attributes Map + */ + public static void exposeRequestAttributes(PortletRequest request, Map attributes) { + Assert.notNull(request, "Request must not be null"); + Assert.notNull(attributes, "attributes Map must not be null"); + Iterator it = attributes.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + if (!(entry.getKey() instanceof String)) { + throw new IllegalArgumentException( + "Invalid key [" + entry.getKey() + "] in attributes Map - only Strings allowed as attribute keys"); + } + request.setAttribute((String) entry.getKey(), entry.getValue()); + } + } + + + /** + * Check if a specific input type="submit" parameter was sent in the request, + * either via a button (directly with name) or via an image (name + ".x" or + * name + ".y"). + * @param request current portlet request + * @param name name of the parameter + * @return if the parameter was sent + * @see org.springframework.web.util.WebUtils#SUBMIT_IMAGE_SUFFIXES + */ + public static boolean hasSubmitParameter(PortletRequest request, String name) { + return getSubmitParameter(request, name) != null; + } + + /** + * Return the full name of a specific input type="submit" parameter + * if it was sent in the request, either via a button (directly with name) + * or via an image (name + ".x" or name + ".y"). + * @param request current portlet request + * @param name name of the parameter + * @return the actual parameter name with suffix if needed - null if not present + * @see org.springframework.web.util.WebUtils#SUBMIT_IMAGE_SUFFIXES + */ + public static String getSubmitParameter(PortletRequest request, String name) { + Assert.notNull(request, "Request must not be null"); + if (request.getParameter(name) != null) { + return name; + } + for (int i = 0; i < WebUtils.SUBMIT_IMAGE_SUFFIXES.length; i++) { + String suffix = WebUtils.SUBMIT_IMAGE_SUFFIXES[i]; + String parameter = name + suffix; + if (request.getParameter(parameter) != null) { + return parameter; + } + } + return null; + } + + /** + * Return a map containing all parameters with the given prefix. + * Maps single values to String and multiple values to String array. + *

For example, with a prefix of "spring_", "spring_param1" and + * "spring_param2" result in a Map with "param1" and "param2" as keys. + *

Similar to portlet + * {@link javax.portlet.PortletRequest#getParameterMap()}, + * but more flexible. + * @param request portlet request in which to look for parameters + * @param prefix the beginning of parameter names + * (if this is null or the empty string, all parameters will match) + * @return map containing request parameters without the prefix, + * containing either a String or a String array as values + * @see javax.portlet.PortletRequest#getParameterNames + * @see javax.portlet.PortletRequest#getParameterValues + * @see javax.portlet.PortletRequest#getParameterMap + */ + public static Map getParametersStartingWith(PortletRequest request, String prefix) { + Assert.notNull(request, "Request must not be null"); + Enumeration paramNames = request.getParameterNames(); + Map params = new TreeMap(); + if (prefix == null) { + prefix = ""; + } + while (paramNames != null && paramNames.hasMoreElements()) { + String paramName = (String) paramNames.nextElement(); + if ("".equals(prefix) || paramName.startsWith(prefix)) { + String unprefixed = paramName.substring(prefix.length()); + String[] values = request.getParameterValues(paramName); + if (values == null || values.length == 0) { + // Do nothing, no values found at all. + } + else if (values.length > 1) { + params.put(unprefixed, values); + } + else { + params.put(unprefixed, values[0]); + } + } + } + return params; + } + + /** + * Return the target page specified in the request. + * @param request current portlet request + * @param paramPrefix the parameter prefix to check for + * (e.g. "_target" for parameters like "_target1" or "_target2") + * @param currentPage the current page, to be returned as fallback + * if no target page specified + * @return the page specified in the request, or current page if not found + */ + public static int getTargetPage(PortletRequest request, String paramPrefix, int currentPage) { + Enumeration paramNames = request.getParameterNames(); + while (paramNames.hasMoreElements()) { + String paramName = (String) paramNames.nextElement(); + if (paramName.startsWith(paramPrefix)) { + for (int i = 0; i < WebUtils.SUBMIT_IMAGE_SUFFIXES.length; i++) { + String suffix = WebUtils.SUBMIT_IMAGE_SUFFIXES[i]; + if (paramName.endsWith(suffix)) { + paramName = paramName.substring(0, paramName.length() - suffix.length()); + } + } + return Integer.parseInt(paramName.substring(paramPrefix.length())); + } + } + return currentPage; + } + + + /** + * Pass all the action request parameters to the render phase by putting them into + * the action response object. This may not be called when the action will call + * {@link javax.portlet.ActionResponse#sendRedirect sendRedirect}. + * @param request the current action request + * @param response the current action response + * @see javax.portlet.ActionResponse#setRenderParameter + */ + public static void passAllParametersToRenderPhase(ActionRequest request, ActionResponse response) { + try { + Enumeration en = request.getParameterNames(); + while (en.hasMoreElements()) { + String param = (String) en.nextElement(); + String values[] = request.getParameterValues(param); + response.setRenderParameter(param, values); + } + } + catch (IllegalStateException ex) { + // Ignore in case sendRedirect was already set. + } + } + + /** + * Clear all the render parameters from the {@link javax.portlet.ActionResponse}. + * This may not be called when the action will call + * {@link ActionResponse#sendRedirect sendRedirect}. + * @param response the current action response + * @see ActionResponse#setRenderParameters + */ + public static void clearAllRenderParameters(ActionResponse response) { + try { + response.setRenderParameters(new HashMap()); + } + catch (IllegalStateException ex) { + // Ignore in case sendRedirect was already set. + } + } + +} diff --git a/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/package.html b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/package.html new file mode 100644 index 00000000000..d86eb8897bc --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/org/springframework/portlet/util/package.html @@ -0,0 +1,7 @@ + + + +Miscellaneous portlet utility classes. + + + diff --git a/org.springframework.web.portlet/src/main/java/overview.html b/org.springframework.web.portlet/src/main/java/overview.html new file mode 100644 index 00000000000..1eb7a2e8c19 --- /dev/null +++ b/org.springframework.web.portlet/src/main/java/overview.html @@ -0,0 +1,7 @@ + + +

+The Spring Data Binding framework, an internal library used by Spring Web Flow. +

+ + \ No newline at end of file diff --git a/org.springframework.web.portlet/src/test/resources/log4j.xml b/org.springframework.web.portlet/src/test/resources/log4j.xml new file mode 100644 index 00000000000..767b96d6206 --- /dev/null +++ b/org.springframework.web.portlet/src/test/resources/log4j.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/org.springframework.web.portlet/template.mf b/org.springframework.web.portlet/template.mf new file mode 100644 index 00000000000..6c2dbd06d23 --- /dev/null +++ b/org.springframework.web.portlet/template.mf @@ -0,0 +1,24 @@ +Bundle-SymbolicName: org.springframework.web.portlet +Bundle-Name: Spring Web Portlet +Bundle-Vendor: SpringSource +Bundle-ManifestVersion: 2 +Import-Template: + javax.portlet.*;version="[1.0.0, 2.0.0)", + javax.servlet.*;version="[2.4.0, 3.0.0)", + org.apache.commons.fileupload.*;version="[1.2.0, 2.0.0)";resolution:=optional, + org.apache.commons.logging.*;version="[1.1.1, 2.0.0)", + org.springframework.beans.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.context.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.core.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.stereotype;version="[2.5.5.A, 2.5.5.A]", + org.springframework.ui.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.util.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.validation.*;version="[2.5.5.A, 2.5.5.A]", + org.springframework.web.*;version="[2.5.5.A, 2.5.5.A]" +Unversioned-Imports: + org.xml.sax.* +Ignored-Existing-Headers: + Bnd-LastModified, + Import-Package, + Export-Package, + Tool