A plug & play solution to add a 'Please Wait' page to any Struts action

A while back I was tasked with providing 'please wait' functionality to the PANTHER website. The requirement was that the solution be easily added to any action that the programmer felt needed it. We already decided that using Javascript/CSS/hiding layers/etc. was not the right solution for us. I thought to myself, "Self, given Struts' page flow flexibility and general extensibility, there's got to be a way to do this elegantly". I couldn't really find anything on the web that showed a satisfactory solution. When I finally came up with a solution, I was surprised that the Struts package didn't already come with a feature like it.

The entire solution itself involves five elements:

  1. An addition of a parameter=processWithWait attribute in any action-mapping in the struts-config.xml file for an action that would take a long time.

  2. An extension of both the processActionPerform and the processValidate methods in the Struts RequestProcessor.

  3. A simple Java bean, which I call ForwardActionBean.

  4. A jsp, pleaseWait.jsp, which represents the 'Please Wait' page itself.
  5. The addition of a global forwards in the struts-config.xml that points to the pleaseWait.jsp, and of course, tell Struts that you're using another RequestProcessor.

Once you've implemented the steps above, it's just a matter of doing step 1 whenever you want to add a 'Please Wait' page to your action. It's that simple!

Here's a sample action mapping in struts-config.xml for an action that takes a long time to process and needs a please wait page:

1. The struts-config.xml Action Mapping

<action path="/bakeATurkey"
            type="com.takesforever.webapp.BakeATurkeyAction"
            scope="request"
            input="/turkeyForm.jsp"
            name="TurkeyForm"
            parameter="processWithWait"
            validate="true">
      <forward name="failure" path="/error.jsp" />
      <forward name="success" path="/turkeyDone.jsp" />
    </action>

Ok, so now you have this attribute, parameter="processWithWait". What is Struts going to do with it? As you may know, all incoming client requests go through the Struts ActionServlet then over to the Struts RequestProcessor class. Depending on your configuration, this class then forwards to the appropriate Struts Action class for your actual business processing.

But we don't want Struts to go right to our action just yet. We want it to serve up our 'Please Wait' page first and then forward to our action. That way, our user has a nice Please Wait page with some animation that they can swear at while our computers chug away in the background. Here's where our overridden RequestProcessor.processActionPerform method comes in. Now before you take a look at the lines and lines of code below (mostly comments), it's good to understand the following flow:

  1. RequestProcessor's processActionPerform receives a request.

  2. It checks the action mapping to see if it's a long transaction. If not, call super.processActionPerform and it's business as usual.

  3. The ActionMapping specifies a long transaction. Check a session attribute to make sure this isn't the SECOND time we've received this same request (this will make more sense later). It isn't.

  4. We create a ForwardActionBean object. If this is a GET request, we store away the entire request string, form parameters and all, into this bean. We store the bean in the user's session. If it is a POST request, we store away the request in the bean and we store the bean away into the user's session. We ALSO clone the current form (already validated) and store that away into the user's session as well.

  5. After doing this bookkeeping, we forward off the please wait jsp page.
  6. The please wait jsp contains a META tag which basically re-requests the same request that is specified by the ForwardActionBean which is stored away in the user's session. The client browser now basically "hangs" on this please wait page.

  7. RequestProcessor's processActionPerform receives a request for a long transaction.

  8. It checks the same session attribute it checked in step 2. Since the session attribute is NOT null, this means we've seen this request before and we're receiving it a SECOND time.
  9. We set this session attribute to null.
  10. If it's a GET request, we simply call super.processActionPerform with the current form. If it's a POST request, we simply call super.processActionPerform with the cloned form that we've stored away in the session. (NB: our overridden processValidate method makes sure that we revalidate the cloned form for the POST requests NOT the new form. Depending on the browser you're using, this new form is usually empty for roundtrip POST requests.)

2. Here is my overriden Struts RequestProcessor.

package com.takesforever.webapp;

import org.apache.struts.action.*;

/**
 * LongWaitRequestProcessor overrides the original RequestProcessor that
 * comes packaged with Struts.  It allows for the handling of long transactions,
 * and overrides the processActionPerform method to do this.
 * To use this functionality, the following element must be added to the
 * struts-config.xml:
 * <controller
 *  contentType="text/html;charset=UTF-8"
 *  debug="3"
 *  locale="true"
 *  nocache="true"
 *  processorClass="com.takesforever.webapp.LongWaitRequestProcessor"
 * />
 * Then the following parameter must be added to your specific action
 * mapping:
 * parameter="processWithWait"
 * @author Baq Haidri
 * @version 1.0
 */
public class LongWaitRequestProcessor extends RequestProcessor {

  /**
   * This method has been overridden to handle the case where forms are submitted via
   * the POST request.  In a POST request, form parameters are not available directly
   * from the request URI, so they cannot be saved to the ForwardActionBean like in
   * a GET request.  However, the POST data is saved into the Struts ActionForms.
   * This data can be stored away in the session.  We must guard against Struts
   * resetting the form data between the requests, so the form bean is cloned, then
   * stored.
   * @param request
   * @param response
   * @param form
   * @param mapping
   * @return a boolean indicating whether validation was successful
   * @throws java.io.IOException
   * @throws javax.servlet.ServletException
   */
  protected boolean processValidate(javax.servlet.http.HttpServletRequest request,
                                    javax.servlet.http.HttpServletResponse response,
                                    ActionForm form,
                                    ActionMapping mapping)
      throws java.io.IOException, javax.servlet.ServletException {
    if (mapping.getParameter() == null) {
      return super.processValidate(request, response, form, mapping);
    } else if (mapping.getParameter().equals("processWithWait")) {
      javax.servlet.http.HttpSession session = request.getSession(true);
      if (session.getAttribute("action_path_key") == null) {
        return super.processValidate(request,response, form, mapping);
      } else {
        // Retrieve the saved form, it's not null, that means the previous request was
        // sent via POST and we'll need this form because the current one has had
        // all its values reset to null
        ActionForm nonResetForm = (ActionForm)session.getAttribute("current_form_key");
        if (nonResetForm != null) {
          session.setAttribute(mapping.getName(), nonResetForm);
          //System.out.println("*** Calling super.processValidate on nonResetForm ***");
          return super.processValidate(request, response, nonResetForm, mapping);
        } else {
          return super.processValidate(request, response, form, mapping);
        }
      }
    } else {
      return super.processValidate(request, response, form, mapping);
    }
  }

  /**
   * This method has been overridden to appropriately forward the 'pleaseWait.jsp'
   * if the user specifies
   * @param request
   * @param response
   * @param action
   * @param form
   * @param mapping
   * @return the ActionForward object indicating what jsp page to forward to.
   * @throws java.io.IOException
   * @throws javax.servlet.ServletException
   */
  protected ActionForward processActionPerform(javax.servlet.http.HttpServletRequest request,
                                               javax.servlet.http.HttpServletResponse response,
                                               Action action,
                                               ActionForm form,
                                               ActionMapping mapping) throws
      java.io.IOException, javax.servlet.ServletException {

    // Get the parameter from the action mapping to find out whether or not this
    // action requires long transaction support.  If it doesn't continue processing
    // as usual.  If it does, then process with a wait page.
    if (mapping.getParameter() == null) {
      //System.out.println("*** There is no wait parameter given, process as usual ***");
      return super.processActionPerform(request, response, action, form, mapping);
    } else if (mapping.getParameter().equals("processWithWait")) {
      //System.out.println("*** Parameter present, process with wait page ***");
      javax.servlet.http.HttpSession session = request.getSession(true);
      // If the action_path_key attribute is null, then it is the first time this
      // long transaction Action is being accessed.  This means we send down a
      // wait page with a meta-refresh tag that calls on the action again.
      if (session.getAttribute("action_path_key") == null) {
        //System.out.println("*** First time the action has been requested ***");
        //System.out.println("*** forward user to wait page, have wait page ***");
        ForwardActionBean b = new ForwardActionBean();
        if (request.getQueryString() != null)
          b.setActionPath(request.getRequestURI()+"?"+request.getQueryString());
        else
          b.setActionPath(request.getRequestURI());
        //System.out.println("*** re-request the following " + b.getActionPath() +" ***");
        session.setAttribute("action_path_key", b);
        // Save the current state of the form into the session because the
        // next time we arrive here, the form will have been reset.
        // This will be used only for requests that were sent via the POST method.
        if (request.getMethod().equalsIgnoreCase("POST")) {
          //System.out.println("*** Request sent via the POST method ***");
          ActionForm nonResetForm = null;
          try {
            nonResetForm = (ActionForm) org.apache.commons.beanutils.BeanUtils.cloneBean(form);
            session.setAttribute("current_form_key", nonResetForm);
          } catch (Exception e) {
            System.out.println("*** Error cloning the form for processWithWait ***");
            // Your error handling goes here.
          }
        }
        return mapping.findForward("pleaseWait");
      // The action has been called on a second time, from the wait
      // page itself.  This time we actually execute the action.  We reset
      // the action path key so that the next time this action is requested, the
      // wait page is NOT served.
      } else {
        //System.out.println("*** Second time the action has been requested ***");
        //System.out.println("*** forward user to actual action ***");
        session.setAttribute("action_path_key", null);
        // Retrieve the form, it's not null, that means the previous request was
        // sent via POST and we'll need this form because the current one has had
        // all its values reset to null
        ActionForm nonResetForm = (ActionForm)session.getAttribute("current_form_key");
        if (nonResetForm != null) {
          session.setAttribute("current_form_key", null);
          return super.processActionPerform(request, response, action, nonResetForm, mapping);
        } else {
          return super.processActionPerform(request, response, action, form, mapping);
        }
      }
    // A parameter has been set in the mapping but it is not the value we are
    // looking for
    } else {
      return super.processActionPerform(request, response, action, form, mapping);
    }

  }
}

3. Here's what the ForwardActionBean looks like

package com.takesforever.webapp;

/**
 * This class is a utility class that goes along with the
 * LongWaitRequestProcessor and helps in processing requests that require
 * long transaction support.
 * @author Baq Haidri
 * @version 1.0
 */
public class ForwardActionBean {

  private String actionPath;

  public void setActionPath(String v) {
    this.actionPath = v;
  }

  public String getActionPath() {
    return this.actionPath;
  }
}

4. Here's what the pleaseWait.jsp page looks like

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib uri="/WEB-INF/struts-bean.tld" prefix="bean" %>
<%@ taglib uri="/WEB-INF/struts-html.tld" prefix="html" %>
<%@ taglib uri="/WEB-INF/struts-logic.tld" prefix="logic" %>

<html:html>
<head>
  <!-- The following two lines are required, they instruct the browser to not cache the wait page. -->
  <!-- This way we can better guarantee that when the user hits the 'Back' button, they don't get the wait page instead of the form. -->
  <META http-equiv="CACHE-CONTROL" content="NO-CACHE">  <!-- For HTTP 1.1 -->
  <META http-equiv="PRAGMA" content="NO-CACHE">         <!-- For HTTP 1.0 -->
  <META http-equiv="refresh" content="0; URL=<bean:write name="action_path_key" property="actionPath"/>">
  <link href="/css/pleasewait.css" type=text/css rel=stylesheet>
  <title>Please wait...</title>
</head>

<body onload="javascript:window.focus();">
  <table cellpadding="10">
         <tr>
           <td align="left"><h1>Please wait...</h1></td>
         </tr>
  </table>
</body>

</html:html>

5. And here's an excerpt of a struts-config.xml file

<!-- ========== Controller Configuration ================================ -->
  <controller
    contentType="text/html;charset=UTF-8"
    debug="3"
    locale="true"
    nocache="true"
    processorClass="com.takesforever.webapp.LongWaitRequestProcessor"
  />

  <global-forwards>
    <forward name="pleaseWait"   path="/pleaseWait.jsp"/>
  </global-forwards>

A Note

Firstly, Thanks, I used this solution to great succes but noticed one issue. The line

nonResetForm = (ActionForm) org.apache.commons.beanutils.BeanUtils.cloneBean(form);

does not copy the ActionForm servlet property (pretty sure it fails because there is no ActionForm.getServlet, only set). For some reason our code used it. To preserve that property, I edited the processActionPerform code when it tries to perform the real action like so:

if (nonResetForm != null) {
   session.setAttribute("current_form_key", null);
   //TO Needs servlet
   nonResetForm.setServlet(this.servlet);
   return super.processActionPerform(request, response, action, nonResetForm, mapping);
}

HTH ToddOgin

StrutsPleaseWait (last edited 2009-09-20 23:12:41 by localhost)