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

com.google.gwt.user.client.CommandExecutor Maven / Gradle / Ivy

There is a newer version: 2.10.0
Show newest version
/*
 * Copyright 2007 Google Inc.
 *
 * Licensed 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 com.google.gwt.user.client;

import com.google.gwt.core.client.Duration;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * Class which executes {@link Command}s and {@link IncrementalCommand}s after
 * all currently pending event handlers have completed. This class attempts to
 * protect against slow script warnings by running commands in small time
 * increments.
 *
 * 

* It is still possible that a poorly written command could cause a slow script * warning which a user may choose to cancel. In that event, a * {@link CommandCanceledException} or an * {@link IncrementalCommandCanceledException} is reported through * {@link GWT#reportUncaughtException} depending on the type of command which * caused the warning. All other commands will continue to be executed. *

* * TODO(mmendez): Can an SSW be detected without using a timer? Currently, if a * {@link Command} or an {@link IncrementalCommand} calls either * {@link Window#alert(String)} or the JavaScript alert(String) * methods directly or indirectly then the cancellation timer can fire, * resulting in a false SSW cancellation detection. */ class CommandExecutor { /** * A circular iterator used by this class. This iterator will wrap back to * zero when it hits the end of the commands. */ private class CircularIterator implements Iterator { /** * Index of the element where this iterator should wrap back to the * beginning of the collection. */ private int end; /** * Index of the last item returned by {@link #next()}. */ private int last = -1; /** * Index of the next command to execute. */ private int next = 0; /** * Returns true if there are more commands in the queue. * * @return true if there are more commands in the queue. */ public boolean hasNext() { return next < end; } /** * Returns the next command from the queue. When the end of the dispatch * region is reached it will wrap back to the start. * * @return next command from the queue. */ public Object next() { last = next; Object command = commands.get(next++); if (next >= end) { next = 0; } return command; } /** * Removes the command which was previously returned by {@link #next()}. * */ public void remove() { assert (last >= 0); commands.remove(last); --end; if (last <= next) { if (--next < 0) { next = 0; } } last = -1; } /** * Returns the last element returned by {@link #next()}. * * @return last element returned by {@link #next()} */ private Object getLast() { assert (last >= 0); return commands.get(last); } private void setEnd(int end) { assert (end >= next); this.end = end; } private void setLast(int last) { this.last = last; } private boolean wasRemoved() { return last == -1; } } /** * Default amount of time to wait before assuming that a script cancellation * has taken place. This should be a platform dependent value, ultimately we * may need to acquire this value based on a rebind decision. For now, we * chose the smallest value known to cause an SSW. */ private static final int DEFAULT_CANCELLATION_TIMEOUT_MILLIS = 10000; /** * Default amount of time to spend dispatching commands before we yield to the * system. */ private static final int DEFAULT_TIME_SLICE_MILLIS = 100; /** * Returns true the end time has been reached or exceeded. * * @param currentTimeMillis current time in milliseconds * @param startTimeMillis end time in milliseconds * @return true if the end time has been reached */ private static boolean hasTimeSliceExpired(double currentTimeMillis, double startTimeMillis) { return currentTimeMillis - startTimeMillis >= DEFAULT_TIME_SLICE_MILLIS; } /** * Timer used to recover from script cancellations arising from slow script * warnings. */ private final Timer cancellationTimer = new Timer() { @Override public void run() { if (!isExecuting()) { /* * If we are not executing, then the cancellation timer expired right * about the time that the command dispatcher finished -- we are okay so * we just exit. */ return; } doCommandCanceled(); } }; /** * Commands that need to be executed. */ private final List commands = new ArrayList(); /** * Set to true when we are actively dispatching commands. */ private boolean executing = false; /** * Timer used to drive the dispatching of commands in the background. */ private final Timer executionTimer = new Timer() { @Override public void run() { assert (!isExecuting()); setExecutionTimerPending(false); doExecuteCommands(Duration.currentTimeMillis()); } }; /** * Set to true when we are waiting for a dispatch timer event * to fire. */ private boolean executionTimerPending = false; /** * The single circular iterator instance that we use to iterate over the * collection of commands. */ private final CircularIterator iterator = new CircularIterator(); /** * Submits a {@link Command} for execution. * * @param command command to submit */ public void submit(Command command) { commands.add(command); maybeStartExecutionTimer(); } /** * Submits an {@link IncrementalCommand} for execution. * * @param command command to submit */ public void submit(IncrementalCommand command) { commands.add(command); maybeStartExecutionTimer(); } /** * Removes the command from the queue and throws either a * {@link CommandCanceledException} or an * {@link IncrementalCommandCanceledException} depending on type of the * command. */ protected void doCommandCanceled() { Object cmd = iterator.getLast(); iterator.remove(); assert (cmd != null); if (cmd instanceof Command) { throw new CommandCanceledException((Command) cmd); } else if (cmd instanceof IncrementalCommand) { throw new IncrementalCommandCanceledException((IncrementalCommand) cmd); } setExecuting(false); maybeStartExecutionTimer(); } /** * This method will dispatch commands from the command queue. It will dispatch * commands until one of the following conditions is true: *
    *
  • It consumed its dispatching time slice * {@value #DEFAULT_TIME_SLICE_MILLIS}
  • *
  • It encounters a null in the command queue
  • *
  • All commands which were present at the start of the dispatching have * been removed from the command queue
  • *
  • The command that it was processing was canceled due to a false * cancellation -- in this case we exit without updating any state
  • *
* * @param startTimeMillis the time when this method started */ protected void doExecuteCommands(double startTimeMillis) { assert (!isExecutionTimerPending()); boolean wasCanceled = false; try { setExecuting(true); iterator.setEnd(commands.size()); cancellationTimer.schedule(DEFAULT_CANCELLATION_TIMEOUT_MILLIS); while (iterator.hasNext()) { Object element = iterator.next(); boolean removeCommand = true; try { if (element == null) { // null forces a yield or pause in execution return; } if (element instanceof Command) { Command command = (Command) element; command.execute(); } else if (element instanceof IncrementalCommand) { IncrementalCommand incrementalCommand = (IncrementalCommand) element; removeCommand = !incrementalCommand.execute(); } } finally { wasCanceled = iterator.wasRemoved(); if (!wasCanceled) { if (removeCommand) { iterator.remove(); } } } if (hasTimeSliceExpired(Duration.currentTimeMillis(), startTimeMillis)) { // the time slice has expired return; } } } finally { if (!wasCanceled) { cancellationTimer.cancel(); setExecuting(false); maybeStartExecutionTimer(); } } } /** * Starts the dispatch timer if there are commands to dispatch and we are not * waiting for a dispatch timer and we are not actively dispatching. */ protected void maybeStartExecutionTimer() { if (!commands.isEmpty() && !isExecutionTimerPending() && !isExecuting()) { setExecutionTimerPending(true); executionTimer.schedule(1); } } /** * This method is for testing only. */ List getPendingCommands() { return commands; } /** * This method is for testing only. */ void setExecuting(boolean executing) { this.executing = executing; } /** * This method is for testing only. */ void setLast(int last) { iterator.setLast(last); } /** * Returns true if this instance is currently dispatching * commands. * * @return true if this instance is currently dispatching * commands */ private boolean isExecuting() { return executing; } /** * Returns true if a the dispatch timer was scheduled but it * still has not fired. * * @return true if a the dispatch timer was scheduled but it * still has not fired */ private boolean isExecutionTimerPending() { return executionTimerPending; } private void setExecutionTimerPending(boolean pending) { executionTimerPending = pending; } }