package org.peerbox.watchservice.filetree;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.eclipse.jetty.util.ConcurrentHashSet;
import org.peerbox.watchservice.FileEventManager;
import org.peerbox.watchservice.FileWalker;
import org.peerbox.watchservice.IFileEventManager;
import org.peerbox.watchservice.filetree.composite.FileComponent;
import org.peerbox.watchservice.filetree.composite.FileLeaf;
import org.peerbox.watchservice.filetree.composite.FolderComposite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.inject.Inject;
import com.google.inject.Singleton;
@Singleton
public class FileTree implements IFileTree {
private static final Logger logger = LoggerFactory.getLogger(FileTree.class);
private FolderComposite rootOfFileTree;
private SetMultimap<String, FolderComposite> deletedByStructureHash = Multimaps.synchronizedSetMultimap(HashMultimap.create());
private SetMultimap<String, FolderComposite> createdByStructureHash = Multimaps.synchronizedSetMultimap(HashMultimap.create());
private SetMultimap<String, FileComponent> deletedByContentHash = Multimaps.synchronizedSetMultimap(HashMultimap.create());
private SetMultimap<String, FileComponent> createdByContentHash = Multimaps.synchronizedSetMultimap(HashMultimap.create());
private boolean maintainContentHashes;
@Inject
public FileTree(Path rootPath) {
this(rootPath, true);
}
/**
* @param rootPath is the root folder of the tree
* @param maintainContentHashes set to true if content hashes have to be maintained.
* Content hash changes are then propagated upwards to the parent directory.
*/
public FileTree(Path rootPath, boolean maintainContentHashes) {
this.maintainContentHashes = maintainContentHashes;
this.rootOfFileTree = new FolderComposite(rootPath, maintainContentHashes, true);
}
public boolean getMaintainContentHashes(){
return maintainContentHashes;
}
public FolderComposite getRootOfFileTree(){
return rootOfFileTree;
}
@Override
public void putFile(Path dstPath, FileComponent fileToPut) {
rootOfFileTree.putComponent(dstPath, fileToPut);
}
@Override
public FileComponent getFile(Path fileToGet) {
return rootOfFileTree.getComponent(fileToGet);
}
public FileComponent getOrCreateFileComponent(Path path, Boolean isFile, IFileEventManager eventManager) {
FileComponent file = getFile(path);
if(file == null){
logger.trace("FileComponent {} is new and now created.", path);
if(isFile == null){
logger.trace("FileComponent {} has no fileevent.", path);
file = createFileComponent(path, Files.isRegularFile(path));
} else {
logger.trace("FileComponent {} has a fileevent isfile= {}", path, isFile);
file = createFileComponent(path, isFile);
}
file.getAction().setFile(file);
file.getAction().setFileEventManager(eventManager);
}
logger.debug("File {} has state {}", file.getPath(), file.getAction().getCurrentStateName());
return file;
}
public FileComponent getOrCreateFileComponent(Path path, IFileEventManager eventManager){
return getOrCreateFileComponent(path, null, eventManager);
}
private FileComponent createFileComponent(Path path, boolean isFile) {
FileComponent component = null;
if (isFile) {
logger.trace("FileComponent {} created.", path);
component = new FileLeaf(path, getMaintainContentHashes());
} else {
logger.trace("FolderComponent {} created.", path);
component = new FolderComposite(path, getMaintainContentHashes());
}
logger.trace("Content hash of newly created file {} is {}", component.getPath(), component.getContentHash());
// getSynchronizedFiles().add(path);
return component;
}
@Override
public FileComponent deleteFile(Path fileToDelete) {
return rootOfFileTree.deleteComponent(fileToDelete);
}
@Override
public FileComponent updateFile(Path fileToUpdate) {
return null;
}
@Override
public SetMultimap<String, FolderComposite> getDeletedByStructureHash() {
return deletedByStructureHash;
}
public SetMultimap<String, FileComponent> getDeletedByContentHash(){
return deletedByContentHash;
}
/**
* This function runs the FileWalker to discover the structure of the subtree
* at the given location. This means, content hashes are neither computed nor
* propagated upwards. The structure is represented using a hash on the names
* of the contained objects of each folder
* @param filePath represents the root of the subtree
* @return the hash representing the folder's structure
*/
public String discoverSubtreeStructure(Path filePath, FileEventManager manager) {
FileWalker walker = new FileWalker(filePath, manager);
logger.debug("start discovery of subtree structure at : {}", filePath);
return walker.computeStructureHashOfFolder();
}
/**
* This function runs the FileWalker to discover the complete content of a subtree
* at the given location. The content hash of each file is computed, the content hash
* of a folder consists of a hash over contained files' content hashes. If these hashes
* change, the change is propagated to the parent folder
* @param filePath represents the root of the subtree
* @return the complete subtree as a FolderComposite
*/
public void discoverSubtreeCompletely(Path filePath, FileEventManager manager) {
FileWalker walker = new FileWalker(filePath, manager);
logger.debug("start complete subtree discovery at : {}", filePath);
walker.generateLocalCreateEvents();
}
private FileComponent findComponentInSetMultimap(FileComponent toSearch,
SetMultimap<String, ? extends FileComponent> filesByContent){
FileComponent result = null;
String hash = "";
if(toSearch.isFile()){
hash = toSearch.getContentHash();
} else {
hash = ((FolderComposite)toSearch).getStructureHash();
}
logger.trace("Contenthash to search for: {}", hash);
Set<? extends FileComponent> sameContentSet = filesByContent.get(hash);
for(Map.Entry<String, ? extends FileComponent> entry : filesByContent.entries()){
logger.trace("Path: {} Hash: {}", entry.getValue().getPath(), entry.getKey());
}
long minTimeDiff = Long.MAX_VALUE;
for(FileComponent candidate : sameContentSet) {
long timeDiff = toSearch.getAction().getTimestamp() - candidate.getAction().getTimestamp();
if(timeDiff < minTimeDiff) {
minTimeDiff = timeDiff;
result = candidate;
}
}
if(result != null){
boolean isRemoved = sameContentSet.remove(result);
logger.trace("findComponentsInSetMultimap - file: {} removed {}", result.getPath(), isRemoved);
}
return result;
}
@Override
public FileLeaf findCreatedByContent(FileLeaf deletedComponent) {
return (FileLeaf)findComponentInSetMultimap(deletedComponent, getCreatedByContentHash());
}
/**
* Searches the SetMultiMap<String, FileComponent> deletedByContentHash for
* a deleted FileComponent with the same content hash. If several exist, the temporally
* closest is returned.
*
* @param createdComponent The previously deleted component
* @return
*/
@Override
public FileLeaf findDeletedByContent(FileLeaf createdComponent){
return (FileLeaf)findComponentInSetMultimap(createdComponent, getDeletedByContentHash());
}
@Override
public FolderComposite findCreatedByStructure(FolderComposite toSearch) {
return (FolderComposite) findComponentInSetMultimap((FileComponent) toSearch,
getCreatedByStructureHash());
}
@Override
public FolderComposite findDeletedByStructure(FolderComposite toSearch) {
return (FolderComposite) findComponentInSetMultimap((FileComponent) toSearch,
getDeletedByStructureHash());
}
public Path getRootPath() {
return rootOfFileTree.getPath();
}
@Override
public Set<Path> getSynchronizedPathsAsSet() {
Set<Path> synchronizedFiles = new ConcurrentHashSet<Path>();
rootOfFileTree.getSynchronizedChildrenPaths(synchronizedFiles);
return synchronizedFiles;
}
@Override
public SetMultimap<String, FileComponent> getCreatedByContentHash() {
return createdByContentHash;
}
@Override
public SetMultimap<String, FolderComposite> getCreatedByStructureHash() {
return createdByStructureHash;
}
@Override
public List<FileComponent> asList() {
// resulting list
List<FileComponent> list = new ArrayList<>();
// traversal through tree
List<FileComponent> toVisit = new ArrayList<>();
toVisit.add(rootOfFileTree);
while (!toVisit.isEmpty()) {
FileComponent current = toVisit.remove(0);
list.add(current);
// add children to visit in the future
if (current.isFolder()) {
FolderComposite currentFolder = (FolderComposite) current;
toVisit.addAll(currentFolder.getChildren().values());
}
}
return list;
}
}