View Javadoc

    1   /*
    2    * $Id: ExecuteAndWaitInterceptor.java 508914 2007-02-18 16:45:30Z tschneider $
    3    *
    4    * Licensed to the Apache Software Foundation (ASF) under one
    5    * or more contributor license agreements.  See the NOTICE file
    6    * distributed with this work for additional information
    7    * regarding copyright ownership.  The ASF licenses this file
    8    * to you under the Apache License, Version 2.0 (the
    9    * "License"); you may not use this file except in compliance
    10   * with the License.  You may obtain a copy of the License at
    11   *
    12   *  http://www.apache.org/licenses/LICENSE-2.0
    13   *
    14   * Unless required by applicable law or agreed to in writing,
    15   * software distributed under the License is distributed on an
    16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    17   * KIND, either express or implied.  See the License for the
    18   * specific language governing permissions and limitations
    19   * under the License.
    20   */
    21  package org.apache.struts2.interceptor;
    22
    23  import java.util.Collections;
    24  import java.util.Map;
    25
    26  import org.apache.commons.logging.Log;
    27  import org.apache.commons.logging.LogFactory;
    28
    29  import com.opensymphony.xwork2.ActionContext;
    30  import com.opensymphony.xwork2.ActionInvocation;
    31  import com.opensymphony.xwork2.ActionProxy;
    32  import com.opensymphony.xwork2.config.entities.ResultConfig;
    33  import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
    34
    35
    36  /***
    37   * <!-- START SNIPPET: description -->
    38   *
    39   * The ExecuteAndWaitInterceptor is great for running long-lived actions in the background while showing the user a nice
    40   * progress meter. This also prevents the HTTP request from timing out when the action takes more than 5 or 10 minutes.
    41   *
    42   * <p/> Using this interceptor is pretty straight forward. Assuming that you are including struts-default.xml, this
    43   * interceptor is already configured but is not part of any of the default stacks. Because of the nature of this
    44   * interceptor, it must be the <b>last</b> interceptor in the stack.
    45   *
    46   * <p/> This interceptor works on a per-session basis. That means that the same action name (myLongRunningAction, in the
    47   * above example) cannot be run more than once at a time in a given session. On the initial request or any subsequent
    48   * requests (before the action has completed), the <b>wait</b> result will be returned. <b>The wait result is
    49   * responsible for issuing a subsequent request back to the action, giving the effect of a self-updating progress
    50   * meter</b>.
    51   *
    52   * <p/> If no "wait" result is found, Struts will automatically generate a wait result on the fly. This result is
    53   * written in FreeMarker and cannot run unless FreeMarker is installed. If you don't wish to deploy with FreeMarker, you
    54   * must provide your own wait result. This is generally a good thing to do anyway, as the default wait page is very
    55   * plain.
    56   *
    57   * <p/>Whenever the wait result is returned, the <b>action that is currently running in the background will be placed on
    58   * top of the stack</b>. This allows you to display progress data, such as a count, in the wait page. By making the wait
    59   * page automatically reload the request to the action (which will be short-circuited by the interceptor), you can give
    60   * the appearance of an automatic progress meter.
    61   *
    62   * <p/>This interceptor also supports using an initial wait delay. An initial delay is a time in milliseconds we let the
    63   * server wait before the wait page is shown to the user. During the wait this interceptor will wake every 100 millis
    64   * to check if the background process is done premature, thus if the job for some reason doesn't take to long the wait
    65   * page is not shown to the user.
    66   * <br/> This is useful for e.g. search actions that have a wide span of execution time. Using a delay time of 2000
    67   * millis we ensure the user is presented fast search results immediately and for the slow results a wait page is used.
    68   *
    69   * <p/><b>Important</b>: Because the action will be running in a seperate thread, you can't use ActionContext because it
    70   * is a ThreadLocal. This means if you need to access, for example, session data, you need to implement SessionAware
    71   * rather than calling ActionContext.getSesion().
    72   *
    73   * <p/>The thread kicked off by this interceptor will be named in the form <b><u>actionName</u>BrackgroundProcess</b>.
    74   * For example, the <i>search</i> action would run as a thread named <i>searchBackgroundProcess</i>.
    75   *
    76   * <!-- END SNIPPET: description -->
    77   *
    78   * <p/> <u>Interceptor parameters:</u>
    79   *
    80   * <!-- START SNIPPET: parameters -->
    81   *
    82   * <ul>
    83   *
    84   * <li>threadPriority (optional) - the priority to assign the thread. Default is <code>Thread.NORM_PRIORITY</code>.</li>
    85   * <li>delay (optional) - an initial delay in millis to wait before the wait page is shown (returning <code>wait</code> as result code). Default is no initial delay.</li>
    86   * <li>delaySleepInterval (optional) - only used with delay. Used for waking up at certain intervals to check if the background process is already done. Default is 100 millis.</li>
    87   *
    88   * </ul>
    89   *
    90   * <!-- END SNIPPET: parameters -->
    91   *
    92   * <p/> <u>Extending the interceptor:</u>
    93   *
    94   * <p/>
    95   *
    96   * <!-- START SNIPPET: extending -->
    97   *
    98   * If you wish to make special preparations before and/or after the invocation of the background thread, you can extend
    99   * the BackgroundProcess class and implement the beforeInvocation() and afterInvocation() methods. This may be useful
    100  * for obtaining and releasing resources that the background process will need to execute successfully. To use your
    101  * background process extension, extend ExecuteAndWaitInterceptor and implement the getNewBackgroundProcess() method.
    102  *
    103  * <!-- END SNIPPET: extending -->
    104  *
    105  * <p/> <u>Example code:</u>
    106  *
    107  * <pre>
    108  * <!-- START SNIPPET: example -->
    109  * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
    110  *     &lt;interceptor-ref name="completeStack"/&gt;
    111  *     &lt;interceptor-ref name="execAndWait"/&gt;
    112  *     &lt;result name="wait"&gt;longRunningAction-wait.jsp&lt;/result&gt;
    113  *     &lt;result name="success"&gt;longRunningAction-success.jsp&lt;/result&gt;
    114  * &lt;/action&gt;
    115  *
    116  * &lt;%@ taglib prefix="s" uri="/struts" %&gt;
    117  * &lt;html&gt;
    118  *   &lt;head&gt;
    119  *     &lt;title&gt;Please wait&lt;/title&gt;
    120  *     &lt;meta http-equiv="refresh" content="5;url=&lt;a:url includeParams="all" /&gt;"/&gt;
    121  *   &lt;/head&gt;
    122  *   &lt;body&gt;
    123  *     Please wait while we process your request.
    124  *     Click &lt;a href="&lt;a:url includeParams="all" /&gt;">&lt;/a&gt; if this page does not reload automatically.
    125  *   &lt;/body&gt;
    126  * &lt;/html&gt;
    127  * </pre>
    128  *
    129  * <p/> <u>Example code2:</u>
    130  * This example will wait 2 second (2000 millis) before the wait page is shown to the user. Therefore
    131  * if the long process didn't last long anyway the user isn't shown a wait page.
    132  *
    133  * <pre>
    134  * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
    135  *     &lt;interceptor-ref name="completeStack"/&gt;
    136  *     &lt;interceptor-ref name="execAndWait"&gt;
    137  *         &lt;param name="delay"&gt;2000&lt;param&gt;
    138  *     &lt;interceptor-ref&gt;
    139  *     &lt;result name="wait"&gt;longRunningAction-wait.jsp&lt;/result&gt;
    140  *     &lt;result name="success"&gt;longRunningAction-success.jsp&lt;/result&gt;
    141  * &lt;/action&gt;
    142  * </pre>
    143  *
    144  * <p/> <u>Example code3:</u>
    145  * This example will wait 1 second (1000 millis) before the wait page is shown to the user.
    146  * And at every 50 millis this interceptor will check if the background process is done, if so
    147  * it will return before the 1 second has elapsed, and the user isn't shown a wait page.
    148  *
    149  * <pre>
    150  * &lt;action name="someAction" class="com.examples.SomeAction"&gt;
    151  *     &lt;interceptor-ref name="completeStack"/&gt;
    152  *     &lt;interceptor-ref name="execAndWait"&gt;
    153  *         &lt;param name="delay"&gt;1000&lt;param&gt;
    154  *         &lt;param name="delaySleepInterval"&gt;50&lt;param&gt;
    155  *     &lt;interceptor-ref&gt;
    156  *     &lt;result name="wait"&gt;longRunningAction-wait.jsp&lt;/result&gt;
    157  *     &lt;result name="success"&gt;longRunningAction-success.jsp&lt;/result&gt;
    158  * &lt;/action&gt;
    159  * </pre>
    160  *
    161  * <!-- END SNIPPET: example -->
    162  *
    163  */
    164 public class ExecuteAndWaitInterceptor extends MethodFilterInterceptor {
    165
    166     private static final long serialVersionUID = -2754639196749652512L;
    167
    168     private static final Log LOG = LogFactory.getLog(ExecuteAndWaitInterceptor.class);
    169
    170     public static final String KEY = "__execWait";
    171     public static final String WAIT = "wait";
    172     protected int delay;
    173     protected int delaySleepInterval = 100; // default sleep 100 millis before checking if background process is done
    174     protected boolean executeAfterValidationPass = false;
    175
    176     private int threadPriority = Thread.NORM_PRIORITY;
    177
    178     /* (non-Javadoc)
    179      * @see com.opensymphony.xwork2.interceptor.Interceptor#init()
    180      */
    181     public void init() {
    182     }
    183
    184     /***
    185      * Creates a new background process
    186      *
    187      * @param name The process name
    188      * @param actionInvocation The action invocation
    189      * @param threadPriority The thread priority
    190      * @return The new process
    191      */
    192     protected BackgroundProcess getNewBackgroundProcess(String name, ActionInvocation actionInvocation, int threadPriority) {
    193         return new BackgroundProcess(name + "BackgroundThread", actionInvocation, threadPriority);
    194     }
    195
    196     /***
    197      * Returns the name to associate the background process.  Override to change the way background processes
    198      * are mapped to requests.
    199      *
    200      * @param actionInvocation The action invocation
    201      * @return the name of the background thread
    202      */
    203     protected String getBackgroundProcessName(ActionProxy proxy) {
    204         return proxy.getActionName();
    205     }
    206
    207     /* (non-Javadoc)
    208      * @see com.opensymphony.xwork2.interceptor.MethodFilterInterceptor#doIntercept(com.opensymphony.xwork2.ActionInvocation)
    209      */
    210     protected String doIntercept(ActionInvocation actionInvocation) throws Exception {
    211         ActionProxy proxy = actionInvocation.getProxy();
    212         String name = getBackgroundProcessName(proxy);
    213         ActionContext context = actionInvocation.getInvocationContext();
    214         Map session = context.getSession();
    215
    216         Boolean secondTime  = true;
    217         if (executeAfterValidationPass) {
    218             secondTime = (Boolean) context.get(KEY);
    219             if (secondTime == null) {
    220                 context.put(KEY, true);
    221                 secondTime = false;
    222             } else {
    223                 secondTime = true;
    224                 context.put(KEY, null);
    225             }
    226         }
    227
    228         synchronized (session) {
    229             BackgroundProcess bp = (BackgroundProcess) session.get(KEY + name);
    230
    231             if ((!executeAfterValidationPass || secondTime) && bp == null) {
    232                 bp = getNewBackgroundProcess(name, actionInvocation, threadPriority);
    233                 session.put(KEY + name, bp);
    234                 performInitialDelay(bp); // first time let some time pass before showing wait page
    235                 secondTime = false;
    236             }
    237
    238             if ((!executeAfterValidationPass || !secondTime) && bp != null && !bp.isDone()) {
    239                 actionInvocation.getStack().push(bp.getAction());
    240                 Map results = proxy.getConfig().getResults();
    241                 if (!results.containsKey(WAIT)) {
    242                     LOG.warn("ExecuteAndWait interceptor has detected that no result named 'wait' is available. " +
    243                             "Defaulting to a plain built-in wait page. It is highly recommend you " +
    244                             "provide an action-specific or global result named '" + WAIT +
    245                             "'! This requires FreeMarker support and won't work if you don't have it installed");
    246                     // no wait result? hmm -- let's try to do dynamically put it in for you!
    247                     ResultConfig rc = new ResultConfig(WAIT, "org.apache.struts2.views.freemarker.FreemarkerResult",
    248                             Collections.singletonMap("location", "/org/apache/struts2/interceptor/wait.ftl"));
    249                     results.put(WAIT, rc);
    250                 }
    251
    252                 return WAIT;
    253             } else if ((!executeAfterValidationPass || !secondTime) && bp != null && bp.isDone()) {
    254                 session.remove(KEY + name);
    255                 actionInvocation.getStack().push(bp.getAction());
    256
    257                 // if an exception occured during action execution, throw it here
    258                 if (bp.getException() != null) {
    259                     throw bp.getException();
    260                 }
    261
    262                 return bp.getResult();
    263             } else {
    264                 // this is the first instance of the interceptor and there is no existing action
    265                 // already run in the background, so let's just let this pass through. We assume
    266                 // the action invocation will be run in the background on the subsequent pass through
    267                 // this interceptor
    268                 return actionInvocation.invoke();
    269             }
    270         }
    271     }
    272
    273
    274     /* (non-Javadoc)
    275      * @see com.opensymphony.xwork2.interceptor.Interceptor#destroy()
    276      */
    277     public void destroy() {
    278     }
    279
    280     /***
    281      * Performs the initial delay.
    282      * <p/>
    283      * When this interceptor is executed for the first time this methods handles any provided initial delay.
    284      * An initial delay is a time in miliseconds we let the server wait before we continue.
    285      * <br/> During the wait this interceptor will wake every 100 millis to check if the background
    286      * process is done premature, thus if the job for some reason doesn't take to long the wait
    287      * page is not shown to the user.
    288      *
    289      * @param bp the background process
    290      * @throws InterruptedException is thrown by Thread.sleep
    291      */
    292     protected void performInitialDelay(BackgroundProcess bp) throws InterruptedException {
    293         if (delay <= 0 || delaySleepInterval <= 0) {
    294             return;
    295         }
    296
    297         int steps = delay / delaySleepInterval;
    298         if (LOG.isDebugEnabled()) {
    299             LOG.debug("Delaying for " + delay + " millis. (using " + steps + " steps)");
    300         }
    301         int step;
    302         for (step = 0; step < steps && !bp.isDone(); step++) {
    303             Thread.sleep(delaySleepInterval);
    304         }
    305         if (LOG.isDebugEnabled()) {
    306             LOG.debug("Sleeping ended after " + step + " steps and the background process is " + (bp.isDone() ? " done" : " not done"));
    307         }
    308     }
    309
    310     /***
    311      * Sets the thread priority of the background process.
    312      *
    313      * @param threadPriority the priority from <code>Thread.XXX</code>
    314      */
    315     public void setThreadPriority(int threadPriority) {
    316         this.threadPriority = threadPriority;
    317     }
    318
    319     /***
    320      * Sets the initial delay in millis (msec).
    321      *
    322      * @param delay in millis. (0 for not used)
    323      */
    324     public void setDelay(int delay) {
    325         this.delay = delay;
    326     }
    327
    328     /***
    329      * Sets the sleep interval in millis (msec) when performing the initial delay.
    330      *
    331      * @param delaySleepInterval in millis (0 for not used)
    332      */
    333     public void setDelaySleepInterval(int delaySleepInterval) {
    334         this.delaySleepInterval = delaySleepInterval;
    335     }
    336
    337     /***
    338      * Whether to start the background process after the second pass (first being validation)
    339      * or not
    340      *
    341      * @param executeAfterValidationPass the executeAfterValidationPass to set
    342      */
    343     public void setExecuteAfterValidationPass(boolean executeAfterValidationPass) {
    344         this.executeAfterValidationPass = executeAfterValidationPass;
    345     }
    346
    347
    348 }