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