/*
* Copyright (C) 2022 Archie L. Cobbs. All rights reserved.
*/
package org.dellroad.stuff.vaadin24.util;
import com.google.common.base.Preconditions;
import com.vaadin.flow.server.VaadinSession;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.function.Consumer;
import java.util.function.Function;
import org.dellroad.stuff.util.LongMap;
/**
* A simplified asynchronous task manager.
*
*
* This class wraps an underlying {@link AsyncTaskManager} and provides a simplified interface,
* where each task has its own result type, and success and error handlers.
*
*
* All of the safety guarantees provided by {@link AsyncTaskManager} are provided by this class.
* As with {@link AsyncTaskManager}, only one task can be executing at a time, and starting a new task
* automatically cancels any previous task that is still outstanding.
*/
public class SimpleTaskManager {
protected final AsyncTaskManager taskManager;
private final LongMap handlerMap = new LongMap<>();
// Constructors
/**
* Default constructor.
*
*
* Caller must configure an async executor via {@link #setAsyncExecutor setAsyncExecutor()}.
*
* @throws IllegalStateException if there is no {@link VaadinSession} associated with the current thread
*/
public SimpleTaskManager() {
this(new AsyncTaskManager<>());
}
/**
* Constructor.
*
* @param executor the executor used to execute async tasks, or null for none
* @throws IllegalStateException if there is no {@link VaadinSession} associated with the current thread
*/
public SimpleTaskManager(Function super Runnable, ? extends Future>> executor) {
this(new AsyncTaskManager<>(executor));
}
private SimpleTaskManager(AsyncTaskManager taskManager) {
this.taskManager = taskManager;
this.taskManager.addAsyncTaskStatusChangeListener(this::handleTaskResult);
}
// Public Methods
/**
* Get the {@link VaadinSession} to which this instance is associated.
*
* @return this instance's {@link VaadinSession}, never null
*/
public VaadinSession getVaadinSession() {
return this.taskManager.getVaadinSession();
}
/**
* Configure the executor used for async tasks.
*
*
* The executor must execute tasks with {@linkplain #getVaadinSession this instance's VaadinSession} unlocked.
*
*
* Note: when an in-progress task is canceled via {@link #cancelTask}, then {@link Future#cancel Future.cancel()}
* will be invoked on the {@link Future} returned by the executor.
*
* @param executor the thing that launches background tasks, or null for none
*/
public void setAsyncExecutor(final Function super Runnable, ? extends Future>> executor) {
this.taskManager.setAsyncExecutor(executor);
}
// Public Methods
/**
* Start a new task that returns some value.
*
*
* Any previous task that is still executing will be cancelled.
*
* @param action the task to perform
* @param onSuccess callback when task is successful
* @param onError callback when task fails or is interrupted
* @param task result type
* @throws IllegalStateException if the current thread is not associated with
* {@linkplain #getVaadinSession this instance's session}
*/
public void startTask(Callable extends T> action, Consumer super T> onSuccess, Consumer super Throwable> onError) {
Preconditions.checkArgument(action != null, "null action");
Preconditions.checkArgument(onSuccess != null, "null onSuccess");
Preconditions.checkArgument(onError != null, "null onError");
Preconditions.checkState(this.taskManager != null, "not initialized");
Preconditions.checkState(!this.taskManager.isBusy(), "a task is already executing");
final long id = this.taskManager.startTask(ignored -> action.call());
Preconditions.checkState(!this.handlerMap.containsKey(id), "internal error");
this.handlerMap.put(id, new Handlers(onSuccess, onError));
}
/**
* Start a new task that returns nothing.
*
*
* Any previous task that is still executing will be cancelled.
*
* @param action the task to perform
* @param onSuccess callback when task is successful
* @param onError callback when task fails or is interrupted
* @throws IllegalStateException if the current thread is not associated with
* {@linkplain #getVaadinSession this instance's session}
*/
public void startTask(ThrowingRunnable action, Runnable onSuccess, Consumer super Throwable> onError) {
Preconditions.checkArgument(action != null, "null action");
Preconditions.checkArgument(onSuccess != null, "null onSuccess");
this.startTask(
() -> {
action.run();
return null;
},
ignored -> onSuccess.run(),
onError);
}
/**
* Determine whether there is an outstanding asynchronous task in progress.
*
* @return true if an asynchronous task is currently executing, otherwise false
* @throws IllegalStateException if the current thread is not associated with
* {@linkplain #getVaadinSession this instance's session}
*/
public boolean isBusy() {
return this.taskManager.isBusy();
}
/**
* Cancel current task, if any
*
* @return true if task was cancelled, otherwise false
* @throws IllegalStateException if the current thread is not associated with
* {@linkplain #getVaadinSession this instance's session}
*/
public boolean cancelTask() {
return this.taskManager.cancelTask() != 0;
}
// ThrowingRunnable
@FunctionalInterface
public interface ThrowingRunnable {
/**
* Perform some action.
*
* @throws Exception if an error occurs
*/
void run() throws Exception;
}
// Private Methods
private void handleTaskResult(AsyncTaskStatusChangeEvent event) {
// Ignore starting event
if (event.getStatus() == AsyncTaskStatusChangeEvent.STARTED)
return;
// Get handlers
final Handlers handlers = this.handlerMap.remove(event.getTaskId());
Preconditions.checkState(handlers != null, "internal error");
// Handle success/failure
switch (event.getStatus()) {
case AsyncTaskStatusChangeEvent.COMPLETED:
this.deliverResult(handlers.onSuccess(), event.getResult());
break;
case AsyncTaskStatusChangeEvent.FAILED:
case AsyncTaskStatusChangeEvent.CANCELED:
handlers.onError().accept(event.getException());
break;
default:
throw new RuntimeException("internal error");
}
}
// Capture the unchecked cast
@SuppressWarnings("unchecked")
private void deliverResult(Consumer handler, Object obj) {
handler.accept((T)obj);
}
// Handlers
private record Handlers(Consumer> onSuccess, Consumer super Throwable> onError) { }
}