// Copyright 2012 Google Inc. All Rights Reserved.
//
// 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.collide.client.util;
import com.google.common.base.Preconditions;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.RepeatingCommand;
/**
* Executor of a cancellable repeating command.
*
* <p>Execution can be cancelled with {@link #cancel()} and rescheduled
* with consequent {@link #schedule(int)} invocations.
*
* <p>Generally method {@link #execute()} is invoked after
* (<i>{@link #tickDurationMs} * {@link #tickCount}</i>) ms.
* If this method return {@code true} then it is called again
* after {@link #tickDurationMs} ms.
*
* <p>If method {@link #schedule(int)} is called before {@link #execute()} had
* finally returned {@code false} then {@link #execute()} will be called
* only after (<i>{@link #tickDurationMs} * {@link #tickCount}</i>) ms.
*
* <p>At any time there is at most one scheduled {@link RepeatingCommand}.
* Thus duration to the next {@link #execute()} invocation is not exact.
* Actually duration can be less than expected by, at most,
* {@link #tickDurationMs}.
*/
public abstract class DeferredCommandExecutor {
/**
* A time granule size.
*
* <p>Bigger values make scheduling less accurate.
* Lesser values lead to more frequent idle cycles.
*/
private final int tickDurationMs;
/**
* A number of the idle cycles before action is executed.
*
* <p>{@code -1} means that action is not going to be executed.
*/
private int tickCount;
/**
* Indicator that shows if {@link #repeatingCommand} is still scheduled.
*/
private boolean repeatingCommandAlive;
/**
* Synchronisation / anti-recursion safeguard.
*
* <p>{@code true} when action is executed from timer callback.
*/
private boolean isExecuting;
/**
* Command that is regularly executed to check if it is time to run action.
*/
private final RepeatingCommand repeatingCommand = new RepeatingCommand() {
@Override
public boolean execute() {
isExecuting = true;
try {
repeatingCommandAlive = DeferredCommandExecutor.this.onTick();
} finally {
isExecuting = false;
}
if (!repeatingCommandAlive) {
tickCount = -1;
}
return repeatingCommandAlive;
}
};
private boolean onTick() {
// If disarmed
if (tickCount < 0) {
return false;
}
// It is not time yet
if (tickCount > 0) {
tickCount--;
}
if (tickCount > 0) {
return true;
}
return execute();
}
/**
* Method that is invoked on schedule.
*
* <p>After scheduled invocation, method will be re-invoked each
* {@link #tickDurationMs}ms until it return {@code false}, or
* {@link #cancel()} / {@link #schedule(int)} is called.
*/
protected abstract boolean execute();
/**
* Schedule / reschedule {@link #execute()} invocation.
*/
public void schedule(int tickCount) {
Preconditions.checkState(!isExecuting);
Preconditions.checkArgument(tickCount > 0);
this.tickCount = tickCount;
if (!repeatingCommandAlive) {
repeatingCommandAlive = true;
Scheduler.get().scheduleFixedDelay(repeatingCommand, tickDurationMs);
}
}
/**
* Cancel scheduled {@link #execute()} invocation.
*/
public void cancel() {
Preconditions.checkState(!isExecuting);
tickCount = -1;
}
/**
* Check if executor is going to invoke {@link #execute()} again.
*/
public boolean isScheduled() {
Preconditions.checkState(!isExecuting);
return tickCount >= 0;
}
protected DeferredCommandExecutor(int tickDurationMs) {
this.tickDurationMs = tickDurationMs;
}
}