View Javadoc

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