package games.strategy.util;
import java.util.Collection;
import java.util.Observable;
import java.util.Observer;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import games.strategy.debug.ClientLogger;
/**
* A way to put a timer on a runnable task. Instead of just interrupting the task,
* we can also notify observers about the time left to complete, notify them that it completed successfully or not,
* and we can also return a default object if needed.
*/
// this is an ill-fated at a shot clock for the game...
public class TimerClock<T> extends Observable {
public interface ITimerClockNotification {
int getSecondsLeft();
boolean areWeInterrupting();
}
public TimerClock() {}
public T start(final Runnable task, final T defaultReturnValue, final int interruptAfterSecondsIfNotFinished,
final int delaySeconds, final Collection<Class<? extends RuntimeException>> exceptionsToIgnoreOnInterrupt,
final Observer observer) {
if (observer != null) {
addObserver(observer);
}
final CountDownLatch latch = new CountDownLatch(1);
final AtomicBoolean runnableFinishedSuccessfully = new AtomicBoolean(false);
final AtomicBoolean runnableHadRuntimeException = new AtomicBoolean(false);
// we want to catch exceptions and propagate them back up
final AtomicReference<RuntimeException> exception = new AtomicReference<>();
// start the task
final Thread t = new Thread(() -> {
try {
task.run();
runnableFinishedSuccessfully.set(true);
if (latch != null) {
latch.countDown();
}
} catch (final RuntimeException e) {
exception.set(e);
runnableHadRuntimeException.set(true);
if (latch != null) {
latch.countDown();
}
}
});
t.start();
final long delay = delaySeconds * 1000;
// count every second
final long period = 1000;
final Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
int seconds = interruptAfterSecondsIfNotFinished;
@Override
public void run() {
// update listeners
TimerClock.this.setChanged();
TimerClock.this.notifyObservers(new TimerClockNotification(seconds, false));
// count down our timer
if (seconds-- <= 0) {
timer.cancel();
if (latch != null) {
latch.countDown();
}
}
}
}, delay, period);
// wait for the latch
if (latch != null) {
try {
latch.await();
} catch (final InterruptedException e) {
// if we are planning on interrupting this clock, we should change this
ClientLogger.logQuietly(e);
}
}
// interrupt the task if it is not yet done
boolean interrupted = false;
timer.cancel();
if (!runnableFinishedSuccessfully.get() && !runnableHadRuntimeException.get()) {
// notify listeners
setChanged();
notifyObservers(new TimerClockNotification(0, true));
// wait a second to gracefully allow a remote player to receive the notice that they are out of time
ThreadUtil.sleep(1000);
interrupted = true;
t.interrupt();
try {
t.join();
} catch (final InterruptedException e) {
// if we are planning on interrupting this clock, we should change this
ClientLogger.logQuietly(e);
}
}
deleteObservers();
if (exception.get() != null
&& !(interrupted && exceptionsToIgnoreOnInterrupt.contains(exception.get().getClass()))) {
// throw the exception back up
throw exception.get();
}
// return default value if one is specified
return defaultReturnValue;
}
public class TimerClockNotification implements ITimerClockNotification {
public final int m_secondsLeft;
public final boolean m_areWeInterrupting;
public TimerClockNotification(final int secondsLeft, final boolean areWeInterrupting) {
m_secondsLeft = secondsLeft;
m_areWeInterrupting = areWeInterrupting;
}
@Override
public int getSecondsLeft() {
return m_secondsLeft;
}
@Override
public boolean areWeInterrupting() {
return m_areWeInterrupting;
}
@Override
public String toString() {
return "Seconds left: " + getSecondsLeft() + " Interrupting: " + areWeInterrupting();
}
}
}