de.unibremen.informatik.st.libvcs4j.FSTree Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of libvcs4j-api Show documentation
Show all versions of libvcs4j-api Show documentation
A Java Library for Repository Mining (API)
package de.unibremen.informatik.st.libvcs4j;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Allows to represent a collection of {@link VCSFile} instances as a file
* system tree with a generic value that may be attached to a file.
*
* @param
* The type of the values attached to the files.
*/
public class FSTree {
/**
* This path is used for empty trees.
*/
public static final String EMPTY_DIRECTORY = "";
/**
* This path is used for trees with multiple root nodes.
*/
public static final String ROOT_DIRECTORY = "/";
/**
* A simple visitor to process {@link FSTree} instances.
*
* @param
* The type of the values attached to the files.
*/
public static class Visitor {
/**
* Delegates {@code pTree} to {@link #visitDirectory(FSTree)} or
* {@link #visitFile(FSTree)} depending on whether {@code pTree}
* represents a directory or a file.
*
* @param pTree
* The tree to visit and delegate.
*/
public void visit(final FSTree pTree) {
if (pTree.isDirectory()) {
visitDirectory(pTree);
} else {
visitFile(pTree);
}
}
/**
* Visits the given directory.
*
* @param pDirectory
* The directory to visit.
*/
protected void visitDirectory(final FSTree pDirectory) {
assert pDirectory.nodes != null;
pDirectory.nodes.forEach(this::visit);
}
/**
* Visits the given file.
*
* @param pFile
* The tree containing the file to visit.
*/
protected void visitFile(final FSTree pFile) {
visitFile(pFile.file);
}
/**
* Visits the given file.
*
* @param pFile
* The file to visit.
*/
protected void visitFile(final VCSFile pFile) {
// Do nothing.
}
}
/**
* The parent of a tree. Is {@code null} for the root node.
*/
private final FSTree parent;
/**
* The relative path of the referenced file (if {@link #file} is present)
* or directory (if {@link #nodes} is present). The path is relative to
* {@link VCSEngine#getRoot()}, except for the root node which may use
* {@link #ROOT_DIRECTORY} in case of a tree with multiple root nodes or
* {@link #EMPTY_DIRECTORY} in case of an "empty" tree.
*/
private final String path;
/**
* The referenced file. Is {@code null} if {@link #nodes} is present.
*/
private final VCSFile file;
/**
* The value attached to {@link #file}. May be {@code null}.
*/
private final V value;
/**
* The referenced sub files and directories. Is {@code null} if
* {@link #file} is present.
*/
private final List> nodes;
/**
* Is used to calculate the value of a directory by aggregating the values
* of all sub files and directories.
*/
private final BinaryOperator aggregator;
/**
* Creates a file with given parent, {@link VCSFile}, and value function.
*
* @param pParent
* The parent of the file to create. Pass {@code null} for root nodes.
* @param pFile
* The referenced {@link VCSFile} instance.
* @param pValueOf
* The function that is used to map a file to its value. The function
* may return {@code null}.
* @throws NullPointerException
* If {@code pFile} or {@code pValueOf} is {@code null}.
*/
private FSTree(final FSTree pParent, final VCSFile pFile,
final Function pValueOf) {
parent = pParent;
file = Validate.notNull(pFile);
path = file.toRelativePath().toString();
value = Validate.notNull(pValueOf).apply(file);
nodes = null;
aggregator = null;
}
/**
* Creates a directory with given parent, relative path, and aggregation
* function.
*
* @param pParent
* The parent of the directory to create. Pass {@code null} for root
* nodes.
* @param pPath
* The relative path of the directory to create.
* @param pAggregator
* The aggregation function used to calculate the value of a
* directory. The function must not handle {@code null} values.
* @throws NullPointerException
* If {@code pPath} or {@code pAggregator} is {@code null}.
*/
private FSTree(final FSTree pParent, final String pPath,
final BinaryOperator pAggregator) {
parent = pParent;
path = Validate.notNull(pPath);
nodes = new ArrayList<>();
aggregator = Validate.notNull(pAggregator);
file = null;
value = null;
}
/**
* Creates a tree from the given list of {@link VCSFile} instances.
* {@code null} values and duplicates (according to
* {@link Object#equals(Object)}) are filtered.
*
* @param pFiles
* The files to create the tree from.
* @param pValueOf
* The function that is used to map a file to its value. The function
* may return {@code null}.
* @param pAggregator
* The aggregation function used to calculate the value of a
* directory. The function must not handle {@code null} values.
* @param
* The type of the values attached to files.
* @return
* A tree representing the list of files.
* @throws NullPointerException
* If any of the given arguments is {@code null}.
*/
public static FSTree of(final Collection pFiles,
final Function pValueOf,
final BinaryOperator pAggregator)
throws NullPointerException {
Validate.notNull(pFiles);
Validate.notNull(pValueOf);
Validate.notNull(pAggregator);
final Set> treesWithoutParent = new HashSet<>();
final Map> cache = new HashMap<>();
pFiles.stream().filter(Objects::nonNull).distinct().forEach(f -> {
// (1) "home/user/file.txt" -> "home", "user", "file.txt"
final Path relativePath = f.toRelativePath();
final List parts = new ArrayList<>();
relativePath.forEach(p -> parts.add(p.toString()));
// (2) For each directory ("home", "user")...
FSTree parent = null;
for (int i = 0; i < parts.size() - 1; i++) {
final String path = String.join(
File.separator, parts.subList(0, i + 1));
FSTree dir = cache.get(path);
if (dir == null) {
dir = new FSTree<>(parent, path, pAggregator);
cache.put(path, dir);
if (parent != null) {
parent.nodes.add(dir);
}
}
if (parent == null) {
treesWithoutParent.add(dir);
}
parent = dir;
}
// (3) Add file ("file.txt").
final String relativePathStr = relativePath.toString();
FSTree file = cache.get(relativePathStr);
if (file == null) {
file = new FSTree<>(parent, f, pValueOf);
cache.put(relativePathStr, file);
if (parent != null) {
parent.nodes.add(file);
}
}
if (parent == null) {
treesWithoutParent.add(file);
}
});
if (treesWithoutParent.isEmpty()) {
return new FSTree<>(null, EMPTY_DIRECTORY, pAggregator);
} else if (treesWithoutParent.size() > 1) {
final FSTree root = new FSTree<>(
null, ROOT_DIRECTORY, pAggregator);
root.nodes.addAll(treesWithoutParent);
return root;
} else {
return treesWithoutParent.iterator().next();
}
}
/**
* Creates a tree from the given list of {@link VCSFile} instances. The
* created tree has no value (see {@link #getValue()}). {@code null} values
* and duplicates (according to {@link Object#equals(Object)}) are
* filtered.
*
* @param pFiles
* The files to create the tree from.
* @return
* A tree representing the list of files.
* @throws NullPointerException
* If {@code pFile} is {@code null}.
*/
public static FSTree of(final Collection pFiles)
throws NullPointerException {
return of(pFiles, f -> null, (v1, v2) -> null);
}
/**
* Returns the parent of this tree.
*
* @return
* The parent of this tree.
*/
public Optional> getParent() {
return Optional.ofNullable(parent);
}
/**
* Returns the relative path of this file (if {@link #getFile()} is
* present) or directory (if {@link #getNodes()} is present).
*
* @return
* The relative path of this file or directory.
*/
public String getPath() {
return path;
}
/**
* Returns the name of this file (if {@link #getFile()} is present) or
* directory (if {@link #getNodes()} is present).
*
* @return
* The name of this file or directory.
*/
public String getName() {
return isVirtualRoot()
? path
: Paths.get(path).getFileName().toString();
}
/**
* Returns the referenced {@link VCSFile} if this tree is a file.
*
* @return
* The referenced {@link VCSFile}.
*/
public Optional getFile() {
return Optional.ofNullable(file);
}
/**
* Either returns the value of this file (if this tree represents a file)
* or the aggregated values of all (recursively) sub files (if this tree
* represents a directory).
*
* @return
* The value of this file or the aggregated values of all
* (recursively) sub files. If this file has no value (or this
* directory contains only files without a value), an empty
* {@link Optional} is returned.
*/
public Optional getValue() {
final List values = new ArrayList<>();
final Visitor visitor = new Visitor() {
@Override
protected void visitFile(final FSTree pTree) {
if (pTree.value != null) {
values.add(pTree.value);
}
super.visitFile(pTree);
}
};
visitor.visit(this);
if (values.isEmpty()) {
return Optional.empty();
} else if (values.size() == 1) {
return Optional.ofNullable(values.get(0));
} else {
return values.stream().reduce(aggregator);
}
}
/**
* Returns the sub files and directories of this tree if this tree is a
* directory. If this tree is a file, an empty list is returned.
*
* @return
* The sub files and directories of this tree.
*/
public List> getNodes() {
return nodes == null
? Collections.emptyList()
: new ArrayList<>(nodes);
}
/**
* Returns the sub directories of this tree if this tree is a directory. If
* this tree is a file, an empty List is returned.
*
* @return
* The sub directories of this tree.
*/
public List> getDirectories() {
return getNodes().stream()
.filter(FSTree::isDirectory)
.collect(Collectors.toList());
}
/**
* Returns all (recursively) sub directories of this tree if this tree is a
* directory. If this tree is a file, an empty list is returned.
*
* @return
* All (recursively) sub directories of this tree.
*/
public List> getAllDirectories() {
final List> directories = new ArrayList<>();
final Visitor visitor = new Visitor() {
@Override
protected void visitDirectory(final FSTree pDirectory) {
if (pDirectory != FSTree.this) {
directories.add(pDirectory);
}
super.visitDirectory(pDirectory);
}
};
visitor.visit(this);
return directories;
}
/**
* Returns the sub files of this tree if this tree is a directory. If this
* tree is a file, an empty list is returned.
*
* @return
* The sub files of this tree.
*/
public List> getFiles() {
return getNodes().stream()
.filter(FSTree::isFile)
.collect(Collectors.toList());
}
/**
* Returns all (recursively) sub files of this tree if this tree is a
* directory. If this tree is a file, an empty list is returned.
*
* @return
* Al (recursively) sub files of this tree.
*/
public List> getAllFiles() {
final List> files = new ArrayList<>();
final Visitor visitor = new Visitor() {
@Override
protected void visitFile(FSTree pFile) {
if (pFile != FSTree.this) {
files.add(pFile);
}
super.visitFile(pFile);
}
};
visitor.visit(this);
return files;
}
/**
* Returns the root of this tree.
*
* @return
* The root of this tree.
*/
public FSTree getRoot() {
FSTree root = this;
Optional> op;
while ((op = root.getParent()).isPresent()) {
root = op.get();
}
return root;
}
/**
* Navigates to the tree located at {@code path}. Unlike conventional
* navigation rules, this method allows to navigate "beyond" a regular
* file. That is, for instance, a file's parent may be addressed like this:
*
* "src/A.java/.."
*
* where "A.java" is a regular file and ".." points to its parent. If
* {@code path} is empty, {@code this} is returned.
*
* @param path
* The relative path of the tree to navigate to.
* @return
* The tree located at {@code path}, if such a tree exists.
* @throws NullPointerException
* If {@code path} is {@code null}.
*/
public Optional> navigateTo(final String path) {
Validate.notNull(path);
if (path.isEmpty()) {
return Optional.of(this);
}
final Queue parts = new ArrayDeque<>();
parts.addAll(Arrays.asList(path.replace("\\", "/").split("/")));
final String head = parts.poll();
final String tail = String.join("/", parts);
switch (head) {
case ".":
return navigateTo(tail);
case "..":
return getParent()
// Navigate to parent and process tail.
.map(p -> p.navigateTo(tail))
// Stay here and process tail.
.orElseGet(() -> navigateTo(tail));
default:
if (isFile()) {
return tail.isEmpty() && hasFileName(head)
? Optional.of(this) : Optional.empty();
} else {
return nodes.stream()
.filter(n -> n.hasFileName(head))
.findFirst()
.map(n -> n.navigateTo(tail))
.filter(Optional::isPresent)
.map(Optional::get);
}
}
}
/**
* Compacts this tree such that each node is either a file, or a directory
* containing only files or at least two sub directories. If this tree is a
* sequence of single directories, an "empty" directory is returned. This
* method does not modify this tree or any of its sub nodes, but creates a
* flat copy it.
*
* @return
* A Tree consisting of files, and directories containing only files
* or at least two sub directories. An "empty" directory if this tree
* is a sequence of single directories.
*/
public FSTree compact() {
return compact(this, null);
}
/**
* Recursively computes the result specified by {@link #compact()}.
*
* @param pTree
* The tree to compact.
* @param pParent
* The parent to use.
* @param
* The type of the values attached to the files.
* @return
* The compacted version of {@code pTree}.
*/
private static FSTree compact(final FSTree pTree,
final FSTree pParent) {
// Compact pTree...
FSTree current = pTree;
// ...if it is a directory...
while (current.isDirectory()
// ...containing a single directory.
&& current.nodes.size() == 1
&& current.nodes.get(0).isDirectory()) {
current = current.nodes.get(0);
}
final String path = current.path;
final T value = current.value;
final BinaryOperator aggregator = current.aggregator;
final FSTree compacted = current.isDirectory()
? new FSTree<>(pParent, path, aggregator)
: new FSTree<>(pParent, current.file, f -> value);
// Compact sub nodes in case of a directory. Ignore empty directories.
if (compacted.isDirectory()) {
current.nodes.forEach(node -> {
// compactedNode may be an empty directory.
final FSTree compactedNode = compact(node, compacted);
if (compactedNode.isFile() || !compactedNode.nodes.isEmpty()) {
compacted.nodes.add(compactedNode);
}
});
}
return compacted;
}
/**
* Returns whether the filename of this tree matches the given filename.
*
* @param pFilename
* The filename to match with the filename of this tree.
* @return
* {@code true} if the filename of this tree matches
* {@code pFilename}, {@code false} otherwise.
*/
private boolean hasFileName(final String pFilename) {
return getName().equals(pFilename);
}
/**
* Returns whether this tree is a file.
*
* @return
* {@code true} if this tree is a file, {@code false} otherwise.
*/
public boolean isFile() {
return file != null;
}
/**
* Returns whether this tree is a directory.
*
* @return
* {@code true} if this tree is a directory, {@code false} otherwise.
*/
public boolean isDirectory() {
return file == null;
}
/**
* Returns whether this tree is a root node.
*
* @return
* {@code true} if this tree is a root node, {@code false} otherwise.
*/
public boolean isRoot() {
return parent == null;
}
/**
* Returns whether this tree is a virtual root directory that has been
* created to cover multiple root nodes.
*
* @return
* {@code true} if this tree is a virtual root directory,
* {@code false} otherwise.
*/
public boolean isVirtualRoot() {
return path.equals(EMPTY_DIRECTORY) || path.equals(ROOT_DIRECTORY);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy