All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.struts2.interceptor.ExecuteAndWaitInterceptor Maven / Gradle / Ivy

There is a newer version: 6.4.0
Show newest version
/*
 * 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.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.util.TokenHelper;
import org.apache.struts2.views.freemarker.FreemarkerResult;

import javax.servlet.http.HttpSession;
import java.util.Map;

/**
 * 
 * 

* 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. *

* *

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 last interceptor in the stack. *

* *

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 wait result will be returned. The wait result is * responsible for issuing a subsequent request back to the action, giving the effect of a self-updating progress * meter. *

* *

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. *

* *

Whenever the wait result is returned, the action that is currently running in the background will be placed on * top of the stack. 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. *

* *

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. *
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. *

* *

Important: 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(). *

* *

The thread kicked off by this interceptor will be named in the form actionNameBackgroundProcess. * For example, the search action would run as a thread named searchBackgroundProcess. *

* * *

Interceptor parameters:

* * * *
    * *
  • threadPriority (optional) - the priority to assign the thread. Default is Thread.NORM_PRIORITY.
  • *
  • delay (optional) - an initial delay in millis to wait before the wait page is shown (returning wait as result code). Default is no initial delay.
  • *
  • 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.
  • * *
* * * *

Extending the interceptor:

* * *

* 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. *

* * *

Example code:

* *
 * 
 * <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>
 * 
* *

Example code2:

*

* 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. *

* *
 * <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>
 * 
* *

Example code3:

*

* 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. *

* *
 * <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>
 * 
* * * */ 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; @Inject public void setContainer(Container container) { this.container = container; } /** * 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 BackgroundProcess(name + "BackgroundThread", actionInvocation, 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) */ protected String doIntercept(ActionInvocation actionInvocation) throws Exception { ActionProxy proxy = actionInvocation.getProxy(); String name = getBackgroundProcessName(proxy); ActionContext context = actionInvocation.getInvocationContext(); Map session = context.getSession(); HttpSession httpSession = ServletActionContext.getRequest().getSession(true); 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); } } //sync on the real HttpSession as the session from the context is a wrap that is created //on every request synchronized (httpSession) { BackgroundProcess bp = (BackgroundProcess) session.get(KEY + name); //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) { session.remove(KEY + name); bp = null; } if ((!executeAfterValidationPass || secondTime) && bp == null) { bp = getNewBackgroundProcess(name, actionInvocation, threadPriority); session.put(KEY + name, bp); performInitialDelay(bp); // first time let some time pass before showing wait page secondTime = false; } if ((!executeAfterValidationPass || !secondTime) && bp != null && !bp.isDone()) { actionInvocation.getStack().push(bp.getAction()); final String token = TokenHelper.getToken(); if (token != null) { TokenHelper.setSessionToken(TokenHelper.getTokenName(), token); } Map 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()) { session.remove(KEY + name); actionInvocation.getStack().push(bp.getAction()); // if an exception occured during action execution, throw it here if (bp.getException() != null) { throw bp.getException(); } return bp.getResult(); } else { // 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(); } } } /** *

* Performs the initial delay. *

* *

* 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. *
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. *

* * @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 Thread.XXX */ 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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy