package org.andork.task; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import org.andork.util.StringUtils; /** * A task that performs some computation (typically on a background thread) and * can notify listeners when its status or progress changes. It may run subtasks * within itself, so the progress and status message can be hierarchical. * * @author Andy Edwards * * @param <R> * the task result type. */ public abstract class NewTask<R> implements Callable<R> { private static Double add(Double a, Double b) { return a == null || b == null ? null : a + b; } private static Double multiply(Double a, Double b) { return a == null || b == null ? null : a * b; } private volatile Thread thread; private volatile NewTask<?> parent; private volatile NewTask<?> subtask; private volatile Double subtaskProportion; private final List<ChangeListener> listeners = new ArrayList<>(); private volatile String status; private volatile Double progress; private volatile boolean canceled; public void addChangeListener(ChangeListener listener) { listeners.add(listener); } /** * Runs this task. It must not be running before this call is made. * * @return the result of calling {@link #doCall()}. * @throws IllegalStateException * if this task is already running. * @throws Exception * if {@link #doCall()} threw it */ @Override public final R call() throws Exception { start(); try { return doCall(); } finally { stop(); } } protected final <R2> R2 callSubtask(Double proportion, NewTask<R2> subtask) throws Exception { setSubtask(proportion, subtask); try { return subtask.call(); } finally { clearSubtask(); } } /** * Same as {@link #setCanceled(boolean) setCanceled(true)}. */ public void cancel() { canceled = true; } private void clearSubtask() { synchronized (this) { synchronized (subtask) { subtask.parent = null; subtask = null; subtaskProportion = null; } } fireChanged(); } /** * Called by {@link #run()} (which safely sets the state of this task before * and after). */ public abstract R doCall() throws Exception; protected final void fireChanged() { ChangeEvent event = new ChangeEvent(this); for (ChangeListener listener : listeners) { listener.stateChanged(event); } NewTask<?> parent = this.parent; if (parent != null) { parent.fireChanged(); } } /** * @return the combined progress of this task and its subtasks. */ public Double getCombinedProgress() { NewTask<?> subtask = this.subtask; if (subtask != null) { return add(progress, multiply(subtaskProportion, subtask.getCombinedProgress())); } return progress; } /** * @return the combined status of this task and its subtasks. */ public String getCombinedStatus() { NewTask<?> subtask = this.subtask; String status = this.status; if (subtask != null) { String subtaskStatus = subtask.getCombinedStatus(); if (!StringUtils.isNullOrEmpty(subtaskStatus)) { if (status == null) { return subtaskStatus; } return status + ": " + subtaskStatus; } } if (status == null) { return ""; } return status + "..."; } public NewTask<?> getDeepestSubtask() { NewTask<?> subtask = this.subtask; return subtask == null ? this : subtask.getDeepestSubtask(); } public Double getDeepestSubtaskProgress() { return getDeepestSubtask().getProgress(); } public String getDeepestSubtaskStatus() { return getDeepestSubtask().getStatus(); } public NewTask<?> getParent() { return parent; } /** * @return the progress. {@code null} means progress is indeterminate. */ public Double getProgress() { return progress; } public String getStatus() { return status; } public NewTask<?> getSubtask() { return subtask; } public boolean isCanceled() { return canceled; } public boolean isCanceledOrAncestorCanceled() { NewTask<?> parent = this.parent; if (parent != null) { return parent.isCanceledOrAncestorCanceled() || canceled; } return canceled; } public boolean isRunning() { return thread != null; } public void removeChangeListener(ChangeListener listener) { listeners.remove(listener); } /** * Sets whether this task is canceled. You may call this at any time; this * task does not have to be running. */ public void setCanceled(boolean canceled) { this.canceled = canceled; fireChanged(); } /** * Sets the progress and notifies listeners if it was changed. * * @param newProgress * the new progress. Should be between 0 and 1 (but this is only * convention) or {@code null} (which means progress is * indeterminate). */ public void setProgress(Double newProgress) { this.progress = newProgress; fireChanged(); } public void setStatus(String newStatus) { this.status = newStatus; fireChanged(); } private void setSubtask(Double proportion, NewTask<?> subtask) { synchronized (this) { if (thread == null) { throw new IllegalStateException("a subtask may only be run when this task is running"); } if (Thread.currentThread() != thread) { throw new IllegalStateException("a subtask must be run on the same thread as this task"); } if (this.subtask != null) { throw new IllegalStateException("the current subtask has not finished"); } synchronized (subtask) { if (subtask.parent != null) { throw new IllegalStateException("subtask is already running under another task"); } this.subtaskProportion = proportion; this.subtask = subtask; subtask.parent = this; } } fireChanged(); } private void start() { synchronized (this) { if (thread != null) { throw new IllegalStateException("already running"); } thread = Thread.currentThread(); } fireChanged(); } private void stop() { synchronized (this) { thread = null; } fireChanged(); } }