diff --git a/src/main/java/org/cryptomator/common/settings/Settings.java b/src/main/java/org/cryptomator/common/settings/Settings.java index 46aed5ea0..4100b4714 100644 --- a/src/main/java/org/cryptomator/common/settings/Settings.java +++ b/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; + 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 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 theme = new SimpleObjectProperty<>(DEFAULT_THEME); - private final ObjectProperty keychainProvider = new SimpleObjectProperty<>(DEFAULT_KEYCHAIN_PROVIDER); - private final ObjectProperty 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); + private final ObservableList 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 theme; + private final StringProperty keychainProvider; + private final ObjectProperty 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 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 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 parseEnum(String value, Class 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 saveCmd) { this.saveCmd = saveCmd; } @@ -120,6 +214,7 @@ public class Settings { } /* Getter/Setter */ + // TODO: remove accessors, make fields public public ObservableList 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 keychainProvider() {return keychainProvider;} + public StringProperty keychainProvider() {return keychainProvider;} public ObjectProperty userInterfaceOrientation() { return userInterfaceOrientation; @@ -197,7 +292,7 @@ public class Settings { return windowHeight; } - public ObjectProperty displayConfigurationProperty() { + public StringProperty displayConfigurationProperty() { return displayConfiguration; } diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJson.java b/src/main/java/org/cryptomator/common/settings/SettingsJson.java new file mode 100644 index 000000000..4bd23bec7 --- /dev/null +++ b/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 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; + +} diff --git a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java deleted file mode 100644 index abc6f629f..000000000 --- a/src/main/java/org/cryptomator/common/settings/SettingsJsonAdapter.java +++ /dev/null @@ -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 { - - 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) 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 readVaultSettingsArray(JsonReader in) throws IOException { - List result = new ArrayList<>(); - in.beginArray(); - while (!JsonToken.END_ARRAY.equals(in.peek())) { - result.add(vaultSettingsJsonAdapter.read(in)); - } - in.endArray(); - return result; - } -} \ No newline at end of file diff --git a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java b/src/main/java/org/cryptomator/common/settings/SettingsProvider.java index 594a36e2e..2b07c38eb 100644 --- a/src/main/java/org/cryptomator/common/settings/SettingsProvider.java +++ b/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 { + 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 { private final Supplier 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 { } 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 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 { 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); diff --git a/src/main/java/org/cryptomator/common/settings/UiTheme.java b/src/main/java/org/cryptomator/common/settings/UiTheme.java index 24e73dad6..4161ff430 100644 --- a/src/main/java/org/cryptomator/common/settings/UiTheme.java +++ b/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"); diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettings.java b/src/main/java/org/cryptomator/common/settings/VaultSettings.java index 3a116b4ce..8989858e1 100644 --- a/src/main/java/org/cryptomator/common/settings/VaultSettings.java +++ b/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 = 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 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; + 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 actionAfterUnlock; + private final BooleanProperty autoLockWhenIdle; + private final IntegerProperty autoLockIdleSeconds; + private final ObjectProperty mountPoint; private final StringExpression mountName; - private final ObjectProperty 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; diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJson.java new file mode 100644 index 000000000..2381203e5 --- /dev/null +++ b/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; + +} diff --git a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java b/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java deleted file mode 100644 index cffeb3aa7..000000000 --- a/src/main/java/org/cryptomator/common/settings/VaultSettingsJsonAdapter.java +++ /dev/null @@ -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; - } - } - -} diff --git a/src/main/java/org/cryptomator/common/settings/WhenUnlocked.java b/src/main/java/org/cryptomator/common/settings/WhenUnlocked.java index fe023222c..86aed19d5 100644 --- a/src/main/java/org/cryptomator/common/settings/WhenUnlocked.java +++ b/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; diff --git a/src/main/java/org/cryptomator/launcher/SupportedLanguages.java b/src/main/java/org/cryptomator/launcher/SupportedLanguages.java index 0838c420e..2aa7c6abd 100644 --- a/src/main/java/org/cryptomator/launcher/SupportedLanguages.java +++ b/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(); - 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())) // diff --git a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java b/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java index 6c60e74b7..0df67cb53 100644 --- a/src/main/java/org/cryptomator/ui/preferences/VolumePreferencesController.java +++ b/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 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; } diff --git a/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java b/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java deleted file mode 100644 index 2096497ea..000000000 --- a/src/test/java/org/cryptomator/common/settings/SettingsJsonAdapterTest.java +++ /dev/null @@ -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 = { // - "", // - "", // - "{invalidjson}" // - }) - public void testDeserializeMalformed(String input) { - Assertions.assertThrows(IOException.class, () -> { - adapter.fromJson(input); - }); - } - -} diff --git a/src/test/java/org/cryptomator/common/settings/SettingsJsonTest.java b/src/test/java/org/cryptomator/common/settings/SettingsJsonTest.java new file mode 100644 index 000000000..da78ee5bc --- /dev/null +++ b/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 = { // + "", // + "", // + "{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\"")); + } + +} \ No newline at end of file diff --git a/src/test/java/org/cryptomator/common/settings/SettingsTest.java b/src/test/java/org/cryptomator/common/settings/SettingsTest.java index 7f4c0ee06..4f18ee866 100644 --- a/src/test/java/org/cryptomator/common/settings/SettingsTest.java +++ b/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 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); diff --git a/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java b/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java deleted file mode 100644 index b7a0dade4..000000000 --- a/src/test/java/org/cryptomator/common/settings/VaultSettingsJsonAdapterTest.java +++ /dev/null @@ -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\"")) - ); - } -}