From 03dfd3e887bf78ddb2b4360f2289f8f94f46d3dd Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Tue, 10 Jul 2018 14:51:33 +0200 Subject: [PATCH] Replaced AsyncTaskService by new Tasks utility using javafx.concurrent API --- .../ui/controllers/UnlockController.java | 70 ++--- .../ui/controllers/UnlockedController.java | 29 +-- .../ui/controllers/UpgradeController.java | 32 ++- .../ui/controllers/WelcomeController.java | 57 +++-- .../cryptomator/ui/util/AsyncTaskService.java | 241 ------------------ .../java/org/cryptomator/ui/util/Tasks.java | 192 ++++++++++++++ 6 files changed, 285 insertions(+), 336 deletions(-) delete mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java create mode 100644 main/ui/src/main/java/org/cryptomator/ui/util/Tasks.java diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java index b6588be5c..2001e50b6 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockController.java @@ -8,41 +8,21 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; -import java.io.IOException; -import java.nio.file.*; +import javax.inject.Inject; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Comparator; import java.util.Objects; import java.util.Optional; - -import javax.inject.Inject; - -import javafx.beans.binding.Bindings; -import javafx.scene.layout.HBox; -import org.apache.commons.lang3.CharUtils; -import org.apache.commons.lang3.SystemUtils; -import org.cryptomator.common.settings.VolumeImpl; -import org.cryptomator.common.settings.Settings; -import org.cryptomator.common.settings.VaultSettings; -import org.cryptomator.cryptolib.api.InvalidPassphraseException; -import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; -import org.cryptomator.frontend.webdav.ServerLifecycleException; -import org.cryptomator.keychain.KeychainAccess; -import org.cryptomator.ui.controls.SecPasswordField; -import org.cryptomator.ui.l10n.Localization; -import org.cryptomator.ui.model.Vault; -import org.cryptomator.ui.model.WindowsDriveLetters; -import org.cryptomator.ui.util.AsyncTaskService; -import org.cryptomator.ui.util.DialogBuilderUtil; -import org.fxmisc.easybind.EasyBind; -import org.fxmisc.easybind.Subscription; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import java.util.concurrent.ExecutorService; import com.google.common.base.CharMatcher; import com.google.common.base.Strings; - import javafx.application.Application; +import javafx.beans.binding.Bindings; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.event.ActionEvent; @@ -59,8 +39,28 @@ import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; import javafx.scene.layout.GridPane; +import javafx.scene.layout.HBox; import javafx.scene.text.Text; import javafx.util.StringConverter; +import org.apache.commons.lang3.CharUtils; +import org.apache.commons.lang3.SystemUtils; +import org.cryptomator.common.settings.Settings; +import org.cryptomator.common.settings.VaultSettings; +import org.cryptomator.common.settings.VolumeImpl; +import org.cryptomator.cryptolib.api.InvalidPassphraseException; +import org.cryptomator.cryptolib.api.UnsupportedVaultFormatException; +import org.cryptomator.frontend.webdav.ServerLifecycleException; +import org.cryptomator.keychain.KeychainAccess; +import org.cryptomator.ui.controls.SecPasswordField; +import org.cryptomator.ui.l10n.Localization; +import org.cryptomator.ui.model.Vault; +import org.cryptomator.ui.model.WindowsDriveLetters; +import org.cryptomator.ui.util.DialogBuilderUtil; +import org.cryptomator.ui.util.Tasks; +import org.fxmisc.easybind.EasyBind; +import org.fxmisc.easybind.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class UnlockController implements ViewController { @@ -73,23 +73,23 @@ public class UnlockController implements ViewController { private final Application app; private final Localization localization; - private final AsyncTaskService asyncTaskService; private final WindowsDriveLetters driveLetters; private final ChangeListener driveLetterChangeListener = this::winDriveLetterDidChange; private final Optional keychainAccess; private final Settings settings; + private final ExecutorService executor; private Vault vault; private Optional listener = Optional.empty(); private Subscription vaultSubs = Subscription.EMPTY; @Inject - public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, WindowsDriveLetters driveLetters, Optional keychainAccess, Settings settings) { + public UnlockController(Application app, Localization localization, WindowsDriveLetters driveLetters, Optional keychainAccess, Settings settings, ExecutorService executor) { this.app = app; this.localization = localization; - this.asyncTaskService = asyncTaskService; this.driveLetters = driveLetters; this.keychainAccess = keychainAccess; this.settings = settings; + this.executor = executor; } @FXML @@ -423,7 +423,7 @@ public class UnlockController implements ViewController { progressIndicator.setVisible(true); CharSequence password = passwordField.getCharacters(); - asyncTaskService.asyncTaskOf(() -> { + Tasks.create(() -> { vault.unlock(password); if (keychainAccess.isPresent() && savePassword.isSelected()) { keychainAccess.get().storePassphrase(vault.getId(), password); @@ -450,16 +450,16 @@ public class UnlockController implements ViewController { }).onError(ServerLifecycleException.class, e -> { LOG.error("Unlock failed for technical reasons.", e); messageText.setText(localization.getString("unlock.errorMessage.unlockFailed")); - }).onError(IOException.class, e -> { - LOG.error("Unlock failed for technical reasons.", e); - messageText.setText(localization.getString("unlock.errorMessage.unlockFailed")); + }).onError(Exception.class, e -> { + LOG.error("Unlock failed for technical reasons.", e); + messageText.setText(localization.getString("unlock.errorMessage.unlockFailed")); }).andFinally(() -> { if (!savePassword.isSelected()) { passwordField.swipe(); } advancedOptions.setDisable(false); progressIndicator.setVisible(false); - }).run(); + }).runOnce(executor); } /* callback */ diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java index 27664be52..3143f8c3d 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UnlockedController.java @@ -8,14 +8,13 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; -import java.util.Optional; - import javax.inject.Inject; +import java.util.Optional; +import java.util.concurrent.ExecutorService; import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.Timeline; -import javafx.beans.binding.ObjectExpression; import javafx.beans.property.ObjectProperty; import javafx.beans.property.SimpleObjectProperty; import javafx.event.ActionEvent; @@ -35,11 +34,10 @@ import javafx.scene.control.ToggleButton; import javafx.scene.layout.VBox; import javafx.stage.PopupWindow.AnchorLocation; import javafx.util.Duration; -import org.cryptomator.frontend.webdav.mount.Mounter.CommandFailedException; import org.cryptomator.ui.l10n.Localization; import org.cryptomator.ui.model.Vault; -import org.cryptomator.ui.util.AsyncTaskService; import org.cryptomator.ui.util.DialogBuilderUtil; +import org.cryptomator.ui.util.Tasks; import org.fxmisc.easybind.EasyBind; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,9 +52,8 @@ public class UnlockedController implements ViewController { private static final double IO_SAMPLING_INTERVAL = 0.25; private final Localization localization; - private final AsyncTaskService asyncTaskService; + private final ExecutorService executor; private final ObjectProperty vault = new SimpleObjectProperty<>(); - private final ObjectExpression vaultState = ObjectExpression.objectExpression(EasyBind.select(vault).selectObject(Vault::stateProperty)); private Optional listener = Optional.empty(); private Timeline ioAnimation; @@ -79,9 +76,9 @@ public class UnlockedController implements ViewController { private VBox root; @Inject - public UnlockedController(Localization localization, AsyncTaskService asyncTaskService) { + public UnlockedController(Localization localization, ExecutorService executor) { this.localization = localization; - this.asyncTaskService = asyncTaskService; + this.executor = executor; } @Override @@ -115,14 +112,14 @@ public class UnlockedController implements ViewController { } private void regularLockVault(Runnable onSuccess) { - asyncTaskService.asyncTaskOf(() -> { + Tasks.create(() -> { vault.get().lock(false); }).onSuccess(() -> { LOG.trace("Regular unmount succeeded."); onSuccess.run(); }).onError(Exception.class, e -> { onRegularUnmountVaultFailed(e, onSuccess); - }).run(); + }).runOnce(executor); } private void onRegularUnmountVaultFailed(Exception e, Runnable onSuccess) { @@ -147,7 +144,7 @@ public class UnlockedController implements ViewController { } private void forcedLockVault(Runnable onSuccess) { - asyncTaskService.asyncTaskOf(() -> { + Tasks.create(() -> { vault.get().lock(true); }).onSuccess(() -> { LOG.trace("Forced unmount succeeded."); @@ -155,7 +152,7 @@ public class UnlockedController implements ViewController { }).onError(Exception.class, e -> { LOG.error("Forced unmount failed.", e); messageLabel.setText(localization.getString("unlocked.label.unmountFailed")); - }).run(); + }).runOnce(executor); } @FXML @@ -174,15 +171,15 @@ public class UnlockedController implements ViewController { } void revealVault(Vault vault) { - asyncTaskService.asyncTaskOf(() -> { + Tasks.create(() -> { vault.reveal(); }).onSuccess(() -> { LOG.trace("Reveal succeeded."); messageLabel.setText(null); - }).onError(CommandFailedException.class, e -> { + }).onError(Exception.class, e -> { LOG.error("Reveal failed.", e); messageLabel.setText(localization.getString("unlocked.label.revealFailed")); - }).run(); + }).runOnce(executor); } // **************************************** diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java index 2d7cb7fa9..1eb0c7307 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/UpgradeController.java @@ -5,19 +5,10 @@ *******************************************************************************/ package org.cryptomator.ui.controllers; +import javax.inject.Inject; import java.util.Objects; import java.util.Optional; - -import javax.inject.Inject; - -import org.cryptomator.ui.controls.SecPasswordField; -import org.cryptomator.ui.l10n.Localization; -import org.cryptomator.ui.model.UpgradeStrategies; -import org.cryptomator.ui.model.UpgradeStrategy; -import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException; -import org.cryptomator.ui.model.Vault; -import org.cryptomator.ui.util.AsyncTaskService; -import org.fxmisc.easybind.EasyBind; +import java.util.concurrent.ExecutorService; import javafx.beans.binding.BooleanExpression; import javafx.beans.property.ObjectProperty; @@ -30,19 +21,26 @@ import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.layout.GridPane; +import org.cryptomator.ui.controls.SecPasswordField; +import org.cryptomator.ui.model.UpgradeStrategies; +import org.cryptomator.ui.model.UpgradeStrategy; +import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException; +import org.cryptomator.ui.model.Vault; +import org.cryptomator.ui.util.Tasks; +import org.fxmisc.easybind.EasyBind; public class UpgradeController implements ViewController { private final ObjectProperty strategy = new SimpleObjectProperty<>(); private final UpgradeStrategies strategies; - private final AsyncTaskService asyncTaskService; + private final ExecutorService executor; private Optional listener = Optional.empty(); private Vault vault; @Inject - public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) { + public UpgradeController(UpgradeStrategies strategies, ExecutorService executor) { this.strategies = strategies; - this.asyncTaskService = asyncTaskService; + this.executor = executor; } @FXML @@ -122,8 +120,8 @@ public class UpgradeController implements ViewController { private void upgrade(UpgradeStrategy instruction) { passwordField.setDisable(true); progressIndicator.setVisible(true); - asyncTaskService // - .asyncTaskOf(() -> { + Tasks // + .create(() -> { if (!instruction.isApplicable(vault)) { throw new IllegalStateException("No ugprade needed for " + vault.getPath()); } @@ -137,7 +135,7 @@ public class UpgradeController implements ViewController { progressIndicator.setVisible(false); passwordField.setDisable(false); passwordField.swipe(); - }).run(); + }).runOnce(executor); } private void showNextUpgrade() { diff --git a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java index 6754afdb3..72e6e7fc2 100644 --- a/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java +++ b/main/ui/src/main/java/org/cryptomator/ui/controllers/WelcomeController.java @@ -8,6 +8,20 @@ ******************************************************************************/ package org.cryptomator.ui.controllers; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import java.net.URI; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Comparator; +import java.util.Map; +import java.util.Optional; +import java.util.ResourceBundle; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; @@ -28,23 +42,10 @@ import jdk.incubator.http.HttpResponse; import org.apache.commons.lang3.SystemUtils; import org.cryptomator.common.settings.Settings; import org.cryptomator.ui.l10n.Localization; -import org.cryptomator.ui.util.AsyncTaskService; +import org.cryptomator.ui.util.Tasks; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.inject.Inject; -import javax.inject.Named; -import javax.inject.Singleton; -import java.net.URI; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.time.Duration; -import java.util.Comparator; -import java.util.Map; -import java.util.Optional; -import java.util.ResourceBundle; -import java.util.concurrent.TimeUnit; - import static org.cryptomator.ui.util.DialogBuilderUtil.buildYesNoDialog; @Singleton @@ -57,17 +58,17 @@ public class WelcomeController implements ViewController { private final Localization localization; private final Settings settings; private final Comparator semVerComparator; - private final AsyncTaskService asyncTaskService; + private final ScheduledExecutorService executor; @Inject public WelcomeController(Application app, @Named("applicationVersion") Optional applicationVersion, Localization localization, Settings settings, @Named("SemVer") Comparator semVerComparator, - AsyncTaskService asyncTaskService) { + ScheduledExecutorService executor) { this.app = app; this.applicationVersion = applicationVersion; this.localization = localization; this.settings = settings; this.semVerComparator = semVerComparator; - this.asyncTaskService = asyncTaskService; + this.executor = executor; } @FXML @@ -110,7 +111,7 @@ public class WelcomeController implements ViewController { } private void askForUpdateCheck() { - asyncTaskService.runDelayedOnUiThread(1, TimeUnit.SECONDS, () -> { + Tasks.create(() -> {}).onSuccess(() -> { Optional result = buildYesNoDialog( localization.getString("welcome.askForUpdateCheck.dialog.title"), localization.getString("welcome.askForUpdateCheck.dialog.header"), @@ -123,13 +124,13 @@ public class WelcomeController implements ViewController { if (settings.checkForUpdates().get()) { this.checkForUpdates(); } - }); + }).scheduleOnce(executor, 1, TimeUnit.SECONDS); } private void checkForUpdates() { checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking")); checkForUpdatesIndicator.setVisible(true); - asyncTaskService.asyncTaskOf(() -> { + Tasks.create(() -> { String userAgent = String.format("Cryptomator VersionChecker/%s %s %s (%s)", applicationVersion.orElse("SNAPSHOT"), SystemUtils.OS_NAME, SystemUtils.OS_VERSION, SystemUtils.OS_ARCH); HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder() @@ -138,7 +139,8 @@ public class WelcomeController implements ViewController { .header("User-Agent", userAgent) .timeout(Duration.ofSeconds(5)) .build(); - HttpResponse response = client.send(request, HttpResponse.BodyHandler.asString(StandardCharsets.UTF_8)); + return client.send(request, HttpResponse.BodyHandler.asString(StandardCharsets.UTF_8)); + }).onSuccess(response -> { if (response.statusCode() == 200) { Gson gson = new GsonBuilder().setLenient().create(); Map map = gson.fromJson(response.body(), new TypeToken>() { @@ -147,13 +149,16 @@ public class WelcomeController implements ViewController { this.compareVersions(map); } } + }).onError(Exception.class, e -> { + LOG.error("Error checking for updates", e); }).andFinally(() -> { checkForUpdatesStatus.setText(""); checkForUpdatesIndicator.setVisible(false); - }).run(); + }).runOnce(executor); } private void compareVersions(final Map latestVersions) { + assert Platform.isFxApplicationThread(); final String latestVersion; if (SystemUtils.IS_OS_MAC_OSX) { latestVersion = latestVersions.get("mac"); @@ -169,11 +174,9 @@ public class WelcomeController implements ViewController { LOG.info("Current version: {}, lastest version: {}", currentVersion, latestVersion); if (currentVersion != null && semVerComparator.compare(currentVersion, latestVersion) < 0) { final String msg = String.format(localization.getString("welcome.newVersionMessage"), latestVersion, currentVersion); - Platform.runLater(() -> { - this.updateLink.setText(msg); - this.updateLink.setVisible(true); - this.updateLink.setDisable(false); - }); + this.updateLink.setText(msg); + this.updateLink.setVisible(true); + this.updateLink.setDisable(false); } } diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java b/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java deleted file mode 100644 index d8e488522..000000000 --- a/main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java +++ /dev/null @@ -1,241 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt). - * All rights reserved. This program and the accompanying materials - * are made available under the terms of the accompanying LICENSE file. - *******************************************************************************/ -package org.cryptomator.ui.util; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import javax.inject.Inject; -import javax.inject.Singleton; - -import javafx.application.Platform; -import org.cryptomator.common.ConsumerThrowingException; -import org.cryptomator.common.RunnableThrowingException; -import org.cryptomator.common.SupplierThrowingException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import static java.util.Objects.requireNonNull; -import static java.util.concurrent.TimeUnit.MILLISECONDS; - -@Singleton -public class AsyncTaskService { - - private static final Logger LOG = LoggerFactory.getLogger(AsyncTaskService.class); - - private final ScheduledExecutorService executor; - - @Inject - public AsyncTaskService(ScheduledExecutorService executor) { - this.executor = executor; - } - - /** - * Creates a new async task - * - * @param task Tasks to be invoked in a background thread. - * @return The async task - */ - public AsyncTaskWithoutSuccessHandler asyncTaskOf(RunnableThrowingException task) { - return new AsyncTaskImpl<>(() -> { - task.run(); - return null; - }); - } - - public void runDelayedOnUiThread(long duration, TimeUnit unit, RunnableThrowingException task) { - asyncTaskOf(() -> null).onSuccess(task).runDelayedBy(duration, unit); - } - - /** - * Creates a new async task - * - * @param task Tasks to be invoked in a background thread. - * @return The async task - */ - public AsyncTaskWithoutSuccessHandler asyncTaskOf(SupplierThrowingException task) { - return new AsyncTaskImpl<>(task); - } - - private class AsyncTaskImpl implements AsyncTaskWithoutSuccessHandler { - - private final SupplierThrowingException task; - - private ConsumerThrowingException successHandler = value -> { - }; - private final List> errorHandlers = new ArrayList<>(); - private RunnableThrowingException finallyHandler = () -> { - }; - - public AsyncTaskImpl(SupplierThrowingException task) { - this.task = task; - } - - @Override - public AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException handler) { - successHandler = handler; - return this; - } - - @Override - public AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException handler) { - return onSuccess(result -> handler.run()); - } - - @SuppressWarnings({"unchecked", "rawtypes"}) - @Override - public AsyncTaskWithoutErrorHandler onError(Class type, ConsumerThrowingException handler) { - errorHandlers.add((ErrorHandler) new ErrorHandler<>(type, handler)); - return this; - } - - @Override - public AsyncTaskWithoutErrorHandler onError(Class type, RunnableThrowingException handler) { - return onError(type, error -> handler.run()); - } - - @Override - public AsyncTask andFinally(RunnableThrowingException handler) { - finallyHandler = handler; - return this; - } - - @Override - public void run() { - runDelayedBy(0, MILLISECONDS); - } - - @Override - public void runDelayedBy(long duration, TimeUnit unit) { - requireNonNull(unit, "unit must not be null"); - if (duration < 0) { - throw new IllegalArgumentException("duration must not be negative"); - } - errorHandlers.add(ErrorHandler.LOGGING_HANDLER); - executor.schedule(() -> logExceptions(() -> { - try { - ResultType result = task.get(); - Platform.runLater(() -> { - try { - successHandler.accept(result); - } catch (Throwable e) { - LOG.error("Uncaught exception", e); - } - }); - } catch (Throwable e) { - ErrorHandler errorHandler = errorHandlerFor(e); - Platform.runLater(toRunnableLoggingException(() -> errorHandler.accept(e))); - } finally { - Platform.runLater(toRunnableLoggingException(finallyHandler)); - } - }), duration, unit); - } - - private ErrorHandler errorHandlerFor(Throwable e) { - return errorHandlers.stream().filter(handler -> handler.handles(e)).findFirst().get(); - } - - } - - private static Runnable toRunnableLoggingException(RunnableThrowingException delegate) { - return () -> logExceptions(delegate); - } - - private static void logExceptions(RunnableThrowingException delegate) { - try { - delegate.run(); - } catch (Throwable e) { - LOG.error("Uncaught exception", e); - } - } - - private static class ErrorHandler implements ConsumerThrowingException { - - public static final ErrorHandler LOGGING_HANDLER = new ErrorHandler(Throwable.class, error -> { - LOG.error("Uncaught exception", error); - }); - - private final Class type; - private final ConsumerThrowingException delegate; - - public ErrorHandler(Class type, ConsumerThrowingException delegate) { - this.type = type; - this.delegate = delegate; - } - - public boolean handles(Throwable error) { - return type.isInstance(error); - } - - @Override - public void accept(ErrorType error) throws Throwable { - delegate.accept(error); - } - - } - - public interface AsyncTaskWithoutSuccessHandler extends AsyncTaskWithoutErrorHandler { - - /** - * @param handler Tasks to be invoked on the JavaFX application thread. - * @return The async task - */ - AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException handler); - - /** - * @param handler Tasks to be invoked on the JavaFX application thread. - * @return The async task - */ - AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException handler); - - } - - public interface AsyncTaskWithoutErrorHandler extends AsyncTaskWithoutFinallyHandler { - - /** - * @param type Exception type to catch - * @param handler Tasks to be invoked on the JavaFX application thread. - * @return The async task - */ - AsyncTaskWithoutErrorHandler onError(Class type, ConsumerThrowingException handler); - - /** - * @param type Exception type to catch - * @param handler Tasks to be invoked on the JavaFX application thread. - * @return The async task - */ - AsyncTaskWithoutErrorHandler onError(Class type, RunnableThrowingException handler); - - } - - public interface AsyncTaskWithoutFinallyHandler extends AsyncTask { - - /** - * @param handler Tasks to be invoked on the JavaFX application thread. - * @return The async task - */ - AsyncTask andFinally(RunnableThrowingException handler); - - } - - public interface AsyncTask extends Runnable { - - /** - * Starts the async task. - */ - @Override - void run(); - - /** - * Starts the async task delayed by {@code duration unit} - */ - void runDelayedBy(long duration, TimeUnit unit); - - } - -} diff --git a/main/ui/src/main/java/org/cryptomator/ui/util/Tasks.java b/main/ui/src/main/java/org/cryptomator/ui/util/Tasks.java new file mode 100644 index 000000000..e3fd77861 --- /dev/null +++ b/main/ui/src/main/java/org/cryptomator/ui/util/Tasks.java @@ -0,0 +1,192 @@ +/******************************************************************************* + * Copyright (c) 2018 Skymatic UG (haftungsbeschränkt). + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the accompanying LICENSE file. + *******************************************************************************/ +package org.cryptomator.ui.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import javafx.application.Platform; +import javafx.concurrent.ScheduledService; +import javafx.concurrent.Task; +import javafx.util.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class Tasks { + + private static final Logger LOG = LoggerFactory.getLogger(Tasks.class); + public static TaskBuilder create(Callable callable) { + return new TaskBuilder<>(callable); + } + + public static TaskBuilder create(VoidCallable callable) { + return create(() -> { + callable.call(); + return null; + }); + } + + public static class TaskBuilder { + + private final Callable callable; + private Consumer successHandler = x -> { + }; + private List> errorHandlers = new ArrayList<>(); + private Runnable finallyHandler = () -> {}; + + TaskBuilder(Callable callable) { + this.callable = callable; + } + + public TaskBuilder onSuccess(Runnable successHandler) { + this.successHandler = x -> { + successHandler.run(); + }; + return this; + } + + public TaskBuilder onSuccess(Consumer successHandler) { + this.successHandler = successHandler; + return this; + } + + public TaskBuilder onError(Class type, Consumer exceptionHandler) { + errorHandlers.add(new ErrorHandler<>(type, exceptionHandler)); + return this; + } + + public TaskBuilder andFinally(Runnable finallyHandler) { + this.finallyHandler = finallyHandler; + return this; + } + + private Task build() { + return new TaskImpl<>(callable, successHandler, errorHandlers, finallyHandler); + } + + public Task runOnce(ExecutorService executorService) { + Task task = build(); + executorService.submit(task); + return task; + } + + public Task scheduleOnce(ScheduledExecutorService executorService, long delay, TimeUnit unit) { + Task task = build(); + executorService.schedule(task, delay, unit); + return task; + } + + public ScheduledService schedulePeriodically(ExecutorService executorService, Duration initialDelay, Duration period) { + ScheduledService service = new RestartingService<>(this::build); + service.setExecutor(executorService); + service.setDelay(initialDelay); + service.setPeriod(period); + Platform.runLater(service::start); + return service; + } + + } + + private static class ErrorHandler { + + private final Class type; + private final Consumer errorHandler; + + public ErrorHandler(Class type, Consumer errorHandler) { + this.type = type; + this.errorHandler = errorHandler; + } + + public boolean handles(Throwable error) { + return type.isInstance(error); + } + + public void handle(Throwable error) throws ClassCastException { + ErrorType typedError = type.cast(error); + errorHandler.accept(typedError); + } + + } + + /** + * Adapter between java.util.function.* and javafx.concurrent.* + */ + private static class TaskImpl extends Task { + + private final Callable callable; + private final Consumer successHandler; + private List> errorHandlers; + private final Runnable finallyHandler; + + TaskImpl(Callable callable, Consumer successHandler, List> errorHandlers, Runnable finallyHandler) { + this.callable = callable; + this.successHandler = successHandler; + this.errorHandlers = errorHandlers; + this.finallyHandler = finallyHandler; + } + + @Override + protected T call() throws Exception { + return callable.call(); + } + + @Override + protected void succeeded() { + try { + successHandler.accept(getValue()); + } finally { + finallyHandler.run(); + } + } + + @Override + protected void failed() { + try { + Throwable exception = getException(); + errorHandlers.stream().filter(handler -> handler.handles(exception)).findFirst().ifPresentOrElse(exceptionHandler -> { + exceptionHandler.handle(exception); + }, () -> { + LOG.error("Unhandled exception", exception); + }); + } finally { + finallyHandler.run(); + } + } + + } + + /** + * A service that keeps executing the next task long as tasks are succeeding. + */ + private static class RestartingService extends ScheduledService { + + private final Supplier> taskFactory; + + RestartingService(Supplier> taskFactory) { + this.taskFactory = taskFactory; + } + + @Override + protected Task createTask() { + return taskFactory.get(); + } + + } + + public interface VoidCallable { + + void call() throws Exception; + + } + +} +