Prechádzať zdrojové kódy

Removed static resources in WebDavServer, FXThreads and Settings with
dependency injection. Replaced static references to MainApplication in
the context of closing resources with an injected DeferredCloser. Using
controller factory for dependency injection into FX controllers.

Tillmann Gaida 10 rokov pred
rodič
commit
def70c5891

+ 1 - 6
main/core/src/main/java/org/cryptomator/webdav/WebDavServer.java

@@ -38,16 +38,11 @@ public final class WebDavServer {
 	private static final int MAX_THREADS = 200;
 	private static final int MIN_THREADS = 4;
 	private static final int THREAD_IDLE_SECONDS = 20;
-	private static final WebDavServer INSTANCE = new WebDavServer();
 	private final Server server;
 	private final ServerConnector localConnector;
 	private final ContextHandlerCollection servletCollection;
 
-	public static WebDavServer getInstance() {
-		return INSTANCE;
-	}
-
-	private WebDavServer() {
+	public WebDavServer() {
 		final BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(MAX_PENDING_REQUESTS);
 		final ThreadPool tp = new QueuedThreadPool(MAX_THREADS, MIN_THREADS, THREAD_IDLE_SECONDS, queue);
 		server = new Server(tp);

+ 0 - 7
main/pom.xml

@@ -34,7 +34,6 @@
 		<commons-codec.version>1.10</commons-codec.version>
 		<jackson-databind.version>2.4.4</jackson-databind.version>
 		<mockito.version>1.10.19</mockito.version>
-		<axetDesktop.version>2.2.3</axetDesktop.version>
 	</properties>
 
 	<dependencyManagement>
@@ -126,12 +125,6 @@
 				<version>${mockito.version}</version>
 				<scope>test</scope>
 			</dependency>
-
-			<dependency>
-				<groupId>com.github.axet</groupId>
-				<artifactId>desktop</artifactId>
-				<version>${axetDesktop.version}</version>
-			</dependency>
 		</dependencies>
 	</dependencyManagement>
 

+ 7 - 0
main/ui/pom.xml

@@ -60,7 +60,14 @@
 		<dependency>
 			<groupId>com.github.axet</groupId>
 			<artifactId>desktop</artifactId>
+			<version>2.2.3</version>
 		</dependency> -->
+
+		<dependency>
+			<groupId>com.google.inject</groupId>
+			<artifactId>guice</artifactId>
+			<version>3.0</version>
+		</dependency>
 	</dependencies>
 
 	<build>

+ 59 - 15
main/ui/src/main/java/org/cryptomator/ui/MainApplication.java

@@ -14,7 +14,6 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ResourceBundle;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 
 import javafx.application.Application;
 import javafx.application.Platform;
@@ -25,14 +24,16 @@ import javafx.stage.Stage;
 
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.ui.model.Vault;
-import org.cryptomator.ui.settings.Settings;
+import org.cryptomator.ui.MainModule.ControllerFactory;
 import org.cryptomator.ui.util.ActiveWindowStyleSupport;
+import org.cryptomator.ui.util.DeferredCloser;
 import org.cryptomator.ui.util.SingleInstanceManager;
 import org.cryptomator.ui.util.SingleInstanceManager.LocalInstance;
 import org.cryptomator.ui.util.TrayIconUtil;
-import org.cryptomator.webdav.WebDavServer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
 
 public class MainApplication extends Application {
 
@@ -40,7 +41,38 @@ public class MainApplication extends Application {
 
 	private static final Logger LOG = LoggerFactory.getLogger(MainApplication.class);
 
-	private ExecutorService executorService;
+	private final CleanShutdownPerformer cleanShutdownPerformer = new CleanShutdownPerformer();
+
+	private final ExecutorService executorService;
+
+	private final ControllerFactory controllerFactory;
+
+	private final DeferredCloser closer;
+
+	public MainApplication() {
+		this(getInjector());
+	}
+
+	private static Injector getInjector() {
+		try {
+			return Guice.createInjector(new MainModule());
+		} catch (Exception e) {
+			throw e;
+		}
+	}
+
+	public MainApplication(Injector injector) {
+		this(injector.getInstance(ExecutorService.class),
+				injector.getInstance(ControllerFactory.class),
+				injector.getInstance(DeferredCloser.class));
+	}
+
+	public MainApplication(ExecutorService executorService, ControllerFactory controllerFactory, DeferredCloser closer) {
+		super();
+		this.executorService = executorService;
+		this.controllerFactory = controllerFactory;
+		this.closer = closer;
+	}
 
 	@Override
 	public void start(final Stage primaryStage) throws IOException {
@@ -55,12 +87,12 @@ public class MainApplication extends Application {
 			}
 		});
 
-		executorService = Executors.newCachedThreadPool();
+		Runtime.getRuntime().addShutdownHook(cleanShutdownPerformer);
 
-		WebDavServer.getInstance().start();
 		chooseNativeStylesheet();
 		final ResourceBundle rb = ResourceBundle.getBundle("localization");
 		final FXMLLoader loader = new FXMLLoader(getClass().getResource("/fxml/main.fxml"), rb);
+		loader.setControllerFactory(controllerFactory);
 		final Parent root = loader.load();
 		final MainController ctrl = loader.getController();
 		ctrl.setStage(primaryStage);
@@ -83,14 +115,10 @@ public class MainApplication extends Application {
 			Main.OPEN_FILE_HANDLER.complete(file -> handleCommandLineArg(ctrl, file.getAbsolutePath()));
 		}
 
-		final LocalInstance cryptomatorGuiInstance = SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService);
-		cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
+		LocalInstance cryptomatorGuiInstance = closer.closeLater(
+				SingleInstanceManager.startLocalInstance(APPLICATION_KEY, executorService), LocalInstance::close).get().get();
 
-		Main.addShutdownTask(() -> {
-			cryptomatorGuiInstance.close();
-			Settings.save();
-			executorService.shutdown();
-		});
+		cryptomatorGuiInstance.registerListener(arg -> handleCommandLineArg(ctrl, arg));
 	}
 
 	void handleCommandLineArg(final MainController ctrl, String arg) {
@@ -131,11 +159,27 @@ public class MainApplication extends Application {
 
 	private void quit() {
 		Platform.runLater(() -> {
-			WebDavServer.getInstance().stop();
-			Settings.save();
+			stop();
 			Platform.exit();
 			System.exit(0);
 		});
 	}
 
+	@Override
+	public void stop() {
+		closer.close();
+		try {
+			Runtime.getRuntime().removeShutdownHook(cleanShutdownPerformer);
+		} catch (Exception e) {
+
+		}
+	}
+
+	private class CleanShutdownPerformer extends Thread {
+		@Override
+		public void run() {
+			closer.close();
+		}
+	}
+
 }

+ 16 - 1
main/ui/src/main/java/org/cryptomator/ui/MainController.java

@@ -39,6 +39,7 @@ import javafx.stage.Stage;
 import javafx.stage.WindowEvent;
 
 import org.cryptomator.ui.InitializeController.InitializationListener;
+import org.cryptomator.ui.MainModule.ControllerFactory;
 import org.cryptomator.ui.UnlockController.UnlockListener;
 import org.cryptomator.ui.UnlockedController.LockListener;
 import org.cryptomator.ui.controls.DirectoryListCell;
@@ -47,6 +48,8 @@ import org.cryptomator.ui.settings.Settings;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.inject.Inject;
+
 public class MainController implements Initializable, InitializationListener, UnlockListener, LockListener {
 
 	private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
@@ -73,11 +76,22 @@ public class MainController implements Initializable, InitializationListener, Un
 
 	private ResourceBundle rb;
 
+	private final ControllerFactory controllerFactory;
+
+	private final Settings settings;
+
+	@Inject
+	public MainController(ControllerFactory controllerFactory, Settings settings) {
+		super();
+		this.controllerFactory = controllerFactory;
+		this.settings = settings;
+	}
+
 	@Override
 	public void initialize(URL url, ResourceBundle rb) {
 		this.rb = rb;
 
-		final ObservableList<Vault> items = FXCollections.observableList(Settings.load().getDirectories());
+		final ObservableList<Vault> items = FXCollections.observableList(settings.getDirectories());
 		directoryList.setItems(items);
 		directoryList.setCellFactory(this::createDirecoryListCell);
 		directoryList.getSelectionModel().getSelectedItems().addListener(this::selectedDirectoryDidChange);
@@ -202,6 +216,7 @@ public class MainController implements Initializable, InitializationListener, Un
 	private <T> T showView(String fxml) {
 		try {
 			final FXMLLoader loader = new FXMLLoader(getClass().getResource(fxml), rb);
+			loader.setControllerFactory(controllerFactory);
 			final Parent root = loader.load();
 			contentPane.getChildren().clear();
 			contentPane.getChildren().add(root);

+ 68 - 0
main/ui/src/main/java/org/cryptomator/ui/MainModule.java

@@ -0,0 +1,68 @@
+/*******************************************************************************
+ * Copyright (c) 2014 cryptomator.org
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ * 
+ * Contributors:
+ *     Tillmann Gaida - initial implementation
+ ******************************************************************************/
+package org.cryptomator.ui;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.inject.Singleton;
+
+import org.cryptomator.ui.settings.Settings;
+import org.cryptomator.ui.util.DeferredCloser;
+import org.cryptomator.ui.util.DeferredCloser.Closer;
+import org.cryptomator.webdav.WebDavServer;
+
+import javafx.util.Callback;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Injector;
+import com.google.inject.Provides;
+
+public class MainModule extends AbstractModule {
+	DeferredCloser deferredCloser = new DeferredCloser();
+
+	public static interface ControllerFactory extends Callback<Class<?>, Object> {
+
+	}
+
+	@Override
+	protected void configure() {
+		bind(DeferredCloser.class).toInstance(deferredCloser);
+	}
+
+	@Provides
+	@Singleton
+	ControllerFactory getControllerFactory(Injector injector) {
+		return cls -> injector.getInstance(cls);
+	}
+
+	@Provides
+	@Singleton
+	ExecutorService getExec() {
+		return closeLater(Executors.newCachedThreadPool(), ExecutorService::shutdown);
+	}
+
+	@Provides
+	@Singleton
+	Settings getSettings() {
+		return closeLater(Settings.load(), Settings::save);
+	}
+
+	@Provides
+	@Singleton
+	WebDavServer getServer() {
+		final WebDavServer webDavServer = new WebDavServer();
+		webDavServer.start();
+		return closeLater(webDavServer, WebDavServer::stop);
+	}
+
+	<T> T closeLater(T object, Closer<T> closer) {
+		return deferredCloser.closeLater(object, closer).get().get();
+	}
+}

+ 23 - 4
main/ui/src/main/java/org/cryptomator/ui/UnlockController.java

@@ -16,6 +16,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.ResourceBundle;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
 
 import javafx.application.Platform;
@@ -40,9 +41,13 @@ import org.cryptomator.ui.controls.SecPasswordField;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.ui.util.FXThreads;
 import org.cryptomator.ui.util.MasterKeyFilter;
+import org.cryptomator.ui.util.DeferredCloser;
+import org.cryptomator.webdav.WebDavServer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.inject.Inject;
+
 public class UnlockController implements Initializable {
 
 	private static final Logger LOG = LoggerFactory.getLogger(UnlockController.class);
@@ -72,6 +77,20 @@ public class UnlockController implements Initializable {
 	@FXML
 	private Label messageLabel;
 
+	private final WebDavServer server;
+
+	private final ExecutorService exec;
+
+	private final DeferredCloser closer;
+
+	@Inject
+	public UnlockController(WebDavServer server, ExecutorService exec, DeferredCloser closer) {
+		super();
+		this.server = server;
+		this.exec = exec;
+		this.closer = closer;
+	}
+
 	@Override
 	public void initialize(URL url, ResourceBundle rb) {
 		this.rb = rb;
@@ -107,15 +126,15 @@ public class UnlockController implements Initializable {
 			masterKeyInputStream = Files.newInputStream(masterKeyPath, StandardOpenOption.READ);
 			directory.setVerifyFileIntegrity(checkIntegrity.isSelected());
 			directory.getCryptor().decryptMasterKey(masterKeyInputStream, password);
-			if (!directory.startServer()) {
+			if (!directory.startServer(server, closer)) {
 				messageLabel.setText(rb.getString("unlock.messageLabel.startServerFailed"));
 				directory.getCryptor().swipeSensitiveData();
 				return;
 			}
 			directory.setUnlocked(true);
-			final Future<Boolean> futureMount = FXThreads.runOnBackgroundThread(directory::mount);
-			FXThreads.runOnMainThreadWhenFinished(futureMount, this::didUnlockAndMount);
-			FXThreads.runOnMainThreadWhenFinished(futureMount, (result) -> {
+			final Future<Boolean> futureMount = exec.submit(() -> directory.mount(closer));
+			FXThreads.runOnMainThreadWhenFinished(exec, futureMount, this::didUnlockAndMount);
+			FXThreads.runOnMainThreadWhenFinished(exec, futureMount, (result) -> {
 				setControlsDisabled(false);
 			});
 		} catch (DecryptFailedException | IOException ex) {

+ 11 - 1
main/ui/src/main/java/org/cryptomator/ui/UnlockedController.java

@@ -29,6 +29,8 @@ import org.cryptomator.crypto.CryptorIOSampling;
 import org.cryptomator.ui.model.Vault;
 import org.cryptomator.webdav.WebDavServer;
 
+import com.google.inject.Inject;
+
 public class UnlockedController implements Initializable {
 
 	private static final int IO_SAMPLING_STEPS = 100;
@@ -47,6 +49,14 @@ public class UnlockedController implements Initializable {
 	@FXML
 	private NumberAxis xAxis;
 
+	private final WebDavServer server;
+
+	@Inject
+	public UnlockedController(WebDavServer server) {
+		super();
+		this.server = server;
+	}
+
 	@Override
 	public void initialize(URL url, ResourceBundle rb) {
 		this.rb = rb;
@@ -124,7 +134,7 @@ public class UnlockedController implements Initializable {
 
 	public void setDirectory(Vault directory) {
 		this.directory = directory;
-		final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), WebDavServer.getInstance().getPort());
+		final String msg = String.format(rb.getString("unlocked.messageLabel.runningOnPort"), server.getPort());
 		messageLabel.setText(msg);
 
 		if (directory.getCryptor() instanceof CryptorIOSampling) {

+ 21 - 42
main/ui/src/main/java/org/cryptomator/ui/model/Vault.java

@@ -6,6 +6,7 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.text.Normalizer;
 import java.text.Normalizer.Form;
+import java.util.Optional;
 
 import javafx.beans.property.ObjectProperty;
 import javafx.beans.property.SimpleObjectProperty;
@@ -14,8 +15,9 @@ import org.apache.commons.lang3.StringUtils;
 import org.cryptomator.crypto.Cryptor;
 import org.cryptomator.crypto.SamplingDecorator;
 import org.cryptomator.crypto.aes256.Aes256Cryptor;
-import org.cryptomator.ui.Main;
 import org.cryptomator.ui.util.MasterKeyFilter;
+import org.cryptomator.ui.util.DeferredClosable;
+import org.cryptomator.ui.util.DeferredCloser;
 import org.cryptomator.ui.util.mount.CommandFailedException;
 import org.cryptomator.ui.util.mount.WebDavMount;
 import org.cryptomator.ui.util.mount.WebDavMounter;
@@ -38,12 +40,11 @@ public class Vault implements Serializable {
 
 	private final Cryptor cryptor = SamplingDecorator.decorate(new Aes256Cryptor());
 	private final ObjectProperty<Boolean> unlocked = new SimpleObjectProperty<Boolean>(this, "unlocked", Boolean.FALSE);
-	private final Runnable shutdownTask = new ShutdownTask();
 	private final Path path;
 	private boolean verifyFileIntegrity;
 	private String mountName;
-	private ServletLifeCycleAdapter webDavServlet;
-	private WebDavMount webDavMount;
+	private DeferredClosable<ServletLifeCycleAdapter> webDavServlet = DeferredClosable.empty();
+	private DeferredClosable<WebDavMount> webDavMount = DeferredClosable.empty();
 
 	public Vault(final Path vaultDirectoryPath) {
 		if (!Files.isDirectory(vaultDirectoryPath) || !vaultDirectoryPath.getFileName().toString().endsWith(VAULT_FILE_EXTENSION)) {
@@ -62,34 +63,32 @@ public class Vault implements Serializable {
 		return MasterKeyFilter.filteredDirectory(path).iterator().hasNext();
 	}
 
-	public synchronized boolean startServer() {
-		if (webDavServlet != null && webDavServlet.isRunning()) {
+	public synchronized boolean startServer(WebDavServer server, DeferredCloser closer) {
+		Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
+		if (o.isPresent() && o.get().isRunning()) {
 			return false;
 		}
-		webDavServlet = WebDavServer.getInstance().createServlet(path, verifyFileIntegrity, cryptor, getMountName());
-		if (webDavServlet.start()) {
-			Main.addShutdownTask(shutdownTask);
+		ServletLifeCycleAdapter servlet = server.createServlet(path, verifyFileIntegrity, cryptor, getMountName());
+		if (servlet.start()) {
+			webDavServlet = closer.closeLater(servlet, ServletLifeCycleAdapter::stop);
 			return true;
-		} else {
-			return false;
 		}
+		return false;
 	}
 
 	public void stopServer() {
-		if (webDavServlet != null && webDavServlet.isRunning()) {
-			Main.removeShutdownTask(shutdownTask);
-			this.unmount();
-			webDavServlet.stop();
-			cryptor.swipeSensitiveData();
-		}
+		unmount();
+		webDavServlet.close();
+		cryptor.swipeSensitiveData();
 	}
 
-	public boolean mount() {
-		if (webDavServlet == null || !webDavServlet.isRunning()) {
+	public boolean mount(DeferredCloser closer) {
+		Optional<ServletLifeCycleAdapter> o = webDavServlet.get();
+		if (!o.isPresent() || !o.get().isRunning()) {
 			return false;
 		}
 		try {
-			webDavMount = WebDavMounter.mount(webDavServlet.getServletUri(), getMountName());
+			webDavMount = closer.closeLater(WebDavMounter.mount(o.get().getServletUri(), getMountName()), WebDavMount::unmount);
 			return true;
 		} catch (CommandFailedException e) {
 			LOG.warn("mount failed", e);
@@ -97,17 +96,8 @@ public class Vault implements Serializable {
 		}
 	}
 
-	public boolean unmount() {
-		try {
-			if (webDavMount != null) {
-				webDavMount.unmount();
-				webDavMount = null;
-			}
-			return true;
-		} catch (CommandFailedException e) {
-			LOG.warn("unmount failed", e);
-			return false;
-		}
+	public void unmount() {
+		webDavMount.close();
 	}
 
 	/* Getter/Setter */
@@ -208,15 +198,4 @@ public class Vault implements Serializable {
 		}
 	}
 
-	/* graceful shutdown */
-
-	private class ShutdownTask implements Runnable {
-
-		@Override
-		public void run() {
-			stopServer();
-		}
-
-	}
-
 }

+ 16 - 23
main/ui/src/main/java/org/cryptomator/ui/settings/Settings.java

@@ -36,7 +36,6 @@ public class Settings implements Serializable {
 	private static final Path SETTINGS_DIR;
 	private static final String SETTINGS_FILE = "settings.json";
 	private static final ObjectMapper JSON_OM = new ObjectMapper();
-	private static Settings INSTANCE = null;
 
 	static {
 		final String appdata = System.getenv("APPDATA");
@@ -61,31 +60,25 @@ public class Settings implements Serializable {
 	}
 
 	public static synchronized Settings load() {
-		if (INSTANCE == null) {
-			try {
-				Files.createDirectories(SETTINGS_DIR);
-				final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
-				final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ);
-				INSTANCE = JSON_OM.readValue(in, Settings.class);
-				return INSTANCE;
-			} catch (IOException e) {
-				LOG.warn("Failed to load settings, creating new one.");
-				INSTANCE = Settings.defaultSettings();
-			}
+		try {
+			Files.createDirectories(SETTINGS_DIR);
+			final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
+			final InputStream in = Files.newInputStream(settingsFile, StandardOpenOption.READ);
+			return JSON_OM.readValue(in, Settings.class);
+		} catch (IOException e) {
+			LOG.warn("Failed to load settings, creating new one.");
+			return Settings.defaultSettings();
 		}
-		return INSTANCE;
 	}
 
-	public static synchronized void save() {
-		if (INSTANCE != null) {
-			try {
-				Files.createDirectories(SETTINGS_DIR);
-				final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
-				final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
-				JSON_OM.writeValue(out, INSTANCE);
-			} catch (IOException e) {
-				LOG.error("Failed to save settings.", e);
-			}
+	public synchronized void save() {
+		try {
+			Files.createDirectories(SETTINGS_DIR);
+			final Path settingsFile = SETTINGS_DIR.resolve(SETTINGS_FILE);
+			final OutputStream out = Files.newOutputStream(settingsFile, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE);
+			JSON_OM.writeValue(out, this);
+		} catch (IOException e) {
+			LOG.error("Failed to save settings.", e);
 		}
 	}
 

+ 43 - 0
main/ui/src/main/java/org/cryptomator/ui/util/DeferredClosable.java

@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2014 cryptomator.org
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ * 
+ * Contributors:
+ *     Tillmann Gaida - initial implementation
+ ******************************************************************************/
+package org.cryptomator.ui.util;
+
+import java.util.Optional;
+
+/**
+ * Wrapper around an object, which should be closed later - explicitly or by a
+ * {@link DeferredCloser}. The wrapped object can be accessed as long as the
+ * resource has not been closed.
+ * 
+ * @author Tillmann Gaida
+ *
+ * @param <T>
+ *            any type
+ */
+public interface DeferredClosable<T> extends AutoCloseable {
+	/**
+	 * Returns the wrapped Object.
+	 * 
+	 * @return empty if the object has been closed.
+	 */
+	public Optional<T> get();
+
+	/**
+	 * Quietly closes the Object. If the object was closed before, nothing
+	 * happens.
+	 */
+	public void close();
+
+	/**
+	 * @return an empty object.
+	 */
+	public static <T> DeferredClosable<T> empty() {
+		return DeferredCloser.empty();
+	}
+}

+ 123 - 0
main/ui/src/main/java/org/cryptomator/ui/util/DeferredCloser.java

@@ -0,0 +1,123 @@
+/*******************************************************************************
+ * Copyright (c) 2014 cryptomator.org
+ * This file is licensed under the terms of the MIT license.
+ * See the LICENSE.txt file for more info.
+ * 
+ * Contributors:
+ *     Tillmann Gaida - initial implementation
+ ******************************************************************************/
+package org.cryptomator.ui.util;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentSkipListMap;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.cryptomator.ui.MainController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * <p>
+ * Tries to bring open-close symmetry in contexts where the resource outlives
+ * the current scope by introducing a manager, which closes the resources if
+ * they haven't been closed before.
+ * </p>
+ * 
+ * <p>
+ * If you have a {@link DeferredCloser} instance present, call
+ * {@link #closeLater(Object, Closer)} immediately after you have opened the
+ * resource and return a resource handle. If {@link #close()} is called, the
+ * resource will be closed. Calling {@link DeferredClosable#close()} on the resource
+ * handle will also close the resource and prevent a second closing by
+ * {@link #close()}.
+ * </p>
+ * 
+ * @author Tillmann Gaida
+ */
+public class DeferredCloser implements AutoCloseable {
+	public static interface Closer<T> {
+		void close(T object) throws Exception;
+	}
+
+	static class EmptyResource<T> implements DeferredClosable<T> {
+		@Override
+		public Optional<T> get() {
+			return Optional.empty();
+		}
+
+		@Override
+		public void close() {
+
+		}
+	}
+
+	private static final Logger LOG = LoggerFactory.getLogger(MainController.class);
+
+	final Map<Long, ManagedResource<?>> cleanups = new ConcurrentSkipListMap<>();
+
+	final AtomicLong counter = new AtomicLong();
+
+	public class ManagedResource<T> implements DeferredClosable<T> {
+		private final long number = counter.incrementAndGet();
+
+		private final AtomicReference<T> object = new AtomicReference<>();
+		private final Closer<T> closer;
+
+		ManagedResource(T object, Closer<T> closer) {
+			super();
+			this.object.set(object);
+			this.closer = closer;
+		}
+
+		public void close() {
+			final T oldObject = object.getAndSet(null);
+			if (oldObject != null) {
+				cleanups.remove(number);
+
+				try {
+					closer.close(oldObject);
+				} catch (Exception e) {
+					LOG.error("exception closing resource", e);
+				}
+			}
+		}
+
+		public Optional<T> get() throws IllegalStateException {
+			return Optional.ofNullable(object.get());
+		}
+	}
+
+	/**
+	 * Closes all added objects which have not been closed before.
+	 */
+	public void close() {
+		for (ManagedResource<?> closableProvider : cleanups.values()) {
+			closableProvider.close();
+		}
+	}
+
+	public <T> DeferredClosable<T> closeLater(T object, Closer<T> closer) {
+		Objects.requireNonNull(object);
+		Objects.requireNonNull(closer);
+		final ManagedResource<T> resource = new ManagedResource<T>(object, closer);
+		cleanups.put(resource.number, resource);
+		return resource;
+	}
+
+	public <T extends AutoCloseable> DeferredClosable<T> closeLater(T object) {
+		Objects.requireNonNull(object);
+		final ManagedResource<T> resource = new ManagedResource<T>(object, AutoCloseable::close);
+		cleanups.put(resource.number, resource);
+		return resource;
+	}
+
+	private static final EmptyResource<?> EMPTY_RESOURCE = new EmptyResource<>();
+
+	@SuppressWarnings("unchecked")
+	public static <T> DeferredClosable<T> empty() {
+		return (DeferredClosable<T>) EMPTY_RESOURCE;
+	}
+}

+ 15 - 66
main/ui/src/main/java/org/cryptomator/ui/util/FXThreads.java

@@ -10,9 +10,8 @@
  ******************************************************************************/
 package org.cryptomator.ui.util;
 
-import java.util.concurrent.Callable;
+import java.util.Objects;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 
 import javafx.application.Platform;
@@ -48,61 +47,14 @@ import javafx.application.Platform;
  */
 public final class FXThreads {
 
-	private static final ExecutorService EXECUTOR = Executors.newCachedThreadPool();
 	private static final CallbackWhenTaskFailed DUMMY_EXCEPTION_CALLBACK = (e) -> {
 		// ignore.
 	};
 
-	private FXThreads() {
-		throw new AssertionError("Not instantiable.");
-	}
-
-	/**
-	 * Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
-	 * {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
-	 * 
-	 * <pre>
-	 * // examples:
-	 * 
-	 * Future&lt;String&gt; futureBookName1 = runOnBackgroundThread(restResource::getBookName);
-	 * 
-	 * Future&lt;String&gt; futureBookName2 = runOnBackgroundThread(() -&gt; {
-	 * 	return restResource.getBookName();
-	 * });
-	 * </pre>
-	 * 
-	 * @param task The task to be executed on a background thread.
-	 * @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
-	 */
-	public static <T> Future<T> runOnBackgroundThread(Callable<T> task) {
-		return EXECUTOR.submit(task);
-	}
-
-	/**
-	 * Executes the given task on a background thread. If you want to react on the result on your JavaFX main thread, use
-	 * {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
-	 * 
-	 * <pre>
-	 * // examples:
-	 * 
-	 * Future&lt;?&gt; futureDone1 = runOnBackgroundThread(this::doSomeComplexCalculation);
-	 * 
-	 * Future&lt;?&gt; futureDone2 = runOnBackgroundThread(() -&gt; {
-	 * 	doSomeComplexCalculation();
-	 * });
-	 * </pre>
-	 * 
-	 * @param task The task to be executed on a background thread.
-	 * @return A future result object, which you can use in {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished)}.
-	 */
-	public static Future<?> runOnBackgroundThread(Runnable task) {
-		return EXECUTOR.submit(task);
-	}
-
 	/**
 	 * Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
 	 * called. If you are interested in the exception, use
-	 * {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
+	 * {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
 	 * 
 	 * <pre>
 	 * // example:
@@ -111,21 +63,21 @@ public final class FXThreads {
 	 * 	myLabel.setText(bookName);
 	 * });
 	 * </pre>
-	 * 
+	 * @param executor
 	 * @param task The task to wait for.
 	 * @param successCallback The action to perform, when the task finished.
 	 */
-	public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
-		runOnBackgroundThread(() -> {
+	public static <T> void runOnMainThreadWhenFinished(ExecutorService executor, Future<T> task, CallbackWhenTaskFinished<T> successCallback) {
+		executor.submit(() -> {
 			return "asd";
 		});
-		FXThreads.runOnMainThreadWhenFinished(task, successCallback, DUMMY_EXCEPTION_CALLBACK);
+		runOnMainThreadWhenFinished(executor, task, successCallback, DUMMY_EXCEPTION_CALLBACK);
 	}
 
 	/**
 	 * Waits for the given task to complete and notifies the given successCallback. If an exception occurs, the callback will never be
 	 * called. If you are interested in the exception, use
-	 * {@link #runOnMainThreadWhenFinished(Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
+	 * {@link #runOnMainThreadWhenFinished(ExecutorService, Future, CallbackWhenTaskFinished, CallbackWhenTaskFailed)} instead.
 	 * 
 	 * <pre>
 	 * // example:
@@ -137,14 +89,17 @@ public final class FXThreads {
 	 * });
 	 * </pre>
 	 * 
+	 * @param executor
+	 *            The service to execute the background task on
 	 * @param task The task to wait for.
 	 * @param successCallback The action to perform, when the task finished.
+	 * @param exceptionCallback
 	 */
-	public static <T> void runOnMainThreadWhenFinished(Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
-		assertParamNotNull(task, "task must not be null.");
-		assertParamNotNull(successCallback, "successCallback must not be null.");
-		assertParamNotNull(exceptionCallback, "exceptionCallback must not be null.");
-		EXECUTOR.execute(() -> {
+	public static <T> void runOnMainThreadWhenFinished(ExecutorService executor, Future<T> task, CallbackWhenTaskFinished<T> successCallback, CallbackWhenTaskFailed exceptionCallback) {
+		Objects.requireNonNull(task, "task must not be null.");
+		Objects.requireNonNull(successCallback, "successCallback must not be null.");
+		Objects.requireNonNull(exceptionCallback, "exceptionCallback must not be null.");
+		executor.execute(() -> {
 			try {
 				final T result = task.get();
 				Platform.runLater(() -> {
@@ -158,12 +113,6 @@ public final class FXThreads {
 		});
 	}
 
-	private static void assertParamNotNull(Object param, String msg) {
-		if (param == null) {
-			throw new IllegalArgumentException(msg);
-		}
-	}
-
 	public interface CallbackWhenTaskFinished<T> {
 		void taskFinished(T result);
 	}

+ 2 - 2
main/ui/src/main/java/org/cryptomator/ui/util/SingleInstanceManager.java

@@ -1,10 +1,10 @@
 /*******************************************************************************
- * Copyright (c) 2014 Sebastian Stenzel
+ * Copyright (c) 2014 cryptomator.org
  * This file is licensed under the terms of the MIT license.
  * See the LICENSE.txt file for more info.
  * 
  * Contributors:
- *     Sebastian Stenzel - initial API and implementation
+ *     Tillmann Gaida - initial implementation
  ******************************************************************************/
 package org.cryptomator.ui.util;
 

+ 12 - 0
main/ui/src/test/java/org/cryptomator/ui/MainApplicationTest.java

@@ -0,0 +1,12 @@
+package org.cryptomator.ui;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class MainApplicationTest {
+	@Test
+	public void testInjection() throws Exception {
+		new MainApplication();
+	}
+}

+ 48 - 0
main/ui/src/test/java/org/cryptomator/ui/util/DeferredCloserTest.java

@@ -0,0 +1,48 @@
+package org.cryptomator.ui.util;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.*;
+
+import java.io.Closeable;
+
+import org.junit.Test;
+
+public class DeferredCloserTest {
+	@Test
+	public void testBasicFunctionality() throws Exception {
+		DeferredCloser closer = new DeferredCloser();
+
+		final Closeable obj = mock(Closeable.class);
+
+		final DeferredClosable<Closeable> resource = closer.closeLater(obj);
+
+		assertTrue(resource.get().isPresent());
+		assertTrue(resource.get().get() == obj);
+
+		closer.close();
+
+		assertFalse(resource.get().isPresent());
+		verify(obj).close();
+	}
+
+	@Test
+	public void testAutoremoval() throws Exception {
+		DeferredCloser closer = new DeferredCloser();
+
+		final DeferredClosable<Closeable> resource = closer.closeLater(mock(Closeable.class));
+		final DeferredClosable<Closeable> resource2 = closer.closeLater(mock(Closeable.class));
+
+		resource.close();
+
+		assertFalse(resource.get().isPresent());
+		assertEquals(1, closer.cleanups.size());
+
+		assertTrue(resource2.get().isPresent());
+
+		closer.close();
+
+		assertFalse(resource2.get().isPresent());
+
+		assertEquals(0, closer.cleanups.size());
+	}
+}