Improved error handling

* Created AsyncTaskService to build async UI operations which always log
uncaught exceptions
* Changed all executor service invocations in the UI to invocations of
AsyncTaskService
* Improved error handling in some other places, especially
try-with-resources
* Unlocking read/write locks in NioFile when opening of a channel fails
This commit is contained in:
Markus Kreusch
2016-07-14 13:58:17 +02:00
parent 9a2f602d6c
commit 7022a80c95
15 changed files with 351 additions and 107 deletions

View File

@@ -1,7 +1,7 @@
package org.cryptomator.common;
@FunctionalInterface
public interface ConsumerThrowingException<T, E extends Exception> {
public interface ConsumerThrowingException<T, E extends Throwable> {
void accept(T t) throws E;

View File

@@ -1,7 +1,7 @@
package org.cryptomator.common;
@FunctionalInterface
public interface RunnableThrowingException<T extends Exception> {
public interface RunnableThrowingException<T extends Throwable> {
void run() throws T;

View File

@@ -0,0 +1,56 @@
/*******************************************************************************
* Copyright (c) 2016 Markus Kreusch and others.
* This file is licensed under the terms of the MIT license.
* See the LICENSE.txt file for more info.
*
* Contributors:
* Markus Kreusch - initial implementation
*******************************************************************************/
package org.cryptomator.common;
import java.util.stream.Stream;
/**
* Utility to print stack traces while analyzing issues.
*
* @author Markus Kreusch
*/
public class StackTrace {
public static void print(String message) {
Thread thread = Thread.currentThread();
System.err.println(stackTraceFor(message, thread));
}
private static String stackTraceFor(String message, Thread thread) {
StringBuilder result = new StringBuilder();
appendMessageAndThreadName(result, message, thread);
appendStackTrace(thread, result);
return result.toString();
}
private static void appendStackTrace(Thread thread, StringBuilder result) {
Stream.of(thread.getStackTrace()) //
.skip(4) //
.forEach(stackTraceElement -> append(stackTraceElement, result));
}
private static void appendMessageAndThreadName(StringBuilder result, String message, Thread thread) {
result //
.append('[') //
.append(thread.getName()) //
.append("] ") //
.append(message);
}
private static void append(StackTraceElement stackTraceElement, StringBuilder result) {
String className = stackTraceElement.getClassName();
String methodName = stackTraceElement.getMethodName();
String fileName = stackTraceElement.getFileName();
int lineNumber = stackTraceElement.getLineNumber();
result.append('\n') //
.append(className).append(':').append(methodName) //
.append(" (").append(fileName).append(':').append(lineNumber).append(')');
}
}

View File

@@ -0,0 +1,8 @@
package org.cryptomator.common;
@FunctionalInterface
public interface SupplierThrowingException<T, E extends Throwable> {
T get() throws E;
}

View File

@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -28,7 +29,9 @@ public final class FileContents {
* @return The file's content interpreted in this FileContents' charset.
*/
public String readContents(File file) {
try (Reader reader = Channels.newReader(file.openReadable(), charset.newDecoder(), -1)) {
try ( //
ReadableByteChannel channel = file.openReadable(); //
Reader reader = Channels.newReader(channel, charset.newDecoder(), -1)) {
return IOUtils.toString(reader);
} catch (IOException e) {
throw new UncheckedIOException(e);

View File

@@ -16,6 +16,7 @@ import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -52,10 +53,14 @@ class Masterkeys {
public Cryptor decrypt(Folder vaultLocation, CharSequence passphrase) throws InvalidPassphraseException {
File masterkeyFile = vaultLocation.file(MASTERKEY_FILENAME);
Cryptor cryptor = cryptorProvider.get();
boolean success = false;
try {
readMasterKey(masterkeyFile, cryptor, passphrase);
} catch (UncheckedIOException e) {
cryptor.destroy();
success = true;
} finally {
if (!success) {
cryptor.destroy();
}
}
return cryptor;
}
@@ -86,7 +91,9 @@ class Masterkeys {
/* I/O */
private static void readMasterKey(File file, Cryptor cryptor, CharSequence passphrase) throws UncheckedIOException, InvalidPassphraseException {
try (InputStream in = Channels.newInputStream(file.openReadable())) {
try ( //
ReadableByteChannel channel = file.openReadable(); //
InputStream in = Channels.newInputStream(channel)) {
final byte[] fileContents = IOUtils.toByteArray(in);
cryptor.readKeysFromMasterkeyFile(fileContents, passphrase);
} catch (IOException e) {

View File

@@ -64,8 +64,10 @@ class InMemoryReadableFile implements ReadableFile {
@Override
public void close() throws UncheckedIOException {
open.set(false);
readLock.unlock();
if (open.get()) {
open.set(false);
readLock.unlock();
}
}
}

View File

@@ -30,13 +30,21 @@ class NioFile extends NioNode implements File {
@Override
public ReadableFile openReadable() throws UncheckedIOException {
if (lock.getWriteHoldCount() > 0) {
throw new IllegalStateException("Current thread is currently writing this file");
throw new IllegalStateException("Current thread is currently writing " + path);
}
if (lock.getReadHoldCount() > 0) {
throw new IllegalStateException("Current thread is already reading this file");
throw new IllegalStateException("Current thread is already reading " + path);
}
lock.readLock().lock();
return instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
ReadableFile result = null;
try {
result = instanceFactory.readableNioFile(path, sharedChannel, this::unlockReadLock);
} finally {
if (result == null) {
unlockReadLock();
}
}
return result;
}
private void unlockReadLock() {
@@ -46,13 +54,21 @@ class NioFile extends NioNode implements File {
@Override
public WritableFile openWritable() throws UncheckedIOException {
if (lock.getWriteHoldCount() > 0) {
throw new IllegalStateException("Current thread is already writing this file");
throw new IllegalStateException("Current thread is already writing " + path);
}
if (lock.getReadHoldCount() > 0) {
throw new IllegalStateException("Current thread is currently reading this file");
throw new IllegalStateException("Current thread is currently reading " + path);
}
lockWriteLock();
return instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
WritableFile result = null;
try {
result = instanceFactory.writableNioFile(fileSystem(), path, sharedChannel, this::unlockWriteLock);
} finally {
if (result == null) {
unlockWriteLock();
}
}
return result;
}
// visible for testing

View File

@@ -99,10 +99,11 @@ public class NioFileTest {
@Test
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
inTest.openReadable();
thrown.expect(IllegalStateException.class);
thrown.expectMessage("already reading this file");
thrown.expectMessage("already reading " + path);
inTest.openReadable();
}
@@ -111,7 +112,7 @@ public class NioFileTest {
public void testOpenReadableInvokedAfterAfterCloseOperationCreatesNewReadableFile() {
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null, readableNioFile);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(mock(ReadableNioFile.class), readableNioFile);
inTest.openReadable();
captor.getValue().run();
@@ -122,10 +123,11 @@ public class NioFileTest {
@Test
public void testOpenReadableInvokedBeforeInvokingAfterCloseOperationOfOpenWritableThrowsIllegalStateException() {
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
inTest.openWritable();
thrown.expect(IllegalStateException.class);
thrown.expectMessage("currently writing this file");
thrown.expectMessage("currently writing " + path);
inTest.openReadable();
}
@@ -133,7 +135,7 @@ public class NioFileTest {
@Test
public void testOpenReadableInvokedAfterInvokingAfterCloseOperationWorks() {
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class));
inTest.openWritable();
captor.getValue().run();
@@ -154,7 +156,7 @@ public class NioFileTest {
public void testOpenWritableInvokedAfterAfterCloseOperationCreatesNewWritableFile() {
WritableNioFile writableNioFile = mock(WritableNioFile.class);
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(null, writableNioFile);
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), captor.capture())).thenReturn(mock(WritableNioFile.class), writableNioFile);
inTest.openWritable();
captor.getValue().run();
@@ -165,28 +167,31 @@ public class NioFileTest {
@Test
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationThrowsIllegalStateException() {
when(instanceFactory.writableNioFile(same(fileSystem), same(path), same(channel), any())).thenReturn(mock(WritableNioFile.class));
inTest.openWritable();
thrown.expect(IllegalStateException.class);
thrown.expectMessage("already writing this file");
thrown.expectMessage("already writing " + path);
inTest.openWritable();
}
@Test
public void testOpenWritableInvokedBeforeInvokingAfterCloseOperationFromOpenReadableThrowsIllegalStateException() {
when(instanceFactory.readableNioFile(same(path), same(channel), any())).thenReturn(mock(ReadableNioFile.class));
inTest.openReadable();
thrown.expect(IllegalStateException.class);
thrown.expectMessage("currently reading this file");
thrown.expectMessage("currently reading " + path);
inTest.openWritable();
}
@Test
public void testOpenWritableInvokedAfterInvokingAfterCloseOperationWorks() {
ReadableNioFile readableNioFile = mock(ReadableNioFile.class);
ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(null);
when(instanceFactory.readableNioFile(same(path), same(channel), captor.capture())).thenReturn(readableNioFile);
inTest.openReadable();
captor.getValue().run();

View File

@@ -14,6 +14,7 @@ import javax.inject.Singleton;
import org.cryptomator.ui.controllers.MainController;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.AsyncTaskService;
import org.cryptomator.ui.util.DeferredCloser;
import dagger.Component;
@@ -21,6 +22,9 @@ import dagger.Component;
@Singleton
@Component(modules = CryptomatorModule.class)
interface CryptomatorComponent {
AsyncTaskService asyncTaskService();
ExecutorService executorService();
DeferredCloser deferredCloser();

View File

@@ -11,7 +11,6 @@ package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.Comparator;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
@@ -27,6 +26,7 @@ import org.cryptomator.ui.controls.SecPasswordField;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.AsyncTaskService;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -56,7 +56,7 @@ public class UnlockController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
private final Application app;
private final ExecutorService exec;
private final AsyncTaskService asyncTaskService;
private final Lazy<FrontendFactory> frontendFactory;
private final Settings settings;
private final WindowsDriveLetters driveLetters;
@@ -65,10 +65,10 @@ public class UnlockController extends LocalizedFXMLViewController {
private Optional<UnlockListener> listener = Optional.empty();
@Inject
public UnlockController(Application app, Localization localization, ExecutorService exec, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters) {
public UnlockController(Application app, Localization localization, AsyncTaskService asyncTaskService, Lazy<FrontendFactory> frontendFactory, Settings settings, WindowsDriveLetters driveLetters) {
super(localization);
this.app = app;
this.exec = exec;
this.asyncTaskService = asyncTaskService;
this.frontendFactory = frontendFactory;
this.settings = settings;
this.driveLetters = driveLetters;
@@ -275,8 +275,7 @@ public class UnlockController extends LocalizedFXMLViewController {
progressIndicator.setVisible(true);
downloadsPageLink.setVisible(false);
CharSequence password = passwordField.getCharacters();
exec.submit(() -> this.unlock(vault.get(), password));
asyncTaskService.asyncTaskOf(() -> this.unlock(vault.get(), password)).run();
}
private void unlock(Vault vault, CharSequence password) {

View File

@@ -10,7 +10,6 @@ package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import javax.inject.Provider;
@@ -19,6 +18,7 @@ import org.cryptomator.frontend.CommandFailedException;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.ActiveWindowStyleSupport;
import org.cryptomator.ui.util.AsyncTaskService;
import org.fxmisc.easybind.EasyBind;
import javafx.animation.Animation;
@@ -52,16 +52,16 @@ public class UnlockedController extends LocalizedFXMLViewController {
private final Stage macWarningsWindow = new Stage();
private final MacWarningsController macWarningsController;
private final ExecutorService exec;
private final AsyncTaskService asyncTaskService;
private final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
private Optional<LockListener> listener = Optional.empty();
private Timeline ioAnimation;
@Inject
public UnlockedController(Localization localization, Provider<MacWarningsController> macWarningsControllerProvider, ExecutorService exec) {
public UnlockedController(Localization localization, Provider<MacWarningsController> macWarningsControllerProvider, AsyncTaskService asyncTaskService) {
super(localization);
this.macWarningsController = macWarningsControllerProvider.get();
this.exec = exec;
this.asyncTaskService = asyncTaskService;
macWarningsController.vault.bind(this.vault);
}
@@ -116,18 +116,14 @@ public class UnlockedController extends LocalizedFXMLViewController {
@FXML
private void didClickLockVault(ActionEvent event) {
exec.submit(() -> {
try {
vault.get().unmount();
} catch (CommandFailedException e) {
Platform.runLater(() -> {
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
});
return;
}
asyncTaskService.asyncTaskOf(() -> {
vault.get().unmount();
vault.get().deactivateFrontend();
listener.ifPresent(this::invokeListenerLater);
});
}).onError(CommandFailedException.class, () -> {
messageLabel.setText(localization.getString("unlocked.label.unmountFailed"));
}).andFinally(() -> {
listener.ifPresent(listener -> listener.didLock(this));
}).run();
}
@FXML
@@ -142,15 +138,11 @@ public class UnlockedController extends LocalizedFXMLViewController {
@FXML
private void didClickRevealVault(ActionEvent event) {
exec.submit(() -> {
try {
vault.get().reveal();
} catch (CommandFailedException e) {
Platform.runLater(() -> {
messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
});
}
});
asyncTaskService.asyncTaskOf(() -> {
vault.get().reveal();
}).onError(CommandFailedException.class, () -> {
messageLabel.setText(localization.getString("unlocked.label.revealFailed"));
}).run();
}
@FXML
@@ -258,12 +250,6 @@ public class UnlockedController extends LocalizedFXMLViewController {
this.listener = Optional.ofNullable(listener);
}
private void invokeListenerLater(LockListener listener) {
Platform.runLater(() -> {
listener.didLock(this);
});
}
@FunctionalInterface
interface LockListener {
void didLock(UnlockedController ctrl);

View File

@@ -3,7 +3,6 @@ package org.cryptomator.ui.controllers;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
@@ -13,11 +12,9 @@ import org.cryptomator.ui.model.UpgradeStrategy;
import org.cryptomator.ui.model.UpgradeStrategy.UpgradeFailedException;
import org.cryptomator.ui.model.Vault;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.util.AsyncTaskService;
import org.fxmisc.easybind.EasyBind;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.ActionEvent;
@@ -28,19 +25,17 @@ import javafx.scene.control.ProgressIndicator;
public class UpgradeController extends LocalizedFXMLViewController {
private static final Logger LOG = LoggerFactory.getLogger(UpgradeController.class);
final ObjectProperty<Vault> vault = new SimpleObjectProperty<>();
final ObjectProperty<Optional<UpgradeStrategy>> strategy = new SimpleObjectProperty<>();
private final UpgradeStrategies strategies;
private final ExecutorService exec;
private final AsyncTaskService asyncTaskService;
private Optional<UpgradeListener> listener = Optional.empty();
@Inject
public UpgradeController(Localization localization, UpgradeStrategies strategies, ExecutorService exec) {
public UpgradeController(Localization localization, UpgradeStrategies strategies, AsyncTaskService asyncTaskService) {
super(localization);
this.strategies = strategies;
this.exec = exec;
this.asyncTaskService = asyncTaskService;
}
@FXML
@@ -103,26 +98,22 @@ public class UpgradeController extends LocalizedFXMLViewController {
Vault v = Objects.requireNonNull(vault.getValue());
passwordField.setDisable(true);
progressIndicator.setVisible(true);
exec.submit(() -> {
if (!instruction.isApplicable(v)) {
LOG.error("No upgrade needed for " + v.path().getValue());
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
}
try {
instruction.upgrade(v, passwordField.getCharacters());
Platform.runLater(this::showNextUpgrade);
} catch (UpgradeFailedException e) {
Platform.runLater(() -> {
asyncTaskService //
.asyncTaskOf(() -> {
if (!instruction.isApplicable(v)) {
throw new IllegalStateException("No ugprade needed for " + v.path().getValue());
}
instruction.upgrade(v, passwordField.getCharacters());
}) //
.onSuccess(this::showNextUpgrade) //
.onError(UpgradeFailedException.class, e -> {
errorLabel.setText(e.getLocalizedMessage());
});
} finally {
Platform.runLater(() -> {
}) //
.andFinally(() -> {
progressIndicator.setVisible(false);
passwordField.setDisable(false);
passwordField.swipe();
});
}
});
}).run();
}
private void showNextUpgrade() {

View File

@@ -8,14 +8,12 @@
******************************************************************************/
package org.cryptomator.ui.controllers;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import javax.inject.Named;
@@ -31,6 +29,7 @@ import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.SystemUtils;
import org.cryptomator.ui.settings.Localization;
import org.cryptomator.ui.settings.Settings;
import org.cryptomator.ui.util.AsyncTaskService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -54,15 +53,15 @@ public class WelcomeController extends LocalizedFXMLViewController {
private final Application app;
private final Settings settings;
private final Comparator<String> semVerComparator;
private final ExecutorService executor;
private final AsyncTaskService asyncTaskService;
@Inject
public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, ExecutorService executor) {
public WelcomeController(Application app, Localization localization, Settings settings, @Named("SemVer") Comparator<String> semVerComparator, AsyncTaskService asyncTaskService) {
super(localization);
this.app = app;
this.settings = settings;
this.semVerComparator = semVerComparator;
this.executor = executor;
this.asyncTaskService = asyncTaskService;
}
@FXML
@@ -82,7 +81,7 @@ public class WelcomeController extends LocalizedFXMLViewController {
if (areUpdatesManagedExternally()) {
checkForUpdatesContainer.setVisible(false);
} else if (settings.isCheckForUpdatesEnabled()) {
executor.execute(this::checkForUpdates);
this.checkForUpdates();
}
}
@@ -100,16 +99,14 @@ public class WelcomeController extends LocalizedFXMLViewController {
}
private void checkForUpdates() {
Platform.runLater(() -> {
checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking"));
checkForUpdatesIndicator.setVisible(true);
});
final HttpClient client = new HttpClient();
final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT"));
client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
client.getParams().setConnectionManagerTimeout(5000);
try {
checkForUpdatesStatus.setText(localization.getString("welcome.checkForUpdates.label.currentlyChecking"));
checkForUpdatesIndicator.setVisible(true);
asyncTaskService.asyncTaskOf(() -> {
final HttpClient client = new HttpClient();
final HttpMethod method = new GetMethod("https://cryptomator.org/downloads/latestVersion.json");
client.getParams().setParameter(HttpClientParams.USER_AGENT, "Cryptomator VersionChecker/" + applicationVersion().orElse("SNAPSHOT"));
client.getParams().setCookiePolicy(CookiePolicy.IGNORE_COOKIES);
client.getParams().setConnectionManagerTimeout(5000);
client.executeMethod(method);
final InputStream responseBodyStream = method.getResponseBodyAsStream();
if (method.getStatusCode() == HttpStatus.SC_OK && responseBodyStream != null) {
@@ -121,14 +118,10 @@ public class WelcomeController extends LocalizedFXMLViewController {
this.compareVersions(map);
}
}
} catch (IOException e) {
// no error handling required. Maybe next time the version check is successful.
} finally {
Platform.runLater(() -> {
checkForUpdatesStatus.setText("");
checkForUpdatesIndicator.setVisible(false);
});
}
}).andFinally(() -> {
checkForUpdatesStatus.setText("");
checkForUpdatesIndicator.setVisible(false);
}).run();
}
private Optional<String> applicationVersion() {

View File

@@ -0,0 +1,174 @@
package org.cryptomator.ui.util;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.cryptomator.common.ConsumerThrowingException;
import org.cryptomator.common.RunnableThrowingException;
import org.cryptomator.common.SupplierThrowingException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javafx.application.Platform;
@Singleton
public class AsyncTaskService {
private static final Logger LOG = LoggerFactory.getLogger(AsyncTaskService.class);
private final ExecutorService executor;
@Inject
public AsyncTaskService(ExecutorService executor) {
this.executor = executor;
}
public AsyncTaskWithoutSuccessHandler<Void> asyncTaskOf(RunnableThrowingException<?> task) {
return new AsyncTaskImpl<>(() -> {
task.run();
return null;
});
}
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 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() {
errorHandlers.add(ErrorHandler.LOGGING_HANDLER);
executor.execute(() -> 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));
}
}));
}
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 {
AsyncTaskWithoutErrorHandler onSuccess(ConsumerThrowingException<ResultType, ?> handler);
AsyncTaskWithoutErrorHandler onSuccess(RunnableThrowingException<?> handler);
}
public interface AsyncTaskWithoutErrorHandler extends AsyncTaskWithoutFinallyHandler {
<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, ConsumerThrowingException<ErrorType, ?> handler);
<ErrorType extends Throwable> AsyncTaskWithoutErrorHandler onError(Class<ErrorType> type, RunnableThrowingException<?> handler);
}
public interface AsyncTaskWithoutFinallyHandler extends AsyncTask {
AsyncTask andFinally(RunnableThrowingException<?> handler);
}
public interface AsyncTask extends Runnable {
}
}