1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 * <action name="someAction" class="com.examples.SomeAction">
107 * <interceptor-ref name="completeStack"/>
108 * <interceptor-ref name="execAndWait"/>
109 * <result name="wait">longRunningAction-wait.jsp</result>
110 * <result name="success">longRunningAction-success.jsp</result>
111 * </action>
112 *
113 * <%@ taglib prefix="s" uri="/struts" %>
114 * <html>
115 * <head>
116 * <title>Please wait</title>
117 * <meta http-equiv="refresh" content="5;url=<a:url includeParams="all" />"/>
118 * </head>
119 * <body>
120 * Please wait while we process your request.
121 * Click <a href="<a:url includeParams="all" />"></a> if this page does not reload automatically.
122 * </body>
123 * </html>
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 * <action name="someAction" class="com.examples.SomeAction">
132 * <interceptor-ref name="completeStack"/>
133 * <interceptor-ref name="execAndWait">
134 * <param name="delay">2000<param>
135 * <interceptor-ref>
136 * <result name="wait">longRunningAction-wait.jsp</result>
137 * <result name="success">longRunningAction-success.jsp</result>
138 * </action>
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 * <action name="someAction" class="com.examples.SomeAction">
148 * <interceptor-ref name="completeStack"/>
149 * <interceptor-ref name="execAndWait">
150 * <param name="delay">1000<param>
151 * <param name="delaySleepInterval">50<param>
152 * <interceptor-ref>
153 * <result name="wait">longRunningAction-wait.jsp</result>
154 * <result name="success">longRunningAction-success.jsp</result>
155 * </action>
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;
171 protected boolean executeAfterValidationPass = false;
172
173 private int threadPriority = Thread.NORM_PRIORITY;
174
175
176
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
194
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);
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
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
243 if (bp.getException() != null) {
244 throw bp.getException();
245 }
246
247 return bp.getResult();
248 } else {
249
250
251
252
253 return actionInvocation.invoke();
254 }
255 }
256 }
257
258
259
260
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 }