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.config.entities.ResultConfig;
31 import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
32 import com.opensymphony.xwork2.util.logging.Logger;
33 import com.opensymphony.xwork2.util.logging.LoggerFactory;
34 import org.apache.struts2.util.TokenHelper;
35
36
37 /***
38 * <!-- START SNIPPET: description -->
39 *
40 * The ExecuteAndWaitInterceptor is great for running long-lived actions in the background while showing the user a nice
41 * progress meter. This also prevents the HTTP request from timing out when the action takes more than 5 or 10 minutes.
42 *
43 * <p/> Using this interceptor is pretty straight forward. Assuming that you are including struts-default.xml, this
44 * interceptor is already configured but is not part of any of the default stacks. Because of the nature of this
45 * interceptor, it must be the <b>last</b> interceptor in the stack.
46 *
47 * <p/> This interceptor works on a per-session basis. That means that the same action name (myLongRunningAction, in the
48 * above example) cannot be run more than once at a time in a given session. On the initial request or any subsequent
49 * requests (before the action has completed), the <b>wait</b> result will be returned. <b>The wait result is
50 * responsible for issuing a subsequent request back to the action, giving the effect of a self-updating progress
51 * meter</b>.
52 *
53 * <p/> If no "wait" result is found, Struts will automatically generate a wait result on the fly. This result is
54 * written in FreeMarker and cannot run unless FreeMarker is installed. If you don't wish to deploy with FreeMarker, you
55 * must provide your own wait result. This is generally a good thing to do anyway, as the default wait page is very
56 * plain.
57 *
58 * <p/>Whenever the wait result is returned, the <b>action that is currently running in the background will be placed on
59 * top of the stack</b>. This allows you to display progress data, such as a count, in the wait page. By making the wait
60 * page automatically reload the request to the action (which will be short-circuited by the interceptor), you can give
61 * the appearance of an automatic progress meter.
62 *
63 * <p/>This interceptor also supports using an initial wait delay. An initial delay is a time in milliseconds we let the
64 * server wait before the wait page is shown to the user. During the wait this interceptor will wake every 100 millis
65 * to check if the background process is done premature, thus if the job for some reason doesn't take to long the wait
66 * page is not shown to the user.
67 * <br/> This is useful for e.g. search actions that have a wide span of execution time. Using a delay time of 2000
68 * millis we ensure the user is presented fast search results immediately and for the slow results a wait page is used.
69 *
70 * <p/><b>Important</b>: Because the action will be running in a seperate thread, you can't use ActionContext because it
71 * is a ThreadLocal. This means if you need to access, for example, session data, you need to implement SessionAware
72 * rather than calling ActionContext.getSession().
73 *
74 * <p/>The thread kicked off by this interceptor will be named in the form <b><u>actionName</u>BackgroundProcess</b>.
75 * For example, the <i>search</i> action would run as a thread named <i>searchBackgroundProcess</i>.
76 *
77 * <!-- END SNIPPET: description -->
78 *
79 * <p/> <u>Interceptor parameters:</u>
80 *
81 * <!-- START SNIPPET: parameters -->
82 *
83 * <ul>
84 *
85 * <li>threadPriority (optional) - the priority to assign the thread. Default is <code>Thread.NORM_PRIORITY</code>.</li>
86 * <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>
87 * <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>
88 *
89 * </ul>
90 *
91 * <!-- END SNIPPET: parameters -->
92 *
93 * <p/> <u>Extending the interceptor:</u>
94 *
95 * <p/>
96 *
97 * <!-- START SNIPPET: extending -->
98 *
99 * If you wish to make special preparations before and/or after the invocation of the background thread, you can extend
100 * the BackgroundProcess class and implement the beforeInvocation() and afterInvocation() methods. This may be useful
101 * for obtaining and releasing resources that the background process will need to execute successfully. To use your
102 * background process extension, extend ExecuteAndWaitInterceptor and implement the getNewBackgroundProcess() method.
103 *
104 * <!-- END SNIPPET: extending -->
105 *
106 * <p/> <u>Example code:</u>
107 *
108 * <pre>
109 * <!-- START SNIPPET: example -->
110 * <action name="someAction" class="com.examples.SomeAction">
111 * <interceptor-ref name="completeStack"/>
112 * <interceptor-ref name="execAndWait"/>
113 * <result name="wait">longRunningAction-wait.jsp</result>
114 * <result name="success">longRunningAction-success.jsp</result>
115 * </action>
116 *
117 * <%@ taglib prefix="s" uri="/struts" %>
118 * <html>
119 * <head>
120 * <title>Please wait</title>
121 * <meta http-equiv="refresh" content="5;url=<s:url includeParams="all" />"/>
122 * </head>
123 * <body>
124 * Please wait while we process your request.
125 * Click <a href="<s:url includeParams="all" />"></a> if this page does not reload automatically.
126 * </body>
127 * </html>
128 * </pre>
129 *
130 * <p/> <u>Example code2:</u>
131 * This example will wait 2 second (2000 millis) before the wait page is shown to the user. Therefore
132 * if the long process didn't last long anyway the user isn't shown a wait page.
133 *
134 * <pre>
135 * <action name="someAction" class="com.examples.SomeAction">
136 * <interceptor-ref name="completeStack"/>
137 * <interceptor-ref name="execAndWait">
138 * <param name="delay">2000<param>
139 * <interceptor-ref>
140 * <result name="wait">longRunningAction-wait.jsp</result>
141 * <result name="success">longRunningAction-success.jsp</result>
142 * </action>
143 * </pre>
144 *
145 * <p/> <u>Example code3:</u>
146 * This example will wait 1 second (1000 millis) before the wait page is shown to the user.
147 * And at every 50 millis this interceptor will check if the background process is done, if so
148 * it will return before the 1 second has elapsed, and the user isn't shown a wait page.
149 *
150 * <pre>
151 * <action name="someAction" class="com.examples.SomeAction">
152 * <interceptor-ref name="completeStack"/>
153 * <interceptor-ref name="execAndWait">
154 * <param name="delay">1000<param>
155 * <param name="delaySleepInterval">50<param>
156 * <interceptor-ref>
157 * <result name="wait">longRunningAction-wait.jsp</result>
158 * <result name="success">longRunningAction-success.jsp</result>
159 * </action>
160 * </pre>
161 *
162 * <!-- END SNIPPET: example -->
163 *
164 */
165 public class ExecuteAndWaitInterceptor extends MethodFilterInterceptor {
166
167 private static final long serialVersionUID = -2754639196749652512L;
168
169 private static final Logger LOG = LoggerFactory.getLogger(ExecuteAndWaitInterceptor.class);
170
171 public static final String KEY = "__execWait";
172 public static final String WAIT = "wait";
173 protected int delay;
174 protected int delaySleepInterval = 100;
175 protected boolean executeAfterValidationPass = false;
176
177 private int threadPriority = Thread.NORM_PRIORITY;
178
179
180
181
182 public void init() {
183 }
184
185 /***
186 * Creates a new background process
187 *
188 * @param name The process name
189 * @param actionInvocation The action invocation
190 * @param threadPriority The thread priority
191 * @return The new process
192 */
193 protected BackgroundProcess getNewBackgroundProcess(String name, ActionInvocation actionInvocation, int threadPriority) {
194 return new BackgroundProcess(name + "BackgroundThread", actionInvocation, threadPriority);
195 }
196
197 /***
198 * Returns the name to associate the background process. Override to change the way background processes
199 * are mapped to requests.
200 *
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.Builder(WAIT, "org.apache.struts2.views.freemarker.FreemarkerResult")
248 .addParams(Collections.singletonMap("location", "/org/apache/struts2/interceptor/wait.ftl"))
249 .build();
250 results.put(WAIT, rc);
251 }
252
253 if (TokenHelper.getToken() != null) {
254 session.put(TokenHelper.getTokenName(), TokenHelper.getToken());
255 }
256
257 return WAIT;
258 } else if ((!executeAfterValidationPass || !secondTime) && bp != null && bp.isDone()) {
259 session.remove(KEY + name);
260 actionInvocation.getStack().push(bp.getAction());
261
262
263 if (bp.getException() != null) {
264 throw bp.getException();
265 }
266
267 return bp.getResult();
268 } else {
269
270
271
272
273 return actionInvocation.invoke();
274 }
275 }
276 }
277
278
279
280
281
282 public void destroy() {
283 }
284
285 /***
286 * Performs the initial delay.
287 * <p/>
288 * When this interceptor is executed for the first time this methods handles any provided initial delay.
289 * An initial delay is a time in miliseconds we let the server wait before we continue.
290 * <br/> During the wait this interceptor will wake every 100 millis to check if the background
291 * process is done premature, thus if the job for some reason doesn't take to long the wait
292 * page is not shown to the user.
293 *
294 * @param bp the background process
295 * @throws InterruptedException is thrown by Thread.sleep
296 */
297 protected void performInitialDelay(BackgroundProcess bp) throws InterruptedException {
298 if (delay <= 0 || delaySleepInterval <= 0) {
299 return;
300 }
301
302 int steps = delay / delaySleepInterval;
303 if (LOG.isDebugEnabled()) {
304 LOG.debug("Delaying for " + delay + " millis. (using " + steps + " steps)");
305 }
306 int step;
307 for (step = 0; step < steps && !bp.isDone(); step++) {
308 Thread.sleep(delaySleepInterval);
309 }
310 if (LOG.isDebugEnabled()) {
311 LOG.debug("Sleeping ended after " + step + " steps and the background process is " + (bp.isDone() ? " done" : " not done"));
312 }
313 }
314
315 /***
316 * Sets the thread priority of the background process.
317 *
318 * @param threadPriority the priority from <code>Thread.XXX</code>
319 */
320 public void setThreadPriority(int threadPriority) {
321 this.threadPriority = threadPriority;
322 }
323
324 /***
325 * Sets the initial delay in millis (msec).
326 *
327 * @param delay in millis. (0 for not used)
328 */
329 public void setDelay(int delay) {
330 this.delay = delay;
331 }
332
333 /***
334 * Sets the sleep interval in millis (msec) when performing the initial delay.
335 *
336 * @param delaySleepInterval in millis (0 for not used)
337 */
338 public void setDelaySleepInterval(int delaySleepInterval) {
339 this.delaySleepInterval = delaySleepInterval;
340 }
341
342 /***
343 * Whether to start the background process after the second pass (first being validation)
344 * or not
345 *
346 * @param executeAfterValidationPass the executeAfterValidationPass to set
347 */
348 public void setExecuteAfterValidationPass(boolean executeAfterValidationPass) {
349 this.executeAfterValidationPass = executeAfterValidationPass;
350 }
351
352
353 }