// 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 com.google.collide.client.workspace;
import com.google.collide.client.AppContext;
import com.google.collide.client.bootstrap.BootstrapSession;
import com.google.collide.client.communication.FrontendApi.ApiCallback;
import com.google.collide.client.communication.MessageFilter;
import com.google.collide.client.communication.MessageFilter.MessageRecipient;
import com.google.collide.client.status.StatusMessage;
import com.google.collide.client.status.StatusMessage.MessageType;
import com.google.collide.client.util.PathUtil;
import com.google.collide.client.util.logging.Log;
import com.google.collide.client.workspace.FileTreeModel.NodeRequestCallback;
import com.google.collide.client.workspace.FileTreeModel.RootNodeRequestCallback;
import com.google.collide.dto.DirInfo;
import com.google.collide.dto.GetDirectoryResponse;
import com.google.collide.dto.Mutation;
import com.google.collide.dto.NodeMutationDto.MutationType;
import com.google.collide.dto.RoutingTypes;
import com.google.collide.dto.ServerError.FailureReason;
import com.google.collide.dto.TreeNodeInfo;
import com.google.collide.dto.WorkspaceTreeUpdate;
import com.google.collide.dto.WorkspaceTreeUpdateBroadcast;
import com.google.collide.dto.client.DtoClientImpls.GetDirectoryImpl;
import com.google.collide.json.shared.JsonArray;
import com.google.collide.shared.FrontendConstants;
import com.google.collide.shared.util.JsonCollections;
import com.google.common.base.Preconditions;
import javax.annotation.Nullable;
/**
* Controller responsible for receiving DTOs that come off the {@link MessageFilter} that were sent
* from the frontend, and updating the {@link FileTreeModel}.
*
*/
class FileTreeModelNetworkController implements FileTreeInvalidatedEvent.Handler {
/**
* Static factory method to obtain an instance of FileTreeModelNetworkController.
*/
public static FileTreeModelNetworkController create(FileTreeModel fileTreeModel,
AppContext appContext, WorkspacePlace currentPlace) {
FileTreeModelNetworkController networkController =
new FileTreeModelNetworkController(fileTreeModel, appContext);
currentPlace.registerSimpleEventHandler(FileTreeInvalidatedEvent.TYPE, networkController);
networkController.registerForInvalidations(appContext.getMessageFilter());
// Load the tree.
networkController.reloadDirectory(PathUtil.WORKSPACE_ROOT);
return networkController;
}
/**
* A controller for the outgoing network requests for fetching nodes. This logic is used mainly
* for lazy tree loading.
*
* <p>
* This class is a static class to prevent circular instance dependencies between the
* {@link FileTreeModel} and the {@link FileTreeModelNetworkController}.
*/
static class OutgoingController {
private final AppContext appContext;
OutgoingController(AppContext appContext) {
this.appContext = appContext;
}
/**
* @see FileTreeModel#requestWorkspaceNode
*/
void requestWorkspaceNode(final FileTreeModel fileTreeModel, final PathUtil path,
final NodeRequestCallback callback) {
// Wait until the root node has been loaded.
fileTreeModel.requestWorkspaceRoot(new RootNodeRequestCallback() {
@Override
public void onRootNodeAvailable(FileTreeNode root) {
// Find the closest node in the tree, which might be the node we want.
final FileTreeNode closest = root.findClosestChildNode(path);
if (closest == null) {
// The node does not exist in the tree.
callback.onNodeUnavailable();
return;
} else if (path.equals(closest.getNodePath())) {
// The node is already in the tree.
callback.onNodeAvailable(closest);
return;
}
// Get the directory that contains the path.
final PathUtil dirPath = PathUtil.createExcludingLastN(path, 1);
// Request the node and its parents, starting from the closest node.
/*
* TODO: We should revisit deep linking in the file tree and possible only show
* the directory that is deep linked. Otherwise, we may have to load a lot of parent
* directories when deep linking to a very deep directory.
*/
GetDirectoryImpl getDirectoryAndPath = GetDirectoryImpl.make()
.setPath(dirPath.getPathString())
.setDepth(FrontendConstants.DEFAULT_FILE_TREE_DEPTH)
.setRootId(fileTreeModel.getLastAppliedTreeMutationRevision());
// Include the root path so we load parent directories leading up to the file.
//.setRootPath(closest.getNodePath().getPathString());
appContext.getFrontendApi().GET_DIRECTORY.send(
getDirectoryAndPath, new ApiCallback<GetDirectoryResponse>() {
@Override
public void onMessageReceived(GetDirectoryResponse response) {
DirInfo baseDir = response.getBaseDirectory();
if (baseDir == null) {
/*
* The folder was most probably deleted before the server received our request.
* We should receive a tango notification to update the client.
*/
return;
}
FileTreeNode incomingSubtree = FileTreeNode.transform(baseDir);
fileTreeModel.replaceNode(
new PathUtil(response.getPath()), incomingSubtree, response.getRootId());
// Check if the node now exists.
FileTreeNode child = fileTreeModel.getWorkspaceRoot().findChildNode(path);
if (child == null) {
callback.onNodeUnavailable();
} else {
callback.onNodeAvailable(child);
}
}
@Override
public void onFail(FailureReason reason) {
Log.error(getClass(), "Failed to retrieve directory path "
+ dirPath.getPathString());
// Update the callback.
callback.onError(reason);
}
});
}
});
}
/**
* @see FileTreeModel#requestDirectoryChildren
*/
void requestDirectoryChildren(
final FileTreeModel fileTreeModel, FileTreeNode node, final NodeRequestCallback callback) {
Preconditions.checkArgument(node.isDirectory(), "Cannot request children of a file");
final PathUtil path = node.getNodePath();
GetDirectoryImpl getDirectory = GetDirectoryImpl.make()
.setPath(path.getPathString())
.setDepth(FrontendConstants.DEFAULT_FILE_TREE_DEPTH)
.setRootId(fileTreeModel.getLastAppliedTreeMutationRevision());
appContext.getFrontendApi().GET_DIRECTORY.send(getDirectory,
new ApiCallback<GetDirectoryResponse>() {
@Override
public void onMessageReceived(GetDirectoryResponse response) {
DirInfo baseDir = response.getBaseDirectory();
if (baseDir == null) {
/*
* The folder was most probably deleted before the server received our request. We
* should receive a tango notification to update the client.
*/
if (callback != null) {
callback.onNodeUnavailable();
}
return;
}
FileTreeNode incomingSubtree = FileTreeNode.transform(baseDir);
fileTreeModel.replaceNode(
new PathUtil(response.getPath()), incomingSubtree, response.getRootId());
if (callback != null) {
callback.onNodeAvailable(incomingSubtree);
}
}
@Override
public void onFail(FailureReason reason) {
Log.error(getClass(), "Failed to retrieve children for directory "
+ path.getPathString());
if (callback != null) {
callback.onError(reason);
} else {
StatusMessage fatal = new StatusMessage(
appContext.getStatusManager(), MessageType.FATAL,
"Could not retrieve children of directory. Please try again.");
fatal.setDismissable(true);
fatal.fire();
}
}
});
}
}
private final AppContext appContext;
private final FileTreeModel fileTreeModel;
FileTreeModelNetworkController(FileTreeModel fileTreeModel, AppContext appContext) {
this.fileTreeModel = fileTreeModel;
this.appContext = appContext;
}
/**
* Adds a node to our model from a broadcasted workspace tree mutation.
*/
private void handleExternalAdd(String newPath, TreeNodeInfo newNode, String newTipId) {
String ourClientId = BootstrapSession.getBootstrapSession().getActiveClientId();
PathUtil path = new PathUtil(newPath);
FileTreeNode rootNode = fileTreeModel.getWorkspaceRoot();
// If the root is null... then we are receiving mutations before we even got
// the workspace in the first place. So we should ignore.
// TODO: This is a potential race. We need to schedule a pending
// referesh for the tree!!
if (rootNode == null) {
Log.warn(getClass(), "Receiving ADD tree mutations before the root node is set for node: "
+ path.getPathString());
return;
}
FileTreeNode existingNode = rootNode.findChildNode(path);
if (existingNode == null) {
fileTreeModel.addNode(path, (FileTreeNode) newNode, newTipId);
} else {
// If it's already there, it's probably a placeholder node we created.
existingNode.setFileEditSessionKey(newNode.getFileEditSessionKey());
// Because adding new node via context menu disable change notifications,
// we need explicitly notify listeners.
fileTreeModel.dispatchAddNode(existingNode.getParent(), existingNode, newTipId);
}
}
private void handleExternalCopy(String newpath, TreeNodeInfo newNode, String newTipId) {
PathUtil path = new PathUtil(newpath);
FileTreeNode rootNode = fileTreeModel.getWorkspaceRoot();
// If the root is null... then we are receiving mutations before we even got
// the workspace in the first place. So we should ignore.
// TODO: This is a potential race. We need to schedule a pending
// referesh for the tree!!
if (rootNode == null) {
Log.warn(getClass(), "Receiving COPY tree mutations before the root node is set for node: "
+ path.getPathString());
return;
}
FileTreeNode installedNode = (FileTreeNode) newNode;
fileTreeModel.addNode(path, installedNode, newTipId);
}
/**
* Removes a node from the model and from the rendered Tree.
*/
private void handleExternalDelete(JsonArray<Mutation> deletedNodes, String newTipId) {
// Note that for deletes we do NOT currently optimistically update the UI.
// So we need to remove the node, even if we triggered said delete.
JsonArray<PathUtil> pathsToDelete = JsonCollections.createArray();
for (int i = 0; i < deletedNodes.size(); i++) {
pathsToDelete.add(new PathUtil(deletedNodes.get(i).getOldPath()));
}
fileTreeModel.removeNodes(pathsToDelete, newTipId);
}
private void handleExternalMove(String oldPathStr, String newPathStr, String newTipId) {
PathUtil oldPath = new PathUtil(oldPathStr);
PathUtil newPath = new PathUtil(newPathStr);
FileTreeNode rootNode = fileTreeModel.getWorkspaceRoot();
// If the root is null... then we are receiving mutations before we even got
// the workspace in the first place. So we should ignore.
// TODO: This is a potential race. We need to schedule a pending
// referesh for the tree!!
if (rootNode == null) {
Log.warn(getClass(), "Receiving MOVE tree mutations before the root node is set for node: "
+ oldPath.getPathString());
return;
}
fileTreeModel.moveNode(oldPath, newPath, newTipId);
}
/**
* Handles ADD, RENAME, DELETE, or COPY messages.
*/
private void handleFileTreeMutation(WorkspaceTreeUpdateBroadcast treeUpdate) {
JsonArray<Mutation> mutations = treeUpdate.getMutations();
for (int i = 0, n = mutations.size(); i < n; i++) {
Mutation mutation = mutations.get(i);
switch (mutation.getMutationType()) {
case ADD:
handleExternalAdd(
mutation.getNewPath(), mutation.getNewNodeInfo(), treeUpdate.getNewTreeVersion());
break;
case DELETE:
handleExternalDelete(treeUpdate.getMutations(), treeUpdate.getNewTreeVersion());
break;
case MOVE:
handleExternalMove(mutation.getOldPath(), mutation.getNewPath(), treeUpdate.getNewTreeVersion());
break;
case COPY:
handleExternalCopy(mutation.getNewPath(), mutation.getNewNodeInfo(), treeUpdate.getNewTreeVersion());
break;
default:
assert (false) : "We got some kind of malformed workspace tree mutation!";
break;
}
// Bump the tracked tip.
fileTreeModel.maybeSetLastAppliedTreeMutationRevision(treeUpdate.getNewTreeVersion());
}
}
public void handleSubtreeReplaced(GetDirectoryResponse response) {
FileTreeNode incomingSubtree = FileTreeNode.transform(response.getBaseDirectory());
fileTreeModel.replaceNode(
new PathUtil(response.getPath()), incomingSubtree, response.getRootId());
if (PathUtil.WORKSPACE_ROOT.equals(new PathUtil(response.getPath()))) {
fileTreeModel.maybeSetLastAppliedTreeMutationRevision(response.getRootId());
}
}
@Override
public void onFileTreeInvalidated(PathUtil invalidatedPath) {
// Check if the invalidated path points to a file
if (!invalidatedPath.equals(PathUtil.WORKSPACE_ROOT)) {
if (fileTreeModel.getWorkspaceRoot() != null) {
FileTreeNode invalidatedNode =
fileTreeModel.getWorkspaceRoot().findChildNode(invalidatedPath);
if (invalidatedNode == null) {
// Our lazy tree does not contain the node, we don't have to do anything
return;
}
if (invalidatedNode.isFile()) {
reloadDirectory(PathUtil.createExcludingLastN(invalidatedPath, 1));
return;
}
} else {
/*
* We don't have enough information yet to invalidate the specific node, so we just
* invalidate the workspace root
*/
invalidatedPath = PathUtil.WORKSPACE_ROOT;
}
}
reloadDirectory(invalidatedPath);
}
private void reloadDirectory(final PathUtil invalidatedPath) {
GetDirectoryImpl request = GetDirectoryImpl.make().setRootId(
fileTreeModel.getLastAppliedTreeMutationRevision())
.setDepth(FrontendConstants.DEFAULT_FILE_TREE_DEPTH);
// Fetch the parent directory of the file
request.setPath(invalidatedPath.toString());
appContext.getFrontendApi().GET_DIRECTORY.send(request,
new ApiCallback<GetDirectoryResponse>() {
@Override
public void onMessageReceived(GetDirectoryResponse response) {
handleSubtreeReplaced(response);
}
@Override
public void onFail(FailureReason reason) {
Log.error(getClass(), "Failed to retrieve file metadata for workspace.");
StatusMessage fatal = new StatusMessage(
appContext.getStatusManager(), MessageType.FATAL,
"There was a problem refreshing changes within the file tree :(.");
fatal.addAction(StatusMessage.RELOAD_ACTION);
fatal.setDismissable(true);
fatal.fire();
}
});
}
public void registerForInvalidations(MessageFilter messageFilter) {
messageFilter.registerMessageRecipient(RoutingTypes.WORKSPACETREEUPDATEBROADCAST,
new MessageRecipient<WorkspaceTreeUpdateBroadcast>() {
@Override
public void onMessageReceived(WorkspaceTreeUpdateBroadcast update) {
if (update != null) {
handleFileTreeMutation(update);
} else {
// Either the invalidation was not the next sequential one or we
// didn't get the payload. Reload the entire tree.
onFileTreeInvalidated(PathUtil.WORKSPACE_ROOT);
}
}
});
}
}