/* * Copyright 2000-2014 JetBrains s.r.o. * * 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 jetbrains.buildServer.vcs.patches.fs; import jetbrains.buildServer.vcs.patches.util.Assert; import java.util.Collection; import java.util.Map; import java.util.TreeMap; /** * Stores the paths (denoting files or directories) along with several additional flags * in a prefix tree (or trie, see Trie * for more details). *
* In few words, the string is split by slash symbol ('/'
), and the result component
* set denotes a path in a trie (from the root to a leaf). Thus each path can be mapped
* to a leaf node, and addition/deletion of the path results in addition/deletion of the leaf.
*
* Example: the following trie
* * foo/ * bar/ * File1 * File2 * baz/ * File3 * File4 ** corresponds to a series of additions:
foo/bar/
, foo/bar/File1
,
* foo/bar/File2
, foo/baz/File3
and foo/File4
.
*
* The nodes that correspond to "foo" and "baz" are auxiliary. They haven't been added
* explicitly, but are needed to store other nodes. For each of these paths a contains
* call will return false
, but containsNode
will return true
.
*
* The node "bar" and all file nodes are not auxiliary (let's name them "marked") as they've been
* added explicitly. For each of them both contains
and containsNode
calls
* return true
.
*
* @author Maxim Podkolzine (maxim.podkolzine@gmail.com)
*/
public class MemoryFileSystemImpl {
private final Node root = new Node();
/*************************************************************************************************
* Public API.
************************************************************************************************/
/**
* Adds a path to the trie.
*
* Note: isNew
makes sense only if isFile
is true
,
* because directory can not be modified.
*
* @param path the path to add
* @param isFile specifies if it is a file or a directory
* @param isNew specifies if the file is new
* @return true if the trie already contains such node, false otherwise
*/
public boolean add(String path, boolean isFile, boolean isNew) {
String[] components = path.split("/");
Node node = root;
for (int i = 0; i < components.length; ++i) {
Edge edge = node.findEdge(components[i]);
if (edge != null) {
node = edge.to;
} else {
appendString(components, i, node, isFile, isNew);
return false;
}
}
return true;
}
/**
* A shortcut (mostly for testing purposes).
*
* @see #add(String, boolean, boolean)
*/
public boolean add(String path) {
boolean isNew = isNew(path);
boolean isFile = isFile(path);
String refinedPath = path.substring(isNew ? 1 : 0, path.length() - (isFile ? 0 : 1));
return add(refinedPath, isFile, isNew);
}
/**
* Removes a path from the trie.
*
* Along with the actual node representing path
several auxiliary nodes
* might be removed. For instance, consider the trie:
* * foo/bar/baz/File1 * foo/File2 ** Then the call
remove("foo/bar/baz/File1", true)
will remove 3 nodes ("bar",
* "baz", and "File1"), while remove("foo/File2", true)
will remove only one node
* ("File2").
*
* Also note that a directory deletion leads to all inner files and directories deletion.
* That is remove("foo", false)
will clear the trie (assuming that "foo" node is
* marked).
*
* @param path the path to remove
* @param isFile specifies if it is a file or a directory
* @return true if the trie contained such node and it was actually removed, false otherwise
*/
public boolean remove(String path, boolean isFile) {
Node node = findString(path, isFile);
if (node == null) {
return false;
}
do {
Node parent = node.parent.from;
parent.removeEdge(node.parent.value);
node.nullify();
node = parent;
} while (node != root && node.children.size() == 0 && !node.marker);
return true;
}
/**
* A shortcut (mostly for testing purposes).
*
* @see #remove(String, boolean)
*/
public boolean remove(String path) {
return remove(path, isFile(path));
}
/**
* Removes the path from the trie.
*
* This is a more effifient version of {@link #remove(String, boolean)} method. No nodes are
* actually removed, the target node is just set unmarked. So the result is that a
* contains
will no longer return true
.
*
* @param path the path to remove
* @param isFile specifies if it is a file or a directory
* @return true if the trie contained such node, false otherwise
*/
public boolean removeMarker(String path, boolean isFile) {
Node node = findString(path, isFile);
if (node == null) {
return false;
}
node.marker = false;
return true;
}
/**
* Returns whether the trie contains a specified path or not, i.e. contains a marked node
* corresponding to path
.
*
* Note: isNew
makes sense only if isFile
is true
.
*
* @param path the path to check
* @param isFile specifies if it is a file or a directory
* @param isNew specifies if the file is new
* @return true if the trie contains a path
, false otherwise
* @see #containsNode(String)
*/
public boolean contains(String path, boolean isFile, boolean isNew) {
Node node = findString(path, isFile);
return node != null && node.marker && node.isNew == isNew;
}
/**
* A shortcut (when there is no need to distinguish if file is new).
*
* @see #contains(String, boolean, boolean)
*/
public boolean contains(String path, boolean isFile) {
Node node = findString(path, isFile);
return node != null && node.marker;
}
/**
* A shortcut (mostly for testing purposes).
*
* @see #contains(String, boolean, boolean)
*/
public boolean contains(String path) {
return contains(path, isFile(path));
}
/**
* Returns whether the trie contains an ancestor directory of the path
.
*
* For instance, consider the trie
* * foo/bar/baz/File1 * foo/File2 ** in which "bar" directory is marked, others are auxiliary. Then *
containsAncestor("foo") == false
,
* containsAncestor("foo/bar") == false
,
* containsAncestor("foo/bar/baz") == true
, and
* containsAncestor("foo/bar/baz/any/path/here") == true
.
*
* @param path the path to check
* @return true if the trie contains an ancestor directory, false otherwise
*/
public boolean containsAncestor(String path) {
String[] components = path.split("/");
Node node = root;
for (int i = 0; i < components.length - 1; i++) {
String component = components[i];
Edge edge = node.findEdge(component);
if (edge != null) {
node = edge.to;
if (!node.isFile && node.marker) {
return true;
}
} else {
return false;
}
}
return false;
}
/**
* Returns whether the trie contains a node corresponding to the path
,
* no matter if the node is marked or not.
*
* @param path the path to check
* @return true if the trie contains a node corresponding to the path
,
* false otherwise
* @see #contains(String, boolean)
*/
public boolean containsNode(String path) {
return findString(path, true) != null || findString(path, false) != null;
}
/**
* Fills the specified collections with marked nodes of three types:
* newDirectories
),
* isNew == true
flag (go to newFiles
),
* isNew == false
flag (go to modifiedFiles
).
*