package org.peerbox.filerecovery; import java.net.URL; import java.nio.file.Path; import java.nio.file.Paths; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.ResourceBundle; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.concurrent.Task; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.TableColumn; import javafx.scene.control.TableColumn.CellDataFeatures; import javafx.scene.control.TableView; import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; import javafx.util.Callback; import org.apache.commons.io.FileUtils; import org.controlsfx.control.StatusBar; import org.hive2hive.core.model.IFileVersion; import org.hive2hive.processframework.exceptions.InvalidProcessStateException; import org.hive2hive.processframework.exceptions.ProcessRollbackException; import org.peerbox.app.manager.ProcessHandle; import org.peerbox.app.manager.file.IFileManager; import org.peerbox.utils.DialogUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; /** * This controller is associated with the file recovery view. * * After setting {@link #setFileToRecover(Path)}, calling {@link #loadVersions()} starts * the recovery procedure. The overall procedure looks as follows: * * +----------+----------+ * | |CONTROLLER| * | +----->+----------+-----------+ * (4) select | | |(1) start process * version | |(3) versions | * v | (2) available v * +------+---+----+ versions +---+ * |VERSIONSELECTOR+<---------------------+H2H| * +------+--------+ +-+-+ * | ^ * | | * +---------------------+-----------+ * (5) selected version * * * The version selector receives available versions from H2H and waits (blocks) until the user * selects a version, which is then returned to H2H. * * * @author albrecht * */ public final class RecoverFileController implements Initializable, IFileVersionSelectorListener { private static final Logger logger = LoggerFactory.getLogger(RecoverFileController.class); @FXML private TableView<IFileVersion> tblFileVersions; @FXML private TableColumn<IFileVersion, Integer> tblColIndex; @FXML private TableColumn<IFileVersion, String> tblColDate; @FXML private TableColumn<IFileVersion, String> tblColSize; @FXML private Label lblNumberOfVersions; @FXML private Button btnRecover; @FXML private AnchorPane pane; // JavaFX Control from ControlsFX, but @FXML is not supported private StatusBar statusBar; private final BooleanProperty busyProperty; private final StringProperty fileToRecoverProperty; private final StringProperty statusProperty; private Path fileToRecover; private final ObservableList<IFileVersion> fileVersions; private final FileVersionSelector versionSelector; private final IFileManager fileManager; private RecoverFileTask recoverFileTask; @Inject public RecoverFileController(IFileManager fileManager) { this.fileManager = fileManager; this.fileToRecoverProperty = new SimpleStringProperty(); this.statusProperty = new SimpleStringProperty(); this.busyProperty = new SimpleBooleanProperty(false); this.fileVersions = FXCollections.observableArrayList(); this.versionSelector = new FileVersionSelector(this); } @Override public void initialize(URL location, ResourceBundle resources) { initializeTable(); initializeStatusBar(); lblNumberOfVersions.textProperty().bind(Bindings.size(fileVersions).asString()); btnRecover.disableProperty().bind( tblFileVersions.getSelectionModel().selectedItemProperty().isNull() .or(busyProperty)); } private void initializeStatusBar() { statusBar = new StatusBar(); pane.getChildren().add(statusBar); AnchorPane.setBottomAnchor(statusBar, 0.0); AnchorPane.setLeftAnchor(statusBar, 0.0); AnchorPane.setRightAnchor(statusBar, 0.0); busyProperty.addListener(new ChangeListener<Boolean>() { @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { if(newValue != null && newValue.booleanValue()) { statusBar.setProgress(-1); } else { statusBar.setProgress(0); } } }); statusBar.textProperty().bind(statusProperty); } private void initializeTable() { tblFileVersions.setItems(fileVersions); initializeColumns(); sortTable(); } private void initializeColumns() { tblColIndex.setCellValueFactory( new Callback<CellDataFeatures<IFileVersion, Integer>, ObservableValue<Integer>>() { public ObservableValue<Integer> call(CellDataFeatures<IFileVersion, Integer> p) { // p.getValue() returns the IFileVersion instance for a particular TableView row return new SimpleIntegerProperty(p.getValue().getIndex()).asObject(); } } ); tblColDate.setCellValueFactory( new Callback<CellDataFeatures<IFileVersion, String>, ObservableValue<String>>() { public ObservableValue<String> call(CellDataFeatures<IFileVersion, String> p) { Date date = new Date(p.getValue().getDate()); DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); String dateFormatted = formatter.format(date); return new SimpleStringProperty(dateFormatted); } } ); tblColSize.setCellValueFactory( new Callback<CellDataFeatures<IFileVersion, String>, ObservableValue<String>>() { public ObservableValue<String> call(CellDataFeatures<IFileVersion, String> p) { String humanReadableSize = FileUtils.byteCountToDisplaySize(p.getValue().getSize()); return new SimpleStringProperty(humanReadableSize); } } ); } private void sortTable() { // sorting by index DESC tblColIndex.setSortType(TableColumn.SortType.DESCENDING); tblFileVersions.getSortOrder().add(tblColIndex); tblFileVersions.sort(); } public void loadVersions() { if (fileToRecover == null || fileToRecover.toString().isEmpty()) { throw new IllegalArgumentException("fileToRecover not set, cannot be null or empty"); } recoverFileTask = new RecoverFileTask(fileToRecover); new Thread(recoverFileTask).start(); } @Override public void onAvailableVersionsReceived(final List<IFileVersion> availableVersions) { Runnable versions = new Runnable() { @Override public void run() { setBusy(false); if (availableVersions.isEmpty()) { setStatus("No versions available for this file."); } else { setStatus("Select version to recover"); fileVersions.addAll(availableVersions); setStatus(""); sortTable(); } } }; if (Platform.isFxApplicationThread()) { versions.run(); } else { Platform.runLater(versions); } } private void onFileRecoverySucceeded() { Runnable succeeded = new Runnable() { @Override public void run() { setBusy(false); setStatus(""); Alert dlg = DialogUtils.createAlert(AlertType.INFORMATION); dlg.setTitle("File Recovered"); dlg.setHeaderText("File recovery finished"); dlg.setContentText(String.format("The name of the recovered file is: %s", versionSelector.getRecoveredFileName())); dlg.showAndWait(); getStage().close(); } }; if (Platform.isFxApplicationThread()) { succeeded.run(); } else { Platform.runLater(succeeded); } } private void onFileRecoveryFailed(final String message) { Runnable failed = new Runnable() { @Override public void run() { setBusy(false); setStatus(""); if(!versionSelector.isCancelled()) { // show error if user did not initiate cancel action Alert dlg = DialogUtils.createAlert(AlertType.ERROR); dlg.setTitle("File Recovery Failed"); dlg.setHeaderText("File recovery did not succeed."); dlg.setContentText(message); dlg.showAndWait(); } getStage().close(); } }; if (Platform.isFxApplicationThread()) { failed.run(); } else { Platform.runLater(failed); } } public void recoverAction(ActionEvent event) { IFileVersion selectedVersion = tblFileVersions.getSelectionModel().getSelectedItem(); if(selectedVersion != null) { // only allow 1 recovery btnRecover.disableProperty().unbind(); btnRecover.setDisable(true); setBusy(true); setStatus("Downloading file..."); versionSelector.selectVersion(selectedVersion, fileToRecover); } } public void cancelAction(ActionEvent event) { cancel(); } public void cancel() { try { if(versionSelector != null) { versionSelector.cancel(); } if(recoverFileTask != null) { recoverFileTask.cancel(); } } finally { Platform.runLater(() -> { getStage().close(); }); } } public String getFileToRecover() { return fileToRecoverProperty.get(); } public void setFileToRecover(String fileName) { this.fileToRecover = Paths.get(fileName); this.fileToRecoverProperty.setValue(fileName); } public void setFileToRecover(final Path fileToRecover) { this.fileToRecover = fileToRecover; this.fileToRecoverProperty.setValue(fileToRecover.toString()); } public StringProperty fileToRecoverProperty() { return fileToRecoverProperty; } public String getStatus() { return statusProperty.get(); } public void setStatus(String status) { this.statusProperty.set(status); } public StringProperty statusProperty() { return statusProperty; } public Boolean getBusy() { return busyProperty.get(); } public void setBusy(Boolean busy) { this.busyProperty.set(busy); } public BooleanProperty busyProperty() { return busyProperty; } private Stage getStage() { return (Stage)tblFileVersions.getScene().getWindow(); } private class RecoverFileTask extends Task<Void> { private ProcessHandle<Void> process; private final Path fileToRecover; public RecoverFileTask(final Path fileToRecover) { this.fileToRecover = fileToRecover; } @Override protected Void call() throws Exception { try { setBusy(true); setStatus("Retrieving available versions..."); process = fileManager.recover(fileToRecover, versionSelector); process.execute(); } catch (Exception e) { logger.warn("Exception while recovering file: {}", e.getMessage(), e); throw e; } return null; } @Override protected void succeeded() { super.succeeded(); updateMessage("Done!"); onFileRecoverySucceeded(); } @Override protected void failed() { super.failed(); updateMessage("Failed!"); onFileRecoveryFailed("File Recovery Failed."); } @Override protected void cancelled() { super.cancelled(); updateMessage("Cancelled!"); try { process.getProcess().rollback(); } catch (InvalidProcessStateException | ProcessRollbackException e) { // ignore } } } }