package org.peerbox.watchservice.filetree.composite;
import java.nio.file.Path;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.concurrent.ConcurrentSkipListMap;
import org.hive2hive.core.security.HashUtil;
import org.peerbox.watchservice.PathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Claudio
* Represents a folder in the application internal FileTree.
*/
public class FolderComposite extends AbstractFileComponent {
private static final Logger logger = LoggerFactory.getLogger(FolderComposite.class);
private final SortedMap<Path, FileComponent> children;
private String structureHash;
private boolean isRoot = false;
public FolderComposite(final Path path, boolean updateContentHash, boolean isRoot) {
super(path, updateContentHash);
this.children = new ConcurrentSkipListMap<Path, FileComponent>();
computeStructureHash();
this.isRoot = isRoot;
if (isRoot) {
setIsUploaded(true);
setIsSynchronized(true);
}
//
// if (updateContentHash) {
// computeContentHash();
// }
}
public FolderComposite(final Path path, boolean updateContentHash) {
this(path, updateContentHash, false);
}
public FolderComposite(final Path path, boolean updateContentHash, String contentHash){
this(path, updateContentHash, false);
setContentHash(contentHash);
}
public synchronized FileComponent getComponent(Path remainingPath) {
if (remainingPath.equals(getPath())) {
return this;
}
remainingPath = stripOffPrefix(remainingPath, getPath());
Path nextLevelPath = remainingPath.getName(0);
FileComponent nextLevel = children.get(nextLevelPath);
if (nextLevel == null) {
// next level child not found
return null;
} else if (remainingPath.getNameCount() == 1) {
// nextLevel is the last level of path: return it
return nextLevel;
} else if (nextLevel.isFolder()) {
// go to next level if it is a folder
Path newRemainingPath = remainingPath.subpath(1, remainingPath.getNameCount());
return ((FolderComposite)nextLevel).getComponent(newRemainingPath);
} else {
// not possible to contintue recursion.
return null;
}
}
protected Path stripOffPrefix(Path path, final Path prefix) {
if (path.startsWith(prefix)) {
path = prefix.relativize(path);
}
return path;
}
/**
* Appends a new component to the FolderComposite. Inexistent folders are added on the
* fly. Existing items are replaced. Triggers updates of content and name hashes.
*/
public synchronized void putComponent(Path remainingPath, FileComponent component) {
remainingPath = stripOffPrefix(remainingPath, getPath());
Path nextLevelPath = remainingPath.getName(0);
// if we are at the last recursion, perform the add, else recursively continue
if (remainingPath.getNameCount() == 1) {
deleteComponent(nextLevelPath);
addComponentToChildren(nextLevelPath, component);
} else {
FileComponent nextLevel = children.get(nextLevelPath);
if (nextLevel == null) {
// next level does not exist yet, create it
Path childPath = constructFullPath(nextLevelPath);
nextLevel = new FolderComposite(childPath, updateContentHash);
nextLevel.getAction().setFileEventManager(getAction().getFileEventManager());
addComponentToChildren(nextLevelPath, nextLevel);
}
Path newRemainingPath = remainingPath.subpath(1, remainingPath.getNameCount());
((FolderComposite) nextLevel).putComponent(newRemainingPath, component);
}
}
/*
* Because of the new children, the content hash of the directory may change and is propagated
*/
protected void addComponentToChildren(final Path nextLevelPath, final FileComponent component) {
children.remove(nextLevelPath);
children.put(nextLevelPath, component);
Path childPath = getPath().resolve(nextLevelPath);
component.setPath(childPath);
component.setParent(this);
if (updateContentHash) {
updateContentHash();
}
updateParentPathInChild(component);
if (component.isFolder()) {
FolderComposite componentAsFolder = (FolderComposite) component;
componentAsFolder.propagatePathChangeToChildren();
}
updateStructureHash();
if(component.isSynchronized()){
bubbleIsSynchronized(true);
}
}
private void bubbleIsSynchronized(boolean b) {
setIsSynchronized(true);
FolderComposite parent = getParent();
if(parent != null){
parent.setIsSynchronized(true);
}
}
private Path updateParentPathInChild(final FileComponent child) {
Path childName = child.getPath().getFileName();
Path newChildPath = getPath().resolve(childName);
child.setPath(newChildPath);
return newChildPath;
}
/**
* Deletes the FileComponent at location remainingPath. Triggers updates of
* content and name hashes.
*
* @return The deleted component. If it does not exist, null is returned
*/
public synchronized FileComponent deleteComponent(Path remainingPath) {
remainingPath = stripOffPrefix(remainingPath, getPath());
Path nextLevelPath = remainingPath.getName(0);
FileComponent removed = null;
if (remainingPath.getNameCount() == 1) {
children.get(nextLevelPath);
FileComponent toRemove = children.get(nextLevelPath);
if(toRemove != null){
if(toRemove instanceof FolderComposite){
removeBottomUp((FolderComposite)toRemove);
}
removed = children.remove(nextLevelPath);
}
if (updateContentHash) {
updateContentHash();
}
updateStructureHash();
} else {
FileComponent nextLevel = children.get(nextLevelPath);
if (nextLevel != null && nextLevel.isFolder()) {
Path newRemainingPath = remainingPath.subpath(1, remainingPath.getNameCount());
removed = ((FolderComposite)nextLevel).deleteComponent(newRemainingPath);
}
}
return removed;
}
private void removeBottomUp(FolderComposite folder){
for(FileComponent child : folder.getChildren().values()){
if(child instanceof FolderComposite){
removeBottomUp((FolderComposite)child);
}
this.getAction().getFileEventManager().getFileComponentQueue().remove(child);
}
}
protected Path constructFullPath(final Path name) {
Path completePath = getPath().resolve(name);
return completePath;
}
/**
* Computes the content hash for this object by appending the content hashes of contained
* components and hashing over it again.
*
* @return
*/
private boolean computeStructureHash() {
String nameHashInput = "";
String oldNamesHash = structureHash;
for (Map.Entry<Path, FileComponent> child : children.entrySet()) {
if(child.getValue().isSynchronized()){
nameHashInput = nameHashInput.concat(child.getKey().toString());
}
}
byte[] rawHash = HashUtil.hash(nameHashInput.getBytes());
structureHash = PathUtils.base64Encode(rawHash);
boolean hasChanged = !structureHash.equals(oldNamesHash);
return hasChanged;
}
public void updateStructureHash() {
boolean hasChanged = computeStructureHash();
if (hasChanged && getParent() != null) {
getParent().updateStructureHash();
}
}
@Override
protected boolean computeContentHash() {
String hashOfChildren = "";
for (FileComponent child : children.values()) {
if(child.isSynchronized()){
hashOfChildren = hashOfChildren.concat(child.getContentHash());
}
}
byte[] rawHash = HashUtil.hash(hashOfChildren.getBytes());
String newHash = PathUtils.base64Encode(rawHash);
if (!getContentHash().equals(newHash)) {
setContentHash(newHash);
return true;
} else {
return false;
}
}
/**
* If a subtree is appended, the children of the subtree need to update their paths.
* This function starts a recursive update. Furthermore, the filePath of the action
* related to each FileComponent is updates as well.
*
* @param parentPath
*/
private void propagatePathChangeToChildren() {
for (FileComponent child : children.values()) {
updateParentPathInChild(child);
if (child.isFolder()) {
FolderComposite childAsFolder = (FolderComposite) child;
childAsFolder.propagatePathChangeToChildren();
}
}
}
public void updateStateOnLocalDelete(){
getAction().setCurrentState(getAction().getCurrentState().changeStateOnLocalDelete());
for(FileComponent child : children.values()){
if(child.isFolder()){
((FolderComposite)child).updateStateOnLocalDelete();
} else {
getAction().setCurrentState(getAction().getCurrentState().changeStateOnLocalDelete());
}
}
}
public void getSynchronizedChildrenPaths(Set<Path> synchronizedPaths) {
if (isSynchronized()) {
synchronizedPaths.add(getPath());
}
for (Map.Entry<Path, FileComponent> entry : children.entrySet()) {
if (entry.getValue().isSynchronized()) {
synchronizedPaths.add(entry.getValue().getPath());
if(entry.getValue().isFolder()){
((FolderComposite)entry.getValue()).getSynchronizedChildrenPaths(synchronizedPaths);
}
}
}
}
public String getStructureHash() {
return structureHash;
}
public void setStructureHash(String structureHash) {
this.structureHash = structureHash;
}
@Override
public void setIsSynchronized(boolean isSynchronized) {
super.setIsSynchronized(isSynchronized);
}
@Override
public boolean isFile() {
return false;
}
@Override
public boolean isReady() {
if (isRoot) {
return true;
} else {
boolean parentIsUploaded = getParent().isUploaded();
return parentIsUploaded;
}
}
public SortedMap<Path, FileComponent> getChildren() {
return children;
}
@Override
public String toString() {
String children = "";
for(Path p : getChildren().keySet()) {
children += p.getFileName().toString() + ",";
}
String s = String.format("Folder[path(%s), contentHash(%s), structureHash(%s), isUploaded(%s), isSynchronized(%s), children(%s)]",
getPath(), getContentHash(), getStructureHash(), isUploaded(), isSynchronized(), children);
return s;
}
public void setIsSynchronizedRecursively(boolean b) {
setIsSynchronized(b);
for(FileComponent comp : children.values()){
if(comp.isFolder()){
((FolderComposite)comp).setIsSynchronizedRecursively(b);
} else {
((FileLeaf)comp).setIsSynchronized(b);
}
}
}
}