1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 * <action name="someAction" class="com.examples.SomeAction">
110 * <interceptor-ref name="completeStack"/>
111 * <interceptor-ref name="execAndWait"/>
112 * <result name="wait">longRunningAction-wait.jsp</result>
113 * <result name="success">longRunningAction-success.jsp</result>
114 * </action>
115 *
116 * <%@ taglib prefix="s" uri="/struts" %>
117 * <html>
118 * <head>
119 * <title>Please wait</title>
120 * <meta http-equiv="refresh" content="5;url=<a:url includeParams="all" />"/>
121 * </head>
122 * <body>
123 * Please wait while we process your request.
124 * Click <a href="<a:url includeParams="all" />"></a> if this page does not reload automatically.
125 * </body>
126 * </html>
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 * <action name="someAction" class="com.examples.SomeAction">
135 * <interceptor-ref name="completeStack"/>
136 * <interceptor-ref name="execAndWait">
137 * <param name="delay">2000<param>
138 * <interceptor-ref>
139 * <result name="wait">longRunningAction-wait.jsp</result>
140 * <result name="success">longRunningAction-success.jsp</result>
141 * </action>
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 * <action name="someAction" class="com.examples.SomeAction">
151 * <interceptor-ref name="completeStack"/>
152 * <interceptor-ref name="execAndWait">
153 * <param name="delay">1000<param>
154 * <param name="delaySleepInterval">50<param>
155 * <interceptor-ref>
156 * <result name="wait">longRunningAction-wait.jsp</result>
157 * <result name="success">longRunningAction-success.jsp</result>
158 * </action>
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;
174 protected boolean executeAfterValidationPass = false;
175
176 private int threadPriority = Thread.NORM_PRIORITY;
177
178
179
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
208
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);
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
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
258 if (bp.getException() != null) {
259 throw bp.getException();
260 }
261
262 return bp.getResult();
263 } else {
264
265
266
267
268 return actionInvocation.invoke();
269 }
270 }
271 }
272
273
274
275
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 }