From 0af0a9e4400143ee944775ea8ed939d498ba2a5a Mon Sep 17 00:00:00 2001 From: Armin Schrenk Date: Wed, 17 May 2023 14:51:43 +0200 Subject: [PATCH] refactor location ui in addVault workflow to new locationPreset framework --- .../cryptomator/common/LocationPreset.java | 64 -------- .../LocationPresetsProvider.java | 73 +++++++++ .../CreateNewVaultLocationController.java | 58 ++++--- .../ObservedLocationPresets.java | 141 ------------------ .../resources/fxml/addvault_new_location.fxml | 15 +- 5 files changed, 104 insertions(+), 247 deletions(-) delete mode 100644 src/main/java/org/cryptomator/common/LocationPreset.java delete mode 100644 src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java diff --git a/src/main/java/org/cryptomator/common/LocationPreset.java b/src/main/java/org/cryptomator/common/LocationPreset.java deleted file mode 100644 index 7861c6266..000000000 --- a/src/main/java/org/cryptomator/common/LocationPreset.java +++ /dev/null @@ -1,64 +0,0 @@ -package org.cryptomator.common; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.List; - -/** - * Enum of common cloud providers and their default local storage location path. - */ -public enum LocationPreset { - - DROPBOX("Dropbox", "~/Library/CloudStorage/Dropbox", "~/Dropbox"), - ICLOUDDRIVE("iCloud Drive", "~/Library/Mobile Documents/com~apple~CloudDocs", "~/iCloudDrive"), - GDRIVE("Google Drive", "~/Google Drive/My Drive", "~/Google Drive"), - MEGA("MEGA", "~/MEGA"), - ONEDRIVE("OneDrive", "~/OneDrive"), - PCLOUD("pCloud", "~/pCloudDrive"), - - LOCAL("local"); - - private final String name; - private final List candidates; - - LocationPreset(String name, String... candidates) { - this.name = name; - this.candidates = Arrays.stream(candidates).map(UserHome::resolve).map(Path::of).toList(); - } - - /** - * Checks for this LocationPreset if any of the associated paths exist. - * - * @return the first existing path or null, if none exists. - */ - public Path existingPath() { - return candidates.stream().filter(Files::isDirectory).findFirst().orElse(null); - } - - public String getDisplayName() { - return name; - } - - @Override - public String toString() { - return getDisplayName(); - } - - //this contruct is needed, since static members are initialized after every enum member is initialized - //TODO: refactor this to normal class and use this also in different parts of the project - private static class UserHome { - - private static final String USER_HOME = System.getProperty("user.home"); - - private static String resolve(String path) { - if (path.startsWith("~/")) { - return UserHome.USER_HOME + path.substring(1); - } else { - return path; - } - } - } - -} - diff --git a/src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java b/src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java index c48a45655..cf527a749 100644 --- a/src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java +++ b/src/main/java/org/cryptomator/common/locationpresets/LocationPresetsProvider.java @@ -1,10 +1,22 @@ package org.cryptomator.common.locationpresets; +import org.cryptomator.integrations.common.CheckAvailability; +import org.cryptomator.integrations.common.IntegrationsLoader; +import org.cryptomator.integrations.common.OperatingSystem; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.nio.file.Path; +import java.util.Arrays; +import java.util.ServiceLoader; import java.util.stream.Stream; public interface LocationPresetsProvider { + Logger LOG = LoggerFactory.getLogger(LocationPresetsProvider.class); String USER_HOME = System.getProperty("user.home"); Stream getLocations(); @@ -17,4 +29,65 @@ public interface LocationPresetsProvider { } } + //copied from org.cryptomator.integrations.common.IntegrationsLoader + //TODO: delete, once migrated to integrations-api + static Stream loadAll(Class clazz) { + return ServiceLoader.load(clazz) + .stream() + .filter(LocationPresetsProvider::isSupportedOperatingSystem) + .filter(LocationPresetsProvider::passesStaticAvailabilityCheck) + .map(ServiceLoader.Provider::get) + .peek(impl -> logServiceIsAvailable(clazz, impl.getClass())); + } + + + private static boolean isSupportedOperatingSystem(ServiceLoader.Provider provider) { + var annotations = provider.type().getAnnotationsByType(OperatingSystem.class); + return annotations.length == 0 || Arrays.stream(annotations).anyMatch(OperatingSystem.Value::isCurrent); + } + + private static boolean passesStaticAvailabilityCheck(ServiceLoader.Provider provider) { + return passesStaticAvailabilityCheck(provider.type()); + } + + static boolean passesStaticAvailabilityCheck(Class type) { + return passesAvailabilityCheck(type, null); + } + + private static void logServiceIsAvailable(Class apiType, Class implType) { + if (LOG.isDebugEnabled()) { + LOG.debug("{}: Implementation is available: {}", apiType.getSimpleName(), implType.getName()); + } + } + + private static boolean passesAvailabilityCheck(Class type, @Nullable T instance) { + if (!type.isAnnotationPresent(CheckAvailability.class)) { + return true; // if type is not annotated, skip tests + } + if (!type.getModule().isExported(type.getPackageName(), IntegrationsLoader.class.getModule())) { + LOG.error("Can't run @CheckAvailability tests for class {}. Make sure to export {} to {}!", type.getName(), type.getPackageName(), IntegrationsLoader.class.getPackageName()); + return false; + } + return Arrays.stream(type.getMethods()) + .filter(m -> isAvailabilityCheck(m, instance == null)) + .allMatch(m -> passesAvailabilityCheck(m, instance)); + } + + private static boolean passesAvailabilityCheck(Method m, @Nullable Object instance) { + assert Boolean.TYPE.equals(m.getReturnType()); + try { + return (boolean) m.invoke(instance); + } catch (ReflectiveOperationException e) { + LOG.warn("Failed to invoke @CheckAvailability test {}#{}", m.getDeclaringClass(), m.getName(), e); + return false; + } + } + + private static boolean isAvailabilityCheck(Method m, boolean isStatic) { + return m.isAnnotationPresent(CheckAvailability.class) + && Boolean.TYPE.equals(m.getReturnType()) + && m.getParameterCount() == 0 + && Modifier.isStatic(m.getModifiers()) == isStatic; + } + } diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java index cadccc091..5a36552b0 100644 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java +++ b/src/main/java/org/cryptomator/ui/addvaultwizard/CreateNewVaultLocationController.java @@ -1,6 +1,8 @@ package org.cryptomator.ui.addvaultwizard; import dagger.Lazy; +import org.cryptomator.common.locationpresets.LocationPresetsProvider; +import org.cryptomator.common.locationpresets.LocationPreset; import org.cryptomator.ui.common.FxController; import org.cryptomator.ui.common.FxmlFile; import org.cryptomator.ui.common.FxmlScene; @@ -26,6 +28,7 @@ import javafx.scene.control.Label; import javafx.scene.control.RadioButton; import javafx.scene.control.Toggle; import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.VBox; import javafx.stage.DirectoryChooser; import javafx.stage.Stage; import java.io.File; @@ -34,6 +37,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; import java.util.ResourceBundle; @AddVaultWizardScoped @@ -46,7 +52,7 @@ public class CreateNewVaultLocationController implements FxController { private final Stage window; private final Lazy chooseNameScene; private final Lazy choosePasswordScene; - private final ObservedLocationPresets locationPresets; + private final List locationPresetBtns; private final ObjectProperty vaultPath; private final StringProperty vaultName; private final ResourceBundle resourceBundle; @@ -58,24 +64,18 @@ public class CreateNewVaultLocationController implements FxController { private Path customVaultPath = DEFAULT_CUSTOM_VAULT_PATH; //FXML - public ToggleGroup predefinedLocationToggler; - public RadioButton iclouddriveRadioButton; - public RadioButton dropboxRadioButton; - public RadioButton gdriveRadioButton; - public RadioButton onedriveRadioButton; - public RadioButton megaRadioButton; - public RadioButton pcloudRadioButton; + public ToggleGroup locationPresetsToggler; + public VBox radioButtonVBox; public RadioButton customRadioButton; public Label vaultPathStatus; public FontAwesome5IconView goodLocation; public FontAwesome5IconView badLocation; @Inject - CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy choosePasswordScene, ObservedLocationPresets locationPresets, ObjectProperty vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) { + CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy choosePasswordScene, ObjectProperty vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) { this.window = window; this.chooseNameScene = chooseNameScene; this.choosePasswordScene = choosePasswordScene; - this.locationPresets = locationPresets; this.vaultPath = vaultPath; this.vaultName = vaultName; this.resourceBundle = resourceBundle; @@ -83,6 +83,14 @@ public class CreateNewVaultLocationController implements FxController { this.usePresetPath = new SimpleBooleanProperty(); this.statusText = new SimpleStringProperty(); this.statusGraphic = new SimpleObjectProperty<>(); + this.locationPresetBtns = LocationPresetsProvider.loadAll(LocationPresetsProvider.class) // + .flatMap(LocationPresetsProvider::getLocations) // + .sorted(Comparator.comparing(LocationPreset::name)) // + .map(preset -> { // + var btn = new RadioButton(preset.name()); + btn.setUserData(preset.path()); + return btn; + }).toList(); } private boolean validateVaultPathAndSetStatus() { @@ -127,26 +135,15 @@ public class CreateNewVaultLocationController implements FxController { @FXML public void initialize() { - predefinedLocationToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); - usePresetPath.bind(predefinedLocationToggler.selectedToggleProperty().isNotEqualTo(customRadioButton)); + radioButtonVBox.getChildren().addAll(1, locationPresetBtns); //first item is the list header + locationPresetsToggler.getToggles().addAll(locationPresetBtns); + locationPresetsToggler.selectedToggleProperty().addListener(this::togglePredefinedLocation); + usePresetPath.bind(locationPresetsToggler.selectedToggleProperty().isNotEqualTo(customRadioButton)); } private void togglePredefinedLocation(@SuppressWarnings("unused") ObservableValue observable, @SuppressWarnings("unused") Toggle oldValue, Toggle newValue) { - if (iclouddriveRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getIclouddriveLocation().resolve(vaultName.get())); - } else if (dropboxRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getDropboxLocation().resolve(vaultName.get())); - } else if (gdriveRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getGdriveLocation().resolve(vaultName.get())); - } else if (onedriveRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getOnedriveLocation().resolve(vaultName.get())); - } else if (megaRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getMegaLocation().resolve(vaultName.get())); - } else if (pcloudRadioButton.equals(newValue)) { - vaultPath.set(locationPresets.getPcloudLocation().resolve(vaultName.get())); - } else if (customRadioButton.equals(newValue)) { - vaultPath.set(customVaultPath.resolve(vaultName.get())); - } + var storagePath = Optional.ofNullable((Path) newValue.getUserData()).orElse(customVaultPath); + vaultPath.set(storagePath.resolve(vaultName.get())); } @FXML @@ -197,10 +194,6 @@ public class CreateNewVaultLocationController implements FxController { return validVaultPath.get(); } - public ObservedLocationPresets getObservedLocationPresets() { - return locationPresets; - } - public BooleanProperty usePresetPathProperty() { return usePresetPath; } @@ -210,7 +203,7 @@ public class CreateNewVaultLocationController implements FxController { } public BooleanBinding anyRadioButtonSelectedProperty() { - return predefinedLocationToggler.selectedToggleProperty().isNotNull(); + return locationPresetsToggler.selectedToggleProperty().isNotNull(); } public boolean isAnyRadioButtonSelected() { @@ -232,4 +225,5 @@ public class CreateNewVaultLocationController implements FxController { public Node getStatusGraphic() { return statusGraphic.get(); } + } diff --git a/src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java b/src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java deleted file mode 100644 index 1c988dc04..000000000 --- a/src/main/java/org/cryptomator/ui/addvaultwizard/ObservedLocationPresets.java +++ /dev/null @@ -1,141 +0,0 @@ -package org.cryptomator.ui.addvaultwizard; - -import org.cryptomator.common.LocationPreset; - -import javax.inject.Inject; -import javafx.beans.binding.BooleanBinding; -import javafx.beans.property.ReadOnlyObjectProperty; -import javafx.beans.property.SimpleObjectProperty; -import java.nio.file.Path; - -@AddVaultWizardScoped -public class ObservedLocationPresets { - - private final ReadOnlyObjectProperty iclouddriveLocation; - private final ReadOnlyObjectProperty dropboxLocation; - private final ReadOnlyObjectProperty gdriveLocation; - private final ReadOnlyObjectProperty onedriveLocation; - private final ReadOnlyObjectProperty megaLocation; - private final ReadOnlyObjectProperty pcloudLocation; - private final BooleanBinding foundIclouddrive; - private final BooleanBinding foundDropbox; - private final BooleanBinding foundGdrive; - private final BooleanBinding foundOnedrive; - private final BooleanBinding foundMega; - private final BooleanBinding foundPcloud; - - @Inject - public ObservedLocationPresets() { - this.iclouddriveLocation = new SimpleObjectProperty<>(LocationPreset.ICLOUDDRIVE.existingPath()); - this.dropboxLocation = new SimpleObjectProperty<>(LocationPreset.DROPBOX.existingPath()); - this.gdriveLocation = new SimpleObjectProperty<>(LocationPreset.GDRIVE.existingPath()); - this.onedriveLocation = new SimpleObjectProperty<>(LocationPreset.ONEDRIVE.existingPath()); - this.megaLocation = new SimpleObjectProperty<>(LocationPreset.MEGA.existingPath()); - this.pcloudLocation = new SimpleObjectProperty<>(LocationPreset.PCLOUD.existingPath()); - this.foundIclouddrive = iclouddriveLocation.isNotNull(); - this.foundDropbox = dropboxLocation.isNotNull(); - this.foundGdrive = gdriveLocation.isNotNull(); - this.foundOnedrive = onedriveLocation.isNotNull(); - this.foundMega = megaLocation.isNotNull(); - this.foundPcloud = pcloudLocation.isNotNull(); - } - - /* Observables */ - - public ReadOnlyObjectProperty iclouddriveLocationProperty() { - return iclouddriveLocation; - } - - public Path getIclouddriveLocation() { - return iclouddriveLocation.get(); - } - - public BooleanBinding foundIclouddriveProperty() { - return foundIclouddrive; - } - - public boolean isFoundIclouddrive() { - return foundIclouddrive.get(); - } - - public ReadOnlyObjectProperty dropboxLocationProperty() { - return dropboxLocation; - } - - public Path getDropboxLocation() { - return dropboxLocation.get(); - } - - public BooleanBinding foundDropboxProperty() { - return foundDropbox; - } - - public boolean isFoundDropbox() { - return foundDropbox.get(); - } - - public ReadOnlyObjectProperty gdriveLocationProperty() { - return gdriveLocation; - } - - public Path getGdriveLocation() { - return gdriveLocation.get(); - } - - public BooleanBinding foundGdriveProperty() { - return foundGdrive; - } - - public boolean isFoundGdrive() { - return foundGdrive.get(); - } - - public ReadOnlyObjectProperty onedriveLocationProperty() { - return onedriveLocation; - } - - public Path getOnedriveLocation() { - return onedriveLocation.get(); - } - - public BooleanBinding foundOnedriveProperty() { - return foundOnedrive; - } - - public boolean isFoundOnedrive() { - return foundOnedrive.get(); - } - - public ReadOnlyObjectProperty megaLocationProperty() { - return megaLocation; - } - - public Path getMegaLocation() { - return megaLocation.get(); - } - - public BooleanBinding foundMegaProperty() { - return foundMega; - } - - public boolean isFoundMega() { - return foundMega.get(); - } - - public ReadOnlyObjectProperty pcloudLocationProperty() { - return pcloudLocation; - } - - public Path getPcloudLocation() { - return pcloudLocation.get(); - } - - public BooleanBinding foundPcloudProperty() { - return foundPcloud; - } - - public boolean isFoundPcloud() { - return foundPcloud.get(); - } - -} diff --git a/src/main/resources/fxml/addvault_new_location.fxml b/src/main/resources/fxml/addvault_new_location.fxml index 343a2d580..7920b6aac 100644 --- a/src/main/resources/fxml/addvault_new_location.fxml +++ b/src/main/resources/fxml/addvault_new_location.fxml @@ -19,7 +19,7 @@ spacing="12" alignment="CENTER_LEFT"> - + @@ -29,16 +29,11 @@ - +