Sfoglia il codice sorgente

replace GSON from settings

Sebastian Stenzel 1 anno fa
parent
commit
b1ff94bdd6

+ 144 - 49
src/main/java/org/cryptomator/common/settings/Settings.java

@@ -10,6 +10,8 @@ package org.cryptomator.common.settings;
 
 import org.apache.commons.lang3.SystemUtils;
 import org.cryptomator.common.Environment;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javafx.beans.Observable;
 import javafx.beans.property.BooleanProperty;
@@ -27,59 +29,85 @@ import java.util.function.Consumer;
 
 public class Settings {
 
-	public static final int MIN_PORT = 1024;
-	public static final int MAX_PORT = 65535;
-	public static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
-	public static final boolean DEFAULT_CHECK_FOR_UPDATES = false;
-	public static final boolean DEFAULT_START_HIDDEN = false;
-	public static final boolean DEFAULT_AUTO_CLOSE_VAULTS = false;
-	public static final boolean DEFAULT_USE_KEYCHAIN = true;
-	public static final int DEFAULT_PORT = 42427;
-	public static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
-	public static final boolean DEFAULT_DEBUG_MODE = false;
-	public static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
+	private static final Logger LOG = LoggerFactory.getLogger(Settings.class);
+
+	static final boolean DEFAULT_ASKED_FOR_UPDATE_CHECK = false;
+	static final boolean DEFAULT_CHECK_FOR_UPDATES = false;
+	static final boolean DEFAULT_START_HIDDEN = false;
+	static final boolean DEFAULT_AUTO_CLOSE_VAULTS = false;
+	static final boolean DEFAULT_USE_KEYCHAIN = true;
+	static final int DEFAULT_PORT = 42427;
+	static final int DEFAULT_NUM_TRAY_NOTIFICATIONS = 3;
+	static final boolean DEFAULT_DEBUG_MODE = false;
+	static final UiTheme DEFAULT_THEME = UiTheme.LIGHT;
 	@Deprecated // to be changed to "whatever is available" eventually
-	public static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
-	public static final NodeOrientation DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT;
-	public static final String DEFAULT_LICENSE_KEY = "";
-	public static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
-	public static final String DEFAULT_DISPLAY_CONFIGURATION = "";
-	public static final String DEFAULT_LANGUAGE = null;
-
-
-	private final ObservableList<VaultSettings> directories = FXCollections.observableArrayList(VaultSettings::observables);
-	private final BooleanProperty askedForUpdateCheck = new SimpleBooleanProperty(DEFAULT_ASKED_FOR_UPDATE_CHECK);
-	private final BooleanProperty checkForUpdates = new SimpleBooleanProperty(DEFAULT_CHECK_FOR_UPDATES);
-	private final BooleanProperty startHidden = new SimpleBooleanProperty(DEFAULT_START_HIDDEN);
-	private final BooleanProperty autoCloseVaults = new SimpleBooleanProperty(DEFAULT_AUTO_CLOSE_VAULTS);
-	private final BooleanProperty useKeychain = new SimpleBooleanProperty(DEFAULT_USE_KEYCHAIN);
-	private final IntegerProperty port = new SimpleIntegerProperty(DEFAULT_PORT);
-	private final IntegerProperty numTrayNotifications = new SimpleIntegerProperty(DEFAULT_NUM_TRAY_NOTIFICATIONS);
-	private final BooleanProperty debugMode = new SimpleBooleanProperty(DEFAULT_DEBUG_MODE);
-	private final ObjectProperty<UiTheme> theme = new SimpleObjectProperty<>(DEFAULT_THEME);
-	private final ObjectProperty<String> keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER);
-	private final ObjectProperty<NodeOrientation> userInterfaceOrientation = new SimpleObjectProperty<>(DEFAULT_USER_INTERFACE_ORIENTATION);
-	private final StringProperty licenseKey = new SimpleStringProperty(DEFAULT_LICENSE_KEY);
-	private final BooleanProperty showMinimizeButton = new SimpleBooleanProperty(DEFAULT_SHOW_MINIMIZE_BUTTON);
+	static final String DEFAULT_KEYCHAIN_PROVIDER = SystemUtils.IS_OS_WINDOWS ? "org.cryptomator.windows.keychain.WindowsProtectedKeychainAccess" : SystemUtils.IS_OS_MAC ? "org.cryptomator.macos.keychain.MacSystemKeychainAccess" : "org.cryptomator.linux.keychain.SecretServiceKeychainAccess";
+	static final String DEFAULT_USER_INTERFACE_ORIENTATION = NodeOrientation.LEFT_TO_RIGHT.name();
+	static final boolean DEFAULT_SHOW_MINIMIZE_BUTTON = false;
+
+	private final ObservableList<VaultSettings> directories;
+	private final BooleanProperty askedForUpdateCheck;
+	private final BooleanProperty checkForUpdates;
+	private final BooleanProperty startHidden;
+	private final BooleanProperty autoCloseVaults;
+	private final BooleanProperty useKeychain;
+	private final IntegerProperty port;
+	private final IntegerProperty numTrayNotifications;
+	private final BooleanProperty debugMode;
+	private final ObjectProperty<UiTheme> theme;
+	private final StringProperty keychainProvider;
+	private final ObjectProperty<NodeOrientation> userInterfaceOrientation;
+	private final StringProperty licenseKey;
+	private final BooleanProperty showMinimizeButton;
 	private final BooleanProperty showTrayIcon;
-	private final IntegerProperty windowXPosition = new SimpleIntegerProperty();
-	private final IntegerProperty windowYPosition = new SimpleIntegerProperty();
-	private final IntegerProperty windowWidth = new SimpleIntegerProperty();
-	private final IntegerProperty windowHeight = new SimpleIntegerProperty();
-	private final ObjectProperty<String> displayConfiguration = new SimpleObjectProperty<>(DEFAULT_DISPLAY_CONFIGURATION);
-	private final StringProperty language = new SimpleStringProperty(DEFAULT_LANGUAGE);
-
-
-	private final StringProperty mountService = new SimpleStringProperty();
-
+	private final IntegerProperty windowXPosition;
+	private final IntegerProperty windowYPosition;
+	private final IntegerProperty windowWidth;
+	private final IntegerProperty windowHeight;
+	private final StringProperty displayConfiguration;
+	private final StringProperty language;
+	private final StringProperty mountService;
 
 	private Consumer<Settings> saveCmd;
 
+	public static Settings create(Environment env) {
+		var defaults = new SettingsJson();
+		defaults.showTrayIcon = env.showTrayIcon();
+		return new Settings(defaults);
+	}
+
 	/**
-	 * Package-private constructor; use {@link SettingsProvider}.
+	 * Recreate settings from json
+	 *
+	 * @param json The parsed settings.json
 	 */
-	Settings(Environment env) {
-		this.showTrayIcon = new SimpleBooleanProperty(env.showTrayIcon());
+	Settings(SettingsJson json) {
+		this.directories = FXCollections.observableArrayList(VaultSettings::observables);
+		this.askedForUpdateCheck = new SimpleBooleanProperty(this, "askedForUpdateCheck", json.askedForUpdateCheck);
+		this.checkForUpdates = new SimpleBooleanProperty(this, "checkForUpdates", json.checkForUpdatesEnabled);
+		this.startHidden = new SimpleBooleanProperty(this, "startHidden", json.startHidden);
+		this.autoCloseVaults = new SimpleBooleanProperty(this, "autoCloseVaults", json.autoCloseVaults);
+		this.useKeychain = new SimpleBooleanProperty(this, "useKeychain", json.useKeychain);
+		this.port = new SimpleIntegerProperty(this, "webDavPort", json.port);
+		this.numTrayNotifications = new SimpleIntegerProperty(this, "numTrayNotifications", json.numTrayNotifications);
+		this.debugMode = new SimpleBooleanProperty(this, "debugMode", json.debugMode);
+		this.theme = new SimpleObjectProperty<>(this, "theme", json.theme);
+		this.keychainProvider = new SimpleStringProperty(this, "keychainProvider", json.keychainProvider);
+		this.userInterfaceOrientation = new SimpleObjectProperty<>(this, "userInterfaceOrientation", parseEnum(json.uiOrientation, NodeOrientation.class, NodeOrientation.LEFT_TO_RIGHT));
+		this.licenseKey = new SimpleStringProperty(this, "licenseKey", json.licenseKey);
+		this.showMinimizeButton = new SimpleBooleanProperty(this, "showMinimizeButton", json.showMinimizeButton);
+		this.showTrayIcon = new SimpleBooleanProperty(this, "showTrayIcon", json.showTrayIcon);
+		this.windowXPosition = new SimpleIntegerProperty(this, "windowXPosition", json.windowXPosition);
+		this.windowYPosition = new SimpleIntegerProperty(this, "windowYPosition", json.windowYPosition);
+		this.windowWidth = new SimpleIntegerProperty(this, "windowWidth", json.windowWidth);
+		this.windowHeight = new SimpleIntegerProperty(this, "windowHeight", json.windowHeight);
+		this.displayConfiguration = new SimpleStringProperty(this, "displayConfiguration", json.displayConfiguration);
+		this.language = new SimpleStringProperty(this, "language", json.language);
+		this.mountService = new SimpleStringProperty(this, "mountService", json.mountService);
+
+		this.directories.addAll(json.directories.stream().map(VaultSettings::new).toList());
+
+		migrateLegacySettings(json);
 
 		directories.addListener(this::somethingChanged);
 		askedForUpdateCheck.addListener(this::somethingChanged);
@@ -105,6 +133,72 @@ public class Settings {
 		mountService.addListener(this::somethingChanged);
 	}
 
+	@SuppressWarnings("deprecation")
+	private void migrateLegacySettings(SettingsJson json) {
+		// implicit migration of 1.6.x legacy setting "preferredVolumeImpl":
+		if (this.mountService.get() == null && json.preferredVolumeImpl != null) {
+			this.mountService.set(switch (json.preferredVolumeImpl) {
+				case "Dokany" -> "org.cryptomator.frontend.dokany.mount.DokanyMountProvider";
+				case "FUSE" -> {
+					if (SystemUtils.IS_OS_WINDOWS) {
+						yield "org.cryptomator.frontend.fuse.mount.WinFspNetworkMountProvider";
+					} else if (SystemUtils.IS_OS_MAC) {
+						yield "org.cryptomator.frontend.fuse.mount.MacFuseMountProvider";
+					} else {
+						yield "org.cryptomator.frontend.fuse.mount.LinuxFuseMountProvider";
+					}
+				}
+				default -> {
+					if (SystemUtils.IS_OS_WINDOWS) {
+						yield "org.cryptomator.frontend.webdav.mount.WindowsMounter";
+					} else if (SystemUtils.IS_OS_MAC) {
+						yield "org.cryptomator.frontend.webdav.mount.MacAppleScriptMounter";
+					} else {
+						yield "org.cryptomator.frontend.webdav.mount.LinuxGioMounter";
+					}
+				}
+			});
+		}
+	}
+
+	public SettingsJson serialized() {
+		var json = new SettingsJson();
+		json.directories = directories.stream().map(VaultSettings::serialized).toList();
+		json.askedForUpdateCheck = askedForUpdateCheck.get();
+		json.checkForUpdatesEnabled = checkForUpdates.get();
+		json.startHidden = startHidden.get();
+		json.autoCloseVaults = autoCloseVaults.get();
+		json.useKeychain = useKeychain.get();
+		json.port = port.get();
+		json.numTrayNotifications = numTrayNotifications.get();
+		json.debugMode = debugMode.get();
+		json.theme = theme.get();
+		json.keychainProvider = keychainProvider.get();
+		json.uiOrientation = userInterfaceOrientation.get().name();
+		json.licenseKey = licenseKey.get();
+		json.showMinimizeButton = showMinimizeButton.get();
+		json.showTrayIcon = showTrayIcon.get();
+		json.windowXPosition = windowXPosition.get();
+		json.windowYPosition = windowYPosition.get();
+		json.windowWidth = windowWidth.get();
+		json.windowHeight = windowHeight.get();
+		json.displayConfiguration = displayConfiguration.get();
+		json.language = language.get();
+		json.mountService = mountService.get();
+		return json;
+	}
+
+	private <E extends Enum<E>> E parseEnum(String value, Class<E> clazz, E defaultValue) {
+		try {
+			return Enum.valueOf(clazz, value.toUpperCase());
+		} catch (IllegalArgumentException e) {
+			LOG.warn("No value {}.{}. Defaulting to {}.", clazz.getSimpleName(), value, defaultValue);
+			return defaultValue;
+		}
+	}
+
+
+	// TODO rename to setChangeListener
 	void setSaveCmd(Consumer<Settings> saveCmd) {
 		this.saveCmd = saveCmd;
 	}
@@ -120,6 +214,7 @@ public class Settings {
 	}
 
 	/* Getter/Setter */
+	// TODO: remove accessors, make fields public
 
 	public ObservableList<VaultSettings> getDirectories() {
 		return directories;
@@ -141,7 +236,7 @@ public class Settings {
 		return autoCloseVaults;
 	}
 
-	public BooleanProperty useKeychain() { return useKeychain; }
+	public BooleanProperty useKeychain() {return useKeychain;}
 
 	public IntegerProperty port() {
 		return port;
@@ -163,7 +258,7 @@ public class Settings {
 		return theme;
 	}
 
-	public ObjectProperty<String> keychainProvider() {return keychainProvider;}
+	public StringProperty keychainProvider() {return keychainProvider;}
 
 	public ObjectProperty<NodeOrientation> userInterfaceOrientation() {
 		return userInterfaceOrientation;
@@ -197,7 +292,7 @@ public class Settings {
 		return windowHeight;
 	}
 
-	public ObjectProperty<String> displayConfigurationProperty() {
+	public StringProperty displayConfigurationProperty() {
 		return displayConfiguration;
 	}
 

+ 86 - 0
src/main/java/org/cryptomator/common/settings/SettingsJson.java

@@ -0,0 +1,86 @@
+package org.cryptomator.common.settings;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class SettingsJson {
+
+	@JsonProperty("directories")
+	List<VaultSettingsJson> directories = List.of();
+
+	@JsonProperty("writtenByVersion")
+	String writtenByVersion;
+
+	@JsonProperty("askedForUpdateCheck")
+	boolean askedForUpdateCheck = Settings.DEFAULT_ASKED_FOR_UPDATE_CHECK;
+
+	@JsonProperty("autoCloseVaults")
+	boolean autoCloseVaults = Settings.DEFAULT_AUTO_CLOSE_VAULTS;
+
+	@JsonProperty("checkForUpdatesEnabled")
+	boolean checkForUpdatesEnabled = Settings.DEFAULT_CHECK_FOR_UPDATES;
+
+	@JsonProperty("debugMode")
+	boolean debugMode = Settings.DEFAULT_DEBUG_MODE;
+
+	@JsonProperty("theme")
+	UiTheme theme = Settings.DEFAULT_THEME;
+
+	@JsonProperty("displayConfiguration")
+	String displayConfiguration;
+
+	@JsonProperty("keychainProvider")
+	String keychainProvider = Settings.DEFAULT_KEYCHAIN_PROVIDER;
+
+	@JsonProperty("language")
+	String language;
+
+	@JsonProperty("licenseKey")
+	String licenseKey;
+
+	@JsonProperty("mountService")
+	String mountService;
+
+	@JsonProperty("numTrayNotifications")
+	int numTrayNotifications = Settings.DEFAULT_NUM_TRAY_NOTIFICATIONS;
+
+	@JsonProperty("port")
+	int port = Settings.DEFAULT_PORT;
+
+	@JsonProperty("showMinimizeButton")
+	boolean showMinimizeButton = Settings.DEFAULT_SHOW_MINIMIZE_BUTTON;
+
+	@JsonProperty("showTrayIcon")
+	boolean showTrayIcon;
+
+	@JsonProperty("startHidden")
+	boolean startHidden = Settings.DEFAULT_START_HIDDEN;
+
+	@JsonProperty("uiOrientation")
+	String uiOrientation = Settings.DEFAULT_USER_INTERFACE_ORIENTATION;
+
+	@JsonProperty("useKeychain")
+	boolean useKeychain = Settings.DEFAULT_USE_KEYCHAIN;
+
+	@JsonProperty("windowHeight")
+	int windowHeight;
+
+	@JsonProperty("windowWidth")
+	int windowWidth;
+
+	@JsonProperty("windowXPosition")
+	int windowXPosition;
+
+	@JsonProperty("windowYPosition")
+	int windowYPosition;
+
+	@Deprecated(since = "1.7.0")
+	@JsonProperty(value = "preferredVolumeImpl", access = JsonProperty.Access.WRITE_ONLY) // WRITE_ONLY means value is "written" into the java object during deserialization. Upvote this: https://github.com/FasterXML/jackson-annotations/issues/233
+	String preferredVolumeImpl;
+
+}

+ 0 - 183
src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java

@@ -1,183 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.common.settings;
-
-import com.google.gson.TypeAdapter;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonToken;
-import com.google.gson.stream.JsonWriter;
-import org.apache.commons.lang3.SystemUtils;
-import org.cryptomator.common.Environment;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.inject.Inject;
-import javax.inject.Singleton;
-import javafx.geometry.NodeOrientation;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-@Singleton
-public class SettingsJsonAdapter extends TypeAdapter<Settings> {
-
-	private static final Logger LOG = LoggerFactory.getLogger(SettingsJsonAdapter.class);
-
-	private final VaultSettingsJsonAdapter vaultSettingsJsonAdapter = new VaultSettingsJsonAdapter();
-	private final Environment env;
-
-	@Inject
-	public SettingsJsonAdapter(Environment env) {
-		this.env = env;
-	}
-
-	@Override
-	public void write(JsonWriter out, Settings value) throws IOException {
-		out.beginObject();
-		out.name("writtenByVersion").value(env.getAppVersion() + env.getBuildNumber().map("-"::concat).orElse(""));
-		out.name("directories");
-		writeVaultSettingsArray(out, value.getDirectories());
-		out.name("askedForUpdateCheck").value(value.askedForUpdateCheck().get());
-		out.name("autoCloseVaults").value(value.autoCloseVaults().get());
-		out.name("checkForUpdatesEnabled").value(value.checkForUpdates().get());
-		out.name("debugMode").value(value.debugMode().get());
-		out.name("displayConfiguration").value((value.displayConfigurationProperty().get()));
-		out.name("keychainProvider").value(value.keychainProvider().get());
-		out.name("language").value((value.languageProperty().get()));
-		out.name("licenseKey").value(value.licenseKey().get());
-		out.name("mountService").value(value.mountService().get());
-		out.name("numTrayNotifications").value(value.numTrayNotifications().get());
-		out.name("port").value(value.port().get());
-		out.name("showMinimizeButton").value(value.showMinimizeButton().get());
-		out.name("showTrayIcon").value(value.showTrayIcon().get());
-		out.name("startHidden").value(value.startHidden().get());
-		out.name("theme").value(value.theme().get().name());
-		out.name("uiOrientation").value(value.userInterfaceOrientation().get().name());
-		out.name("useKeychain").value(value.useKeychain().get());
-		out.name("windowHeight").value((value.windowHeightProperty().get()));
-		out.name("windowWidth").value((value.windowWidthProperty().get()));
-		out.name("windowXPosition").value((value.windowXPositionProperty().get()));
-		out.name("windowYPosition").value((value.windowYPositionProperty().get()));
-		out.endObject();
-	}
-
-	private void writeVaultSettingsArray(JsonWriter out, Iterable<VaultSettings> vaultSettings) throws IOException {
-		out.beginArray();
-		for (VaultSettings value : vaultSettings) {
-			vaultSettingsJsonAdapter.write(out, value);
-		}
-		out.endArray();
-	}
-
-	@Override
-	public Settings read(JsonReader in) throws IOException {
-		Settings settings = new Settings(env);
-		//1.6.x legacy
-		String volumeImpl = null;
-		//legacy end
-		in.beginObject();
-		while (in.hasNext()) {
-			String name = in.nextName();
-			switch (name) {
-				case "writtenByVersion" -> in.skipValue(); //noop
-				case "directories" -> settings.getDirectories().addAll(readVaultSettingsArray(in));
-				case "askedForUpdateCheck" -> settings.askedForUpdateCheck().set(in.nextBoolean());
-				case "autoCloseVaults" -> settings.autoCloseVaults().set(in.nextBoolean());
-				case "checkForUpdatesEnabled" -> settings.checkForUpdates().set(in.nextBoolean());
-				case "debugMode" -> settings.debugMode().set(in.nextBoolean());
-				case "displayConfiguration" -> settings.displayConfigurationProperty().set(in.nextString());
-				case "keychainProvider" -> settings.keychainProvider().set(in.nextString());
-				case "language" -> settings.languageProperty().set(in.nextString());
-				case "licenseKey" -> settings.licenseKey().set(in.nextString());
-				case "mountService" -> {
-					var token = in.peek();
-					if (JsonToken.STRING == token) {
-						settings.mountService().set(in.nextString());
-					}
-				}
-				case "numTrayNotifications" -> settings.numTrayNotifications().set(in.nextInt());
-				case "port" -> settings.port().set(in.nextInt());
-				case "showMinimizeButton" -> settings.showMinimizeButton().set(in.nextBoolean());
-				case "showTrayIcon" -> settings.showTrayIcon().set(in.nextBoolean());
-				case "startHidden" -> settings.startHidden().set(in.nextBoolean());
-				case "theme" -> settings.theme().set(parseUiTheme(in.nextString()));
-				case "uiOrientation" -> settings.userInterfaceOrientation().set(parseUiOrientation(in.nextString()));
-				case "useKeychain" -> settings.useKeychain().set(in.nextBoolean());
-				case "windowHeight" -> settings.windowHeightProperty().set(in.nextInt());
-				case "windowWidth" -> settings.windowWidthProperty().set(in.nextInt());
-				case "windowXPosition" -> settings.windowXPositionProperty().set(in.nextInt());
-				case "windowYPosition" -> settings.windowYPositionProperty().set(in.nextInt());
-				//1.6.x legacy
-				case "preferredVolumeImpl" -> volumeImpl = in.nextString();
-				//legacy end
-				default -> {
-					LOG.warn("Unsupported vault setting found in JSON: {}", name);
-					in.skipValue();
-				}
-			}
-
-		}
-		in.endObject();
-
-		//1.6.x legacy
-		if (volumeImpl != null) {
-			settings.mountService().set(convertLegacyVolumeImplToMountService(volumeImpl));
-		}
-		//legacy end
-
-		return settings;
-	}
-
-	private String convertLegacyVolumeImplToMountService(String volumeImpl) {
-		if (volumeImpl.equals("Dokany")) {
-			return "org.cryptomator.frontend.dokany.mount.DokanyMountProvider";
-		} else if (volumeImpl.equals("FUSE")) {
-			if (SystemUtils.IS_OS_WINDOWS) {
-				return "org.cryptomator.frontend.fuse.mount.WinFspNetworkMountProvider";
-			} else if (SystemUtils.IS_OS_MAC) {
-				return "org.cryptomator.frontend.fuse.mount.MacFuseMountProvider";
-			} else {
-				return "org.cryptomator.frontend.fuse.mount.LinuxFuseMountProvider";
-			}
-		} else {
-			if (SystemUtils.IS_OS_WINDOWS) {
-				return "org.cryptomator.frontend.webdav.mount.WindowsMounter";
-			} else if (SystemUtils.IS_OS_MAC) {
-				return "org.cryptomator.frontend.webdav.mount.MacAppleScriptMounter";
-			} else {
-				return "org.cryptomator.frontend.webdav.mount.LinuxGioMounter";
-			}
-		}
-	}
-
-	private UiTheme parseUiTheme(String uiThemeName) {
-		try {
-			return UiTheme.valueOf(uiThemeName.toUpperCase());
-		} catch (IllegalArgumentException e) {
-			LOG.warn("Invalid ui theme {}. Defaulting to {}.", uiThemeName, Settings.DEFAULT_THEME);
-			return Settings.DEFAULT_THEME;
-		}
-	}
-
-	private NodeOrientation parseUiOrientation(String uiOrientationName) {
-		try {
-			return NodeOrientation.valueOf(uiOrientationName.toUpperCase());
-		} catch (IllegalArgumentException e) {
-			LOG.warn("Invalid ui orientation {}. Defaulting to {}.", uiOrientationName, Settings.DEFAULT_USER_INTERFACE_ORIENTATION);
-			return Settings.DEFAULT_USER_INTERFACE_ORIENTATION;
-		}
-	}
-
-	private List<VaultSettings> readVaultSettingsArray(JsonReader in) throws IOException {
-		List<VaultSettings> result = new ArrayList<>();
-		in.beginArray();
-		while (!JsonToken.END_ARRAY.equals(in.peek())) {
-			result.add(vaultSettingsJsonAdapter.read(in));
-		}
-		in.endArray();
-		return result;
-	}
-}

+ 24 - 32
src/main/java/org/cryptomator/common/settings/SettingsProvider.java

@@ -8,12 +8,10 @@
  *******************************************************************************/
 package org.cryptomator.common.settings;
 
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.google.common.base.Suppliers;
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.JsonElement;
 import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
 import org.cryptomator.common.Environment;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -22,12 +20,7 @@ import javax.inject.Inject;
 import javax.inject.Singleton;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.Reader;
-import java.io.Writer;
-import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
 import java.nio.file.Path;
@@ -44,6 +37,7 @@ import java.util.stream.Stream;
 @Singleton
 public class SettingsProvider implements Supplier<Settings> {
 
+	private static final ObjectMapper JSON = new ObjectMapper().setDefaultLeniency(true);
 	private static final Logger LOG = LoggerFactory.getLogger(SettingsProvider.class);
 	private static final long SAVE_DELAY_MS = 1000;
 
@@ -51,16 +45,16 @@ public class SettingsProvider implements Supplier<Settings> {
 	private final Supplier<Settings> settings = Suppliers.memoize(this::load);
 	private final Environment env;
 	private final ScheduledExecutorService scheduler;
-	private final Gson gson;
+//	private final Gson gson;
 
 	@Inject
-	public SettingsProvider(SettingsJsonAdapter settingsJsonAdapter, Environment env, ScheduledExecutorService scheduler) {
+	public SettingsProvider(Environment env, ScheduledExecutorService scheduler) {
 		this.env = env;
 		this.scheduler = scheduler;
-		this.gson = new GsonBuilder() //
-				.setPrettyPrinting().setLenient().disableHtmlEscaping() //
-				.registerTypeAdapter(Settings.class, settingsJsonAdapter) //
-				.create();
+//		this.gson = new GsonBuilder() //
+//				.setPrettyPrinting().setLenient().disableHtmlEscaping() //
+//				.registerTypeAdapter(Settings.class, settingsJsonAdapter) //
+//				.create();
 	}
 
 	@Override
@@ -69,28 +63,25 @@ public class SettingsProvider implements Supplier<Settings> {
 	}
 
 	private Settings load() {
-		Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElse(new Settings(env));
+		Settings settings = env.getSettingsPath().flatMap(this::tryLoad).findFirst().orElseGet(() -> Settings.create(env));
 		settings.setSaveCmd(this::scheduleSave);
 		return settings;
 	}
 
 	private Stream<Settings> tryLoad(Path path) {
 		LOG.debug("Attempting to load settings from {}", path);
-		try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ); //
-			 Reader reader = new InputStreamReader(in, StandardCharsets.UTF_8)) {
-			JsonElement json = JsonParser.parseReader(reader);
-			if (json.isJsonObject()) {
-				Settings settings = gson.fromJson(json, Settings.class);
-				LOG.info("Settings loaded from {}", path);
-				return Stream.of(settings);
-			} else {
-				LOG.warn("Invalid json file {}", path);
-				return Stream.empty();
-			}
+		try (InputStream in = Files.newInputStream(path, StandardOpenOption.READ)) {
+			var json = JSON.reader().readValue(in, SettingsJson.class);
+			LOG.info("Settings loaded from {}", path);
+			var settings = new Settings(json);
+			return Stream.of(settings);
+		} catch (JacksonException e) {
+			LOG.warn("Failed to parse json file {}", path, e);
+			return Stream.empty();
 		} catch (NoSuchFileException e) {
 			return Stream.empty();
-		} catch (IOException | JsonParseException e) {
-			LOG.warn("Exception while loading settings from " + path, e);
+		} catch (IOException e) {
+			LOG.warn("Failed to load json file {}", path, e);
 			return Stream.empty();
 		}
 	}
@@ -116,9 +107,10 @@ public class SettingsProvider implements Supplier<Settings> {
 		try {
 			Files.createDirectories(settingsPath.getParent());
 			Path tmpPath = settingsPath.resolveSibling(settingsPath.getFileName().toString() + ".tmp");
-			try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE); //
-				 Writer writer = new OutputStreamWriter(out, StandardCharsets.UTF_8)) {
-				gson.toJson(settings, writer);
+			try (OutputStream out = Files.newOutputStream(tmpPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE)) {
+				var jsonObj = settings.serialized();
+				jsonObj.writtenByVersion = env.getAppVersion() + env.getBuildNumber().map("-"::concat).orElse("");
+				JSON.writerWithDefaultPrettyPrinter().writeValue(out, jsonObj);
 			}
 			Files.move(tmpPath, settingsPath, StandardCopyOption.REPLACE_EXISTING);
 			LOG.info("Settings saved to {}", settingsPath);

+ 5 - 1
src/main/java/org/cryptomator/common/settings/UiTheme.java

@@ -1,9 +1,13 @@
 package org.cryptomator.common.settings;
 
+import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonProperty;
 import org.apache.commons.lang3.SystemUtils;
 
+@JsonFormat(shape = JsonFormat.Shape.STRING)
 public enum UiTheme {
-	LIGHT("preferences.interface.theme.light"), //
+	@JsonEnumDefaultValue @JsonProperty LIGHT("preferences.interface.theme.light"), //
 	DARK("preferences.interface.theme.dark"), //
 	AUTOMATIC("preferences.interface.theme.automatic");
 

+ 69 - 22
src/main/java/org/cryptomator/common/settings/VaultSettings.java

@@ -6,7 +6,9 @@
 package org.cryptomator.common.settings;
 
 import com.google.common.base.CharMatcher;
+import com.google.common.base.Strings;
 import com.google.common.io.BaseEncoding;
+import org.apache.commons.lang3.SystemUtils;
 
 import javafx.beans.Observable;
 import javafx.beans.binding.Bindings;
@@ -20,6 +22,7 @@ import javafx.beans.property.SimpleObjectProperty;
 import javafx.beans.property.SimpleStringProperty;
 import javafx.beans.property.StringProperty;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.Objects;
 import java.util.Random;
 
@@ -28,33 +31,45 @@ import java.util.Random;
  */
 public class VaultSettings {
 
-	public static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
-	public static final boolean DEFAULT_REVEAL_AFTER_MOUNT = true;
-	public static final boolean DEFAULT_USES_READONLY_MODE = false;
-	public static final String DEFAULT_MOUNT_FLAGS = "";
-	public static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
-	public static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
-	public static final boolean DEFAULT_AUTOLOCK_WHEN_IDLE = false;
-	public static final int DEFAULT_AUTOLOCK_IDLE_SECONDS = 30 * 60;
+	static final boolean DEFAULT_UNLOCK_AFTER_STARTUP = false;
+	static final boolean DEFAULT_REVEAL_AFTER_MOUNT = true;
+	static final boolean DEFAULT_USES_READONLY_MODE = false;
+	static final String DEFAULT_MOUNT_FLAGS = ""; // TODO: remove empty default mount flags and let this property be null if not used
+	static final int DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH = -1;
+	static final WhenUnlocked DEFAULT_ACTION_AFTER_UNLOCK = WhenUnlocked.ASK;
+	static final boolean DEFAULT_AUTOLOCK_WHEN_IDLE = false;
+	static final int DEFAULT_AUTOLOCK_IDLE_SECONDS = 30 * 60;
 
 	private static final Random RNG = new Random();
 
 	private final String id;
-	private final ObjectProperty<Path> path = new SimpleObjectProperty<>();
-	private final StringProperty displayName = new SimpleStringProperty();
-	private final BooleanProperty unlockAfterStartup = new SimpleBooleanProperty(DEFAULT_UNLOCK_AFTER_STARTUP);
-	private final BooleanProperty revealAfterMount = new SimpleBooleanProperty(DEFAULT_REVEAL_AFTER_MOUNT);
-	private final BooleanProperty usesReadOnlyMode = new SimpleBooleanProperty(DEFAULT_USES_READONLY_MODE);
-	private final StringProperty mountFlags = new SimpleStringProperty(DEFAULT_MOUNT_FLAGS); //TODO: remove empty default mount flags and let this property be null if not used
-	private final IntegerProperty maxCleartextFilenameLength = new SimpleIntegerProperty(DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH);
-	private final ObjectProperty<WhenUnlocked> actionAfterUnlock = new SimpleObjectProperty<>(DEFAULT_ACTION_AFTER_UNLOCK);
-	private final BooleanProperty autoLockWhenIdle = new SimpleBooleanProperty(DEFAULT_AUTOLOCK_WHEN_IDLE);
-	private final IntegerProperty autoLockIdleSeconds = new SimpleIntegerProperty(DEFAULT_AUTOLOCK_IDLE_SECONDS);
+	private final ObjectProperty<Path> path;
+	private final StringProperty displayName;
+	private final BooleanProperty unlockAfterStartup;
+	private final BooleanProperty revealAfterMount;
+	private final BooleanProperty usesReadOnlyMode;
+	private final StringProperty mountFlags;
+	private final IntegerProperty maxCleartextFilenameLength;
+	private final ObjectProperty<WhenUnlocked> actionAfterUnlock;
+	private final BooleanProperty autoLockWhenIdle;
+	private final IntegerProperty autoLockIdleSeconds;
+	private final ObjectProperty<Path> mountPoint;
 	private final StringExpression mountName;
-	private final ObjectProperty<Path> mountPoint = new SimpleObjectProperty<>();
 
-	public VaultSettings(String id) {
-		this.id = Objects.requireNonNull(id);
+	VaultSettings(VaultSettingsJson json) {
+		this.id = json.id;
+		this.path = new SimpleObjectProperty<>(this, "path", json.path == null ? null : Paths.get(json.path));
+		this.displayName = new SimpleStringProperty(this, "displayName", json.displayName);
+		this.unlockAfterStartup = new SimpleBooleanProperty(this, "unlockAfterStartup", json.unlockAfterStartup);
+		this.revealAfterMount = new SimpleBooleanProperty(this, "revealAfterMount", json.revealAfterMount);
+		this.usesReadOnlyMode = new SimpleBooleanProperty(this, "usesReadOnlyMode", json.usesReadOnlyMode);
+		this.mountFlags = new SimpleStringProperty(this, "mountFlags", json.mountFlags);
+		this.maxCleartextFilenameLength = new SimpleIntegerProperty(this, "maxCleartextFilenameLength", json.maxCleartextFilenameLength);
+		this.actionAfterUnlock = new SimpleObjectProperty<>(this, "actionAfterUnlock", json.actionAfterUnlock);
+		this.autoLockWhenIdle = new SimpleBooleanProperty(this, "autoLockWhenIdle", json.autoLockWhenIdle);
+		this.autoLockIdleSeconds = new SimpleIntegerProperty(this, "autoLockIdleSeconds", json.autoLockIdleSeconds);
+		this.mountPoint = new SimpleObjectProperty<>(this, "mountPoint", json.mountPoint == null ? null : Path.of(json.mountPoint));
+		// mount name is no longer an explicit setting, see https://github.com/cryptomator/cryptomator/pull/1318
 		this.mountName = StringExpression.stringExpression(Bindings.createStringBinding(() -> {
 			final String name;
 			if (displayName.isEmpty().get()) {
@@ -64,6 +79,18 @@ public class VaultSettings {
 			}
 			return normalizeDisplayName(name);
 		}, displayName, path));
+
+		migrateLegacySettings(json);
+	}
+
+	@SuppressWarnings("deprecation")
+	private void migrateLegacySettings(VaultSettingsJson json) {
+		// implicit migration of 1.6.x legacy setting "customMountPath" / "winDriveLetter":
+		if (json.useCustomMountPath && !Strings.isNullOrEmpty(json.customMountPath)) {
+			this.mountPoint.set(Path.of(json.customMountPath));
+		} else if (!Strings.isNullOrEmpty(json.winDriveLetter)) {
+			this.mountPoint.set(Path.of(json.winDriveLetter + ":\\"));
+		}
 	}
 
 	Observable[] observables() {
@@ -71,7 +98,9 @@ public class VaultSettings {
 	}
 
 	public static VaultSettings withRandomId() {
-		return new VaultSettings(generateId());
+		var defaults = new VaultSettingsJson();
+		defaults.id = generateId();
+		return new VaultSettings(defaults);
 	}
 
 	private static String generateId() {
@@ -80,6 +109,23 @@ public class VaultSettings {
 		return BaseEncoding.base64Url().encode(randomBytes);
 	}
 
+	public VaultSettingsJson serialized() {
+		var json = new VaultSettingsJson();
+		json.id = id;
+		json.path = path.map(Path::toString).getValue();
+		json.displayName = displayName.get();
+		json.unlockAfterStartup = unlockAfterStartup.get();
+		json.revealAfterMount = revealAfterMount.get();
+		json.usesReadOnlyMode = usesReadOnlyMode.get();
+		json.mountFlags = mountFlags.get();
+		json.maxCleartextFilenameLength = maxCleartextFilenameLength.get();
+		json.actionAfterUnlock = actionAfterUnlock.get();
+		json.autoLockWhenIdle = autoLockWhenIdle.get();
+		json.autoLockIdleSeconds = autoLockIdleSeconds.get();
+		json.mountPoint = mountPoint.map(Path::toString).getValue();
+		return json;
+	}
+
 	//visible for testing
 	static String normalizeDisplayName(String original) {
 		if (original.isBlank() || ".".equals(original) || "..".equals(original)) {
@@ -94,6 +140,7 @@ public class VaultSettings {
 	}
 
 	/* Getter/Setter */
+	// TODO: remove accessors, make fields public
 
 	public String getId() {
 		return id;

+ 62 - 0
src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java

@@ -0,0 +1,62 @@
+package org.cryptomator.common.settings;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+class VaultSettingsJson {
+
+	@JsonProperty(value = "id", required = true)
+	String id;
+
+	@JsonProperty(value = "path")
+	String path;
+
+	@JsonProperty("displayName")
+	String displayName;
+
+	@JsonProperty("unlockAfterStartup")
+	boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
+
+	@JsonProperty("revealAfterMount")
+	boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT;
+
+	@JsonProperty("mountPoint")
+	String mountPoint;
+
+	@JsonProperty("usesReadOnlyMode")
+	boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
+
+	@JsonProperty("mountFlags")
+	String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
+
+	@JsonProperty("maxCleartextFilenameLength")
+	int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
+
+	@JsonProperty("actionAfterUnlock")
+	WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
+
+	@JsonProperty("autoLockWhenIdle")
+	boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
+
+	@JsonProperty("autoLockIdleSeconds")
+	int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS;
+
+	@Deprecated(since = "1.7.0")
+	@JsonProperty(value = "winDriveLetter", access = JsonProperty.Access.WRITE_ONLY) // WRITE_ONLY means value is "written" into the java object during deserialization. Upvote this: https://github.com/FasterXML/jackson-annotations/issues/233
+	String winDriveLetter;
+
+	@Deprecated(since = "1.7.0")
+	@JsonProperty(value = "useCustomMountPath", access = JsonProperty.Access.WRITE_ONLY) // WRITE_ONLY means value is "written" into the java object during deserialization. Upvote this: https://github.com/FasterXML/jackson-annotations/issues/233
+	@JsonAlias("usesIndividualMountPath")
+	boolean useCustomMountPath;
+
+	@Deprecated(since = "1.7.0")
+	@JsonProperty(value = "customMountPath", access = JsonProperty.Access.WRITE_ONLY) // WRITE_ONLY means value is "written" into the java object during deserialization. Upvote this: https://github.com/FasterXML/jackson-annotations/issues/233
+	@JsonAlias("individualMountPath")
+	String customMountPath;
+
+}

+ 0 - 142
src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java

@@ -1,142 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.common.settings;
-
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonToken;
-import com.google.gson.stream.JsonWriter;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.nio.file.InvalidPathException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-
-class VaultSettingsJsonAdapter {
-
-	private static final Logger LOG = LoggerFactory.getLogger(VaultSettingsJsonAdapter.class);
-
-	public void write(JsonWriter out, VaultSettings value) throws IOException {
-		out.beginObject();
-		out.name("id").value(value.getId());
-		out.name("path").value(value.path().get().toString());
-		out.name("displayName").value(value.displayName().get());
-		out.name("unlockAfterStartup").value(value.unlockAfterStartup().get());
-		out.name("revealAfterMount").value(value.revealAfterMount().get());
-		var mountPoint = value.mountPoint().get();
-		out.name("mountPoint").value(mountPoint != null ? mountPoint.toAbsolutePath().toString() : null);
-		out.name("usesReadOnlyMode").value(value.usesReadOnlyMode().get());
-		out.name("mountFlags").value(value.mountFlags().get());
-		out.name("maxCleartextFilenameLength").value(value.maxCleartextFilenameLength().get());
-		out.name("actionAfterUnlock").value(value.actionAfterUnlock().get().name());
-		out.name("autoLockWhenIdle").value(value.autoLockWhenIdle().get());
-		out.name("autoLockIdleSeconds").value(value.autoLockIdleSeconds().get());
-		out.endObject();
-	}
-
-	public VaultSettings read(JsonReader in) throws IOException {
-		String id = null;
-		String path = null;
-		String mountName = null; //see https://github.com/cryptomator/cryptomator/pull/1318
-		String displayName = null;
-		boolean unlockAfterStartup = VaultSettings.DEFAULT_UNLOCK_AFTER_STARTUP;
-		boolean revealAfterMount = VaultSettings.DEFAULT_REVEAL_AFTER_MOUNT;
-		boolean usesReadOnlyMode = VaultSettings.DEFAULT_USES_READONLY_MODE;
-		String mountFlags = VaultSettings.DEFAULT_MOUNT_FLAGS;
-		Path mountPoint = null;
-		int maxCleartextFilenameLength = VaultSettings.DEFAULT_MAX_CLEARTEXT_FILENAME_LENGTH;
-		WhenUnlocked actionAfterUnlock = VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
-		boolean autoLockWhenIdle = VaultSettings.DEFAULT_AUTOLOCK_WHEN_IDLE;
-		int autoLockIdleSeconds = VaultSettings.DEFAULT_AUTOLOCK_IDLE_SECONDS;
-
-		//legacy from 1.6.x
-		boolean useCustomMountPath = false;
-		String customMountPath = "";
-		String winDriveLetter = "";
-		//legacy end
-
-		in.beginObject();
-		while (in.hasNext()) {
-			String name = in.nextName();
-			switch (name) {
-				case "id" -> id = in.nextString();
-				case "path" -> path = in.nextString();
-				case "mountName" -> mountName = in.nextString(); //see https://github.com/cryptomator/cryptomator/pull/1318
-				case "displayName" -> displayName = in.nextString();
-				case "unlockAfterStartup" -> unlockAfterStartup = in.nextBoolean();
-				case "revealAfterMount" -> revealAfterMount = in.nextBoolean();
-				case "usesReadOnlyMode" -> usesReadOnlyMode = in.nextBoolean();
-				case "mountFlags" -> mountFlags = in.nextString();
-				case "mountPoint" -> {
-					if (JsonToken.NULL == in.peek()) {
-						in.nextNull();
-					} else {
-						mountPoint = parseMountPoint(in.nextString());
-					}
-				}
-				case "maxCleartextFilenameLength" -> maxCleartextFilenameLength = in.nextInt();
-				case "actionAfterUnlock" -> actionAfterUnlock = parseActionAfterUnlock(in.nextString());
-				case "autoLockWhenIdle" -> autoLockWhenIdle = in.nextBoolean();
-				case "autoLockIdleSeconds" -> autoLockIdleSeconds = in.nextInt();
-				//legacy from 1.6.x
-				case "winDriveLetter" -> winDriveLetter = in.nextString();
-				case "usesIndividualMountPath", "useCustomMountPath" -> useCustomMountPath = in.nextBoolean();
-				case "individualMountPath", "customMountPath" -> customMountPath = in.nextString();
-				//legacy end
-				default -> {
-					LOG.warn("Unsupported vault setting found in JSON: {}", name);
-					in.skipValue();
-				}
-			}
-		}
-		in.endObject();
-
-		VaultSettings vaultSettings = (id == null) ? VaultSettings.withRandomId() : new VaultSettings(id);
-		if (displayName != null) { //see https://github.com/cryptomator/cryptomator/pull/1318
-			vaultSettings.displayName().set(displayName);
-		} else {
-			vaultSettings.displayName().set(mountName);
-		}
-		vaultSettings.path().set(Paths.get(path));
-		vaultSettings.unlockAfterStartup().set(unlockAfterStartup);
-		vaultSettings.revealAfterMount().set(revealAfterMount);
-		vaultSettings.usesReadOnlyMode().set(usesReadOnlyMode);
-		vaultSettings.mountFlags().set(mountFlags);
-		vaultSettings.maxCleartextFilenameLength().set(maxCleartextFilenameLength);
-		vaultSettings.actionAfterUnlock().set(actionAfterUnlock);
-		vaultSettings.autoLockWhenIdle().set(autoLockWhenIdle);
-		vaultSettings.autoLockIdleSeconds().set(autoLockIdleSeconds);
-		vaultSettings.mountPoint().set(mountPoint);
-		//legacy from 1.6.x
-		if(useCustomMountPath && !customMountPath.isBlank()) {
-			vaultSettings.mountPoint().set(parseMountPoint(customMountPath));
-		} else if(!winDriveLetter.isBlank() ) {
-			vaultSettings.mountPoint().set(parseMountPoint(winDriveLetter+":\\"));
-		}
-		//legacy end
-		return vaultSettings;
-	}
-
-	private Path parseMountPoint(String mountPoint) {
-		try {
-			return Path.of(mountPoint);
-		} catch (InvalidPathException e) {
-			LOG.warn("Invalid string as mount point. Defaulting to null.");
-			return null;
-		}
-	}
-
-	private WhenUnlocked parseActionAfterUnlock(String actionAfterUnlockName) {
-		try {
-			return WhenUnlocked.valueOf(actionAfterUnlockName.toUpperCase());
-		} catch (IllegalArgumentException e) {
-			LOG.warn("Invalid action after unlock {}. Defaulting to {}.", actionAfterUnlockName, VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK);
-			return VaultSettings.DEFAULT_ACTION_AFTER_UNLOCK;
-		}
-	}
-
-}

+ 5 - 1
src/main/java/org/cryptomator/common/settings/WhenUnlocked.java

@@ -1,9 +1,13 @@
 package org.cryptomator.common.settings;
 
+import com.fasterxml.jackson.annotation.JsonEnumDefaultValue;
+import com.fasterxml.jackson.annotation.JsonFormat;
+
+@JsonFormat(shape = JsonFormat.Shape.STRING)
 public enum WhenUnlocked {
 	IGNORE("vaultOptions.general.actionAfterUnlock.ignore"),
 	REVEAL("vaultOptions.general.actionAfterUnlock.reveal"),
-	ASK("vaultOptions.general.actionAfterUnlock.ask");
+	@JsonEnumDefaultValue ASK("vaultOptions.general.actionAfterUnlock.ask");
 
 	private String displayName;
 

+ 1 - 1
src/main/java/org/cryptomator/launcher/SupportedLanguages.java

@@ -34,7 +34,7 @@ public class SupportedLanguages {
 		var collator = Collator.getInstance(preferredLocale);
 		collator.setStrength(Collator.PRIMARY);
 		var sorted = new ArrayList<String>();
-		sorted.add(0, Settings.DEFAULT_LANGUAGE);
+		sorted.add(0, null);
 		sorted.add(1, ENGLISH);
 		LANGUAGE_TAGS.stream() //
 				.sorted((a, b) -> collator.compare(Locale.forLanguageTag(a).getDisplayName(), Locale.forLanguageTag(b).getDisplayName())) //

+ 3 - 1
src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java

@@ -27,6 +27,8 @@ import java.util.concurrent.atomic.AtomicReference;
 public class VolumePreferencesController implements FxController {
 
 	private static final String DOCS_MOUNTING_URL = "https://docs.cryptomator.org/en/1.7/desktop/volume-type/";
+	private static final int MIN_PORT = 1024;
+	private static final int MAX_PORT = 65535;
 
 	private final Settings settings;
 	private final ObservableValue<MountService> selectedMountService;
@@ -85,7 +87,7 @@ public class VolumePreferencesController implements FxController {
 		try {
 			int port = Integer.parseInt(loopbackPortField.getText());
 			return port == 0 // choose port automatically
-					|| port >= Settings.MIN_PORT && port <= Settings.MAX_PORT; // port within range
+					|| port >= MIN_PORT && port <= MAX_PORT; // port within range
 		} catch (NumberFormatException e) {
 			return false;
 		}

+ 0 - 61
src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java

@@ -1,61 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.common.settings;
-
-import org.cryptomator.common.Environment;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
-import org.mockito.Mockito;
-
-import java.io.IOException;
-
-public class SettingsJsonAdapterTest {
-
-	private final Environment env = Mockito.mock(Environment.class);
-	private final SettingsJsonAdapter adapter = new SettingsJsonAdapter(env);
-
-	@Test
-	public void testDeserialize() throws IOException {
-		String json = """
-				{
-					"directories": [
-						{"id": "1", "path": "/vault1", "mountName": "vault1", "winDriveLetter": "X"},
-						{"id": "2", "path": "/vault2", "mountName": "vault2", "winDriveLetter": "Y"}
-					],
-					"autoCloseVaults" : true,
-					"checkForUpdatesEnabled": true,
-					"port": 8080,
-					"language": "de-DE",
-					"numTrayNotifications": 42
-				}
-				""";
-
-		Settings settings = adapter.fromJson(json);
-
-		Assertions.assertTrue(settings.checkForUpdates().get());
-		Assertions.assertEquals(2, settings.getDirectories().size());
-		Assertions.assertEquals(8080, settings.port().get());
-		Assertions.assertEquals(true, settings.autoCloseVaults().get());
-		Assertions.assertEquals("de-DE", settings.languageProperty().get());
-		Assertions.assertEquals(42, settings.numTrayNotifications().get());
-	}
-
-	@SuppressWarnings("SpellCheckingInspection")
-	@ParameterizedTest(name = "fromJson() should throw IOException for input: {0}")
-	@ValueSource(strings = { //
-			"", //
-			"<html>", //
-			"{invalidjson}" //
-	})
-	public void testDeserializeMalformed(String input) {
-		Assertions.assertThrows(IOException.class, () -> {
-			adapter.fromJson(input);
-		});
-	}
-
-}

+ 79 - 0
src/test/java/org/cryptomator/common/settings/SettingsJsonTest.java

@@ -0,0 +1,79 @@
+package org.cryptomator.common.settings;
+
+import com.fasterxml.jackson.core.JacksonException;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.hamcrest.MatcherAssert;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+public class SettingsJsonTest {
+
+	@Test
+	public void testDeserialize() throws IOException {
+		String jsonStr = """
+				{
+					"directories": [
+						{"id": "1", "path": "/vault1", "mountName": "vault1", "winDriveLetter": "X", "shouldBeIgnored": true},
+						{"id": "2", "path": "/vault2", "mountName": "vault2", "winDriveLetter": "Y", "mountFlags":"--foo --bar"}
+					],
+					"autoCloseVaults" : true,
+					"checkForUpdatesEnabled": true,
+					"port": 8080,
+					"language": "de-DE",
+					"numTrayNotifications": 42
+				}
+				""";
+
+		var jsonObj = new ObjectMapper().reader().readValue(jsonStr, SettingsJson.class);
+
+		Assertions.assertTrue(jsonObj.checkForUpdatesEnabled);
+		Assertions.assertEquals(2, jsonObj.directories.size());
+		Assertions.assertEquals("/vault1", jsonObj.directories.get(0).path);
+		Assertions.assertEquals("/vault2", jsonObj.directories.get(1).path);
+		Assertions.assertEquals("--foo --bar", jsonObj.directories.get(1).mountFlags);
+		Assertions.assertEquals(8080, jsonObj.port);
+		Assertions.assertTrue(jsonObj.autoCloseVaults);
+		Assertions.assertEquals("de-DE", jsonObj.language);
+		Assertions.assertEquals(42, jsonObj.numTrayNotifications);
+	}
+
+	@SuppressWarnings("SpellCheckingInspection")
+	@ParameterizedTest(name = "throw JacksonException for input: {0}")
+	@ValueSource(strings = { //
+			"", //
+			"<html>", //
+			"{invalidjson}" //
+	})
+	public void testDeserializeMalformed(String input) {
+		var objectMapper = new ObjectMapper().reader();
+
+		Assertions.assertThrows(JacksonException.class, () -> {
+			objectMapper.readValue(input, SettingsJson.class);
+		});
+	}
+
+	@Test
+	public void testSerialize() throws JsonProcessingException {
+		var jsonObj = new SettingsJson();
+		jsonObj.directories = List.of(new VaultSettingsJson(), new VaultSettingsJson());
+		jsonObj.directories.get(0).id = "test";
+		jsonObj.theme = UiTheme.DARK;
+		jsonObj.showTrayIcon = false;
+
+		var jsonStr = new ObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(jsonObj);
+
+		MatcherAssert.assertThat(jsonStr, containsString("\"theme\" : \"DARK\""));
+		MatcherAssert.assertThat(jsonStr, containsString("\"showTrayIcon\" : false"));
+		MatcherAssert.assertThat(jsonStr, containsString("\"useKeychain\" : true"));
+		MatcherAssert.assertThat(jsonStr, containsString("\"actionAfterUnlock\" : \"ASK\""));
+	}
+
+}

+ 1 - 1
src/test/java/org/cryptomator/common/settings/SettingsTest.java

@@ -18,7 +18,7 @@ public class SettingsTest {
 		Environment env = Mockito.mock(Environment.class);
 		@SuppressWarnings("unchecked") Consumer<Settings> changeListener = Mockito.mock(Consumer.class);
 
-		Settings settings = new Settings(env);
+		Settings settings = Settings.create(env);
 		settings.setSaveCmd(changeListener);
 		VaultSettings vaultSettings = VaultSettings.withRandomId();
 		Mockito.verify(changeListener, Mockito.times(0)).accept(settings);

+ 0 - 67
src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java

@@ -1,67 +0,0 @@
-/*******************************************************************************
- * Copyright (c) 2017 Skymatic UG (haftungsbeschränkt).
- * All rights reserved. This program and the accompanying materials
- * are made available under the terms of the accompanying LICENSE file.
- *******************************************************************************/
-package org.cryptomator.common.settings;
-
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
-import org.junit.jupiter.api.Test;
-
-import java.io.IOException;
-import java.io.StringReader;
-import java.io.StringWriter;
-import java.nio.file.Paths;
-
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.assertAll;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-public class VaultSettingsJsonAdapterTest {
-
-	private final VaultSettingsJsonAdapter adapter = new VaultSettingsJsonAdapter();
-
-	@Test
-	public void testDeserialize() throws IOException {
-		String json = "{\"id\": \"foo\", \"path\": \"/foo/bar\", \"displayName\": \"test\", \"winDriveLetter\": \"X\", \"shouldBeIgnored\": true, \"individualMountPath\": \"/home/test/crypto\", \"mountFlags\":\"--foo --bar\"}";
-		JsonReader jsonReader = new JsonReader(new StringReader(json));
-
-		VaultSettings vaultSettings = adapter.read(jsonReader);
-
-		assertAll(
-				() -> assertEquals("foo", vaultSettings.getId()),
-				() -> assertEquals(Paths.get("/foo/bar"), vaultSettings.path().get()),
-				() -> assertEquals("test", vaultSettings.displayName().get()),
-				() -> assertEquals("--foo --bar", vaultSettings.mountFlags().get())
-		);
-	}
-
-	@SuppressWarnings("SpellCheckingInspection")
-	@Test
-	public void testSerialize() throws IOException {
-		VaultSettings vaultSettings = new VaultSettings("test");
-		vaultSettings.path().set(Paths.get("/foo/bar"));
-		vaultSettings.displayName().set("mountyMcMountFace");
-		vaultSettings.mountFlags().set("--foo --bar");
-
-		StringWriter buf = new StringWriter();
-		JsonWriter jsonWriter = new JsonWriter(buf);
-		adapter.write(jsonWriter, vaultSettings);
-		String result = buf.toString();
-
-		assertAll(
-				() -> assertThat(result, containsString("\"id\":\"test\"")),
-				() -> {
-					if (System.getProperty("os.name").contains("Windows")) {
-						assertThat(result, containsString("\"path\":\"\\\\foo\\\\bar\""));
-					} else {
-						assertThat(result, containsString("\"path\":\"/foo/bar\""));
-					}
-				},
-				() -> assertThat(result, containsString("\"displayName\":\"mountyMcMountFace\"")),
-				() -> assertThat(result, containsString("\"mountFlags\":\"--foo --bar\""))
-		);
-	}
-}