package com.yoursway.utils;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
/**
* A <code>java.util.Timer</code> wrapper that makes it easy to cancel and
* reschedule tasks by associating a set of <em>tags</em> with each task. Both
* one-time and periodical tasks are available.
*
* <p>
* Any non-<code>null</code> object can serve as a tag. For example, tasks
* associated with a specific window can be tagged with that window object. When
* the window is closed, it would be easy to cancel all corresponding tasks.
* </p>
*
* <p>
* You can schedule new one-time tasks using either <code>schedule</code> or
* <code>reschedule</code>. The difference is that <code>reschedule</code>
* cancels any existing tasks that match the given tags before scheduling a new
* task. The tags you pass to <code>reschedule</code> must match no more than
* one task. Additionally, the matched task, if any, has to be a one-time task,
* i.e. cannot be a periodical task.
* </p>
*
* <p>
* It is recommended that you always use <code>reschedule</code> for both
* initial scheduling and rescheduling of tasks that you intend to only have a
* single copy of. Please limit the use of <code>schedule</code> to only the
* tasks that you want to have multiple copies of. (This recommendation is not
* enforced, however, to avoid complicating API contracts.)
* </p>
*
* <p>
* Periodical tasks are scheduled using <code>reschedulePeriodical</code>. There
* is no analog of <code>schedule</code> for periodical tasks, because we
* figured that adding multiple copies of a repeated task by mistake is too
* dangerous opportunity to allow.
* </p>
*
* <p>
* Note that tags passed to <code>reschedule</code>,
* <code>reschedulePeriodical</code> and <code>cancel</code> match tasks that
* were tagged with all of the given tags. I.e.
* <code>reschedule(..., "foo", "bar")</code> will match (and cancel) a task
* tagged with <code>"foo"</code>, <code>"bar"</code> and <code>"boz"</code>,
* but won't match a task tagged only with <code>"foo"</code>.
* </p>
*
* <p>
* If you're planning to use <code>reschedule</code> or
* <code>reschedulePeriodical</code> with multiple kinds of tasks, a recommended
* approach is to also tag tasks with string constants (defined as static final
* <code>String</code>-typed members) according to their kind.
* </p>
*
* <p>
* Also note that this class never calls <code>hashCode</code> or
* <code>equals</code> on the runnables that you specify, and never compares
* them for equality. It does not care if the runnables are equal or not; in
* particular, rescheduling is based solely on tags.
* </p>
*
* <p>
* You are free to use whatever runnables suit your purpose. We can, however,
* recommend the following pattern for your tasks:
* </p>
*
* <pre>
* class MyTask implements Runnable {
*
* private static final String TAG = MyTask.class.getName();
*
* private final DataType1 value1;
* private final DataType2 value2;
*
* public MyTask(DataType1 value1, DataType2 value2) {
* this.value1 = value1;
* this.value2 = value2;
* }
*
* public void scheduleIn(TagTimer timer, long delay) {
* timer.reschedule(delay, 60000, this, value1, value2, TAG);
* }
*
* public static void cancelAllTasks(TagTimer timer, DataType1 value1) {
* timer.cancel(value1, TAG);
* }
*
* public static void cancelAllTasks(TagTimer timer, DataType2 value2) {
* timer.cancel(value2, TAG);
* }
*
* public void run() {
* // ...
* }
*
* }
* </pre>
*
* @author Andrey Tarantsov <andreyvit@gmail.com>
*/
public final class TagTimer {
public static final long INFINITE = Long.MAX_VALUE;
private final Timer timer;
private Map<Object, Set<Task>> tagsToTasks = new HashMap<Object, Set<Task>>();
public TagTimer(String threadName, boolean daemonThread) {
this(new Timer(threadName, daemonThread));
}
public TagTimer(Timer timer) {
if (timer == null)
throw new NullPointerException("timer is null");
this.timer = timer;
}
/**
* Schedules the specified task for execution after the specified delay
*
* @param delayMillis
* delay in milliseconds before task is to be executed.
* @param runnable
* task to be scheduled.
* @param tags
* tags to associate with the scheduled task.
*/
public synchronized final void schedule(long delayMillis, Runnable runnable, Object... tags) {
if (runnable == null)
throw new NullPointerException("runnable is null");
checkNoNullTags(tags);
timer.schedule(index(new OneTimeTask(this, runnable, tags(tags), System.currentTimeMillis())),
delayMillis);
}
/**
* Schedules the specified task for execution after the specified delay,
* cancelling any existing scheduled tasks matching the specified tags. At
* least one tags thus has to be specified.
*
* <p>
* The tags you pass to <code>reschedule</code> must match no more than one
* task. Additionally, the matched task, if any, has to be a one-time task,
* i.e. cannot be a periodical task.
* </p>
*
* <p>
* Additionally you can limit the total delay a task will have to wait in
* the queue because of rescheduling. For example, if you schedule a task to
* run in 5 seconds, but will then be rescheduling it to run in 5 seconds
* every 2 seconds, the task will never have a chance to run. By specifying
* e.g. a limit of 20 seconds, you can guarantee that after waiting for 20
* seconds the task will run anyway.
* </p>
*
* @param delayMillis
* delay in milliseconds before task is to be executed.
* @param maxTotalDelayMillis
* the maximal delay before the task will be executed in spite of
* any further attempts to reschedule it for a later lime.
* @param runnable
* task to be scheduled.
* @param firstTag
* the first tag to match and to associate with the scheduled
* task.
* @param otherTags
* other tags to match and to associate with the scheduled task.
*/
public synchronized final void reschedule(long delayMillis, long maxTotalDelayMillis, Runnable runnable,
Object firstTag, Object... otherTags) {
if (maxTotalDelayMillis <= 0)
throw new IllegalArgumentException(
"maxTotalDelayMillis must be > 0; use TagTimer.INFINITE for no delay");
if (runnable == null)
throw new NullPointerException("runnable is null");
if (firstTag == null)
throw new NullPointerException("firstTag is null");
checkNoNullTags(otherTags);
long effectiveDelay = delayMillis;
long initiallyScheduledAt = System.currentTimeMillis();
Set<Object> tags = tags(firstTag, otherTags);
Set<Task> tasks = findTasksByTags(firstTag, otherTags);
if (tasks.size() > 1)
throw new IllegalArgumentException(
"TagTimer.reschedule must be invoked with a set of tags that matches at most one task");
if (!tasks.isEmpty()) {
Task matchedTask = tasks.iterator().next();
if (matchedTask instanceof PeriodicTask)
throw new IllegalArgumentException(
"TagTimer.reschedule must be invoked with a set of tags that match a one-time task (periodical task has been matched instead)");
OneTimeTask st = (OneTimeTask) matchedTask;
if (st.cancel()) {
effectiveDelay = st.limitScheduleDelay(delayMillis, maxTotalDelayMillis);
initiallyScheduledAt = st.initiallyScheduledAt;
}
}
timer.schedule(index(new OneTimeTask(this, runnable, tags, initiallyScheduledAt)), effectiveDelay);
}
/**
* Schedules the specified task for repeated <i>fixed-delay execution</i>,
* beginning after the specified delay. Subsequent executions take place at
* approximately regular intervals separated by the specified period. Any
* existing scheduled tasks matching the specified tags are cancelled.
*
* <p>
* The tags you pass to <code>reschedulePeriodical</code> must match no more
* than one task.
* </p>
*
* @param delayMillis
* delay in milliseconds before task is to be executed for the
* first time.
* @param periodMillis
* time in milliseconds between successive task executions.
* @param runnable
* task to be scheduled.
* @param firstTag
* the first tag to match and to associate with the scheduled
* task.
* @param otherTags
* other tags to match and to associate with the scheduled task.
*/
public synchronized final void reschedulePeriodical(long delayMillis, long periodMillis,
Runnable runnable, Object firstTag, Object... otherTags) {
if (periodMillis <= 0)
throw new IllegalArgumentException("periodMillis must be > 0");
if (runnable == null)
throw new NullPointerException("runnable is null");
if (firstTag == null)
throw new NullPointerException("firstTag is null");
checkNoNullTags(otherTags);
Set<Object> tags = tags(firstTag, otherTags);
Set<Task> tasks = findTasksByTags(firstTag, otherTags);
if (tasks.size() > 1)
throw new IllegalArgumentException(
"TagTimer.reschedulePeriodical must be invoked with a set of tags that matches at most one task");
for (Task task : tasks)
task.cancel();
timer.schedule(index(new PeriodicTask(this, runnable, tags)), delayMillis, periodMillis);
}
/**
* Cancels any scheduled tasks that were tagged with all of the specified
* tags.
*
* @param firstTag
* the first tag to match.
* @param otherTags
* other tags to match.
*/
public synchronized void cancel(Object firstTag, Object... otherTags) {
if (firstTag == null)
throw new NullPointerException("firstTag is null");
checkNoNullTags(otherTags);
for (Task task : findTasksByTags(firstTag, otherTags))
task.cancel();
}
private Set<Object> tags(Object[] tags) {
return new HashSet<Object>(Arrays.asList(tags));
}
private Set<Object> tags(Object firstTag, Object[] otherTags) {
HashSet<Object> result = new HashSet<Object>(Arrays.asList(otherTags));
result.add(firstTag);
return result;
}
private Set<Task> findTasksByTags(Object firstTag, Object[] otherTags) {
Set<Task> result = tagsToTasks.get(firstTag);
if (result == null)
return Collections.emptySet();
for (Object tag : otherTags) {
Set<Task> tasksThisTime = tagsToTasks.get(tag);
if (tasksThisTime == null)
return Collections.emptySet();
result.retainAll(tasksThisTime);
}
return result;
}
private Task index(Task task) {
for (Object tag : task.getTags()) {
Set<Task> tasks = tagsToTasks.get(tag);
if (tasks == null) {
tasks = new HashSet<Task>();
tagsToTasks.put(tag, tasks);
}
tasks.add(task);
}
return task;
}
void unindex(Task task) {
for (Object tag : task.getTags()) {
Set<Task> tasks = tagsToTasks.get(tag);
if (tasks != null)
tasks.remove(task);
}
}
static void checkNoNullTags(Object[] tags) {
for (Object tag : tags)
if (tag == null)
throw new NullPointerException("tags contains null, which is not allowed by TagTimer");
}
static abstract class Task extends TimerTask {
private final Runnable runnable;
protected final Set<Object> tags;
private final TagTimer scheduledInTimer;
public Task(TagTimer timer, Runnable runnable, Set<Object> tags) {
if (timer == null)
throw new NullPointerException("timer is null");
if (runnable == null)
throw new NullPointerException("runnable is null");
scheduledInTimer = timer;
this.runnable = runnable;
this.tags = tags;
}
Set<Object> getTags() {
return tags;
}
@Override
public void run() {
runnable.run();
}
@Override
public final boolean cancel() {
boolean result = super.cancel();
if (result)
unindex();
return result;
}
protected final void unindex() {
scheduledInTimer.unindex(this);
}
}
static final class OneTimeTask extends Task {
final long initiallyScheduledAt;
public OneTimeTask(TagTimer timer, Runnable runnable, Set<Object> tags, long initiallyScheduledAt) {
super(timer, runnable, tags);
this.initiallyScheduledAt = initiallyScheduledAt;
}
public long limitScheduleDelay(long delay, long maxTotalDelay) {
long now = System.currentTimeMillis();
return Math.max(0, limitScheduledTime(now + delay, maxTotalDelay) - now);
}
public long limitScheduledTime(long wantToScheduleAt, long maxTotalDelay) {
if (maxTotalDelay == INFINITE)
return wantToScheduleAt;
return Math.min(initiallyScheduledAt + maxTotalDelay, wantToScheduleAt);
}
@Override
public final void run() {
unindex();
super.run();
}
}
public static final class PeriodicTask extends Task {
public PeriodicTask(TagTimer timer, Runnable runnable, Set<Object> tags) {
super(timer, runnable, tags);
}
}
}