refactor location ui in addVault workflow to new locationPreset framework

This commit is contained in:
Armin Schrenk
2023-05-17 14:51:43 +02:00
parent 0989c735c0
commit 0af0a9e440
5 changed files with 104 additions and 247 deletions

View File

@@ -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<Path> 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;
}
}
}
}

View File

@@ -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<LocationPreset> getLocations();
@@ -17,4 +29,65 @@ public interface LocationPresetsProvider {
}
}
//copied from org.cryptomator.integrations.common.IntegrationsLoader
//TODO: delete, once migrated to integrations-api
static <T> Stream<T> loadAll(Class<T> 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 <T> boolean passesAvailabilityCheck(Class<? extends T> 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;
}
}

View File

@@ -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<Scene> chooseNameScene;
private final Lazy<Scene> choosePasswordScene;
private final ObservedLocationPresets locationPresets;
private final List<RadioButton> locationPresetBtns;
private final ObjectProperty<Path> 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<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObservedLocationPresets locationPresets, ObjectProperty<Path> vaultPath, @Named("vaultName") StringProperty vaultName, ResourceBundle resourceBundle) {
CreateNewVaultLocationController(@AddVaultWizardWindow Stage window, @FxmlScene(FxmlFile.ADDVAULT_NEW_NAME) Lazy<Scene> chooseNameScene, @FxmlScene(FxmlFile.ADDVAULT_NEW_PASSWORD) Lazy<Scene> choosePasswordScene, ObjectProperty<Path> 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<? extends Toggle> 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();
}
}

View File

@@ -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<Path> iclouddriveLocation;
private final ReadOnlyObjectProperty<Path> dropboxLocation;
private final ReadOnlyObjectProperty<Path> gdriveLocation;
private final ReadOnlyObjectProperty<Path> onedriveLocation;
private final ReadOnlyObjectProperty<Path> megaLocation;
private final ReadOnlyObjectProperty<Path> 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<Path> iclouddriveLocationProperty() {
return iclouddriveLocation;
}
public Path getIclouddriveLocation() {
return iclouddriveLocation.get();
}
public BooleanBinding foundIclouddriveProperty() {
return foundIclouddrive;
}
public boolean isFoundIclouddrive() {
return foundIclouddrive.get();
}
public ReadOnlyObjectProperty<Path> dropboxLocationProperty() {
return dropboxLocation;
}
public Path getDropboxLocation() {
return dropboxLocation.get();
}
public BooleanBinding foundDropboxProperty() {
return foundDropbox;
}
public boolean isFoundDropbox() {
return foundDropbox.get();
}
public ReadOnlyObjectProperty<Path> gdriveLocationProperty() {
return gdriveLocation;
}
public Path getGdriveLocation() {
return gdriveLocation.get();
}
public BooleanBinding foundGdriveProperty() {
return foundGdrive;
}
public boolean isFoundGdrive() {
return foundGdrive.get();
}
public ReadOnlyObjectProperty<Path> onedriveLocationProperty() {
return onedriveLocation;
}
public Path getOnedriveLocation() {
return onedriveLocation.get();
}
public BooleanBinding foundOnedriveProperty() {
return foundOnedrive;
}
public boolean isFoundOnedrive() {
return foundOnedrive.get();
}
public ReadOnlyObjectProperty<Path> megaLocationProperty() {
return megaLocation;
}
public Path getMegaLocation() {
return megaLocation.get();
}
public BooleanBinding foundMegaProperty() {
return foundMega;
}
public boolean isFoundMega() {
return foundMega.get();
}
public ReadOnlyObjectProperty<Path> pcloudLocationProperty() {
return pcloudLocation;
}
public Path getPcloudLocation() {
return pcloudLocation.get();
}
public BooleanBinding foundPcloudProperty() {
return foundPcloud;
}
public boolean isFoundPcloud() {
return foundPcloud.get();
}
}