1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <action name="someAction" class="com.examples.SomeAction">
120 * <interceptor-ref name="completeStack"/>
121 * <interceptor-ref name="execAndWait"/>
122 * <result name="wait">longRunningAction-wait.jsp</result>
123 * <result name="success">longRunningAction-success.jsp</result>
124 * </action>
125 *
126 * <%@ taglib prefix="s" uri="/struts" %>
127 * <html>
128 * <head>
129 * <title>Please wait</title>
130 * <meta http-equiv="refresh" content="5;url=<s:url includeParams="all" />"/>
131 * </head>
132 * <body>
133 * Please wait while we process your request.
134 * Click <a href="<s:url includeParams="all" />"></a> if this page does not reload automatically.
135 * </body>
136 * </html>
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 * <action name="someAction" class="com.examples.SomeAction">
145 * <interceptor-ref name="completeStack"/>
146 * <interceptor-ref name="execAndWait">
147 * <param name="delay">2000<param>
148 * <interceptor-ref>
149 * <result name="wait">longRunningAction-wait.jsp</result>
150 * <result name="success">longRunningAction-success.jsp</result>
151 * </action>
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 * <action name="someAction" class="com.examples.SomeAction">
161 * <interceptor-ref name="completeStack"/>
162 * <interceptor-ref name="execAndWait">
163 * <param name="delay">1000<param>
164 * <param name="delaySleepInterval">50<param>
165 * <interceptor-ref>
166 * <result name="wait">longRunningAction-wait.jsp</result>
167 * <result name="success">longRunningAction-success.jsp</result>
168 * </action>
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;
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
196
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
224
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
246
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);
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
271
272
273
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
288 if (bp.getException() != null) {
289 throw bp.getException();
290 }
291
292 return bp.getResult();
293 } else {
294
295
296
297
298 return actionInvocation.invoke();
299 }
300 }
301 }
302
303
304
305
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 }