// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.client.explorer.commands;
import com.google.appinventor.client.Ode;
import com.google.appinventor.client.explorer.project.Project;
import com.google.appinventor.client.tracking.Tracking;
import com.google.appinventor.shared.rpc.project.ProjectNode;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.Duration;
import com.google.gwt.user.client.Command;
/**
* Abstract base class for chainable commands that perform some action on a
* {@link ProjectNode}. A command may be synchronous or asynchronous.
*
* <p/>Each subclass must implement {@link #willCallExecuteNextCommand}.
*
* <p/>Each subclass whose willCallExecuteNextCommand returns true,
* must call {@link #executeNextCommand} after its own command has completed
* successfully and must call {@link #executionFailedOrCanceled} if its own
* command execution fails or has been canceled.
*
* @author lizlooney@google.com (Liz Looney)
*/
public abstract class ChainableCommand {
// The next chainable command to be executed after this one is finished.
private final ChainableCommand nextCommand;
// Tracks elapsed time for entire chain
private Duration chainDuration;
// Tracks elapsed time for this command within the chain
private Duration linkDuration;
// Name of action this chain of commands represents (for analytics tracking purposes)
private String actionName;
// A command to execute after the chain is finished, regardless of whether it succeeds.
// If any link in the chain is unable to call the next command (willCallExecuteNextCommand
// returns false), the finallyCommand will be executed immediately after that link's execute
// method returns.
private Command finallyCommand;
/**
* Creates a new command.
*/
public ChainableCommand() {
this(null);
}
/**
* Creates a new command.
*
* @param nextCommand the command to execute after this command has completed
*/
public ChainableCommand(ChainableCommand nextCommand) {
this.nextCommand = nextCommand;
}
/**
* Initializes the tracking information on each link in the chain
*
* @param actionName the name of action this chain of commands represents
* @param chainDuration duration to be used for the entire chain
*/
private void initTrackingInformation(String actionName, Duration chainDuration) {
this.actionName = actionName;
// Note that all links in the chain have a reference to the same chainDuration.
this.chainDuration = chainDuration;
if (nextCommand != null) {
nextCommand.initTrackingInformation(actionName, chainDuration);
}
}
/**
* Resets the start time for the link duration timer
*
*/
private final void resetLinkDuration() {
// Note that each link in the chain has a unique linkDuration.
linkDuration = new Duration();
}
/**
* Returns the elapsed milliseconds that this link took to execute
*
*/
protected final int getElapsedMillis() {
return linkDuration.elapsedMillis();
}
/**
* Method that needs to be overridden by subclasses to indicate whether
* or not they will call executeNextCommand when done and
* executionFailedOrCanceled upon failure/cancelation.
*
* <p/>This method should only return false if the subclass cannot determine
* when it has finished.
*/
protected abstract boolean willCallExecuteNextCommand();
/**
* Kick off the chain of commands.
*
* @param actionName the name of action this chain of commands represents
* @param node the project node to which the chain of commands is applied
*/
public final void startExecuteChain(String actionName, ProjectNode node) {
startExecuteChain(actionName, node, null);
}
/**
* Kick off the chain of commands.
*
* @param actionName the name of action this chain of commands represents
* @param node the project node to which the chain of commands is applied
* @param finallyCommand a command to execute after the chain is finished,
* regardless of whether it succeeds
*/
public final void startExecuteChain(String actionName, ProjectNode node,
Command finallyCommand) {
// The node must not be null.
// If you are calling startExecuteChain with null for the node parameter, maybe you should
// question why you are using a ChainableCommand at all. ChainableCommands were designed to
// perform an operation on a ProjectNode.
Preconditions.checkNotNull(node);
setFinallyCommand(finallyCommand);
initTrackingInformation(actionName, new Duration());
executeLink(node);
}
private final void executeLink(ProjectNode node) {
resetLinkDuration();
execute(node);
if (!willCallExecuteNextCommand()) {
// If this command won't end up calling executeNextCommand, execute the finally command
// and do the tracking now. This may result in underestimating the overall time (should we
// skip the time estimates in this case?).
executeFinallyCommand();
track(node);
}
}
/**
* Method that needs to be overridden by subclasses to implement the
* actual behavior of the command. Do not call this directly (call
* startExecuteChain instead)!
*
* @param node the project node to which the command is applied
*/
protected abstract void execute(ProjectNode node);
/**
* Executes the next command in the chain.
*
* @param node the project node to which the command is applied
*/
protected final void executeNextCommand(ProjectNode node) {
if (nextCommand != null) {
nextCommand.executeLink(node);
} else {
// This is the end of the chain. Execute the finally command and track.
executeFinallyCommand();
track(node);
}
}
/**
* Indicates that this command's execution has failed or has been canceled.
*/
protected final void executionFailedOrCanceled() {
executeFinallyCommand();
}
/**
* Sets a command to execute after the chain is finished, regardless of
* whether it succeeds.
*
* @param finallyCommand a command to execute after the chain is finished,
* regardless of whether it succeeds
*/
private void setFinallyCommand(Command finallyCommand) {
// Set the finallyCommand field on each link in the chain.
this.finallyCommand = finallyCommand;
if (nextCommand != null) {
nextCommand.setFinallyCommand(finallyCommand);
}
}
private void executeFinallyCommand() {
if (finallyCommand != null) {
finallyCommand.execute();
}
}
private void track(ProjectNode node) {
if (!actionName.isEmpty()) {
Tracking.trackEvent(Tracking.PROJECT_EVENT, actionName,
node.getName(), chainDuration.elapsedMillis());
}
}
/**
* Returns the project a node belongs to.
*
* @param node the node for which to determine the project
*/
protected final Project getProject(ProjectNode node) {
return Ode.getInstance().getProjectManager().getProject(node);
}
}