| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| package org.apache.struts2.interceptor; |
| |
| import com.opensymphony.xwork2.Action; |
| import com.opensymphony.xwork2.ActionContext; |
| import com.opensymphony.xwork2.ActionInvocation; |
| import com.opensymphony.xwork2.ActionProxy; |
| import com.opensymphony.xwork2.config.entities.ResultConfig; |
| import com.opensymphony.xwork2.inject.Container; |
| import com.opensymphony.xwork2.inject.Inject; |
| import com.opensymphony.xwork2.interceptor.MethodFilterInterceptor; |
| import org.apache.logging.log4j.LogManager; |
| import org.apache.logging.log4j.Logger; |
| import org.apache.struts2.ServletActionContext; |
| import org.apache.struts2.interceptor.exec.BackgroundProcess; |
| import org.apache.struts2.interceptor.exec.ExecutorProvider; |
| import org.apache.struts2.interceptor.exec.StrutsBackgroundProcess; |
| import org.apache.struts2.interceptor.exec.StrutsExecutorProvider; |
| import org.apache.struts2.util.TokenHelper; |
| import org.apache.struts2.views.freemarker.FreemarkerResult; |
| |
| import javax.servlet.http.HttpSession; |
| import java.util.Map; |
| |
| /** |
| * <!-- START SNIPPET: description --> |
| * <p> |
| * The ExecuteAndWaitInterceptor is great for running long-lived actions in the background while showing the user a nice |
| * progress meter. This also prevents the HTTP request from timing out when the action takes more than 5 or 10 minutes. |
| * </p> |
| * |
| * <p> Using this interceptor is pretty straight forward. Assuming that you are including struts-default.xml, this |
| * interceptor is already configured but is not part of any of the default stacks. Because of the nature of this |
| * interceptor, it must be the <b>last</b> interceptor in the stack. |
| * </p> |
| * |
| * <p> This interceptor works on a per-session basis. That means that the same action name (myLongRunningAction, in the |
| * above example) cannot be run more than once at a time in a given session. On the initial request or any subsequent |
| * requests (before the action has completed), the <b>wait</b> result will be returned. <b>The wait result is |
| * responsible for issuing a subsequent request back to the action, giving the effect of a self-updating progress |
| * meter</b>. |
| * </p> |
| * |
| * <p> If no "wait" result is found, Struts will automatically generate a wait result on the fly. This result is |
| * written in FreeMarker and cannot run unless FreeMarker is installed. If you don't wish to deploy with FreeMarker, you |
| * must provide your own wait result. This is generally a good thing to do anyway, as the default wait page is very |
| * plain. |
| * </p> |
| * |
| * <p>Whenever the wait result is returned, the <b>action that is currently running in the background will be placed on |
| * top of the stack</b>. This allows you to display progress data, such as a count, in the wait page. By making the wait |
| * page automatically reload the request to the action (which will be short-circuited by the interceptor), you can give |
| * the appearance of an automatic progress meter. |
| * </p> |
| * |
| * <p>This interceptor also supports using an initial wait delay. An initial delay is a time in milliseconds we let the |
| * server wait before the wait page is shown to the user. During the wait this interceptor will wake every 100 millis |
| * to check if the background process is done premature, thus if the job for some reason doesn't take to long the wait |
| * page is not shown to the user. |
| * <br> This is useful for e.g. search actions that have a wide span of execution time. Using a delay time of 2000 |
| * millis we ensure the user is presented fast search results immediately and for the slow results a wait page is used. |
| * </p> |
| * |
| * <p><b>Important</b>: Because the action will be running in a separate thread, you can't use ActionContext because it |
| * is a ThreadLocal. This means if you need to access, for example, session data, you need to implement SessionAware |
| * rather than calling ActionContext.getSession(). |
| * </p> |
| * |
| * <p>The thread kicked off by this interceptor will be named in the form <b><u>actionName</u>BackgroundProcess</b>. |
| * For example, the <i>search</i> action would run as a thread named <i>searchBackgroundProcess</i>. |
| * </p> |
| * <!-- END SNIPPET: description --> |
| * |
| * <p><u>Interceptor parameters:</u></p> |
| * <p> |
| * <!-- START SNIPPET: parameters --> |
| * |
| * <ul> |
| * |
| * <li>threadPriority (optional) - the priority to assign the thread. Default is <code>Thread.NORM_PRIORITY</code>.</li> |
| * <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> |
| * <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> |
| * |
| * </ul> |
| * <p> |
| * <!-- END SNIPPET: parameters --> |
| * |
| * <p><u>Extending the interceptor:</u></p> |
| * <p> |
| * <!-- START SNIPPET: extending --> |
| * <p> |
| * If you wish to make special preparations before and/or after the invocation of the background thread, you can extend |
| * the BackgroundProcess class and implement the beforeInvocation() and afterInvocation() methods. This may be useful |
| * for obtaining and releasing resources that the background process will need to execute successfully. To use your |
| * background process extension, extend ExecuteAndWaitInterceptor and implement the getNewBackgroundProcess() method. |
| * </p> |
| * <!-- END SNIPPET: extending --> |
| * |
| * <p><u>Example code:</u></p> |
| * |
| * <pre> |
| * <!-- START SNIPPET: example --> |
| * <action name="someAction" class="com.examples.SomeAction"> |
| * <interceptor-ref name="completeStack"/> |
| * <interceptor-ref name="execAndWait"/> |
| * <result name="wait">longRunningAction-wait.jsp</result> |
| * <result name="success">longRunningAction-success.jsp</result> |
| * </action> |
| * |
| * <%@ taglib prefix="s" uri="/struts" %> |
| * <html> |
| * <head> |
| * <title>Please wait</title> |
| * <meta http-equiv="refresh" content="5;url=<s:url includeParams="all" />"/> |
| * </head> |
| * <body> |
| * Please wait while we process your request. |
| * Click <a href="<s:url includeParams="all" />"></a> if this page does not reload automatically. |
| * </body> |
| * </html> |
| * </pre> |
| * |
| * <p><u>Example code2:</u></p> |
| * <p> |
| * This example will wait 2 second (2000 millis) before the wait page is shown to the user. Therefore |
| * if the long process didn't last long anyway the user isn't shown a wait page. |
| * </p> |
| * |
| * <pre> |
| * <action name="someAction" class="com.examples.SomeAction"> |
| * <interceptor-ref name="completeStack"/> |
| * <interceptor-ref name="execAndWait"> |
| * <param name="delay">2000<param> |
| * <interceptor-ref> |
| * <result name="wait">longRunningAction-wait.jsp</result> |
| * <result name="success">longRunningAction-success.jsp</result> |
| * </action> |
| * </pre> |
| * |
| * <p><u>Example code3:</u></p> |
| * <p> |
| * This example will wait 1 second (1000 millis) before the wait page is shown to the user. |
| * And at every 50 millis this interceptor will check if the background process is done, if so |
| * it will return before the 1 second has elapsed, and the user isn't shown a wait page. |
| * </p> |
| * |
| * <pre> |
| * <action name="someAction" class="com.examples.SomeAction"> |
| * <interceptor-ref name="completeStack"/> |
| * <interceptor-ref name="execAndWait"> |
| * <param name="delay">1000<param> |
| * <param name="delaySleepInterval">50<param> |
| * <interceptor-ref> |
| * <result name="wait">longRunningAction-wait.jsp</result> |
| * <result name="success">longRunningAction-success.jsp</result> |
| * </action> |
| * </pre> |
| * <p> |
| * <!-- END SNIPPET: example --> |
| */ |
| public class ExecuteAndWaitInterceptor extends MethodFilterInterceptor { |
| |
| private static final long serialVersionUID = -2754639196749652512L; |
| |
| private static final Logger LOG = LogManager.getLogger(ExecuteAndWaitInterceptor.class); |
| |
| public static final String KEY = "__execWait"; |
| public static final String WAIT = "wait"; |
| protected int delay; |
| protected int delaySleepInterval = 100; // default sleep 100 millis before checking if background process is done |
| protected boolean executeAfterValidationPass = false; |
| |
| private int threadPriority = Thread.NORM_PRIORITY; |
| |
| private Container container; |
| private ExecutorProvider executor; |
| |
| @Inject |
| public void setContainer(Container container) { |
| this.container = container; |
| } |
| |
| @Inject |
| public void setExecutorProvider(ExecutorProvider executorProvider) { |
| this.executor = executorProvider; |
| } |
| |
| /** |
| * Creates a new background process |
| * |
| * @param name The process name |
| * @param actionInvocation The action invocation |
| * @param threadPriority The thread priority |
| * @return The new process |
| */ |
| protected BackgroundProcess getNewBackgroundProcess(String name, ActionInvocation actionInvocation, int threadPriority) { |
| return new StrutsBackgroundProcess(actionInvocation, name + "_background-process", threadPriority); |
| } |
| |
| /** |
| * Returns the name to associate the background process. Override to change the way background processes |
| * are mapped to requests. |
| * |
| * @param proxy action proxy |
| * @return the name of the background thread |
| */ |
| protected String getBackgroundProcessName(ActionProxy proxy) { |
| return proxy.getActionName(); |
| } |
| |
| /* (non-Javadoc) |
| * @see com.opensymphony.xwork2.interceptor.MethodFilterInterceptor#doIntercept(com.opensymphony.xwork2.ActionInvocation) |
| */ |
| @Override |
| protected String doIntercept(ActionInvocation actionInvocation) throws Exception { |
| ActionProxy proxy = actionInvocation.getProxy(); |
| String name = getBackgroundProcessName(proxy); |
| ActionContext context = actionInvocation.getInvocationContext(); |
| Map<String, Object> session = context.getSession(); |
| HttpSession httpSession = ServletActionContext.getRequest().getSession(true); |
| |
| //sync on the real HttpSession as the session from the context is a wrap that is created |
| //on every request |
| synchronized (httpSession) { |
| // State flag processing moved within the synchronization block, to ensure consistency. |
| Boolean secondTime = true; |
| if (executeAfterValidationPass) { |
| secondTime = (Boolean) context.get(KEY); |
| if (secondTime == null) { |
| context.put(KEY, true); |
| secondTime = false; |
| } else { |
| secondTime = true; |
| context.put(KEY, null); |
| } |
| } |
| |
| final String bp_SessionKey = KEY + name; |
| BackgroundProcess bp = (BackgroundProcess) session.get(bp_SessionKey); |
| |
| LOG.debug("Intercepting invocation for BackgroundProcess - session key: {}, value: {}", bp_SessionKey, bp); |
| |
| //WW-4900 Checks if from a de-serialized session? so background thread missed, let's start a new one. |
| if (bp != null && bp.getInvocation() == null) { |
| LOG.trace("BackgroundProcess invocation is null (remove key, clear instance)"); |
| session.remove(bp_SessionKey); |
| bp = null; |
| } |
| |
| if ((!executeAfterValidationPass || secondTime) && bp == null) { |
| LOG.trace("BackgroundProcess instance is null (create new instance) - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime); |
| bp = getNewBackgroundProcess(name, actionInvocation, threadPriority).prepare(); |
| session.put(bp_SessionKey, bp); |
| if (executor == null || executor.isShutdown()) { |
| LOG.warn("Executor is shutting down (or null), cannot execute a new process, invoke next ActionInvocation step and return."); |
| return actionInvocation.invoke(); |
| } |
| executor.execute(bp); |
| performInitialDelay(bp); // first time let some time pass before showing wait page |
| secondTime = false; |
| } |
| |
| if ((!executeAfterValidationPass || !secondTime) && bp != null && !bp.isDone()) { |
| LOG.trace("BackgroundProcess instance is not done (wait processing) - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime); |
| actionInvocation.getStack().push(bp.getAction()); |
| |
| final String token = TokenHelper.getToken(); |
| if (token != null) { |
| TokenHelper.setSessionToken(TokenHelper.getTokenName(), token); |
| } |
| |
| Map<String, ResultConfig> results = proxy.getConfig().getResults(); |
| if (!results.containsKey(WAIT)) { |
| LOG.warn("ExecuteAndWait interceptor has detected that no result named 'wait' is available. " + |
| "Defaulting to a plain built-in wait page. It is highly recommend you " + |
| "provide an action-specific or global result named '{}'.", WAIT); |
| // no wait result? hmm -- let's try to do dynamically put it in for you! |
| |
| //we used to add a fake "wait" result here, since the configuration is unmodifiable, that is no longer |
| //an option, see WW-3068 |
| FreemarkerResult waitResult = new FreemarkerResult(); |
| container.inject(waitResult); |
| waitResult.setLocation("/org/apache/struts2/interceptor/wait.ftl"); |
| waitResult.execute(actionInvocation); |
| |
| return Action.NONE; |
| } |
| |
| return WAIT; |
| } else if ((!executeAfterValidationPass || !secondTime) && bp != null && bp.isDone()) { |
| LOG.trace("BackgroundProcess instance is done (remove key, return result) - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime); |
| session.remove(bp_SessionKey); |
| actionInvocation.getStack().push(bp.getAction()); |
| |
| // if an exception occurred during action execution, throw it here |
| if (bp.getException() != null) { |
| throw bp.getException(); |
| } |
| |
| return bp.getResult(); |
| } else { |
| LOG.trace("BackgroundProcess state fall-through (first instance, pass through), invoke next ActionInvocation step and return - executeAfterValidationPass: {}, secondTime: {}.", executeAfterValidationPass, secondTime); |
| // this is the first instance of the interceptor and there is no existing action |
| // already run in the background, so let's just let this pass through. We assume |
| // the action invocation will be run in the background on the subsequent pass through |
| // this interceptor |
| return actionInvocation.invoke(); |
| } |
| } |
| } |
| |
| /** |
| * <p> |
| * Performs the initial delay. |
| * </p> |
| * |
| * <p> |
| * When this interceptor is executed for the first time this methods handles any provided initial delay. |
| * An initial delay is a time in milliseconds we let the server wait before we continue. |
| * <br> During the wait this interceptor will wake every 100 millis to check if the background |
| * process is done premature, thus if the job for some reason doesn't take to long the wait |
| * page is not shown to the user. |
| * </p> |
| * |
| * @param bp the background process |
| * @throws InterruptedException is thrown by Thread.sleep |
| */ |
| protected void performInitialDelay(BackgroundProcess bp) throws InterruptedException { |
| if (delay <= 0 || delaySleepInterval <= 0) { |
| return; |
| } |
| |
| int steps = delay / delaySleepInterval; |
| LOG.debug("Delaying for {} millis. (using {} steps)", delay, steps); |
| int step; |
| for (step = 0; step < steps && !bp.isDone(); step++) { |
| Thread.sleep(delaySleepInterval); |
| } |
| LOG.debug("Sleeping ended after {} steps and the background process is {}", step, (bp.isDone() ? " done" : " not done")); |
| } |
| |
| /** |
| * Sets the thread priority of the background process. |
| * |
| * @param threadPriority the priority from <code>Thread.XXX</code> |
| */ |
| public void setThreadPriority(int threadPriority) { |
| this.threadPriority = threadPriority; |
| } |
| |
| /** |
| * Sets the initial delay in millis (msec). |
| * |
| * @param delay in millis. (0 for not used) |
| */ |
| public void setDelay(int delay) { |
| this.delay = delay; |
| } |
| |
| /** |
| * Sets the sleep interval in millis (msec) when performing the initial delay. |
| * |
| * @param delaySleepInterval in millis (0 for not used) |
| */ |
| public void setDelaySleepInterval(int delaySleepInterval) { |
| this.delaySleepInterval = delaySleepInterval; |
| } |
| |
| /** |
| * Whether to start the background process after the second pass (first being validation) |
| * or not |
| * |
| * @param executeAfterValidationPass the executeAfterValidationPass to set |
| */ |
| public void setExecuteAfterValidationPass(boolean executeAfterValidationPass) { |
| this.executeAfterValidationPass = executeAfterValidationPass; |
| } |
| |
| @Override |
| public void init() { |
| super.init(); |
| if (executor == null) { |
| LOG.debug("Using: {} as ExecutorProvider", StrutsExecutorProvider.class.getSimpleName()); |
| executor = container.getInstance(StrutsExecutorProvider.class); |
| } |
| } |
| |
| @Override |
| public void destroy() { |
| try { |
| executor.shutdown(); |
| } finally { |
| super.destroy(); |
| } |
| } |
| } |