package org.geotoolkit.gui.javafx.util;
import java.io.Closeable;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.concurrent.Worker;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.logging.Logging;
/**
* Aim of the class is to regroup all time-consuming tasks to allow user having
* a quick look at current running tasks.
*
* @author Alexis Manin (Geomatys)
*/
public class TaskManager implements Closeable {
private static final Logger LOGGER = Logging.getLogger("org.geotoolkit.gui.javafx.util");
private static final TimeUnit TIMEOUT_UNIT = TimeUnit.SECONDS;
private static final int TIMEOUT = 10;
public static final TaskManager INSTANCE = new TaskManager();
private ExecutorService threadPool = Executors.newCachedThreadPool();
private final ObservableList<Task> submittedTasks = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
private final ObservableList<Task> tasksInError = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
// TODO : keep succeeded tasks in a sort of cache
protected TaskManager() {}
public Task submit(final Runnable newTask) {
return submit(new MockTask(newTask));
}
public Task submit(final String title, final Runnable newTask) {
return submit(new MockTask(title, newTask));
}
public <T> Task<T> submit(final Callable<T> newTask) {
return submit(new MockTask(newTask));
}
public <T> Task<T> submit(final String title, final Callable<T> newTask) {
return submit(new MockTask<T>(title, newTask));
}
public synchronized <T> Task<T> submit(final Task<T> newTask) {
ArgumentChecks.ensureNonNull("input task", newTask);
if (!newTask.isDone()) {
/* Automatically move the task from submitted to "In error" when its state change.
* Or just dereference it if succeeded.
*/
new TaskStateListener(newTask);
Platform.runLater(()->submittedTasks.add(newTask));
threadPool.submit(newTask);
}
return newTask;
}
public ObservableList<Task> getSubmittedTasks() {
return submittedTasks;
}
public ObservableList<Task> getTasksInError() {
return tasksInError;
}
@Override
public synchronized void close() throws IOException {
closeTask();
}
public synchronized void reset() {
Task closeTask = closeTask();
try {
closeTask.get();
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
LOGGER.log(Level.FINE, "Interruption requested !");
} catch (Exception ex) {
LOGGER.log(Level.SEVERE, "A thread executor cannot be closed. It's likely to create memory leaks !", ex);
} finally {
threadPool = Executors.newCachedThreadPool();
}
}
private synchronized Task closeTask() {
final Task shutdownTask = new MockTask("Shutdown remaining tasks.", () -> {
try {
threadPool.shutdown();
threadPool.awaitTermination(TIMEOUT, TIMEOUT_UNIT);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
LOGGER.log(Level.WARNING, "Application thread pool interrupted !", ex);
} finally {
final List<Runnable> waitingTasks = threadPool.shutdownNow();
Platform.runLater(()->submittedTasks.removeAll(waitingTasks));
}
});
Platform.runLater(()->submittedTasks.add(shutdownTask));
new TaskStateListener(shutdownTask);
new Thread(shutdownTask).start();
return shutdownTask;
}
/**
* Watch a task state, and move it in the right list at change. We remove it
* from submitted tasks when it's done, and add it to tasks in error if it
* failed.
*/
private class TaskStateListener implements ChangeListener {
private final Task toWatch;
public TaskStateListener(final Task t) {
ArgumentChecks.ensureNonNull("Task to watch", t);
toWatch = t;
t.stateProperty().addListener(this);
}
@Override
public void changed(ObservableValue observable, Object oldValue, Object newValue) {
if (Worker.State.FAILED.equals(newValue)) {
Platform.runLater(()->tasksInError.add(toWatch));
}
if (Worker.State.FAILED.equals(newValue)
|| Worker.State.SUCCEEDED.equals(newValue)
|| Worker.State.CANCELLED.equals(newValue)) {
Platform.runLater(()->submittedTasks.remove(toWatch));
}
}
}
/**
* A wrapper allowing to execute a runnable or a callable using {@link Task} API.
*
* @param <V> Return type to set on the task. It should be the same of embedded
* callable. If a runnable is embedded, task result will always be null, whatever
* type you choose.
*/
public static class MockTask<V> extends Task<V> {
private final Object runnableOrCallable;
public MockTask(final Callable<V> toCall) {
this(null, toCall);
}
public MockTask(final Runnable toRun) {
this(null, toRun);
}
public MockTask(final String title, final Callable<V> toCall) {
ArgumentChecks.ensureNonNull("Callable to execute", toCall);
runnableOrCallable = toCall;
if (title == null || title.isEmpty()) {
updateTitle("Tâche sans titre.");
} else {
updateTitle(title);
}
}
public MockTask(final String title, final Runnable toRun) {
ArgumentChecks.ensureNonNull("Runnable to execute", toRun);
runnableOrCallable = toRun;
if (title == null || title.isEmpty()) {
updateTitle("Tâche sans titre.");
} else {
updateTitle(title);
}
}
@Override
protected V call() throws Exception {
if (Thread.currentThread().isInterrupted()) throw new InterruptedException("Cannot start task. Thread interrupted !");
if (runnableOrCallable instanceof Callable) {
return ((Callable<V>)runnableOrCallable).call();
}else {
((Runnable)runnableOrCallable).run();
}
return null;
}
}
}