View Javadoc

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