/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.gui.tools;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import javax.swing.SwingUtilities;
import javax.swing.event.EventListenerList;
import com.rapidminer.gui.processeditor.results.ResultDisplay;
import com.rapidminer.gui.tools.dialogs.ConfirmDialog;
import com.rapidminer.tools.I18N;
import com.rapidminer.tools.LogService;
import com.rapidminer.tools.ProgressListener;
import com.rapidminer.tools.usagestats.ActionStatisticsCollector;
/**
* <p>
* {@link Runnable}s implementing this class can be execute in a dedicated thread (cached thread
* pool) and automatically display their progress in the status bar. To use this class, define a
* property "gui.progress.KEY.label" in the GUI properties file, and pass KEY to the constructor.
* Then, from within the {@link #run()} method, use {@link #getProgressListener()} to report any
* progress the task makes.
* </p>
* <p>
* By default, {@link ProgressThread}s are executed in parallel. However sometimes a dependency
* (Thread B should wait for Thread A to finish before being executed) is required. This can be
* achieved by setting an optional ID dependency {@link String} via
* {@link #addDependency(String...)}. If a {@link ProgressThread} is started via {@link #start()} or
* {@link #startAndWait()} and there is already a {@link ProgressThread} running or in the queue
* with an ID matching one of the dependencies of the new task, it will wait until they have
* finished execution before being executed itself.
* </p>
* <p>
* This can also be used to queue multiple instances of the same task which should run one after
* another. As long as they all have the same ID and a dependency on said ID, they are executed in
* the order they were queued via {@link #start()} or {@link #startAndWait()}.
* </p>
*
* @author Simon Fischer, Marco Boeck
*/
public abstract class ProgressThread implements Runnable {
/**
* amount of time the blocking {@link #startAndWait()} method waits between each check for
* dependencies
*/
private static final int BUSY_WAITING_INTERVAL = 500;
/** the currently running tasks */
private static List<ProgressThread> currentThreads = Collections.synchronizedList(new LinkedList<ProgressThread>());
/**
* the queue of {@link ProgressThread}s which await execution because they depend on other
* currently running/queued tasks
*/
private static List<ProgressThread> queuedThreads = Collections.synchronizedList(new LinkedList<ProgressThread>());
/** the list of event listeners */
private static EventListenerList listener = new EventListenerList();
/** this is the {@link ExecutorService} from which all {@link ProgressThread}s are started */
private static ExecutorService EXECUTOR = Executors.newCachedThreadPool(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r, "ProgressThread");
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
return thread;
}
});
private static final Object LOCK = new Object();
/** the {@link ProgressDisplay} instance for this task */
private final ProgressDisplay display;
/** the human readable name for this task */
private String name;
/**
* if this flag is <code>true</code>, the progress UI is in the foreground while the task is
* running
*/
private boolean runInForeground;
/** flag indicating if {@link #startAndWait()} has been called */
private boolean isWaiting = false;
/** the i18n key */
private final String key;
/** the dependencies. Every {@link String} here is checked against other task IDs */
private List<String> dependencies;
/** the ProgressThreadListeners */
private final Set<ProgressThreadListener> listeners = new CopyOnWriteArraySet<>();
/** <code>true</code> if the task was cancelled */
private boolean cancelled = false;
/** <code>true</code> if the task is started. (Remains true after canceling.) */
private boolean started = false;
/**
* If {@link #startDialogShowTimer} is set to <code>true</code> and {@link #runInForeground} is
* set to <code>false</code> when starting the progress thread, the progress dialog will be
* shown after this defined amount of time if the progress thread has not finished yet by then.
* The default value is set to 2000 milliseconds (2 seconds).
*/
private long showDialogTimerDelay = 2000;
/**
* If set to <code>true</code> and {@link #runInForeground} is set to <code>false</code>, the
* progress dialog will be shown after the amount of time defined by
* {@link #showDialogTimerDelay} if the progress thread has not finished yet by tehen. The
* amount if time can be defined by changing {@link #setShowDialogTimerDelay(long)} which by
* default is set to 2000 milliseconds (2 seconds).
*/
private boolean startDialogShowTimer = false;
/**
* If set to <code>false</code> the user is not allowed to cancel the progress thread
*/
private boolean isCancelable = true;
/**
* If set to <code>true</code> the progress bar will not have a determinate state
*/
private boolean indeterminate = false;;
/**
* Creates a new {@link ProgressThread} instance with the specified {@link I18N} key. Uses its
* I18N key as an ID to allow other ProgressThreads to depend on it.
*
* @param i18nKey
* used to retrieve the name of the progress thread from GUI properties file. The
* i18N key has to look like this: gui.progress.$i18nKey$.label. The i18N key is also
* used as progress thread ID.
*/
public ProgressThread(String i18nKey) {
this(i18nKey, false);
}
/**
* Creates a new {@link ProgressThread} instance with the specified {@link I18N} key. Also opens
* the window showing currently active {@link ProgressThread}s once it is started. Uses its I18N
* key as an ID to allow other ProgressThreads to depend on it.
*
* @param i18nKey
* used to retrieve the name of the progress thread from GUI properties file. The
* i18N key has to look like this: gui.progress.$i18nKey$.label. The i18N key is also
* used as progress thread ID.
* @param runInForeground
* if set to <code>true</code> the progress thread dialog will be shown when the
* progress thread is started
*/
public ProgressThread(String i18nKey, boolean runInForeground) {
this(i18nKey, runInForeground, new Object[] {});
}
/**
* Creates a new {@link ProgressThread} instance with the specified {@link I18N} key and ID. The
* ID can be used by other {@link ProgressThread}s as a dependency. Also opens the window
* showing currently active {@link ProgressThread}s once it is started if runInForeground is set
* to <code>true</code>.
*
* @param i18nKey
* the key for I18N and ID used for dependency handling
* @param runInForeground
* if <code>true</code>, the dialog will be shown in the foreground
* @param arguments
* the I18N arguments for the I18N key
*/
public ProgressThread(String i18nKey, boolean runInForeground, Object... arguments) {
if (i18nKey == null || "".equals(i18nKey.trim())) {
throw new IllegalArgumentException("i18nKey must not be null!");
}
this.name = I18N.getMessage(I18N.getGUIBundle(), "gui.progress." + i18nKey + ".label", arguments);
this.key = i18nKey;
this.runInForeground = runInForeground;
this.display = new ProgressDisplay(name, this);
this.dependencies = new LinkedList<>();
}
/**
* Returns the human readable name.
*
* @return
*/
public String getName() {
return name;
}
@Override
public String toString() {
return name + (cancelled ? " (cancelled)" : "");
}
/**
* Returns the {@link ProgressListener} of the {@link ResultDisplay}.
*
* @return
*/
public ProgressListener getProgressListener() {
checkCancelled();
return display.getListener();
}
/**
* Returns the ID of this task.
*/
public String getID() {
return key;
}
/**
* This call adds the specified ID(s) as a dependency to this task. What this means is that as
* long as there are other tasks running/in the queue which have an ID which matches one of the
* dependencies, this task will not be executed. Only after all tasks which have been queued
* before and have an ID matching one of the dependencies, this task will be executed. A task
* can have as many dependencies as required.
*
* @param dependencyIDs
* the ID(s) of another {@link ProgressThread} (see {@link #getID()} which must
* finish execution before this task can run
*/
public void addDependency(String... dependencyIDs) {
if (dependencyIDs == null) {
throw new IllegalArgumentException("dependencyIDs must not be null!");
}
for (String dependencyID : dependencyIDs) {
if (dependencyID == null) {
throw new IllegalArgumentException("dependencyID must not be null!");
}
this.dependencies.add(dependencyID);
}
}
/**
* Returns the dependencies of this task.
*
* @return
*/
public List<String> getDependencies() {
return new LinkedList<>(dependencies);
}
/**
* Returns the {@link ResultDisplay}.
*
* @return
*/
public ProgressDisplay getDisplay() {
return display;
}
/**
* Changes the human readable name for the progress display UI.
*
* @param i18nKey
*/
public void setDisplayLabel(String i18nKey) {
name = I18N.getMessage(I18N.getGUIBundle(), "gui.progress." + i18nKey + ".label");
}
/**
* Note that this method has nothing to do with Thread.strart. It merely enqueues this Runnable
* in the Executor's queue.
*/
public void start() {
// no dependency -> start immediately
if (dependencies.isEmpty()) {
EXECUTOR.execute(makeWrapper());
} else {
// see if task is blocked, if not start it anyway
boolean blocked = false;
synchronized (LOCK) {
blocked = isBlockedByDependencies();
}
if (!blocked) {
EXECUTOR.execute(makeWrapper());
} else {
// otherwise add to queue, which is checked once another task finishes execution
synchronized (LOCK) {
queuedThreads.add(this);
}
taskQueued(this);
}
}
}
/**
* Enqueues this task and waits for its completion. If you call this method, you probably want
* to set the runInForeground flag in the constructor to true.
* <p>
* Be careful when using this method for {@link ProgressThread}s with dependencies, this call
* might block for a long time.
* </p>
*/
public void startAndWait() {
// set flag indicating we are a busy waiting task - these are not started automatically by
// #checkQueueForDependenciesAndExecuteUnblockedTasks()
isWaiting = true;
try {
// no dependency -> start immediately
if (dependencies.isEmpty()) {
EXECUTOR.submit(makeWrapper()).get();
} else {
synchronized (LOCK) {
queuedThreads.add(this);
}
taskQueued(this);
// because this method waits, we can't just queue and leave. Instead we check on a
// regular basis and see if it can be executed now.
do {
boolean blocked = true;
synchronized (LOCK) {
blocked = isBlockedByDependencies();
}
if (!blocked) {
// no longer blocked? Execute and wait and afterwards leave loop
synchronized (LOCK) {
queuedThreads.remove(this);
}
EXECUTOR.submit(makeWrapper()).get();
break;
}
Thread.sleep(BUSY_WAITING_INTERVAL);
} while (true);
}
} catch (InterruptedException e) {
LogService.getRoot().log(
Level.SEVERE,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapidminer.gui.tools.ProgressThread.executing_error", name), e);
} catch (ExecutionException e) {
LogService.getRoot().log(
Level.SEVERE,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapidminer.gui.tools.ProgressThread.executing_error", name), e);
}
}
/** Returns true if the thread was cancelled. */
public final boolean isCancelled() {
return cancelled;
}
/**
* If the thread is currently active, calls {@link #executionCancelled()} to notify children. If
* not active, removes the thread from the queue so it won't become active.
*/
public final void cancel() {
boolean dependentThreads = false;
synchronized (LOCK) {
dependentThreads = checkQueuedThreadDependOnCurrentThread();
}
if (dependentThreads) {
if (ConfirmDialog.OK_OPTION != SwingTools.showConfirmDialog("cancel_pg_with_dependencies",
ConfirmDialog.OK_CANCEL_OPTION)) {
return;
} else {
synchronized (LOCK) {
removeQueuedThreadsWithDependency(getID());
}
}
}
synchronized (LOCK) {
cancelled = true;
if (started) {
executionCancelled();
currentThreads.remove(this);
} else {
// cancel and not started? Can only be in queue
queuedThreads.remove(this);
}
}
taskCancelled(this);
}
/**
* <p>
* <strong>ATTENTION: Make sure this is only called from inside a synchronized block!</strong>
* </p>
*
* @return returns <code>true</code> if any of the queued progress threads depend on this
* progress thread
*
*
*/
private final boolean checkQueuedThreadDependOnCurrentThread() {
for (ProgressThread pg : queuedThreads) {
if (pg.getDependencies().contains(getID())) {
return true;
}
}
return false;
}
/**
* Removes all queued threads that depend on the progress threads with the provided IDs. Also
* all thread that depend on the threads that have been removed are removed recursively.
* <p>
* <strong>ATTENTION: Make sure this is only called from inside a synchronized block!</strong>
* </p>
*
* @param ids
* the progress thread IDs the queued progress threads should be checked for
*/
private static final void removeQueuedThreadsWithDependency(String... ids) {
Iterator<ProgressThread> iterator = queuedThreads.iterator();
// iterator over queued threads and remove the remove the ones that depend on one of the
// provided IDs
Set<String> cancelledThreads = new HashSet<>();
while (iterator.hasNext()) {
ProgressThread pg = iterator.next();
if (!Collections.disjoint(Arrays.asList(ids), pg.getDependencies())) {
iterator.remove();
cancelledThreads.add(pg.getID());
}
}
// also remove all the ones depending on the ones that have been cancelled.
if (!cancelledThreads.isEmpty()) {
removeQueuedThreadsWithDependency(cancelledThreads.toArray(new String[cancelledThreads.size()]));
}
}
/** Adds a new ProgressThreadListener **/
public final void addProgressThreadListener(final ProgressThreadListener listener) {
listeners.add(listener);
}
/** Removes a ProgressThreadListener **/
public final void removeProgressThreadListener(final ProgressThreadListener listener) {
listeners.remove(listener);
}
/**
* Subclasses can implemented this method if they want to be notified about cancellation of this
* thread. In most cases, this is not necessary. Subclasses can ask {@link #isCancelled()}
* whenever cancelling is possible, or, even easier, directly call {@link #checkCancelled()}.
*/
protected void executionCancelled() {}
/** If cancelled, throws a RuntimeException to stop the thread. */
protected void checkCancelled() throws ProgressThreadStoppedException {
if (cancelled) {
throw new ProgressThreadStoppedException();
}
}
/**
* Creates a wrapper that executes this class' run method, sets {@link #current} and
* subsequently removes it from the list of pending tasks and shows a
* {@link ProgressThreadDialog} if necessary. As a side effect, calling this method also results
* in adding this ProgressThread to the list of pending tasks.
* */
private Runnable makeWrapper() {
// show dialog if wanted
if (runInForeground) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (!ProgressThreadDialog.getInstance().isVisible()) {
ProgressThreadDialog.getInstance().setVisible(false, true);
}
};
});
}
synchronized (LOCK) {
currentThreads.add(ProgressThread.this);
}
taskStarted(this);
return new Runnable() {
@Override
public void run() {
synchronized (LOCK) {
if (cancelled) {
LogService.getRoot().log(Level.INFO, "com.rapidminer.gui.tools.ProgressThread.task_cancelled",
getName());
return;
}
started = true;
}
final Timer showProgressTimer = new Timer("show-pg-timer", true);
final TimerTask showProgressTask = new TimerTask() {
@Override
public void run() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
runInForeground = true;
if (!ProgressThreadDialog.getInstance().isVisible()) {
ProgressThreadDialog.getInstance().setVisible(false, true);
}
};
});
}
};
if (!isRunInForegroundFlagSet() && isStartDialogShowTimer()) {
showProgressTimer.schedule(showProgressTask, getShowDialogTimerDelay());
}
try {
ActionStatisticsCollector.getInstance().startTimer(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key,
"runtime");
ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key,
"started");
ProgressThread.this.run();
showProgressTimer.cancel();
ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key,
"completed");
} catch (ProgressThreadStoppedException e) {
showProgressTimer.cancel();
ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key,
"stopped");
LogService.getRoot().log(Level.FINE, "com.rapidminer.gui.tools.ProgressThread.progress_thread_aborted",
getName());
} catch (Exception e) {
showProgressTimer.cancel();
ActionStatisticsCollector.getInstance().log(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key,
"failed");
LogService.getRoot().log(
Level.WARNING,
I18N.getMessage(LogService.getRoot().getResourceBundle(),
"com.rapidminer.gui.tools.ProgressThread.error_executing_background_job", name, e), e);
SwingTools.showSimpleErrorMessage("error_executing_background_job", e, name, e);
} finally {
ActionStatisticsCollector.getInstance().stopTimer(ActionStatisticsCollector.TYPE_PROGRESS_THREAD, key,
"runtime");
if (!ProgressThread.this.isCancelled()) {
ProgressThread.this.getProgressListener().complete();
}
synchronized (LOCK) {
currentThreads.remove(ProgressThread.this);
}
for (ProgressThreadListener listener : listeners) {
listener.threadFinished(ProgressThread.this);
}
checkQueueForDependenciesAndExecuteUnblockedTasks();
taskFinished(ProgressThread.this);
}
}
};
}
/**
* If <code>true</code>, this task has been started via {@link #startAndWait()}.
*
* @return
*/
private boolean isWaiting() {
return isWaiting;
}
/**
* If <code>true</code>, the runInForegrund flag has been set.
*
* @return
*/
private boolean isRunInForegroundFlagSet() {
return runInForeground;
}
/**
* Returns <code>true</code> if this task is blocked by its dependencies; <code>false</code>
* otherwise.
* <p>
* A task is blocked by dependencies if a task with an ID matching one or more of the
* dependencies is running or in the queue before this one.
* </p>
* <p>
* <strong>ATTENTION: Make sure this is only called from inside a synchronized block!</strong>
* </p>
*
* @return
*/
private boolean isBlockedByDependencies() {
for (ProgressThread pg : currentThreads) {
if (dependencies.contains(pg.getID())) {
return true;
}
}
// now check tasks in queue as there might be a dependency waiting for one himself
for (ProgressThread pg : queuedThreads) {
// loop over queued tasks until we reach ourself, if no dependencies have been found
// by then, we can start!
if (pg.equals(this)) {
break;
}
if (dependencies.contains(pg.getID())) {
return true;
}
}
return false;
}
/**
* @return the time that must have passed before the progress thread dialog is shown
*/
public long getShowDialogTimerDelay() {
return showDialogTimerDelay;
}
/**
* Allows to define the time in milliseconds after which the progress thread dialog should be
* shown if the task has not finished yet by then. This will have effect only if
* {@link #runInForeground} is set to <code>false</code> and {@link #startDialogShowTimer} is
* set to <code>true</code>. The time is specified in milliseconds.<br/>
* <b>Note:</b> Changing this value will take effect only before starting the progress thread.
*/
public void setShowDialogTimerDelay(long delay) {
if (delay <= 0) {
throw new IllegalArgumentException("Only values above 0 are allowed.");
}
this.showDialogTimerDelay = delay;
}
/**
* @return defines if the progress thread dialog should be shown if the progress thread has not
* yet finished after the time defined by {@link #showDialogTimerDelay}.
*/
public boolean isStartDialogShowTimer() {
return startDialogShowTimer;
}
/**
* Allows to define whether the progress thread dialog should be shown if the progress thread
* has not yet finished after the time specified by {@link #showDialogTimerDelay}. The default
* value is set to 2 seconds and can be changed by calling
* {@link #setShowDialogTimerDelay(long)}.<br/>
* <b>Note:</b> Changing this value will take effect only before starting the progress thread.
*/
public void setStartDialogShowTimer(boolean startDialogShowTimer) {
this.startDialogShowTimer = startDialogShowTimer;
}
/**
* @return whether the progress bar for this progress thread should be in indeterminate mode
*/
public boolean isIndeterminate() {
return indeterminate;
}
/**
* To indicate that a task of unknown length is executing, you can put a progress bar into
* indeterminate mode. While the bar is in indeterminate mode, it animates constantly to show
* that work is occurring. </br><b>Note:</b> Changing this value will take effect only before
* starting the progress thread.
*/
public void setIndeterminate(boolean indeterminate) {
this.indeterminate = indeterminate;
// In case of indeterminate progress thread the message change to a hint that it actually is
// doing something
if (this.isIndeterminate()) {
getProgressListener().setMessage(I18N.getGUILabel("indeterminate.progress"));
}
}
/**
* Allows to define whether the user should be able to cancel the progress thread.
* </br><b>Note:</b> Changing this will only have effect before starting the progress thread.
*/
public void setCancelable(boolean isCancelable) {
this.isCancelable = isCancelable;
}
/**
* @return whether the progress thread should be cancelable or not
*/
public boolean isCancelable() {
return isCancelable;
}
/**
* @return the currently executed tasks.
*/
public static Collection<ProgressThread> getCurrentThreads() {
return new LinkedList<>(currentThreads);
}
/**
* @return the currently queued tasks
*/
public static Collection<ProgressThread> getQueuedThreads() {
return new LinkedList<>(queuedThreads);
}
/**
* @return <code>true</code> if a {@link ProgressThread} is neither being executed nor queued;
* <code>false</code> otherwise.
*/
public static boolean isEmpty() {
return getCurrentThreads().isEmpty() && getQueuedThreads().isEmpty();
}
/**
* @return <code>true</code> if a {@link ProgressThread} is currently running which has the
* inForeground flag set; <code>false</code> otherwise.
*/
public static boolean isForegroundRunning() {
for (ProgressThread pg : getCurrentThreads()) {
if (pg.isRunInForegroundFlagSet()) {
return true;
}
}
return false;
}
/**
* Adds the specified {@link ProgressThreadStateListener} which will be informed of any changes.
*
* @param l
*/
public static void addProgressThreadStateListener(ProgressThreadStateListener l) {
listener.add(ProgressThreadStateListener.class, l);
}
/**
* Removes the specified {@link ProgressThreadStateListener}.
*
* @param l
*/
public static void removeProgressThreadStateListener(ProgressThreadStateListener l) {
listener.remove(ProgressThreadStateListener.class, l);
}
/**
* Checks the currently queued tasks if there are ones which are no longer blocked by
* dependencies and executes them.
*/
private static final void checkQueueForDependenciesAndExecuteUnblockedTasks() {
// a task has finished, now check tasks in queue if there are ones which are no
// longer blocked
List<ProgressThread> toRemove = new LinkedList<>();
synchronized (LOCK) {
for (ProgressThread pg : queuedThreads) {
if (!pg.isBlockedByDependencies()) {
// busy waiting tasks should not be started here, they will notice themselves
if (!pg.isWaiting()) {
toRemove.add(pg);
EXECUTOR.execute(pg.makeWrapper());
}
}
}
}
// remove here to avoid concurrent modifications
for (ProgressThread pg : toRemove) {
synchronized (LOCK) {
queuedThreads.remove(pg);
}
}
}
/**
* Notify listeners that a task was queued.
*
* @param task
*/
private static void taskQueued(ProgressThread task) {
for (ProgressThreadStateListener l : listener.getListeners(ProgressThreadStateListener.class)) {
l.progressThreadQueued(task);
}
}
/**
* Notify listeners that a task was started.
*
* @param task
*/
private static void taskStarted(ProgressThread task) {
for (ProgressThreadStateListener l : listener.getListeners(ProgressThreadStateListener.class)) {
l.progressThreadStarted(task);
}
}
/**
* Notify listeners that a task was cancelled.
*
* @param task
*/
private static void taskCancelled(ProgressThread task) {
for (ProgressThreadStateListener l : listener.getListeners(ProgressThreadStateListener.class)) {
l.progressThreadCancelled(task);
}
}
/**
* Notify listeners that a task was finished.
*
* @param task
*/
private static void taskFinished(ProgressThread task) {
for (ProgressThreadStateListener l : listener.getListeners(ProgressThreadStateListener.class)) {
l.progressThreadFinished(task);
}
}
}