// Copyright 2012 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package collide.client.filetree;
import java.util.Comparator;
import collide.client.treeview.TreeNodeElement;
import com.google.collide.client.util.PathUtil;
import com.google.collide.dto.DirInfo;
import com.google.collide.dto.FileInfo;
import com.google.collide.dto.TreeNodeInfo;
import com.google.collide.dto.client.DtoClientImpls.DirInfoImpl;
import com.google.collide.dto.client.DtoClientImpls.FileInfoImpl;
import com.google.collide.dto.client.DtoClientImpls.TreeNodeInfoImpl;
import com.google.collide.json.client.JsoArray;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.annotations.VisibleForTesting;
/**
* The Model for the FileTree. This data structure is constructed (without
* copying) from the DTO based DirInfo workspace file tree.
*
* We visit our workspace file tree that we JSON.parsed into existence, and
* install back references to their parent nodes. We simply eagerly mutate the
* same object graph (not copy it), and when needed we can cast each node to a
* {@link FileTreeNode} to get access to this API.
*
*/
public class FileTreeNode extends TreeNodeInfoImpl {
private static final Comparator<DirInfo> dirSortFunction = new Comparator<DirInfo>() {
@Override
public int compare(DirInfo a, DirInfo b) {
return compareNames(a, b);
}
};
private static final Comparator<FileInfo> fileSortFunction =
new Comparator<FileInfo>() {
@Override
public int compare(FileInfo a, FileInfo b) {
return compareNames(a, b);
}
};
/**
* Converts the DTO object graph into one suitable to act as the model for our
* FileTree.
*
* @param workspaceFiles {@link DirInfo} that represents the files in a
* workspace. This is literally what we JSON.parsed() off the wire.
* @return (@link FileTreeNode} that is the result of mutating the supplied
* {@link DirInfo} tree
*/
public static FileTreeNode transform(DirInfo workspaceFiles) {
transformImpl(workspaceFiles);
return ((DirInfoImpl) workspaceFiles).cast();
}
static native FileTreeNode installBackRef(DirInfo parent, TreeNodeInfo child) /*-{
child.__parentRef = parent;
return child;
}-*/;
private static int compareNames(TreeNodeInfo a, TreeNodeInfo b) {
return a.getName().compareTo(b.getName());
}
private static FileTreeNode getChildNode(FileTreeNode parentNode, String name) {
if (!parentNode.isDirectory()) {
return null;
}
DirInfoImpl asDirInfo = parentNode.cast();
JsonArray<DirInfo> childDirs = asDirInfo.getSubDirectories();
for (int i = 0, n = childDirs.size(); i < n; i++) {
if (childDirs.get(i).getName().equals(name)) {
return (FileTreeNode) childDirs.get(i);
}
}
JsonArray<FileInfo> childFiles = asDirInfo.getFiles();
for (int i = 0, n = childFiles.size(); i < n; i++) {
if (childFiles.get(i).getName().equals(name)) {
return (FileTreeNode) childFiles.get(i);
}
}
return null;
}
private static void transformImpl(DirInfo node) {
JsonArray<DirInfo> subDirsArray = node.getSubDirectories();
for (int i = 0, n = subDirsArray.size(); i < n; i++) {
DirInfo childDir = subDirsArray.get(i);
installBackRef(node, childDir);
transformImpl(childDir);
}
JsonArray<FileInfo> files = node.getFiles();
for (int i = 0, n = files.size(); i < n; i++) {
FileInfo childFile = files.get(i);
installBackRef(node, childFile);
}
}
protected FileTreeNode() {
}
public final void addChild(FileTreeNode newNode) {
assert (isDirectory()) : "You are only allowed to add new files and folders to a directory!";
DirInfoImpl parentDir = this.cast();
// Install the new node in our model by adding it to the child list of the
// parent.
if (newNode.isFile()) {
parentDir.getFiles().add(newNode.<FileInfoImpl>cast());
} else if (newNode.isDirectory()) {
parentDir.getSubDirectories().add(newNode.<DirInfoImpl>cast());
} else {
throw new IllegalArgumentException(
"We somehow made a new node that was neither a directory not a file.");
}
// Install a backreference to the parent to enable path resolution.
installBackRef(parentDir, newNode);
}
/**
* Finds a node with the given path relative to this node.
*
* @return the node if it was found, or null if it was not found or if this
* node is not a directory
*/
public final FileTreeNode findChildNode(PathUtil path) {
FileTreeNode closest = findClosestChildNode(path);
PathUtil absolutePath = PathUtil.concatenate(getNodePath(), path);
if (closest == null || !absolutePath.equals(closest.getNodePath())) {
return null;
}
return closest;
}
/**
* Finds a node with the given path relative to this node, or the closest parent node if the
* branch is incomplete (not loaded).
*
* @return the node if it was found, or the closest node in the path if the branch is not
* complete, or null if the branch is complete and the node is not found
*/
public final FileTreeNode findClosestChildNode(PathUtil path) {
FileTreeNode parentNode = this;
int dirnameComponents = path.getPathComponentsCount() - 1;
if (dirnameComponents < 0) {
return parentNode;
}
for (int i = 0; i < dirnameComponents; i++) {
if (!parentNode.isComplete()) {
return parentNode;
}
String childName = path.getPathComponent(i);
parentNode = getChildNode(parentNode, childName);
if (parentNode == null) {
return null;
}
}
// The immediate parent is not complete.
if (!parentNode.isComplete()) {
return parentNode;
}
// We match the very last path component against parentNode, which should
// now point to the directory containing the requested resource.
return getChildNode(parentNode, path.getPathComponent(dirnameComponents));
}
/**
* Returns a node for the child of the given name, or null.
*/
public final FileTreeNode getChildNode(String name) {
return getChildNode(this, name);
}
/**
* @return Path from the root node to this node.
*/
public final PathUtil getNodePath() {
// TODO: Consider caching this somewhere.
return new PathUtil.Builder()
.addPathComponents(getNodePathComponents())
.build();
}
private JsonArray<String> getNodePathComponents() {
JsonArray<String> pathArray = JsonCollections.createArray();
// We want to always exclude adding a path entry for the root, since the
// root is an implicit node. Paths should always be implicitly relative to
// this workspace root.
for (FileTreeNode node = this; node.getParent() != null; node = node.getParent()) {
pathArray.add(node.getName());
}
pathArray.reverse();
return pathArray;
}
public native final FileTreeNode getParent() /*-{
return this.__parentRef;
}-*/;
/**
* @return The associated rendered {@link TreeNodeElement}. If there is no
* tree node element rendered yet, then {@code null} is returned.
*/
public final native TreeNodeElement<FileTreeNode> getRenderedTreeNode() /*-{
return this.__renderedNode;
}-*/;
/**
* @return the sub directories concatenated with the files list, guaranteed to
* be sorted and is only computed once (and subsequently cached).
*/
@VisibleForTesting
public final JsoArray<FileTreeNode> getUnifiedChildren() {
assert isDirectory() : "Only directories have children!";
enusureUnifiedChildren();
return getUnifiedChildrenImpl();
}
public final boolean isDirectory() {
return getNodeType() == TreeNodeInfo.DIR_TYPE;
}
/**
* Checks whether or not this directory's children have been loaded.
*
* @return true if children have been loaded, false if not
*/
public final boolean isComplete() {
assert isDirectory() : "Only directories can be complete";
DirInfoImpl dirView = this.cast();
return dirView.isComplete();
}
/**
* Checks whether or not the children of this directory have been requested.
*
* @return true if loading, false if not
*/
public final native boolean isLoading() /*-{
return !!this.__isLoading;
}-*/;
public final boolean isFile() {
return getNodeType() == TreeNodeInfo.FILE_TYPE;
}
/**
* Removes a child node.
*/
public final void removeChild(FileTreeNode child) {
assert (isDirectory()) : "I am not a directory!";
if (child.isDirectory()) {
removeChild(child.getName(), this.<DirInfoImpl>cast().getSubDirectories());
} else {
removeChild(child.getName(), this.<DirInfoImpl>cast().getFiles());
}
invalidateUnifiedChildrenCache();
}
/**
* Removes a node from the subDirectories list that has the same name as the
* specified targetName.
*/
public final void removeChild(String targetName, JsonArray<? extends TreeNodeInfo> children) {
assert (isDirectory()) : "I am not a directory!";
for (int i = 0, n = children.size(); i < n; i++) {
if (targetName.equals(children.get(i).getName())) {
children.remove(i);
return;
}
}
}
/**
* Patches the tree rooted at the current node with an incoming directory sent
* from the server.
*
* We actually replace the current node in the tree with the incoming node by
* walking up one directory and replacing the entry in the subDirectories
* collection.
*
* This method is a no-op if the current node is the workspace root.
*/
public final void replaceWith(FileTreeNode newNode) {
FileTreeNode parent = getParent();
if (parent == null) {
return;
}
installBackRef(parent.<DirInfoImpl>cast(), newNode);
parent.removeChild(this);
parent.addChild(newNode);
parent.invalidateUnifiedChildrenCache();
}
/**
* Specifies whether or not this directory's children have been requested.
*/
public final native void setLoading(boolean isLoading) /*-{
this.__isLoading = isLoading;
}-*/;
/**
* Associates this FileTreeNode with the supplied {@link TreeNodeElement} as
* the rendered node in the tree. This allows us to go from model -> rendered
* tree element in order to reflect model mutations in the tree.
*/
public final native void setRenderedTreeNode(TreeNodeElement<FileTreeNode> renderedNode) /*-{
this.__renderedNode = renderedNode;
}-*/;
final native boolean hasUnifiedChildren() /*-{
return !!this.__childrenCache;
}-*/;
final native void invalidateUnifiedChildrenCache() /*-{
this.__childrenCache = null;
}-*/;
private native void cacheUnifiedChildren(JsoArray<FileTreeNode> children) /*-{
this.__childrenCache = children;
}-*/;
/**
* Sorts the directory and file children arrays (in place), and then caches
* the concatenation of the two.
*/
private void enusureUnifiedChildren() {
assert isDirectory() : "Only directories have children!";
if (!hasUnifiedChildren()) {
DirInfoImpl dirView = this.cast();
JsoArray<DirInfo> dirs = (JsoArray<DirInfo>) dirView.getSubDirectories();
JsoArray<FileInfo> files = (JsoArray<FileInfo>) dirView.getFiles();
dirs.sort(dirSortFunction);
files.sort(fileSortFunction);
cacheUnifiedChildren(JsoArray.concat(dirs, files).<JsoArray<FileTreeNode>>cast());
}
}
private native JsoArray<FileTreeNode> getUnifiedChildrenImpl() /*-{
return this.__childrenCache;
}-*/;
}