package org.peerbox.presenter; import java.io.IOException; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.ResourceBundle; import javafx.application.Platform; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.Task; import javafx.concurrent.WorkerStateEvent; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; import javafx.scene.control.CheckBox; import javafx.scene.control.Label; import javafx.scene.control.PasswordField; import javafx.scene.control.ProgressIndicator; import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.stage.Stage; import javafx.stage.Window; import org.apache.commons.io.FileUtils; import org.hive2hive.core.exceptions.NoPeerConnectionException; import org.peerbox.ResultStatus; import org.peerbox.app.AppContext; import org.peerbox.app.ClientContext; import org.peerbox.app.ClientContextFactory; import org.peerbox.app.Constants; import org.peerbox.app.config.UserConfig; import org.peerbox.app.manager.user.IUserManager; import org.peerbox.forcesync.ForceSync; import org.peerbox.presenter.validation.EmptyTextFieldValidator; import org.peerbox.presenter.validation.RootPathValidator; import org.peerbox.presenter.validation.SelectRootPathUtils; import org.peerbox.presenter.validation.ValidationUtils.ValidationResult; import org.peerbox.utils.UserConfigUtils; import org.peerbox.view.ViewNames; import org.peerbox.view.controls.ErrorLabel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; public class LoginController implements Initializable { private static final Logger logger = LoggerFactory.getLogger(LoginController.class); private final NavigationService fNavigationService; private final AppContext appContext; private final IUserManager userManager; @Inject private ClientContextFactory clientContextFactory; @FXML private TextField txtUsername; @FXML private Label lblUsernameError; @FXML private PasswordField txtPassword; @FXML private Label lblPasswordError; @FXML private PasswordField txtPin; @FXML private Label lblPinError; @FXML private HBox boxRootPath; @FXML private TextField txtRootPath; @FXML private Button btnRootPath; @FXML private Label lblPathError; @FXML private CheckBox chbAutoLogin; @FXML private Button btnLogin; @FXML private Button btnRegister; @FXML private GridPane grdForm; @FXML private ErrorLabel lblError; @FXML private ProgressIndicator piProgress; private EmptyTextFieldValidator usernameValidator; private EmptyTextFieldValidator passwordValidator; private EmptyTextFieldValidator pinValidator; private RootPathValidator pathValidator; private Map<String, UserConfig> availableConfigFiles; private final BooleanProperty disableRootPathProperty; @Inject public LoginController(NavigationService navigationService, AppContext appContext, IUserManager userManager) { this.fNavigationService = navigationService; this.appContext = appContext; this.userManager = userManager; this.disableRootPathProperty = new SimpleBooleanProperty(false); } public void initialize(URL location, ResourceBundle resources) { initializeValidations(); loadUserConfig(); } private void loadUserConfig() { // allow or prevent that user can select / edit root path boxRootPath.disableProperty().bind(disableRootPathProperty); // read config for user and set root path if config file found availableConfigFiles = UserConfigUtils.getAllConfigFiles(); txtUsername.textProperty().addListener(new ChangeListener<String>() { @Override public void changed( ObservableValue<? extends String> observable, String oldValue, String newValue) { boolean disableRootPath = false; String newValueLower = newValue.toLowerCase(); if(availableConfigFiles.containsKey(newValueLower)) { UserConfig cfg = availableConfigFiles.get(newValueLower); logger.info("Found config file for user: '{}' - Path: '{}'", newValueLower, cfg.getConfigFile()); if(cfg.hasRootPath()) { txtRootPath.setText(cfg.getRootPath().toString()); disableRootPath = true; } } // if user can select root path, we propose '.../userhome/synqbox/username' as name if(!disableRootPath) { Path newUserRootPath = Paths.get(FileUtils.getUserDirectoryPath(), Constants.APP_NAME, getUsername()); txtRootPath.setText(newUserRootPath.toString()); } disableRootPathProperty.set(disableRootPath); } }); } private void resetForm() { loadUserConfig(); txtPassword.clear(); txtPin.clear(); grdForm.disableProperty().unbind(); grdForm.setDisable(false); uninstallProgressIndicator(); uninstallValidationDecorations(); } private void initializeValidations() { usernameValidator = new EmptyTextFieldValidator(txtUsername, true, ValidationResult.USERNAME_EMPTY); usernameValidator.setErrorProperty(lblUsernameError.textProperty()); passwordValidator = new EmptyTextFieldValidator(txtPassword, false, ValidationResult.PASSWORD_EMPTY); passwordValidator.setErrorProperty(lblPasswordError.textProperty()); pinValidator = new EmptyTextFieldValidator(txtPin, false, ValidationResult.PIN_EMPTY); pinValidator.setErrorProperty(lblPinError.textProperty()); pathValidator = new RootPathValidator(txtRootPath, lblPathError.textProperty()); } private void uninstallValidationDecorations() { usernameValidator.reset(); passwordValidator.reset(); pinValidator.reset(); pathValidator.reset(); } @FXML public void loginAction(ActionEvent event) { boolean inputValid = false; try { clearError(); ValidationResult validationRes = validateAll(); inputValid = !validationRes.isError() && checkUserExists(); } catch (NoPeerConnectionException e) { inputValid = false; setError("Connection to the network failed."); } if (inputValid) { Task<ResultStatus> task = createLoginTask(); new Thread(task).start(); } } private ValidationResult validateAll() { return (usernameValidator.validate() == ValidationResult.OK & passwordValidator.validate() == ValidationResult.OK & pinValidator.validate() == ValidationResult.OK & pathValidator.validate() == ValidationResult.OK ) ? ValidationResult.OK : ValidationResult.ERROR; } private boolean checkUserExists() throws NoPeerConnectionException { String username = txtUsername.getText().trim(); if (!userManager.isRegistered(username)) { setError("This user profile does not exist."); return false; } return true; } public ResultStatus loginUser(final String username, final String password, final String pin, final Path path) { try { return userManager.loginUser(username, password, pin, path); } catch (NoPeerConnectionException e) { return ResultStatus.error("Could not login user because connection to network failed."); } } private Task<ResultStatus> createLoginTask() { Task<ResultStatus> task = new Task<ResultStatus>() { // credentials final String username = getUsername(); final String password = getPassword(); final String pin = getPin(); final Path path = getRootPath(); @Override public ResultStatus call() { return loginUser(username, password, pin, path); } }; task.setOnScheduled(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent event) { grdForm.disableProperty().bind(task.runningProperty()); installProgressIndicator(); } }); task.setOnFailed(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent event) { onLoginFailed(ResultStatus.error("Could not login user.")); } }); task.setOnSucceeded(new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent event) { ResultStatus result = task.getValue(); if (result.isOk()) { onLoginSucceeded(); } else { onLoginFailed(result); } } }); return task; } private void onLoginFailed(ResultStatus result) { logger.error("Login task failed: {}", result.getErrorMessage()); Platform.runLater(() -> { uninstallProgressIndicator(); grdForm.disableProperty().unbind(); grdForm.requestLayout(); setError(result.getErrorMessage()); }); } private void onLoginSucceeded() { logger.debug("Login task succeeded: user {} logged in.", getUsername()); UserConfig userConfig = UserConfigUtils.createUserConfig(getUsername()); try { userConfig.load(); saveLoginConfig(userConfig); initializeServices(userConfig); } catch (IOException e) { logger.warn("Could not load and save user config.", e); setError("Could not load and save user config."); } resetForm(); fNavigationService.clearPages(); hideWindow(); } private void initializeServices(UserConfig userConfig) { try { ClientContext ctx = clientContextFactory.create(userConfig); appContext.setCurrentClientContext(ctx); ctx.getActionExecutor().start(); ctx.getFolderWatchService().start(userConfig.getRootPath()); ForceSync forceSync = new ForceSync(ctx); forceSync.forceSync(userConfig.getRootPath()); ctx.getRemoteProfilePersister().start(); } catch (Exception e) { logger.warn("Exception: ", e); } } private void saveLoginConfig(UserConfig userConfig) { try { userConfig.setUsername(getUsername()); userConfig.setRootPath(getRootPath()); // auto login is not enabled at the moment // if (chbAutoLogin.isSelected()) { // userConfig.setPassword(getPassword()); // userConfig.setPin(getPin()); // userConfig.setAutoLogin(true); // } else { // userConfig.setPassword(""); // userConfig.setPin(""); // userConfig.setAutoLogin(false); // } } catch (IOException ioex) { logger.warn("Could not save login settings: {}", ioex.getMessage()); setError("Could not save login settings."); } } private void installProgressIndicator() { Platform.runLater(() -> { // center indicator with respect to the grid double xOffset = piProgress.getWidth() / 2.0; double yOffset = piProgress.getHeight() / 2.0; double x = grdForm.getWidth() / 2.0 - xOffset; double y = grdForm.getHeight() / 2.0 - yOffset; piProgress.relocate(x, y); // show piProgress.setVisible(true); }); } private void uninstallProgressIndicator() { Platform.runLater(() -> { piProgress.setVisible(false); }); } @FXML public void registerAction(ActionEvent event) { logger.debug("Navigate to register view."); fNavigationService.navigate(ViewNames.REGISTER_VIEW); } @FXML public void changeRootPathAction(ActionEvent event) { String path = getRootPathAsString(); Window toOpenDialog = grdForm.getScene().getWindow(); path = SelectRootPathUtils.showDirectoryChooser(path, toOpenDialog); txtRootPath.setText(path); } @FXML public void navigateBackAction(ActionEvent event) { logger.debug("Navigate back."); fNavigationService.navigateBack(); } private void hideWindow() { Runnable close = new Runnable() { @Override public void run() { // not not quit application when stage closes Platform.setImplicitExit(false); // hide stage Stage stage = (Stage) grdForm.getScene().getWindow(); stage.close(); } }; if (Platform.isFxApplicationThread()) { close.run(); } else { Platform.runLater(close); } } private String getUsername() { return txtUsername.getText().trim(); } private String getPassword() { return txtPassword.getText(); } private String getPin() { return txtPin.getText(); } private Path getRootPath() { return Paths.get(getRootPathAsString()); } private String getRootPathAsString() { return txtRootPath.getText(); } private void setError(String error) { lblError.setText(error); } private void clearError() { lblError.setText(""); } }