package edu.kit.pse.ws2013.routekit.models;
import java.io.Closeable;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
/**
* Reports progress to attached {@link ProgressListener listeners}.
* <p>
* The progress of a task can be determined in one of two ways:
* <ul>
* <li><em>Subtask-based:</em> You can {@link #setSubTasks(float[]) tell} a task
* in advance how many sub-tasks will be {@link #pushTask(String) pushed} onto
* and {@link #popTask() popped} off of it, and how their individual progress
* should be weighted.</li>
* <li><em>Progress-based:</em> Alternatively, you can directly {@link
* setProgress(float) set} its progress.</li>
* </ul>
* These approaches may not be combined.
*
* @author Lucas Werkmeister
*/
public class ProgressReporter {
private final Set<ProgressListener> listeners = new HashSet<>();
private final LinkedList<Task> taskStack = new LinkedList<>();
/**
* Creates a new {@link ProgressReporter}.
*/
public ProgressReporter() {
}
/**
* Creates a new {@link ProgressReporter} with the specified listener and
* root task.
* <p>
* This is a convenience constructor to allow the creation of a usable
* {@link ProgressReporter} in one line:
*
* <pre>
* {@code
* doSomeAction(new ProgressReporter(listener, "Do Action"));
* }
* </pre>
*
* @param listener
* A {@link ProgressListener} that will be notified about
* progress.
* @see #addProgressListener(ProgressListener)
*/
public ProgressReporter(ProgressListener listener, String rootTask) {
this();
addProgressListener(listener);
pushTask(rootTask);
}
/**
* Begin a new task with the specified name.
* <p>
* A call to {@link #setSubTasks(float[])} should usually occur directly
* after a call to this method unless you want to
* {@link #setProgress(float) set the progress directly}; the two methods
* are separate because this method is usually called before entering a
* subroutine while only the subroutine can know about its subtasks (which
* are internal by nature).
*
* @param name
* The name of the task.
*/
public void pushTask(String name) {
Task newTask = new Task(name);
if (taskStack.isEmpty()) {
reportStartRoot(name);
} else {
taskStack.getLast().addTask(newTask);
}
reportBeginTask(name);
taskStack.addLast(newTask);
}
/**
* Set the weights of subtasks of this task.
* <p>
* This should be called directly after a call to {@link #pushTask(String)};
* for rationale see there.
* <p>
* This is only permitted if the progress hasn’t been set directly; see the
* class documentation for more information.
*
* @param weights
* The weights of the subtasks, which should all be in range
* [0,1] and sum up to 1 (but this is not checked).
* @throws IllegalStateException
* If progress has been set.
*/
public void setSubTasks(float[] weights) {
if (taskStack.getLast().progress != -1) {
throw new IllegalStateException(
"Can’t set subtasks of a task with direct progress!");
}
taskStack.getLast().setSubTasks(weights);
}
/**
* Set the amount of subtasks of this task; they all have the same weight.
*
* @param count
* The number of subtasks.
* @see #setSubTasks(float[])
*/
public void setSubTasks(int count) {
float[] weights = new float[count];
Arrays.fill(weights, 1f / count);
setSubTasks(weights);
}
/**
* End the current task, verifying its name.
* <p>
* If there are unpopped tasks on top of a task with the given name, they
* are popped as well. This can be used for error recovery: if you call this
* method from a {@code finally} block, progress reporting will not be
* disrupted even if some sub-task throws an exception.
* <p>
* If no task with the given name exists, an {@link AssertionError} is
* thrown.
*
* @param name
* The expected name of the task to end.
* @throws AssertionError
* If no task with the given name exists.
* @see #popTask()
*/
public void popTask(String name) {
boolean hasTask = false;
for (Task task : taskStack) {
if (task.name.equals(name)) {
hasTask = true;
break;
}
}
if (!hasTask) {
throw new AssertionError();
}
boolean poppedTask = false;
while (!poppedTask) {
if (taskStack.getLast().name.equals(name)) {
poppedTask = true;
}
popTask();
}
}
/**
* End the current task.
*/
public void popTask() {
String name = taskStack.getLast().name;
taskStack.getLast().finish();
taskStack.removeLast();
reportEndTask(name);
reportProgress();
if (taskStack.isEmpty()) {
reportFinishRoot(name);
}
}
/**
* End the current task and begin a new task with the specified name.
* <p>
* This is a convenience method, equivalent to calling
*
* <pre>
* {@code
* popTask();
* pushTask(name);
* }
* </pre>
*
* @param name
* The name of the new task.
*/
public void nextTask(String name) {
popTask();
pushTask(name);
}
/**
* Start a new task with the given name and end it when the returned object
* is {@link CloseableTask#close() closed}.
* <p>
* This is a convenience method, intended for use in try-with-resources
* statements.
* <p>
* For ending the task, the {@link #popTask(String)} variant is used, which
* means that when you use this method in a try-with-resources statement,
* exceptions thrown in sub-tasks do not disrupt progress reporting.
*
* @param name
* The name of the new task.
* @return A {@link CloseableTask} whose {@link Closeable#close() close()}
* method closes the task again.
*/
public CloseableTask openTask(final String name) {
pushTask(name);
return new CloseableTask(name);
}
public class CloseableTask implements AutoCloseable {
private final String name;
private CloseableTask(String name) {
this.name = name;
}
@Override
public void close() {
popTask(name);
}
}
/**
* Add a {@link ProgressListener} to this reporter.
*
* @param listener
* The new {@link ProgressListener}.
*/
public void addProgressListener(ProgressListener listener) {
listeners.add(listener);
}
/**
* Sets the progress of the current task.
* <p>
* This is only permitted if no subtasks have been set; see the class
* documentation for more information.
*
* @param progress
* The progress.
* @throws IllegalStateException
* If subtasks have been set.
*/
public void setProgress(float progress) {
if (!taskStack.getLast().subTasks.isEmpty()) {
throw new IllegalStateException(
"Can’t set the progress of a task with subtasks!");
}
taskStack.getLast().progress = progress;
reportProgress();
}
/**
* Gets the current progress.
*
* @return The current progress.
*/
public float getProgress() {
if (taskStack.isEmpty()) {
return 1f;
}
return taskStack.getFirst().getProgress();
}
/**
* Gets the current task.
*
* @return The current task.
*/
public String getCurrentTask() {
if (taskStack.isEmpty()) {
return null;
}
return taskStack.getLast().name;
}
private void reportStartRoot(String name) {
for (ProgressListener listener : listeners) {
listener.startRoot(name);
}
}
private void reportBeginTask(String name) {
for (ProgressListener listener : listeners) {
listener.beginTask(name);
}
}
private void reportProgress() {
float progress = getProgress();
String task = getCurrentTask();
for (ProgressListener listener : listeners) {
listener.progress(progress, task);
}
}
private void reportEndTask(String name) {
for (ProgressListener listener : listeners) {
listener.endTask(name);
}
}
private void reportFinishRoot(String name) {
for (ProgressListener listener : listeners) {
listener.finishRoot(name);
}
}
private static class Task {
public final String name;
private float[] weights;
private final LinkedList<Task> subTasks = new LinkedList<>();
private boolean finished = false;
private float progress = -1;
public Task(String name) {
this.name = name;
}
public void setSubTasks(float[] weights) {
if (weights == null) {
throw new NullPointerException();
}
this.weights = weights;
}
public void addTask(Task subTask) {
subTasks.add(subTask);
}
public float getProgress() {
if (finished) {
return 1f;
}
if (subTasks.isEmpty()) {
return progress;
}
float progress = 0f;
int i = 0;
for (Task subTask : subTasks) {
float weight = i < weights.length ? weights[i] : 0f;
progress += weight * subTask.getProgress();
i++;
}
return progress;
}
public void finish() {
finished = true;
}
}
}