001: /*
002: * $Id: ExecuteAndWaitInterceptor.java 508914 2007-02-18 16:45:30Z tschneider $
003: *
004: * Licensed to the Apache Software Foundation (ASF) under one
005: * or more contributor license agreements. See the NOTICE file
006: * distributed with this work for additional information
007: * regarding copyright ownership. The ASF licenses this file
008: * to you under the Apache License, Version 2.0 (the
009: * "License"); you may not use this file except in compliance
010: * with the License. You may obtain a copy of the License at
011: *
012: * http://www.apache.org/licenses/LICENSE-2.0
013: *
014: * Unless required by applicable law or agreed to in writing,
015: * software distributed under the License is distributed on an
016: * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017: * KIND, either express or implied. See the License for the
018: * specific language governing permissions and limitations
019: * under the License.
020: */
021: package org.apache.struts2.interceptor;
022:
023: import java.util.Collections;
024: import java.util.Map;
025:
026: import org.apache.commons.logging.Log;
027: import org.apache.commons.logging.LogFactory;
028:
029: import com.opensymphony.xwork2.ActionContext;
030: import com.opensymphony.xwork2.ActionInvocation;
031: import com.opensymphony.xwork2.ActionProxy;
032: import com.opensymphony.xwork2.config.entities.ResultConfig;
033: import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor;
034:
035: /**
036: * <!-- START SNIPPET: description -->
037: *
038: * The ExecuteAndWaitInterceptor is great for running long-lived actions in the background while showing the user a nice
039: * progress meter. This also prevents the HTTP request from timing out when the action takes more than 5 or 10 minutes.
040: *
041: * <p/> Using this interceptor is pretty straight forward. Assuming that you are including struts-default.xml, this
042: * interceptor is already configured but is not part of any of the default stacks. Because of the nature of this
043: * interceptor, it must be the <b>last</b> interceptor in the stack.
044: *
045: * <p/> This interceptor works on a per-session basis. That means that the same action name (myLongRunningAction, in the
046: * above example) cannot be run more than once at a time in a given session. On the initial request or any subsequent
047: * requests (before the action has completed), the <b>wait</b> result will be returned. <b>The wait result is
048: * responsible for issuing a subsequent request back to the action, giving the effect of a self-updating progress
049: * meter</b>.
050: *
051: * <p/> If no "wait" result is found, Struts will automatically generate a wait result on the fly. This result is
052: * written in FreeMarker and cannot run unless FreeMarker is installed. If you don't wish to deploy with FreeMarker, you
053: * must provide your own wait result. This is generally a good thing to do anyway, as the default wait page is very
054: * plain.
055: *
056: * <p/>Whenever the wait result is returned, the <b>action that is currently running in the background will be placed on
057: * top of the stack</b>. This allows you to display progress data, such as a count, in the wait page. By making the wait
058: * page automatically reload the request to the action (which will be short-circuited by the interceptor), you can give
059: * the appearance of an automatic progress meter.
060: *
061: * <p/>This interceptor also supports using an initial wait delay. An initial delay is a time in milliseconds we let the
062: * server wait before the wait page is shown to the user. During the wait this interceptor will wake every 100 millis
063: * to check if the background process is done premature, thus if the job for some reason doesn't take to long the wait
064: * page is not shown to the user.
065: * <br/> This is useful for e.g. search actions that have a wide span of execution time. Using a delay time of 2000
066: * millis we ensure the user is presented fast search results immediately and for the slow results a wait page is used.
067: *
068: * <p/><b>Important</b>: Because the action will be running in a seperate thread, you can't use ActionContext because it
069: * is a ThreadLocal. This means if you need to access, for example, session data, you need to implement SessionAware
070: * rather than calling ActionContext.getSesion().
071: *
072: * <p/>The thread kicked off by this interceptor will be named in the form <b><u>actionName</u>BrackgroundProcess</b>.
073: * For example, the <i>search</i> action would run as a thread named <i>searchBackgroundProcess</i>.
074: *
075: * <!-- END SNIPPET: description -->
076: *
077: * <p/> <u>Interceptor parameters:</u>
078: *
079: * <!-- START SNIPPET: parameters -->
080: *
081: * <ul>
082: *
083: * <li>threadPriority (optional) - the priority to assign the thread. Default is <code>Thread.NORM_PRIORITY</code>.</li>
084: * <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>
085: * <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>
086: *
087: * </ul>
088: *
089: * <!-- END SNIPPET: parameters -->
090: *
091: * <p/> <u>Extending the interceptor:</u>
092: *
093: * <p/>
094: *
095: * <!-- START SNIPPET: extending -->
096: *
097: * If you wish to make special preparations before and/or after the invocation of the background thread, you can extend
098: * the BackgroundProcess class and implement the beforeInvocation() and afterInvocation() methods. This may be useful
099: * for obtaining and releasing resources that the background process will need to execute successfully. To use your
100: * background process extension, extend ExecuteAndWaitInterceptor and implement the getNewBackgroundProcess() method.
101: *
102: * <!-- END SNIPPET: extending -->
103: *
104: * <p/> <u>Example code:</u>
105: *
106: * <pre>
107: * <!-- START SNIPPET: example -->
108: * <action name="someAction" class="com.examples.SomeAction">
109: * <interceptor-ref name="completeStack"/>
110: * <interceptor-ref name="execAndWait"/>
111: * <result name="wait">longRunningAction-wait.jsp</result>
112: * <result name="success">longRunningAction-success.jsp</result>
113: * </action>
114: *
115: * <%@ taglib prefix="s" uri="/struts" %>
116: * <html>
117: * <head>
118: * <title>Please wait</title>
119: * <meta http-equiv="refresh" content="5;url=<a:url includeParams="all" />"/>
120: * </head>
121: * <body>
122: * Please wait while we process your request.
123: * Click <a href="<a:url includeParams="all" />"></a> if this page does not reload automatically.
124: * </body>
125: * </html>
126: * </pre>
127: *
128: * <p/> <u>Example code2:</u>
129: * This example will wait 2 second (2000 millis) before the wait page is shown to the user. Therefore
130: * if the long process didn't last long anyway the user isn't shown a wait page.
131: *
132: * <pre>
133: * <action name="someAction" class="com.examples.SomeAction">
134: * <interceptor-ref name="completeStack"/>
135: * <interceptor-ref name="execAndWait">
136: * <param name="delay">2000<param>
137: * <interceptor-ref>
138: * <result name="wait">longRunningAction-wait.jsp</result>
139: * <result name="success">longRunningAction-success.jsp</result>
140: * </action>
141: * </pre>
142: *
143: * <p/> <u>Example code3:</u>
144: * This example will wait 1 second (1000 millis) before the wait page is shown to the user.
145: * And at every 50 millis this interceptor will check if the background process is done, if so
146: * it will return before the 1 second has elapsed, and the user isn't shown a wait page.
147: *
148: * <pre>
149: * <action name="someAction" class="com.examples.SomeAction">
150: * <interceptor-ref name="completeStack"/>
151: * <interceptor-ref name="execAndWait">
152: * <param name="delay">1000<param>
153: * <param name="delaySleepInterval">50<param>
154: * <interceptor-ref>
155: * <result name="wait">longRunningAction-wait.jsp</result>
156: * <result name="success">longRunningAction-success.jsp</result>
157: * </action>
158: * </pre>
159: *
160: * <!-- END SNIPPET: example -->
161: *
162: */
163: public class ExecuteAndWaitInterceptor extends MethodFilterInterceptor {
164:
165: private static final long serialVersionUID = -2754639196749652512L;
166:
167: private static final Log LOG = LogFactory
168: .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; // default sleep 100 millis before checking if background process is done
174: protected boolean executeAfterValidationPass = false;
175:
176: private int threadPriority = Thread.NORM_PRIORITY;
177:
178: /* (non-Javadoc)
179: * @see com.opensymphony.xwork2.interceptor.Interceptor#init()
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,
193: ActionInvocation actionInvocation, int threadPriority) {
194: return new BackgroundProcess(name + "BackgroundThread",
195: actionInvocation, threadPriority);
196: }
197:
198: /**
199: * Returns the name to associate the background process. Override to change the way background processes
200: * are mapped to requests.
201: *
202: * @param actionInvocation The action invocation
203: * @return the name of the background thread
204: */
205: protected String getBackgroundProcessName(ActionProxy proxy) {
206: return proxy.getActionName();
207: }
208:
209: /* (non-Javadoc)
210: * @see com.opensymphony.xwork2.interceptor.MethodFilterInterceptor#doIntercept(com.opensymphony.xwork2.ActionInvocation)
211: */
212: protected String doIntercept(ActionInvocation actionInvocation)
213: throws Exception {
214: ActionProxy proxy = actionInvocation.getProxy();
215: String name = getBackgroundProcessName(proxy);
216: ActionContext context = actionInvocation.getInvocationContext();
217: Map session = context.getSession();
218:
219: Boolean secondTime = true;
220: if (executeAfterValidationPass) {
221: secondTime = (Boolean) context.get(KEY);
222: if (secondTime == null) {
223: context.put(KEY, true);
224: secondTime = false;
225: } else {
226: secondTime = true;
227: context.put(KEY, null);
228: }
229: }
230:
231: synchronized (session) {
232: BackgroundProcess bp = (BackgroundProcess) session.get(KEY
233: + name);
234:
235: if ((!executeAfterValidationPass || secondTime)
236: && bp == null) {
237: bp = getNewBackgroundProcess(name, actionInvocation,
238: threadPriority);
239: session.put(KEY + name, bp);
240: performInitialDelay(bp); // first time let some time pass before showing wait page
241: secondTime = false;
242: }
243:
244: if ((!executeAfterValidationPass || !secondTime)
245: && bp != null && !bp.isDone()) {
246: actionInvocation.getStack().push(bp.getAction());
247: Map results = proxy.getConfig().getResults();
248: if (!results.containsKey(WAIT)) {
249: LOG
250: .warn("ExecuteAndWait interceptor has detected that no result named 'wait' is available. "
251: + "Defaulting to a plain built-in wait page. It is highly recommend you "
252: + "provide an action-specific or global result named '"
253: + WAIT
254: + "'! This requires FreeMarker support and won't work if you don't have it installed");
255: // no wait result? hmm -- let's try to do dynamically put it in for you!
256: ResultConfig rc = new ResultConfig(
257: WAIT,
258: "org.apache.struts2.views.freemarker.FreemarkerResult",
259: Collections
260: .singletonMap("location",
261: "/org/apache/struts2/interceptor/wait.ftl"));
262: results.put(WAIT, rc);
263: }
264:
265: return WAIT;
266: } else if ((!executeAfterValidationPass || !secondTime)
267: && bp != null && bp.isDone()) {
268: session.remove(KEY + name);
269: actionInvocation.getStack().push(bp.getAction());
270:
271: // if an exception occured during action execution, throw it here
272: if (bp.getException() != null) {
273: throw bp.getException();
274: }
275:
276: return bp.getResult();
277: } else {
278: // this is the first instance of the interceptor and there is no existing action
279: // already run in the background, so let's just let this pass through. We assume
280: // the action invocation will be run in the background on the subsequent pass through
281: // this interceptor
282: return actionInvocation.invoke();
283: }
284: }
285: }
286:
287: /* (non-Javadoc)
288: * @see com.opensymphony.xwork2.interceptor.Interceptor#destroy()
289: */
290: public void destroy() {
291: }
292:
293: /**
294: * Performs the initial delay.
295: * <p/>
296: * When this interceptor is executed for the first time this methods handles any provided initial delay.
297: * An initial delay is a time in miliseconds we let the server wait before we continue.
298: * <br/> During the wait this interceptor will wake every 100 millis to check if the background
299: * process is done premature, thus if the job for some reason doesn't take to long the wait
300: * page is not shown to the user.
301: *
302: * @param bp the background process
303: * @throws InterruptedException is thrown by Thread.sleep
304: */
305: protected void performInitialDelay(BackgroundProcess bp)
306: throws InterruptedException {
307: if (delay <= 0 || delaySleepInterval <= 0) {
308: return;
309: }
310:
311: int steps = delay / delaySleepInterval;
312: if (LOG.isDebugEnabled()) {
313: LOG.debug("Delaying for " + delay + " millis. (using "
314: + steps + " steps)");
315: }
316: int step;
317: for (step = 0; step < steps && !bp.isDone(); step++) {
318: Thread.sleep(delaySleepInterval);
319: }
320: if (LOG.isDebugEnabled()) {
321: LOG.debug("Sleeping ended after " + step
322: + " steps and the background process is "
323: + (bp.isDone() ? " done" : " not done"));
324: }
325: }
326:
327: /**
328: * Sets the thread priority of the background process.
329: *
330: * @param threadPriority the priority from <code>Thread.XXX</code>
331: */
332: public void setThreadPriority(int threadPriority) {
333: this .threadPriority = threadPriority;
334: }
335:
336: /**
337: * Sets the initial delay in millis (msec).
338: *
339: * @param delay in millis. (0 for not used)
340: */
341: public void setDelay(int delay) {
342: this .delay = delay;
343: }
344:
345: /**
346: * Sets the sleep interval in millis (msec) when performing the initial delay.
347: *
348: * @param delaySleepInterval in millis (0 for not used)
349: */
350: public void setDelaySleepInterval(int delaySleepInterval) {
351: this .delaySleepInterval = delaySleepInterval;
352: }
353:
354: /**
355: * Whether to start the background process after the second pass (first being validation)
356: * or not
357: *
358: * @param executeAfterValidationPass the executeAfterValidationPass to set
359: */
360: public void setExecuteAfterValidationPass(
361: boolean executeAfterValidationPass) {
362: this.executeAfterValidationPass = executeAfterValidationPass;
363: }
364:
365: }
|