/* Copyright 2005-2006 Tim Fennell * * 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 net.sourceforge.stripes.controller; import net.sourceforge.stripes.action.ActionBean; import net.sourceforge.stripes.action.ActionBeanContext; import net.sourceforge.stripes.action.Resolution; import net.sourceforge.stripes.config.Configuration; import net.sourceforge.stripes.exception.StripesServletException; import net.sourceforge.stripes.util.HttpUtil; import net.sourceforge.stripes.util.Log; import net.sourceforge.stripes.validation.BooleanTypeConverter; import net.sourceforge.stripes.validation.expression.ExpressionValidator; import net.sourceforge.stripes.validation.expression.Jsp20ExpressionExecutor; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.jsp.JspFactory; import javax.servlet.jsp.PageContext; import java.lang.reflect.InvocationTargetException; import java.util.Stack; /** * <p>Servlet that controls how requests to the Stripes framework are processed. Uses an instance of * the ActionResolver interface to locate the bean and method used to handle the current request and * then delegates processing to the bean.</p> * * <p>While the DispatcherServlet is structured so that it can be easily subclassed and * overridden much of the processing work is delegated to the {@link DispatcherHelper} class.</p> * * @author Tim Fennell */ public class DispatcherServlet extends HttpServlet { private static final long serialVersionUID = 1L; /** * Configuration key used to lookup up a property that determines whether or not beans' * custom validate() method gets invoked when validation errors are generated during * the binding process */ public static final String RUN_CUSTOM_VALIDATION_WHEN_ERRORS = "Validation.InvokeValidateWhenErrorsExist"; private Boolean alwaysInvokeValidate; /** Log used throughout the class. */ private static final Log log = Log.getInstance(DispatcherServlet.class); /** * <p>Invokes the following instance level methods in order to coordinate the processing * of requests:</p> * * <ul> * <li>{@link #resolveActionBean(ExecutionContext)}</li> * <li>{@link #resolveHandler(ExecutionContext)}</li> * <li>{@link #doBindingAndValidation(ExecutionContext)}</li> * <li>{@link #doCustomValidation(ExecutionContext)}</li> * <li>{@link #handleValidationErrors(ExecutionContext)}</li> * <li>{@link #invokeEventHandler(ExecutionContext)}</li> * </ul> * * <p>If any of the above methods return a {@link Resolution} the rest of the request processing * is aborted and the resolution is executed.</p> * * @param request the HttpServletRequest handed to the class by the container * @param response the HttpServletResponse paired to the request * @throws ServletException thrown when the system fails to process the request in any way */ @Override protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { // It sucks that we have to do this here (in the request cycle), but there doesn't // seem to be a good way to get at the Configuration from the Filter in init() doOneTimeConfiguration(); /////////////////////////////////////////////////////////////////////// // Here beings the real processing of the request! /////////////////////////////////////////////////////////////////////// log.trace("Dispatching request to URL: ", HttpUtil.getRequestedPath(request)); PageContext pageContext = null; final ExecutionContext ctx = new ExecutionContext(); boolean async = false; try { final Configuration config = StripesFilter.getConfiguration(); // First manufacture an ActionBeanContext final ActionBeanContext context = config.getActionBeanContextFactory().getContextInstance(request, response); context.setServletContext(getServletContext()); // Then setup the ExecutionContext that we'll use to process this request ctx.setActionBeanContext(context); try { ActionBeanContext abc = ctx.getActionBeanContext(); // It's unclear whether this usage of the JspFactory will work in all containers. It looks // like it should, but still, we should be careful not to screw up regular request // processing if it should fail. Why do we do this? So we can have a container-agnostic // way of getting an ExpressionEvaluator to do expression based validation. And we only // need it if the Jsp20 executor is used, so maybe soon we can kill it? if (ExpressionValidator.getExecutor() instanceof Jsp20ExpressionExecutor) { pageContext = JspFactory.getDefaultFactory().getPageContext(this, // the servlet inst abc.getRequest(), // req abc.getResponse(), // res null, // error page url (request.getSession(false) != null), // needsSession - don't force a session creation if one doesn't already exist abc.getResponse().getBufferSize(), true); // autoflush DispatcherHelper.setPageContext(pageContext); } } catch (Exception e) { // Don't even log this, this failure gets reported if action beans actually // try and make use of expression validation, otherwise this is just noise } // Resolve the ActionBean, and if an interceptor returns a resolution, bail now saveActionBean(request); Resolution resolution = requestInit(ctx); if (resolution == null) { resolution = resolveActionBean(ctx); if (resolution == null) { resolution = resolveHandler(ctx); if (resolution == null) { // Then run binding and validation resolution = doBindingAndValidation(ctx); if (resolution == null) { // Then continue on to custom validation resolution = doCustomValidation(ctx); if (resolution == null) { // And then validation error handling resolution = handleValidationErrors(ctx); if (resolution == null) { // And finally invoking of the event handler resolution = invokeEventHandler(ctx); } } } } } } // Whatever stage it came from, execute the resolution if (resolution != null) { if (resolution instanceof AsyncResponse) { // special handling for async resolutions : we defer // cleanup to async processing. We register a // "cleanup" callback that the async processing will // invoke when completed. This allows to hide the details // from dispatcher (and to cut the dependency on the class, // so that Stripes still works with Servlet2.x containers.) async = true; final PageContext pc = pageContext; final AsyncResponse asyncResponse = (AsyncResponse)resolution; asyncResponse.setCleanupCallback(new Runnable() { @Override public void run() { log.debug("Cleaning up AsyncResponse ", asyncResponse); if (pc != null) { JspFactory.getDefaultFactory().releasePageContext(pc); DispatcherHelper.setPageContext(null); } requestComplete(ctx); restoreActionBean(request); } }); } executeResolution(ctx, resolution); } } catch (ServletException se) { throw se; } catch (RuntimeException re) { throw re; } catch (InvocationTargetException ite) { if (ite.getTargetException() instanceof ServletException) { throw (ServletException) ite.getTargetException(); } else if (ite.getTargetException() instanceof RuntimeException) { throw (RuntimeException) ite.getTargetException(); } else { throw new StripesServletException ("ActionBean execution threw an exception.", ite.getTargetException()); } } catch (Exception e) { throw new StripesServletException("Exception encountered processing request.", e); } finally { // Make sure to release the page context if (!async) { if (pageContext != null) { JspFactory.getDefaultFactory().releasePageContext(pageContext); DispatcherHelper.setPageContext(null); } requestComplete(ctx); restoreActionBean(request); } } } /** * Calls interceptors listening for RequestInit. There is no Stripes code that * executes for this lifecycle stage. */ private Resolution requestInit(ExecutionContext ctx) throws Exception { ctx.setLifecycleStage(LifecycleStage.RequestInit); ctx.setInterceptors(StripesFilter.getConfiguration().getInterceptors(LifecycleStage.RequestInit)); return ctx.wrap(new Interceptor() {public Resolution intercept(ExecutionContext context) throws Exception {return null;}}); } /** * Calls interceptors listening for RequestComplete. There is no Stripes code * that executes for this lifecycle stage. In addition, any response from * interceptors is ignored because it is too late to execute a Resolution at * this point. */ private void requestComplete(ExecutionContext ctx) { ctx.setLifecycleStage(LifecycleStage.RequestComplete); ctx.setInterceptors(StripesFilter.getConfiguration().getInterceptors(LifecycleStage.RequestComplete)); try { Resolution resolution = ctx.wrap(new Interceptor() {public Resolution intercept(ExecutionContext context) throws Exception {return null;}}); if (resolution != null) log.warn("Resolutions returned from interceptors for ", ctx.getLifecycleStage(), " are ignored because it is too late to execute them."); } catch (Exception e) { log.error(e); } } /** * Responsible for resolving the ActionBean for the current request. Delegates to * {@link DispatcherHelper#resolveActionBean(ExecutionContext)}. */ protected Resolution resolveActionBean(ExecutionContext ctx) throws Exception { return DispatcherHelper.resolveActionBean(ctx); } /** * Responsible for resolving the event handler method for the current request. Delegates to * {@link DispatcherHelper#resolveHandler(ExecutionContext)}. */ protected Resolution resolveHandler(ExecutionContext ctx) throws Exception { return DispatcherHelper.resolveHandler(ctx); } /** * Responsible for executing binding and validation for the current request. Delegates to * {@link DispatcherHelper#doBindingAndValidation(ExecutionContext, boolean)}. */ protected Resolution doBindingAndValidation(ExecutionContext ctx) throws Exception { return DispatcherHelper.doBindingAndValidation(ctx, true); } /** * Responsible for executing custom validation methods for the current request. Delegates to * {@link DispatcherHelper#doCustomValidation(ExecutionContext, boolean)}. */ protected Resolution doCustomValidation(ExecutionContext ctx) throws Exception { return DispatcherHelper.doCustomValidation(ctx, alwaysInvokeValidate); } /** * Responsible for handling any validation errors that arise during validation. Delegates to * {@link DispatcherHelper#handleValidationErrors(ExecutionContext)}. */ protected Resolution handleValidationErrors(ExecutionContext ctx) throws Exception { return DispatcherHelper.handleValidationErrors(ctx); } /** * Responsible for invoking the event handler if no validation errors occur. Delegates to * {@link DispatcherHelper#invokeEventHandler(ExecutionContext)}. */ protected Resolution invokeEventHandler(ExecutionContext ctx) throws Exception { return DispatcherHelper.invokeEventHandler(ctx); } /** * Responsible for executing the Resolution for the current request. Delegates to * {@link DispatcherHelper#executeResolution(ExecutionContext, Resolution)}. */ protected void executeResolution(ExecutionContext ctx, Resolution resolution) throws Exception { DispatcherHelper.executeResolution(ctx, resolution); } /** * Performs a simple piece of one time configuration that requires access to the * Configuration object delivered through the Stripes Filter. */ private void doOneTimeConfiguration() { if (alwaysInvokeValidate == null) { // Check to see if, in this application, validate() methods should always be run // even when validation errors already exist String callValidateWhenErrorsExist = StripesFilter.getConfiguration() .getBootstrapPropertyResolver().getProperty(RUN_CUSTOM_VALIDATION_WHEN_ERRORS); if (callValidateWhenErrorsExist != null) { BooleanTypeConverter c = new BooleanTypeConverter(); this.alwaysInvokeValidate = c.convert(callValidateWhenErrorsExist, Boolean.class, null); } else { this.alwaysInvokeValidate = false; // Default behaviour } } } /** * Fetches, and lazily creates if required, a Stack in the request to store ActionBeans * should the current request involve forwards or includes to other ActionBeans. * * @param request the current HttpServletRequest * @return the Stack if present, or if creation is requested */ @SuppressWarnings("unchecked") protected Stack<ActionBean> getActionBeanStack(HttpServletRequest request, boolean create) { Stack<ActionBean> stack = (Stack<ActionBean>) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN_STACK); if (stack == null && create) { stack = new Stack<ActionBean>(); request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN_STACK, stack); } return stack; } /** * Saves the current value of the 'actionBean' attribute in the request so that it * can be restored at a later date by calling {@link #restoreActionBean(HttpServletRequest)}. * If no ActionBean is currently stored in the request, nothing is changed. * * @param request the current HttpServletRequest */ protected void saveActionBean(HttpServletRequest request) { if (request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN) != null) { Stack<ActionBean> stack = getActionBeanStack(request, true); stack.push((ActionBean) request.getAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN)); } } /** * Restores the previous value of the 'actionBean' attribute in the request. If no * ActionBeans have been saved using {@link #saveActionBean(HttpServletRequest)} then this * method has no effect. * * @param request the current HttpServletRequest */ protected void restoreActionBean(HttpServletRequest request) { Stack<ActionBean> stack = getActionBeanStack(request, false); if (stack != null && !stack.empty()) { request.setAttribute(StripesConstants.REQ_ATTR_ACTION_BEAN, stack.pop()); } } }