package er.coolcomponents;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.webobjects.appserver.WOActionResults;
import com.webobjects.appserver.WOApplication;
import com.webobjects.appserver.WOComponent;
import com.webobjects.appserver.WOContext;
import com.webobjects.foundation._NSUtilities;
import er.extensions.appserver.ERXApplication;
import er.extensions.appserver.ERXNextPageForResultWOAction;
import er.extensions.appserver.ERXWOContext;
import er.extensions.appserver.IERXPerformWOAction;
import er.extensions.appserver.IERXPerformWOActionForResult;
import er.extensions.concurrency.ERXExecutorService;
import er.extensions.concurrency.ERXFutureTask;
import er.extensions.concurrency.IERXPercentComplete;
import er.extensions.concurrency.IERXStoppable;
import er.extensions.foundation.ERXAssert;
import er.extensions.foundation.ERXProperties;
import er.extensions.foundation.ERXRuntimeUtilities;
import er.extensions.foundation.ERXStopWatch;
import er.extensions.foundation.IERXStatus;
/**
* A generic long response page that controls the execution of and provides user feedback on a long
* running task.
* This is designed to be really easy, and flexible, for the developer to re-use.
* <p>
* The common case of running a task and automatically returning to the same originating page is
* really simple and requires just a few lines of code, for example:
* <pre>
* <code>
* public WOActionResults runLongTask() {
Runnable task = new LongRunningTask();
CCAjaxLongResponsePage nextPage = pageWithName(CCAjaxLongResponsePage.class);
nextPage.setLongRunningRunnable(task);
return nextPage;
}
* </code>
* </pre>
* <h3>Usage:</h3>
* <ol>
* <li>
* Create a {@link Runnable} task, or a {@link Callable} task, that returns some result.
* <ol>
* <li>Optionally implement the {@link IERXStatus} interface (just one method to return status message)
* to have the task's status displayed in the long response page.
* <li>Optionally implement the {@link IERXPercentComplete} interface (just one method to return percentage complete)
* to have a progress bar and a percentage complete automatically displayed in the long response page.
* <li>Optionally implement the {@link IERXStoppable} interface to allow stopping of the task by the user.
* </ol>
* </li>
* <li>If you don't just want the originating page to be returned (default behavior) then
* <ol>
* <li>Create a simple class that implements @link {@link er.extensions.appserver.IERXPerformWOActionForResult} interface, or use {@link er.extensions.appserver.ERXNextPageForResultWOAction}, which
* provides a fairly generic implementation of that interface
* <li>This controller class will get the result pushed into it when the task is complete. If the
* task threw an uncaught error during execution, then the error is pushed in as the result.
* <li>The nextPage method of this controller class can do whatever it needs to do with the result
* and return a new page according to your logic in {@link IERXPerformWOActionForResult#performAction()}.
* </ol>
* </li><li>
* In your component action, simply create an instance of this long response page just as you would
* create any other page.
* </li><li>
* Push in an instance of your Runnable (or Callable) task into the long response page using {@link #setTask(Object)}
* </li><li>
* Optionally push in your custom next page controller for execution when the task is finished using {@link #setNextPageForResultController(IERXPerformWOActionForResult)}
* </li><li>
* Just return the long response page in your action method
* </li></ol>
*
* <h3>Customizing the CCAjaxLongResponsePage for your Application</h3>
* <p>This long response page can be easily customized using a custom CSS style sheet and a few system properties.
* <h4>Customizing the Appearance with CSS</h4>
* <p>You can create a custom CSS style sheet, place it in any framework (or your app) and set the following two properties to have it used instead of the default CSS style-sheet:
* <dl>
* <dt><code>er.coolcomponents.CCAjaxLongResponsePage.stylesheet.framework</code></dt>
* <dd>Set this the name of the framework that contains the custom css style sheet,
* or set it to "app" if the style-sheet is in the application bundle.</dd>
* <dt><code>er.coolcomponents.CCAjaxLongResponsePage.stylesheet.filename</code></dt>
* <dd>Set this to the filename of the css style sheet.</dd>
* <dt><code>er.coolcomponents.CCAjaxLongResponsePage.stayOnLongResponsePageIndefinitely</code></dt>
* <dd>
* As a convenience for CSS style sheet development, you can <em>temporarily</em> set this
* property to <code>true</code> to prevent ajax refresh on the long response page and to keep the page open indefinitely
* even after the task has completed. The property is ignored in deployment mode.
* </dd>
* </dl>
*
*
* <h4>Further Configuration Options</h4>
* <p>The following properties can be used to implement additional custom behavior:
* <dl>
* <dt><code>er.coolcomponents.CCAjaxLongResponsePage.defaultStatus</code></dt>
* <dd>This determines the default status text when the task does not implement {@link IERXStatus}</dd>
* <dt><code>er.coolcomponents.CCAjaxLongResponsePage.refreshInterval</code></dt>
* <dd>This value in seconds determines a custom refresh interval for the update container on the page. The default is 2 seconds</dd>
* <dt><code>er.coolcomponents.CCAjaxLongResponsePage.nextPageForErrorResultControllerClassName</code></dt>
* <dd>Defines a custom subclass of IERXPerformWOActionForResult as the default controller class, other than the hard-coded default, for handling task errors for the application. Note that <strong>if</strong> you
* declare a constructor that takes a single WOComponent argument, that constructor <strong>will</strong> be used and the originating page will
* be passed in as the constructor argument for you to use as you please in your custom error handling logic.</dd>
* </dl>
* @author kieran
*
*/
public class CCAjaxLongResponsePage extends WOComponent {
/**
* Do I need to update serialVersionUID?
* See section 5.6 <cite>Type Changes Affecting Serialization</cite> on page 51 of the
* <a href="http://java.sun.com/j2se/1.4/pdf/serial-spec.pdf">Java Object Serialization Spec</a>
*/
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(CCAjaxLongResponsePage.class);
// Constants to determine the CSS stylesheet used for the long response page for this app
private static final String STYLESHEET_FRAMEWORK = ERXProperties.stringForKeyWithDefault("er.coolcomponents.CCAjaxLongResponsePage.stylesheet.framework", "ERCoolComponents");
private static final String STYLESHEET_FILENAME = ERXProperties.stringForKeyWithDefault("er.coolcomponents.CCAjaxLongResponsePage.stylesheet.filename", "CCAjaxLongResponsePage.css");
// Lazy initialization of errorController class
private static class DEFAULT_CONTROLLER {
final static String KEY_FOR_CLASSNAME = "er.coolcomponents.CCAjaxLongResponsePage.nextPageForErrorResultControllerClassName";
// This is the default controller for handling errors
final static Class<? extends IERXPerformWOActionForResult> FOR_ERROR_RESULT = defaultErrorController();
// This is the preferred constructor if a custom user-defined class is used.
// We look for a constructor that takes a single WOComponent argument and we pass in the original referring page to that constructor
// giving the developer an opportunity to implement custom error messages in the original page if he so chooses.
final static Constructor<? extends IERXPerformWOActionForResult> CONSTRUCTOR_FOR_ERROR_RESULT = preferredConstructor();
@SuppressWarnings("unchecked")
// Suppresses warning given by _NSUtilities.classWithName(className)
private static Class<? extends IERXPerformWOActionForResult> defaultErrorController() {
Class<? extends IERXPerformWOActionForResult> clazz = null;
String className = ERXProperties.stringForKey(KEY_FOR_CLASSNAME);
if (className != null) {
clazz = _NSUtilities.classWithName(className);
} else {
clazz = ErrorResultController.class;
}
log.debug("Default error controller class = {}", clazz);
return clazz;
}
private static Constructor<? extends IERXPerformWOActionForResult> preferredConstructor() {
Constructor<? extends IERXPerformWOActionForResult> result = null;
if (!FOR_ERROR_RESULT.getClass().equals(ErrorResultController.class)) {
// We have a custom user-defined error controller
// Check to see if that controller has a constructor with a single WOComponent argument
try {
result = FOR_ERROR_RESULT.getConstructor(WOComponent.class);
} catch (NoSuchMethodException e) {
// No problem, the developer does not want to take advantage of this provision to give him the original calling page.
result = null;
} catch (SecurityException e) {
throw new RuntimeException("Unexpected exception when trying to get Constructor for " + FOR_ERROR_RESULT.getClass().getName());
}
}
return result;
}
}
// Constant to stop all refresh activity on long response page so that it stays open indefinitely allowing the developer
// to develop a custom CSS stylesheet
private static final boolean CSS_STYLE_SHEET_DEVELOPMENT_MODE = ERXApplication.isDevelopmentModeSafe() && ERXProperties.booleanForKeyWithDefault("er.coolcomponents.CCAjaxLongResponsePage.stayOnLongResponsePageIndefinitely", false);
// flag to indicate that the user stopped the task (if it was stoppable and the stop control was visible)
private boolean _wasStoppedByUser = false;
// The page that instantiated this long response page
private final WOComponent _referringPage;
public CCAjaxLongResponsePage(WOContext context) {
super(context);
// Grab the referring page when this long response page is created
_referringPage = context.page();
}
private IERXPerformWOActionForResult _nextPageForResultController;
/**
* @return the page controller that will be given the result of the long task and
* return the next page except for the case where the user stops the task.
*
* */
public IERXPerformWOActionForResult nextPageForResultController() {
if (_nextPageForResultController == null) {
_nextPageForResultController = new ERXNextPageForResultWOAction(_referringPage);
} //~ if (_nextPageForResultController == null)
return _nextPageForResultController;
}
/**
* @param nextPageForResultController the page controller that will be given the result of the long task and
* return the next page except for the case where the user stops the task.
*
**/
public void setNextPageForResultController(IERXPerformWOActionForResult nextPageForResultController){
_nextPageForResultController = nextPageForResultController;
}
private IERXPerformWOAction _nextPageForCancelController;
/** @return the controller that handles the scenario where the user stops a stoppable task */
public IERXPerformWOAction nextPageForCancelController() {
if ( _nextPageForCancelController == null ) {
// By default, return the originating page
_nextPageForCancelController = new ERXNextPageForResultWOAction(_referringPage);;
}
return _nextPageForCancelController;
}
/** @param nextPageForCancelController the controller that handles the scenario where the user stops a stoppable task */
public void setNextPageForCancelController(IERXPerformWOAction nextPageForCancelController){
_nextPageForCancelController = nextPageForCancelController;
}
private IERXPerformWOActionForResult _nextPageForErrorController;
/** @return controller to handle an error result */
public IERXPerformWOActionForResult nextPageForErrorController() {
if ( _nextPageForErrorController == null ) {
// If user has not manually set one, we default to the class defined in system properties or our hard-coded default
try {
// If we have a constructor with WOComponent argument, use that passing the original calling page
// otherwise just default constructor
if (DEFAULT_CONTROLLER.CONSTRUCTOR_FOR_ERROR_RESULT != null) {
_nextPageForErrorController = DEFAULT_CONTROLLER.CONSTRUCTOR_FOR_ERROR_RESULT.newInstance(_referringPage);
} else {
_nextPageForErrorController = DEFAULT_CONTROLLER.FOR_ERROR_RESULT.newInstance();
}
} catch (InstantiationException e1) {
throw new RuntimeException("Failed to instantiate an instance of " + DEFAULT_CONTROLLER.FOR_ERROR_RESULT.getName(), e1);
} catch (IllegalAccessException e2) {
throw new RuntimeException("Failed to instantiate an instance of " + DEFAULT_CONTROLLER.FOR_ERROR_RESULT.getName(), e2);
} catch (InvocationTargetException e3) {
throw new RuntimeException("Failed to instantiate an instance of " + DEFAULT_CONTROLLER.FOR_ERROR_RESULT.getName() + " using the WOComponent argument constructor", e3);
}
}
return _nextPageForErrorController;
}
public void setNextPageForErrorController(IERXPerformWOActionForResult nextPageForErrorController) {
_nextPageForErrorController = nextPageForErrorController;
}
private Object _task;
/** @return the Runnable and/or Callable task */
public Object task() {
return _task;
}
/**
* @param task
* the Runnable and/or Callable task
*/
public void setTask(Object task) {
if ( task instanceof Runnable || task instanceof Callable ) {
_task = task;
} else {
throw new IllegalArgumentException("The task must implement the Runnable or the Callable interface!");
}
}
private String _defaultStatus;
/** @return a status message that is displayed if the task does not provide a status message */
public String defaultStatus() {
if ( _defaultStatus == null ) {
_defaultStatus = ERXProperties.stringForKeyWithDefault("er.coolcomponents.CCAjaxLongResponsePage.defaultStatus", "Please wait...");
}
return _defaultStatus;
}
/** @param defaultStatus a status message that is displayed if the task does not provide a status message */
public void setDefaultStatus(String defaultStatus){
_defaultStatus = defaultStatus;
}
private Integer _refreshInterval;
/** @return the refresh interval in seconds. Defaults to value of er.coolcomponents.CCAjaxLongResponsePage.refreshInterval */
public Integer refreshInterval() {
if ( _refreshInterval == null ) {
_refreshInterval = Integer.valueOf(ERXProperties.intForKeyWithDefault("er.coolcomponents.CCAjaxLongResponsePage.refreshInterval", 2));
}
return _refreshInterval;
}
/** @param refreshInterval the refresh interval in seconds. Defaults to value of er.coolcomponents.CCAjaxLongResponsePage.refreshInterval or 2 seconds. */
public void setRefreshInterval(Integer refreshInterval){
_refreshInterval = refreshInterval;
}
private ERXFutureTask<?> _future;
/**
* @return the {@link Future} that is bound to the long running task.
*
* The first time this method is accessed, it is lazily initialized and
* it starts the long running task.
*
* */
@SuppressWarnings("unchecked") // Unchecked cast
public ERXFutureTask<?> future() {
if ( _future == null ) {
Object task = task();
if (task instanceof Callable) {
_future = new ERXFutureTask<>((Callable<Object>)task);
} else {
// Runnable interface only
_future = new ERXFutureTask<>((Runnable)task, null);
}
// This is where we hand off the task to our executor service to run
// it in a background thread
ERXExecutorService.executorService().execute(_future);
}
return _future;
}
public WOActionResults nextPage() {
// Get the result of the task
Object taskResult = result();
log.debug("nextPage action fired. task result is {}", taskResult);
// The response to be returned to the user after the task is done.
WOActionResults nextPageResponse = null;
// If user canceled, we just call that controller
if (_wasStoppedByUser) {
log.debug("The task was canceled by the user, so now calling {}", nextPageForCancelController());
nextPageResponse = nextPageForCancelController().performAction();
} else if (taskResult instanceof Exception) {
// Invoke error controller
IERXPerformWOActionForResult errorController = nextPageForErrorController();
errorController.setResult(_result);
log.debug("The task had an error, so now calling {}", errorController);
nextPageResponse = errorController.performAction();
} else {
// Invoke the expected result controller
log.debug("The task completed normally. Now setting the result, {}"
+ ", and calling {}", taskResult, nextPageForResultController());
nextPageForResultController().setResult(taskResult);
nextPageResponse = nextPageForResultController().performAction();
}
log.debug("results = {}", nextPageResponse);
return nextPageResponse;
}
private Object _result;
/**
* @return the result of the task
*
**/
public Object result() {
ERXAssert.POST.isTrue(future().isDone());
if ( _result == null ) {
try {
_result = future().get();
} catch (CancellationException cancellationException) {
_result = cancellationException;
} catch (InterruptedException interruptedException) {
_result = interruptedException;
} catch (ExecutionException executionException) {
log.error("Long Response Error:\n" + ERXRuntimeUtilities.informationForException(executionException), executionException);
_result = executionException;
}
}
return _result;
}
private boolean isStopWatchRunning = false;
/**
* @return the elapsedTime since the task started running
*/
public String elapsedTime() {
if (future().isDone() && isStopWatchRunning) {
stopWatch().stop();
isStopWatchRunning = false;
} //~ if (isDone())
return stopWatch().toString();
}
private ERXStopWatch _stopWatch;
/**
* @return a stopwatch timer, lazy initialized and started on first call of this method
**/
public ERXStopWatch stopWatch() {
if ( _stopWatch == null ) {
_stopWatch = new ERXStopWatch();
_stopWatch.start();
isStopWatchRunning = true;
}
return _stopWatch;
}
/**
* @return the javascript snippet that will call the nextPage action when the task is done.
*/
public String controlScriptContent() {
String result = ";";
if (future().isDone() && !CSS_STYLE_SHEET_DEVELOPMENT_MODE) {
// To avoid confusion and users saying it never reaches 100% (which can happen if we complete and return the result
// before the last refresh that _would_ display 100% if we waited), we will wait for a period slightly longer than the
// refresh interval to get one more refresh and let the user visually see 100%.
// Wait one refresh interval plus 900 milliseconds as long as the refresh interval is not customized to some huge value by the
// developer
int delay = Math.min(((refreshInterval().intValue() * 1000) + 900), 2900);
result = "window.setTimeout(performNextPageAction, " + delay + ");";
}
if (log.isDebugEnabled())
log.debug("controlScriptContent on refresh = " + result);
return result;
}
/**
* @return the table cell width value for the finished part of the progress bar, for example "56%".
* The same string can be used to display user-friendly percentage complete value.
*/
public String finishedPercentage() {
String result = "1%";
Double percentComplete = future().percentComplete();
if (percentComplete != null) {
long userPercentComplete = Math.round(percentComplete.doubleValue() * 100.0d);
if (userPercentComplete < 1) {
userPercentComplete = 1;
}
if (userPercentComplete > 100) {
userPercentComplete = 100;
}
result = userPercentComplete + "%";
}
return result;
}
/**
* @return boolean to hide the unfinished table cell to avoid a tiny slice of unfinished when we are at 100%
*/
public boolean hideUnfinishedProgressTableCell() {
return future().isDone() && !wasStoppedByUser();
}
/**
* @return true if logging is Debug level. Used to display page config info in the long response page itself during development.
*/
public boolean isDebugMode() {
return log.isDebugEnabled();
}
/**
* @return the framework containing the CSS stylesheet for this page
*/
public String styleSheetFramework() {
return STYLESHEET_FRAMEWORK;
}
/**
* @return the filename of the CSS stylesheet webserver resource for this page
*/
public String styleSheetFilename() {
return STYLESHEET_FILENAME;
}
/**
* User action to stop the task if it implements {@link IERXStoppable}. If the task is not
* stoppable, this action has no effect.
*
* @return null
*/
public WOActionResults stopTask() {
Object task = future().task();
if (task instanceof IERXStoppable) {
IERXStoppable stoppable = (IERXStoppable)task;
stoppable.stop();
_wasStoppedByUser = true;
}
return null;
}
/**
* @return true if the user stopped the task while it was in progress.
*/
public boolean wasStoppedByUser() {
return _wasStoppedByUser;
}
/**
* @return flag to prevent update container refresh and thus keep the long response page displayed indefinitely for the purpose of developing a CS stylesheet.
*/
public boolean stayOnLongResponsePageIndefinitely() {
return CSS_STYLE_SHEET_DEVELOPMENT_MODE;
}
/**
* Default behavior for error handling
*
*/
static class ErrorResultController implements IERXPerformWOActionForResult {
private Exception _result;
public WOActionResults performAction() {
return WOApplication.application().handleException(_result, ERXWOContext.currentContext());
}
public void setResult(Object result) {
_result = (Exception) result;
}
}
}