package org.peerbox.watchservice;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Set;
import net.engio.mbassy.listener.Handler;
import org.apache.commons.io.FileUtils;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.hive2hive.core.events.framework.interfaces.IFileEventListener;
import org.hive2hive.core.events.framework.interfaces.file.IFileAddEvent;
import org.hive2hive.core.events.framework.interfaces.file.IFileDeleteEvent;
import org.hive2hive.core.events.framework.interfaces.file.IFileMoveEvent;
import org.hive2hive.core.events.framework.interfaces.file.IFileShareEvent;
import org.hive2hive.core.events.framework.interfaces.file.IFileUpdateEvent;
import org.hive2hive.core.events.implementations.FileAddEvent;
import org.hive2hive.core.model.PermissionType;
import org.hive2hive.core.model.UserPermission;
import org.peerbox.app.manager.file.FileInfo;
import org.peerbox.app.manager.file.IFileMessage;
import org.peerbox.app.manager.file.messages.FileExecutionStartedMessage;
import org.peerbox.app.manager.file.messages.LocalFileSoftDeleteMessage;
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.events.MessageBus;
import org.peerbox.forcesync.ForceSyncCompleteMessage;
import org.peerbox.forcesync.ForceSyncMessage;
import org.peerbox.forcesync.IForceSyncHandler;
import org.peerbox.notifications.InformationNotification;
import org.peerbox.watchservice.filetree.FileTree;
import org.peerbox.watchservice.filetree.IFileTree;
import org.peerbox.watchservice.filetree.composite.FileComponent;
import org.peerbox.watchservice.filetree.composite.FolderComposite;
import org.peerbox.watchservice.states.StateType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
/**
* The FileEventManager forms the glue between the events delivered by the
* {@link org.peerbox.watchservice.FolderWatchService FolderWatchService} and
* the PeerWasp core, in which the state for each file is maintained. To fulfill
* this purpose, the FileEventManager provides a set of event handlers, which are
* used by the {@link org.peerbox.watchservice.FolderWatchService FolderWatchService}
* or other code parts (like the GUI) to forward the events to an {@link org.peerbox.
* watchservice.Action Action} object coupled to a file. Depending on the type of
* the event, additional measures may be taken into consideration, like applying events
* recursively in case the triggering object is a folder.
*
* @author Claudio
*/
@Singleton
public class FileEventManager implements IFileEventManager, ILocalFileEventListener, IFileEventListener {
private static final Logger logger = LoggerFactory.getLogger(FileEventManager.class);
/**
* This queue contains FileComponents on which local or remote events happened that require
* some kind of network operation. The objects can be picked from the queu when no new events
* occured for a specified time. Check {@link org.peerbox.watchservice.FileComponentQueue}
*/
private final FileComponentQueue fileComponentQueue;
/**
* Represents the file system view from the perspective
* of PeerWasp, which is influenced by local and remote file events.
*/
private final FileTree fileTree;
/** Used to publish important events system-wide.*/
private final MessageBus messageBus;
/**
* If the execution of an {@link org.peerbox.watchservice.Action Action}
* definitely fails (i.e. repeatedly until the maximal number of attempts to
* re-execute is reached), the path is added to this set. As soon as the PeerWasp
* retries to execute it or clean it up, the file is removed again. This set is
* important to correctly represent failed operations in the {@link org.peerbox.
* presenter.settings.synchronization.Synchronization Synchronzation}.
*/
private final Set<Path> failedOperations;
// private final Set<Path> sharedFolders;
private boolean cleanupRunning;
private Set<Path> pendingEvents = new ConcurrentHashSet<Path>();
private Provider<IForceSyncHandler> forceSyncHandlerProvider;
private IForceSyncHandler forceSyncHandler;
/**
*
* @param fileTree The file tree representation of PeerWasp
* @param messageBus To publish events system-wide
*/
@Inject
public FileEventManager(final FileTree fileTree, MessageBus messageBus) {
this.fileComponentQueue = new FileComponentQueue();
this.fileTree = fileTree;
this.messageBus = messageBus;
this.failedOperations = new ConcurrentHashSet<Path>();
}
@Inject
public void setForceSyncHandlerProvider(Provider<IForceSyncHandler> forceSyncHandlerProvider){
this.forceSyncHandlerProvider = forceSyncHandlerProvider;
forceSyncHandlerProvider.get();
}
public IForceSyncHandler getForceSyncHandler(){
if(forceSyncHandler == null){
forceSyncHandler = forceSyncHandlerProvider.get();
}
return forceSyncHandler;
}
/**
* Handles incoming create events. First of all, it gets or creates the
* corresponding {@link org.peerbox.watchservice.filetree.composite.FileComponent
* FileComponent} from the {@link #fileTree} and markes it as synchronized.
*
* If the created component is a folder, check if the operation is part of a
* move operation by checking the folder's
* structure hash. Otherwise, make a complete content discovery.
*
* If it is a file, check if a move based on file content is possible to trigger
* a conventional move operation, otherwise just handle
* the event as a conventional create.
*
* Assumptions:
* - The file exists, hence this handler is not invoked manually if the file has been
* deleted before.
*/
@Override
public void onLocalFileCreated(final Path path) {
if(cleanupRunning){
pendingEvents.add(path);
return;
}
logger.debug("onLocalFileCreated: {} - Manager ID {}", path, hashCode());
final boolean isFolder = Files.isDirectory(path);
final FileComponent file = fileTree.getOrCreateFileComponent(path, this);
// if(file.isUploaded() && !file.isSynchronized() && file.getAction().getCurrentState() instanceof InitialState){
// logger.trace("The file {} has already been uploaded and was recreated. Resolve conflict.");
// ConflictHandler.resolveConflict(path, true);
// return;
// }
file.setIsSynchronized(true);
if (isFolder) {
String structureHash = fileTree.discoverSubtreeStructure(path, this);
((FolderComposite)file).setStructureHash(structureHash);
}
file.updateContentHash();
file.getAction().handleLocalCreateEvent();
//check if it is a folder and the move detection did not work:
if (isFolder && fileTree.getFile(path).getAction().getCurrentState().getStateType() != StateType.INITIAL) {
logger.trace("No move detected after folder {} was created. Initiate complete discovery.", path);
fileTree.discoverSubtreeCompletely(path, this);
}
}
/**
* Used to handle local update events. The event is ignored if at least one of the
* following requirements is met: The object does not exist on disk, the object is
* a folder, or the objects content hash did not change. Otherwise, the event is forwarded
* to the core.
*/
@Override
public void onLocalFileModified(final Path path) {
if(cleanupRunning){
pendingEvents.add(path);
return;
}
logger.debug("onLocalFileModified: {}", path);
if(!Files.exists(path) || Files.isDirectory(path)){
logger.trace("File {} does not exist on disk or is a folder, discard local update", path);
return;
}
final FileComponent file = fileTree.getOrCreateFileComponent(path, this);
if (file.isFolder()) {
logger.debug("File {} is a folder. Update rejected.", path);
return;
}
boolean hasChanged = file.updateContentHash();
if (!hasChanged) {
logger.debug("Content hash did not change for file {}. Update rejected.", path);
return;
}
file.getAction().handleLocalUpdateEvent();
}
/**
* Forwards the local delete event to the core. Additionally, it publishes a {@link
* org.peerbox.app.manager.file.messages.LocalFileSoftDeleteMessage LocalFileDesyncMessage} using
* the {@link #messageBus} to inform GUI components.
*/
@Override
public void onLocalFileDeleted(final Path path) {
if(cleanupRunning){
pendingEvents.add(path);
return;
}
logger.debug("onLocalFileDelete: {}", path);
final FileComponent file = fileTree.getOrCreateFileComponent(path, this);
if (file.isFolder()) {
logger.debug("onLocalFileDelete: structure hash of {} is '{}'",
path, ((FolderComposite)file).getStructureHash());
}
publishMessage(new LocalFileSoftDeleteMessage(new FileInfo(file)));
file.getAction().handleLocalSoftDeleteEvent();
}
/**
* Triggered by the user using the Windows Explorer context menu option "PeerWasp->Delete" or
* the "Delete from network" option in the context menu of the view "Settings->Synchronization".
* Completely deletes a file from the network such that it is not recoverable anymore.
*/
@Override
public void onLocalFileHardDelete(final Path path) {
if(cleanupRunning){
return;
}
logger.debug("onLocalFileHardDelete: {} - Manager ID {}", path, hashCode());
final FileComponent file = fileTree.getOrCreateFileComponent(path, this);
file.getAction().handleLocalHardDeleteEvent();
}
/**
* Triggered by the user using the view "Settings->Synchronization". By unchecking checkboxes,
* items can be soft-deleted, which is done by this event handler. This handler deletes the
* corresponding file or folder recursively.
*/
@Override
public void onFileSoftDeleted(final Path path) {
if (cleanupRunning) {
return;
}
logger.debug("onFileDesynchronized: {}", path);
final FileComponent file = fileTree.getFile(path);
if (file != null) {
file.setIsSynchronized(false);
FileUtils.deleteQuietly(path.toFile());
} else {
logger.error("onFileDesynchronized: Did not find file component: {}", path);
}
}
/**
* Triggered by the user using the view "Settings->Synchronization". By checking checkboxes,
* soft-deleted items can be restored by downloading them again.
*/
@Override
public void onFileSynchronized(final Path path, boolean isFolder) {
if(cleanupRunning){
return;
}
logger.debug("onFileSynchronized: {}", path);
// is this even required if we do onFileAdd later?
final FileComponent file = fileTree.getOrCreateFileComponent(path, !isFolder, this);
// if (file.isSynchronized()) {
// logger.trace("File {} is still synchronized, return!", path);
// return;
// }
fileTree.putFile(path, file);
// FileCompositeUtils.setIsUploadedWithAncestors(file, true);
file.setIsSynchronized(true);
onFileAdd(new FileAddEvent(path.toFile(), isFolder));
}
private boolean hasSynchronizedAncestor(final Path path) {
// FIXME: maybe stop when rootPath is reached...!
FileComponent file = fileTree.getFile(path);
if (file == null) {
// logger.trace("checkForSynchronizedAncestor: Did not find {}", path);
return hasSynchronizedAncestor(path.getParent());
} else {
// logger.trace("checkForSynchronizedAncestor: {} isSynchronized({})", path, file.isSynchronized());
// return hasSynchronizedAncestor(path.getParent());
return file.isSynchronized();
}
}
/**
* This handler is for remote create events and is called by the network when
* new files are recognized. The file is only downloaded if it has an ancestor
* in the {@link #fileTree} that is existing and synchronized. Otherwise, the
* event is ignored.
*/
@Override
@Handler
public synchronized void onFileAdd(final IFileAddEvent fileEvent){
if(cleanupRunning){
pendingEvents.add(fileEvent.getFile().toPath());
return;
}
final Path path = fileEvent.getFile().toPath();
logger.debug("onFileAdd: {}", path);
final FileComponent file = fileTree.getOrCreateFileComponent(path, fileEvent.isFile(), this);
file.getAction().setFile(file);
file.getAction().setFileEventManager(this);
logger.trace("file {} has ID {}", path, file.hashCode());
if (!hasSynchronizedAncestor(path)) {
logger.debug("File {} is in folder that is not synchronized. Event ignored.", path);
file.setIsSynchronized(false);
getMessageBus().publish(new FileExecutionStartedMessage(new FileInfo(file), StateType.INITIAL));
//return;
} else {
logger.debug("File {} is in folder that is synchronized.", path);
file.setIsSynchronized(true);
file.getAction().handleRemoteCreateEvent();
}
}
/**
* This handler is for remote delete events and is called by the network when
* a file has been definitely deleted. Besides forwarding the event to the core,
* this method publishes a {@link org.peerbox.app.manager.file.messages.RemoteFileDeletedMessage
* RemoteFileDeletedMessage} to notify the GUI.
*/
@Override
@Handler
public void onFileDelete(final IFileDeleteEvent fileEvent) {
if(cleanupRunning){
pendingEvents.add(fileEvent.getFile().toPath());
return;
}
final Path path = fileEvent.getFile().toPath();
logger.debug("onFileDelete: {}", path);
final FileComponent file = fileTree.getOrCreateFileComponent(path, fileEvent.isFile(), this);
file.getAction().handleRemoteDeleteEvent();
FileInfo fileHelper = new FileInfo(file);
messageBus.publish(new RemoteFileDeletedMessage(fileHelper));
}
/**
* This handler is for remote update events and is called by the network when
* a file has been changed remotely. This method only forwards the event to the core.
*/
@Override
@Handler
public void onFileUpdate(final IFileUpdateEvent fileEvent) {
if(cleanupRunning){
pendingEvents.add(fileEvent.getFile().toPath());
return;
}
final Path path = fileEvent.getFile().toPath();
logger.debug("onFileUpdate: {}", path);
final FileComponent file = fileTree.getOrCreateFileComponent(path, this);
file.getAction().handleRemoteUpdateEvent();
}
/**
* This handler is for remote move events and is called by the network when
* a file has been moved remotely. This method forwards the event to the core and
* publishes a {@link org.peerbox.app.manager.file.messages.RemoteFileMovedMessage
* RemoteFileMovedMessage} to inform the GUI.
*/
@Override
@Handler
public void onFileMove(final IFileMoveEvent fileEvent) {
if(cleanupRunning){
pendingEvents.add(fileEvent.getSrcFile().toPath());
pendingEvents.add(fileEvent.getDstFile().toPath());
return;
}
final Path srcPath = fileEvent.getSrcFile().toPath();
final Path dstPath = fileEvent.getDstFile().toPath();
logger.debug("onFileMove: {} -> {}", srcPath, dstPath);
final FileComponent source = fileTree.getOrCreateFileComponent(srcPath, this);
source.getAction().handleRemoteMoveEvent(dstPath);
FileInfo srcFile = new FileInfo(srcPath, fileEvent.isFolder());
FileInfo dstFile = new FileInfo(dstPath, fileEvent.isFolder());
messageBus.publish(new RemoteFileMovedMessage(srcFile, dstFile));
}
@Override
@Handler
public void onFileShare(IFileShareEvent fileEvent) {
String permissionStr = "";
for (UserPermission p : fileEvent.getUserPermissions()) {
permissionStr = permissionStr.concat(p.getUserId() + " ");
if(p.getPermission() == PermissionType.READ){
permissionStr = permissionStr.concat("Read");
} else {
permissionStr = permissionStr.concat("Read / Write");
}
}
logger.info("Share: Invited by: {}, Permission: [{}]", fileEvent.getInvitedBy(), permissionStr, fileEvent.getFile());
fileEvent.getInvitedBy();
Set<UserPermission> permissions = fileEvent.getUserPermissions();
String invitedBy = fileEvent.getInvitedBy();
FileInfo file = new FileInfo(fileEvent);
StringBuilder sb = new StringBuilder();
sb.append("User ").append(invitedBy).append(" shared the folder ").
append(fileEvent.getFile().toPath()).append(" with you.");
getMessageBus().post(new InformationNotification("Shared folder", sb.toString())).now();
publishMessage(new RemoteShareFolderMessage(file, permissions, invitedBy));
}
/**
* @return The {@link #fileComponentQueue}.
*/
@Override
public FileComponentQueue getFileComponentQueue() {
return fileComponentQueue;
}
/**
* @return The {@link #fileTree}.
*/
@Override
public synchronized IFileTree getFileTree() {
return fileTree;
}
/**
* @return The {@link #failedOperations} containing the {@link java.nio.file.Path
* Path}s of all failed Actions.
*/
@Override
public Set<Path> getFailedOperations(){
return failedOperations;
}
/**
* @return The {@link #messageBus} used to publish events system-wide.
*/
public MessageBus getMessageBus() {
return messageBus;
}
private void publishMessage(IFileMessage message) {
if (messageBus != null) {
messageBus.publish(message);
} else {
logger.warn("No message sent, as message bus is null!");
}
}
@Handler
public void onForceSync(ForceSyncMessage message){
logger.trace("onForceSync: Block events and clear fileComponentQueue {}", message.getTopLevel());
setCleanupRunning(true);
fileComponentQueue.clear();
}
@Handler
public void onForceSyncComplete(ForceSyncCompleteMessage message){
logger.trace("Forced synchronization: Event block removed.");
setCleanupRunning(false);
}
public void setCleanupRunning(boolean b) {
cleanupRunning = b;
}
public Set<Path> getPendingEvents() {
return pendingEvents;
}
public void initiateForceSync(Path topLevel){
logger.trace("PeerWasp initiated a force synchronization automatically on {}", topLevel);
IForceSyncHandler handler = getForceSyncHandler();
if(handler != null){
handler.forceSync( topLevel);
}
getMessageBus().publish(new InformationNotification("Forced synchronization", "Try to restore consistency"));
}
}