Browse Source

Replaced AsyncTaskService by new Tasks utility using javafx.concurrent API

Sebastian Stenzel 6 years ago
parent
commit
03dfd3e887

+ 35 - 35
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<Character> driveLetterChangeListener = this::winDriveLetterDidChange;
 	private final Optional<KeychainAccess> keychainAccess;
 	private final Settings settings;
+	private final ExecutorService executor;
 	private Vault vault;
 	private Optional<UnlockListener> listener = Optional.empty();
 	private Subscription vaultSubs = Subscription.EMPTY;
 
 	@Inject
-	public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, WindowsDriveLetters driveLetters, Optional<KeychainAccess> keychainAccess, Settings settings) {
+	public UnlockController(Application app, Localization localization, WindowsDriveLetters driveLetters, Optional<KeychainAccess> 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 */

+ 13 - 16
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> vault = new SimpleObjectProperty<>();
-	private final ObjectExpression<Vault.State> vaultState = ObjectExpression.objectExpression(EasyBind.select(vault).selectObject(Vault::stateProperty));
 	private Optional<LockListener> 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);
 	}
 
 	// ****************************************

+ 15 - 17
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<UpgradeStrategy> strategy = new SimpleObjectProperty<>();
 	private final UpgradeStrategies strategies;
-	private final AsyncTaskService asyncTaskService;
+	private final ExecutorService executor;
 	private Optional<UpgradeListener> 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() {

+ 30 - 27
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<String> semVerComparator;
-	private final AsyncTaskService asyncTaskService;
+	private final ScheduledExecutorService executor;
 
 	@Inject
 	public WelcomeController(Application app, @Named("applicationVersion") Optional<String> applicationVersion, Localization localization, Settings settings, @Named("SemVer") Comparator<String> 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<ButtonType> 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<String> 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<String, String> map = gson.fromJson(response.body(), new TypeToken<Map<String, String>>() {
@@ -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<String, String> 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);
 		}
 	}
 

+ 0 - 241
main/ui/src/main/java/org/cryptomator/ui/util/AsyncTaskService.java

@@ -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<Void> 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 <ResultType> AsyncTaskWithoutSuccessHandler<ResultType> asyncTaskOf(SupplierThrowingException<ResultType, ?> task) {
-		return new AsyncTaskImpl<>(task);
-	}
-
-	private class AsyncTaskImpl<ResultType> implements AsyncTaskWithoutSuccessHandler<ResultType> {
-
-		private final SupplierThrowingException<ResultType, ?> task;
-
-		private ConsumerThrowingException<ResultType, ?> successHandler = value -> {
-		};
-		private final List<ErrorHandler<Throwable>> errorHandlers = new ArrayList<>();
-		private RunnableThrowingException<?> finallyHandler = () -> {
-		};
-
-		public AsyncTaskImpl(SupplierThrowingException<ResultType, ?> task) {
-			this.task = task;
-		}
-
-		@Override
-		public AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> handler) {
-			successHandler = handler;
-			return this;
-		}
-
-		@Override
-		public AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException<?> handler) {
-			return onSuccess(result -> handler.run());
-		}
-
-		@SuppressWarnings({"unchecked", "rawtypes"})
-		@Override
-		public <ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler) {
-			errorHandlers.add((ErrorHandler) new ErrorHandler<>(type, handler));
-			return this;
-		}
-
-		@Override
-		public <ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> 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<Throwable> errorHandler = errorHandlerFor(e);
-					Platform.runLater(toRunnableLoggingException(() -> errorHandler.accept(e)));
-				} finally {
-					Platform.runLater(toRunnableLoggingException(finallyHandler));
-				}
-			}), duration, unit);
-		}
-
-		private ErrorHandler<Throwable> 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<ErrorType> implements ConsumerThrowingException<ErrorType, Throwable> {
-
-		public static final ErrorHandler<Throwable> LOGGING_HANDLER = new ErrorHandler<Throwable>(Throwable.class, error -> {
-			LOG.error("Uncaught exception", error);
-		});
-
-		private final Class<ErrorType> type;
-		private final ConsumerThrowingException<ErrorType, ?> delegate;
-
-		public ErrorHandler(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> 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<ResultType> extends AsyncTaskWithoutErrorHandler {
-
-		/**
-		 * @param handler Tasks to be invoked on the JavaFX application thread.
-		 * @return The async task
-		 */
-		AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> 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
-		 */
-		<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler);
-
-		/**
-		 * @param type Exception type to catch
-		 * @param handler Tasks to be invoked on the JavaFX application thread.
-		 * @return The async task
-		 */
-		<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> 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);
-
-	}
-
-}

+ 192 - 0
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 <T> TaskBuilder<T> create(Callable<T> callable) {
+		return new TaskBuilder<>(callable);
+	}
+
+	public static TaskBuilder<Void> create(VoidCallable callable) {
+		return create(() -> {
+			callable.call();
+			return null;
+		});
+	}
+
+	public static class TaskBuilder<T> {
+
+		private final Callable<T> callable;
+		private Consumer<T> successHandler = x -> {
+		};
+		private List<ErrorHandler<?>> errorHandlers = new ArrayList<>();
+		private Runnable finallyHandler = () -> {};
+
+		TaskBuilder(Callable<T> callable) {
+			this.callable = callable;
+		}
+
+		public TaskBuilder<T> onSuccess(Runnable successHandler) {
+			this.successHandler = x -> {
+				successHandler.run();
+			};
+			return this;
+		}
+
+		public TaskBuilder<T> onSuccess(Consumer<T> successHandler) {
+			this.successHandler = successHandler;
+			return this;
+		}
+
+		public <E extends Throwable> TaskBuilder<T> onError(Class<E> type, Consumer<E> exceptionHandler) {
+			errorHandlers.add(new ErrorHandler<>(type, exceptionHandler));
+			return this;
+		}
+
+		public TaskBuilder<T> andFinally(Runnable finallyHandler) {
+			this.finallyHandler = finallyHandler;
+			return this;
+		}
+
+		private Task<T> build() {
+			return new TaskImpl<>(callable, successHandler, errorHandlers, finallyHandler);
+		}
+
+		public Task<T> runOnce(ExecutorService executorService) {
+			Task<T> task = build();
+			executorService.submit(task);
+			return task;
+		}
+
+		public Task<T> scheduleOnce(ScheduledExecutorService executorService, long delay, TimeUnit unit) {
+			Task<T> task = build();
+			executorService.schedule(task, delay, unit);
+			return task;
+		}
+
+		public ScheduledService<T> schedulePeriodically(ExecutorService executorService, Duration initialDelay, Duration period) {
+			ScheduledService<T> service = new RestartingService<>(this::build);
+			service.setExecutor(executorService);
+			service.setDelay(initialDelay);
+			service.setPeriod(period);
+			Platform.runLater(service::start);
+			return service;
+		}
+
+	}
+
+	private static class ErrorHandler<ErrorType extends Throwable> {
+
+		private final Class<ErrorType> type;
+		private final Consumer<ErrorType> errorHandler;
+
+		public ErrorHandler(Class<ErrorType> type, Consumer<ErrorType> 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<T> extends Task<T> {
+
+		private final Callable<T> callable;
+		private final Consumer<T> successHandler;
+		private List<ErrorHandler<?>> errorHandlers;
+		private final Runnable finallyHandler;
+
+		TaskImpl(Callable<T> callable, Consumer<T> successHandler, List<ErrorHandler<?>> 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<T> extends ScheduledService<T> {
+
+		private final Supplier<Task<T>> taskFactory;
+
+		RestartingService(Supplier<Task<T>> taskFactory) {
+			this.taskFactory = taskFactory;
+		}
+
+		@Override
+		protected Task<T> createTask() {
+			return taskFactory.get();
+		}
+
+	}
+
+	public interface VoidCallable {
+
+		void call() throws Exception;
+
+	}
+
+}
+