Bladeren bron

Merge pull request #32 from Tillerino/injection

Dependency injection instead of static instances
Sebastian Stenzel 10 jaren geleden
bovenliggende
commit
09c26f5e86

+ 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());
+	}
+}