package org.peerbox.presenter.settings.synchronization; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.ResourceBundle; import java.util.Set; import java.util.TreeSet; import java.util.stream.Collectors; import javafx.util.Callback; import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.fxml.Initializable; import javafx.scene.control.Button; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.control.CheckBoxTreeItem; import javafx.stage.Window; import javafx.scene.control.TreeCell; import net.engio.mbassy.listener.Handler; import org.hive2hive.core.exceptions.NoPeerConnectionException; import org.hive2hive.core.exceptions.NoSessionException; import org.hive2hive.core.model.PermissionType; import org.hive2hive.core.model.UserPermission; import org.hive2hive.core.processes.files.list.FileNode; import org.hive2hive.processframework.exceptions.InvalidProcessStateException; import org.hive2hive.processframework.exceptions.ProcessExecutionException; import org.peerbox.app.config.UserConfig; import org.peerbox.app.manager.file.FileInfo; import org.peerbox.app.manager.file.IFileManager; import org.peerbox.app.manager.file.messages.FileExecutionFailedMessage; import org.peerbox.app.manager.file.messages.FileExecutionStartedMessage; import org.peerbox.app.manager.file.messages.FileExecutionSucceededMessage; import org.peerbox.app.manager.file.messages.LocalFileSoftDeleteMessage; import org.peerbox.app.manager.file.messages.LocalShareFolderMessage; import org.peerbox.app.manager.file.messages.RemoteFileDeletedMessage; import org.peerbox.app.manager.file.messages.RemoteFileMovedMessage; import org.peerbox.app.manager.file.messages.RemoteShareFolderMessage; import org.peerbox.filerecovery.IFileRecoveryHandler; import org.peerbox.forcesync.IForceSyncHandler; import org.peerbox.share.IShareFolderHandler; import org.peerbox.watchservice.FileEventManager; import org.peerbox.watchservice.IFileEventManager; import org.peerbox.watchservice.states.StateType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.Inject; import com.google.inject.Provider; /** * This is the presenter class controlling the view "Settings > Synchronization". * @author Claudio * */ public class Synchronization implements Initializable, IExecutionMessageListener{ private static final Logger logger = LoggerFactory.getLogger(Synchronization.class); @FXML private TreeView<PathItem> fileTreeView; @FXML private Button okButton; @FXML private Button cancelButton; @FXML private Button selectAllButton; @FXML private Button unselectAllButton; @FXML private Button refreshButton; private IFileEventManager eventManager; private IFileManager fileManager; /** * These variables are used to build sets containing the paths of files * and folder whose synchronization state has to be changed in the {@link * org.peerbox.watchservice.FileEventManager FileEventManager} */ private TreeSet<FileInfo> toSynchronize = new TreeSet<FileInfo>(new FileInfoComparator()); private TreeSet<FileInfo> toDesynchronize = new TreeSet<FileInfo>(new FileInfoComparator()); private UserConfig userConfig; /** * These variables are build directly from the {@link org.peerbox. * watchservice.FileEventManager FileEventManager} to correctly * populate the TreeView */ // private Set<Path> synchronizedFiles; private Set<Path> failedFiles = new HashSet<Path>(); private Set<Path> executingFiles = new HashSet<Path>(); private final Provider<IShareFolderHandler> shareFolderHandlerProvider; private final Provider<IFileRecoveryHandler> recoverFileHandlerProvider; private final Provider<IForceSyncHandler> forceSyncHandlerProvider; @Inject public Synchronization(IFileManager fileManager, FileEventManager eventManager, UserConfig userConfig, Provider<IFileRecoveryHandler> recoverFileHandlerProvider, Provider<IShareFolderHandler> shareFolderHandlerProvider, Provider<IForceSyncHandler> forceSyncHandlerProvider) { this.eventManager = eventManager; this.fileManager = fileManager; this.userConfig = userConfig; this.recoverFileHandlerProvider = recoverFileHandlerProvider; this.shareFolderHandlerProvider = shareFolderHandlerProvider; this.forceSyncHandlerProvider = forceSyncHandlerProvider; } public Set<FileInfo> getToSynchronize(){ return toSynchronize; } public Set<FileInfo> getToDesynchronize(){ return toDesynchronize; } public IFileEventManager getFileEventManager(){ return eventManager; } @Override public void initialize(URL arg0, ResourceBundle arg1) { logger.debug("Initialize Synchronization!"); // synchronizedFiles = getFileEventManager().getFileTree().getSynchronizedPathsAsSet(); createTreeViewFromNetwork(); } /** * This method is bound to the fxml of the view and is invoked * by clicks on the "Accept" button. * @param event that was fired. */ @FXML public void acceptSyncAction(ActionEvent event) { // synchronizedFiles = eventManager.getFileTree().getSynchronizedPathsAsSet(); for(FileInfo node : toSynchronize){ if(!Files.exists(node.getPath())) eventManager.onFileSynchronized(node.getPath(), node.isFolder()); } for(FileInfo node: toDesynchronize.descendingSet()){ eventManager.onFileSoftDeleted(node.getPath()); } toSynchronize.clear(); toDesynchronize.clear(); if(event.getTarget() != null && event.getTarget() instanceof Button){ Button okButton = (Button)event.getTarget(); Window window = okButton.getScene().getWindow(); window.hide(); } } /** * This method is bound to the fxml of the view and is invoked * by clicks on the "Select All" button. * @param event that was fired. */ @FXML public void selectAllAction(ActionEvent event) { SyncTreeItem root = (SyncTreeItem)fileTreeView.getRoot(); root.setSelected(true); } /** * This method is bound to the fxml of the view and is invoked * by clicks on the "Unselect all" button. * @param event that was fired. */ @FXML public void unselectAllAction(ActionEvent event) { SyncTreeItem root = (SyncTreeItem)fileTreeView.getRoot(); root.setSelected(true); // surprisingly, only setting to false has no effect! root.setSelected(false); } /** * This method is bound to the fxml of the view and is invoked * by clicks on the "Cancel" button. * @param event that was fired. */ @FXML public void cancelAction(ActionEvent event) { if(event.getTarget() != null && event.getTarget() instanceof Button){ Button cancelButton = (Button)event.getTarget(); Window window = cancelButton.getScene().getWindow(); window.hide(); } } /** * This method is bound to the fxml of the view and is invoked * by clicks on the "Refresh" button. * @param event that was fired. */ @FXML public void refreshAction(ActionEvent event){ // synchronizedFiles = getFileEventManager().getFileTree().getSynchronizedPathsAsSet(); failedFiles = getFileEventManager().getFailedOperations(); createTreeViewFromNetwork(); } /** * This handler is automatically invoked when a {@link org.peerbox.app.manager.file.messages.FileExecutionStartedMessage * FileExecutionStartedMessage} is published using the {@link org.peerbox. * events.MessageBus MessageBus}. This method changes the corresponding * {@link javafx.scene.control.CheckBoxTreeItem CheckBoxTreeItem} in the * {@link javafx.scene.control.TreeView TreeView} accordingly. */ @Override @Handler public void onExecutionStarts(FileExecutionStartedMessage message) { logger.trace("onExecutionStarts: {}", message.getFile().getPath()); SyncTreeItem item = getOrCreateItem(message.getFile(), false); item.setProgressState(ProgressState.IN_PROGRESS); } /** * This handler is automatically invoked when a {@link org.peerbox.app.manager.file.messages.RemoteFileDeletedMessage RemoteFileDeletedMessage} is * published using the {@link org.peerbox.events.MessageBus MessageBus}. * This method changes the corresponding {@link javafx.scene.control. * CheckBoxTreeItem CheckBoxTreeItem} in the {@link javafx.scene.control. * TreeView TreeView} accordingly. */ @Override @Handler public void onFileRemotelyDeleted(RemoteFileDeletedMessage message){ logger.trace("onFileRemotelyDeleted: {}", message.getFile().getPath()); javafx.application.Platform.runLater(new Runnable() { @Override public void run() { removeTreeItem(message.getFile().getPath()); } }); } /** * This handler is automatically invoked when a {@link org.peerbox.app.manager.file.messages.RemoteFileMovedMessage RemoteFileMovedMessage} is * published using the {@link org.peerbox.events.MessageBus MessageBus}. * This method changes the corresponding {@link javafx.scene.control. * CheckBoxTreeItem CheckBoxTreeItem} in the {@link javafx.scene.control. * TreeView TreeView} accordingly. */ @Override @Handler public void onFileRemotelyMoved(RemoteFileMovedMessage message){ Path srcFile = message.getSourceFile().getPath(); Path dstFile = message.getDestinationFile().getPath(); logger.trace("onFileRemotelyMoved: {} --> {}", srcFile, dstFile); removeTreeItemInUIThread(srcFile); SyncTreeItem item = getOrCreateItem(message.getFile(), true); item.setProgressState(ProgressState.SUCCESSFUL); // updateIsSelectedInUIThread(item, message.getFile(), true); item.updateIsSelectedInUIThread(true); } /** * This handler is automatically invoked when a {@link org.peerbox.app.manager.file.messages.FileExecutionSucceededMessage * FileExecutionSucceededMessage} is published using the {@link org.peerbox. * events.MessageBus MessageBus}. This method changes the corresponding * {@link javafx.scene.control. CheckBoxTreeItem CheckBoxTreeItem} in the * {@link javafx.scene.control.TreeView TreeView} accordingly. */ @Override @Handler public void onExecutionSucceeds(FileExecutionSucceededMessage message) { logger.trace("Synchronization: onExecutionSucceeds: {}", message.getFile().getPath()); StateType stateType = message.getStateType(); switch(stateType){ case LOCAL_HARD_DELETE: removeTreeItemInUIThread(message.getFile().getPath()); break; case INITIAL: SyncTreeItem item = getTreeItem(message.getFile().getPath()); if(item != null){ item.setProgressState(ProgressState.SUCCESSFUL); } break; case LOCAL_MOVE: Path srcFile = message.getSourceFile().getPath(); // Path dstFile = message.getFile().getPath(); removeTreeItemInUIThread(srcFile); item = getOrCreateItem(message.getFile(), true); item.setProgressState(ProgressState.SUCCESSFUL); item.updateIsSelectedInUIThread(true); // updateIsSelectedInUIThread(item, message.getFile(), true); break; default: item = getOrCreateItem(message.getFile(), true); item.setProgressState(ProgressState.SUCCESSFUL); // updateIsSelectedInUIThread(item, message.getFile(), true); item.updateIsSelectedInUIThread(true); } } /** * This handler is automatically invoked when a {@link org.peerbox.app.manager.file.messages.FileExecutionFailedMessage * FileExecutionFailedMessage} is published using the {@link org.peerbox. * events.MessageBus MessageBus}. This method changes the corresponding * {@link javafx.scene.control. CheckBoxTreeItem CheckBoxTreeItem} in the * {@link javafx.scene.control.TreeView TreeView} accordingly. */ @Override @Handler public void onExecutionFails(FileExecutionFailedMessage message) { logger.trace("onExecutionFails: {}", message.getFile().getPath()); SyncTreeItem item = getOrCreateItem(message.getFile(), false); item.setProgressState(ProgressState.FAILED); final SyncTreeItem item2 = item; javafx.application.Platform.runLater(new Runnable() { @Override public void run() { item2.setSelected(true); } }); } /** * This handler is automatically invoked when a {@link org.peerbox.app.manager.file.messages.LocalFileSoftDeleteMessage LocalFileDesyncMessage} is published * using the {@link org.peerbox.events.MessageBus MessageBus}. This method * changes the corresponding {@link javafx.scene.control. CheckBoxTreeItem * CheckBoxTreeItem} in the {@link javafx.scene.control.TreeView TreeView} * accordingly. */ @Override @Handler public void onFileSoftDeleted(LocalFileSoftDeleteMessage message) { logger.trace("onFileSoftDeleted: {}", message.getFile().getPath()); SyncTreeItem item = getTreeItem(message.getFile().getPath()); item.setProgressState(ProgressState.DEFAULT); final SyncTreeItem item2 = item; javafx.application.Platform.runLater(new Runnable() { @Override public void run() { item2.setSelected(false); } }); } @Override @Handler public void onRemoteFolderShared(RemoteShareFolderMessage message) { logger.trace("onRemoteFolderShared: {}", message.getFile().getPath()); SyncTreeItem item = getOrCreateItem(message.getFile(), false); item.getValue().getPermissions().clear(); item.getValue().getPermissions().addAll(message.getUserPermissions()); item.setIsShared(true); } @Override @Handler public void onLocalFolderShared(LocalShareFolderMessage message) { logger.trace("onLocalFolderShared: {}", message.getFile().getPath()); SyncTreeItem item = getOrCreateItem(message.getFile(), false); item.getValue().getPermissions().add(message.getInvitedUserPermission()); item.setIsShared(true); } private void removeTreeItemInUIThread(Path srcFile) { javafx.application.Platform.runLater(new Runnable() { @Override public void run() { removeTreeItem(srcFile); } }); } private SyncTreeItem getOrCreateItem(FileInfo file, boolean isSynched) { SyncTreeItem item = getTreeItem(file.getPath()); if(item != null){ logger.trace("item != null for {}, change icon!", file.getPath()); } else { item = createItem(file.getPath(), true, file.isFile()); putTreeItem(item); logger.trace("item == null for {}", file.getPath()); } return item; } private SyncTreeItem getTreeItem(Path path){ SyncTreeItem root = (SyncTreeItem)fileTreeView.getRoot(); return findTreeItem(root, path, false); } private void putTreeItem(SyncTreeItem item){ SyncTreeItem root = (SyncTreeItem)fileTreeView.getRoot(); Path prefix = root.getValue().getPath(); Path pathLeft = prefix.relativize(item.getValue().getPath()); putTreeItem(root, item, pathLeft); return; } private SyncTreeItem removeTreeItem(Path path){ SyncTreeItem root = (SyncTreeItem)fileTreeView.getRoot(); return findTreeItem(root, path, true); } private SyncTreeItem findTreeItem(SyncTreeItem item, Path path, boolean remove){ Path wholePath = path; Path prefix = item.getValue().getPath(); if(path.startsWith(prefix)){ path = prefix.relativize(wholePath); } if(path.equals(Paths.get(""))){ if(remove){ item.getParent().getChildren().remove(item); item.unbind(); } return item; } for(TreeItem<PathItem> child : item.getChildren()){ SyncTreeItem castedChild = (SyncTreeItem)child; Path childNextLevel = prefix.relativize(child.getValue().getPath()).getName(0); if(childNextLevel.equals(path.getName(0))){ return findTreeItem(castedChild, wholePath, remove); } } return null; } private void putTreeItem(SyncTreeItem parent, SyncTreeItem toPut, Path pathLeft){ Path parentPath = parent.getValue().getPath(); Path wholePath = parentPath.resolve(pathLeft); if(pathLeft.getNameCount() == 1){ toPut.addEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), new ClickEventHandler(this)); Iterator<TreeItem<PathItem>> iter = parent.getChildren().iterator(); while(iter.hasNext()){ if(iter.next().getValue().getPath().equals(wholePath)){ iter.remove(); } } parent.getChildren().add(toPut); if(!toPut.isSelected() || !toPut.isIndeterminate()){ parent.updateIsSelectedInUIThread(true); } toPut.bindTo(parent); return; } else { Iterator<TreeItem<PathItem>> iter = parent.getChildren().iterator(); Path nextLevel = pathLeft.getName(0); Path pathToSearch = parentPath.resolve(nextLevel); while(iter.hasNext()){ SyncTreeItem child = (SyncTreeItem)iter.next(); if(child.getValue().getPath().equals(pathToSearch)){ putTreeItem(child, toPut, child.getValue().getPath().relativize(wholePath)); return; } } PathItem pathItem = new PathItem(pathToSearch, toPut.isSelected(), null); SyncTreeItem created = new SyncTreeItem(pathItem); toPut.addEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), new ClickEventHandler(this)); parent.getChildren().add(toPut); toPut.bindTo(parent); putTreeItem(created, toPut, toPut.getValue().getPath().relativize(wholePath)); } } private void createTreeViewFromNetwork() { try { FileNode filesFromNetwork = fileManager.listFiles().execute(); if(filesFromNetwork != null){ createTreeView(filesFromNetwork); } else { logger.trace("Files from network are null"); } } catch (NoSessionException | NoPeerConnectionException | InvalidProcessStateException | ProcessExecutionException e) { e.printStackTrace(); } } private void createTreeView(FileNode fileNode){ PathItem pathItem = new PathItem(userConfig.getRootPath(), false, fileNode.getUserPermissions()); SyncTreeItem invisibleRoot = new SyncTreeItem(pathItem); // invisibleRoot.setIndependent(true); fileTreeView.setRoot(invisibleRoot); fileTreeView.setEditable(false); fileTreeView.setCellFactory(new Callback<TreeView<PathItem>, TreeCell<PathItem>>(){ @Override public TreeCell<PathItem> call(TreeView<PathItem> p) { return new CustomizedTreeCell(getFileEventManager(), recoverFileHandlerProvider, shareFolderHandlerProvider, forceSyncHandlerProvider); } }); fileTreeView.setShowRoot(false); addChildrensToTreeView(fileNode); } private void addChildrensToTreeView(FileNode fileNode){ if(fileNode.getChildren() != null){ List<FileNode> sortedChildren = fileNode.getChildren().stream(). sorted((f1, f2) -> f1.getFile().compareTo(f2.getFile())). collect(Collectors.<FileNode>toList()); for(FileNode topLevelNode : sortedChildren){ Path path = topLevelNode.getFile().toPath(); boolean isSynched = Files.exists(path); //synchronizedFiles.contains(path); ProgressState stateToSet = ProgressState.DEFAULT; if(failedFiles.contains(path)){ stateToSet = ProgressState.FAILED; } else if(executingFiles.contains(path)){ stateToSet = ProgressState.IN_PROGRESS; } else { stateToSet = ProgressState.SUCCESSFUL; } SyncTreeItem item = createItem(topLevelNode.getFile().toPath(), isSynched, topLevelNode.isFile(), topLevelNode.getUserPermissions()); if(topLevelNode.isShared()){ item.setIsShared(true); } putTreeItem(item); item.setProgressState(stateToSet); addChildrensToTreeView(topLevelNode); } } } private SyncTreeItem createItem(Path path, boolean isSynched, boolean isFile, Set<UserPermission> permissions) { PathItem pathItem = new PathItem(path, isFile, permissions); SyncTreeItem newItem = new SyncTreeItem(pathItem); newItem.updateIsSelectedInUIThread(isSynched); newItem.setSelected(isSynched); return newItem; } private SyncTreeItem createItem(Path path, boolean isSynched, boolean isFile) { Set<UserPermission> defaultPermissions = new HashSet<UserPermission>(); defaultPermissions.add(new UserPermission(userConfig.getUsername(), PermissionType.WRITE)); return createItem(path, isSynched, isFile, defaultPermissions); } private class FileInfoComparator implements Comparator<FileInfo> { @Override public int compare(FileInfo o1, FileInfo o2) { return o1.getPath().compareTo(o2.getPath()); } } }