com.swoval.files.CachedDirectoryImpl Maven / Gradle / Ivy
package com.swoval.files;
import static com.swoval.functional.Either.leftProjection;
import static com.swoval.functional.Filters.AllPass;
import com.swoval.files.FileTreeDataViews.Converter;
import com.swoval.files.FileTreeDataViews.Entry;
import com.swoval.files.FileTreeViews.Updates;
import com.swoval.functional.Either;
import com.swoval.functional.Filter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
/**
* Provides a mutable in-memory cache of files and subdirectories with basic CRUD functionality. The
* CachedDirectory can be fully recursive as the subdirectories are themselves stored as recursive
* (when the CachedDirectory is initialized without the recursive toggle, the subdirectories are
* stored as {@link Entry} instances. The primary use case is the implementation of {@link
* FileTreeRepository} and {@link NioPathWatcher}. Directly handling CachedDirectory instances is
* discouraged because it is inherently mutable so it's better to let the FileTreeRepository manage
* it and query the cache rather than CachedDirectory directly.
*
* The CachedDirectory should cache all of the files and subdirectories up the maximum depth. A
* maximum depth of zero means that the CachedDirectory should cache the subdirectories, but not
* traverse them. A depth {@code < 0} means that it should not cache any files or subdirectories
* within the directory. In the event that a loop is created by symlinks, the CachedDirectory will
* include the symlink that completes the loop, but will not descend further (inducing a loop).
*
* @param the cache value type.
*/
class CachedDirectoryImpl implements CachedDirectory {
private final AtomicReference> _cacheEntry;
private final int depth;
private final TypedPath typedPath;
private final FileTreeView fileTreeView;
private final Converter converter;
private final Filter super TypedPath> pathFilter;
private final LockableMap> subdirectories = new LockableMap<>();
private final Map> files = new HashMap<>();
private interface ListTransformer {
R apply(final Entry entry);
}
@SuppressWarnings("unchecked")
CachedDirectoryImpl(
final TypedPath typedPath,
final Converter converter,
final int depth,
final Filter super TypedPath> filter,
final FileTreeView fileTreeView) {
this.typedPath = typedPath;
this.converter = converter;
this.depth = depth;
this._cacheEntry = new AtomicReference<>(null);
this.pathFilter = filter;
this._cacheEntry.set(Entries.get(this.typedPath, converter, this.typedPath));
this.fileTreeView = fileTreeView;
}
/**
* Returns the name components of a path in an array.
*
* @param path The path from which we extract the parts.
* @return Empty array if the path is an empty relative path, otherwise return the name parts.
*/
private static List parts(final Path path) {
final Iterator it = path.iterator();
final List result = new ArrayList<>();
while (it.hasNext()) result.add(it.next());
return result;
}
public int getMaxDepth() {
return depth;
}
@Override
public Path getPath() {
return typedPath.getPath();
}
@Override
public TypedPath getTypedPath() {
return typedPath;
}
@Override
public List list(int maxDepth, Filter super TypedPath> filter) {
return list(getPath(), maxDepth, filter);
}
@Override
public List list(
final Path path, final int maxDepth, final Filter super TypedPath> filter) {
if (this.subdirectories.lock()) {
try {
final Either, CachedDirectoryImpl> findResult = find(path);
if (findResult != null) {
if (findResult.isRight()) {
final List result = new ArrayList<>();
findResult
.get()
.listImpl(
maxDepth,
filter,
result,
new ListTransformer() {
@Override
public TypedPath apply(final Entry entry) {
return TypedPaths.getDelegate(
entry.getTypedPath().getPath(), entry.getTypedPath());
}
});
return result;
} else {
final Entry entry = leftProjection(findResult).getValue();
final List result = new ArrayList<>();
if (entry != null && filter.accept(entry.getTypedPath()) && maxDepth == -1)
result.add(
TypedPaths.getDelegate(entry.getTypedPath().getPath(), entry.getTypedPath()));
return result;
}
} else {
return Collections.emptyList();
}
} finally {
this.subdirectories.unlock();
}
} else {
return Collections.emptyList();
}
}
@Override
public List> listEntries(
final Path path, final int maxDepth, final Filter super Entry> filter) {
if (this.subdirectories.lock()) {
try {
final Either, CachedDirectoryImpl> findResult = find(path);
if (findResult != null) {
if (findResult.isRight()) {
final List> result = new ArrayList<>();
findResult
.get()
.>listImpl(
maxDepth,
filter,
result,
new ListTransformer>() {
@Override
public Entry apply(final Entry entry) {
return entry;
}
});
return result;
} else {
final Entry entry = leftProjection(findResult).getValue();
final List> result = new ArrayList<>();
if (entry != null && filter.accept(entry)) result.add(entry);
return result;
}
} else {
return Collections.emptyList();
}
} finally {
this.subdirectories.unlock();
}
} else {
return Collections.emptyList();
}
}
@Override
public List> listEntries(final int maxDepth, final Filter super Entry> filter) {
return listEntries(getPath(), maxDepth, filter);
}
@Override
public Entry getEntry() {
return _cacheEntry.get();
}
@Override
public void close() {
subdirectories.clear();
files.clear();
}
/**
* Updates the CachedDirectory entry for a particular typed typedPath.
*
* @param typedPath the typedPath to update
* @return a list of updates for the typedPath. When the typedPath is new, the updates have the
* oldCachedPath field set to null and will contain all of the children of the new typedPath
* when it is a directory. For an existing typedPath, the List contains a single Updates that
* contains the previous and new {@link Entry}.
* @throws IOException when the updated Path is a directory and an IOException is encountered
* traversing the directory.
*/
@Override
public Updates update(final TypedPath typedPath) throws IOException {
return pathFilter.accept(typedPath)
? updateImpl(
typedPath.getPath().equals(this.getPath())
? new ArrayList()
: parts(this.getPath().relativize(typedPath.getPath())),
typedPath)
: new Updates();
}
/**
* Remove a path from the directory.
*
* @param path the path to remove
* @return a List containing the Entry instances for the removed path. The result also contains
* the cache entries for any children of the path when the path is a non-empty directory.
*/
public List> remove(final Path path) {
if (path.isAbsolute() && path.startsWith(this.getPath())) {
return removeImpl(parts(this.getPath().relativize(path)));
} else {
return Collections.emptyList();
}
}
@Override
public String toString() {
return "CachedDirectory(" + getPath() + ", maxDepth = " + depth + ")";
}
private int subdirectoryDepth() {
return depth == Integer.MAX_VALUE ? depth : depth > 0 ? depth - 1 : 0;
}
@SuppressWarnings({"unchecked", "EmptyCatchBlock"})
private void addDirectory(
final CachedDirectoryImpl currentDir,
final TypedPath typedPath,
final Updates updates) {
final Path path = typedPath.getPath();
final CachedDirectoryImpl dir =
new CachedDirectoryImpl<>(
typedPath, converter, currentDir.subdirectoryDepth(), pathFilter, fileTreeView);
boolean exists = true;
try {
dir.init();
} catch (final NoSuchFileException nsfe) {
exists = false;
} catch (final IOException e) {
}
if (exists) {
final Map> oldEntries = new HashMap<>();
final Map> newEntries = new HashMap<>();
final CachedDirectoryImpl previous =
currentDir.subdirectories.put(path.getFileName(), dir);
if (previous != null) {
oldEntries.put(previous.getPath(), previous.getEntry());
final Iterator> entryIterator =
previous.listEntries(Integer.MAX_VALUE, AllPass).iterator();
while (entryIterator.hasNext()) {
final Entry entry = entryIterator.next();
oldEntries.put(entry.getTypedPath().getPath(), entry);
}
previous.close();
}
newEntries.put(dir.getPath(), dir.getEntry());
final Iterator> it = dir.listEntries(Integer.MAX_VALUE, AllPass).iterator();
while (it.hasNext()) {
final Entry entry = it.next();
newEntries.put(entry.getTypedPath().getPath(), entry);
}
MapOps.diffDirectoryEntries(oldEntries, newEntries, updates);
} else {
final Iterator> it = remove(dir.getPath()).iterator();
while (it.hasNext()) {
updates.onDelete(it.next());
}
}
}
private boolean isLoop(final Path path, final Path realPath) {
return path.startsWith(realPath) && !path.equals(realPath);
}
private Updates updateImpl(final List parts, final TypedPath typedPath)
throws IOException {
final Updates result = new Updates<>();
if (this.subdirectories.lock()) {
try {
if (!parts.isEmpty()) {
final Iterator it = parts.iterator();
CachedDirectoryImpl currentDir = this;
while (it.hasNext() && currentDir != null && currentDir.depth >= 0) {
final Path p = it.next();
if (p.toString().isEmpty()) return result;
final Path resolved = currentDir.getPath().resolve(p);
final Path realPath = typedPath.expanded();
if (!it.hasNext()) {
// We will always return from this block
final boolean isDirectory = typedPath.isDirectory();
if (!isDirectory || currentDir.depth <= 0 || isLoop(resolved, realPath)) {
final CachedDirectoryImpl previousCachedDirectoryImpl =
isDirectory ? currentDir.subdirectories.get(p) : null;
final Entry oldEntry =
previousCachedDirectoryImpl != null
? previousCachedDirectoryImpl.getEntry()
: currentDir.files.get(p);
final Entry newEntry =
Entries.get(
TypedPaths.getDelegate(p, typedPath),
converter,
TypedPaths.getDelegate(resolved, typedPath));
if (isDirectory) {
final CachedDirectoryImpl previous =
currentDir.subdirectories.put(
p,
new CachedDirectoryImpl<>(
TypedPaths.getDelegate(resolved, typedPath),
converter,
-1,
pathFilter,
fileTreeView));
if (previous != null) previous.close();
} else {
currentDir.files.put(p, newEntry);
}
final Entry oldResolvedEntry =
oldEntry == null ? null : Entries.resolve(currentDir.getPath(), oldEntry);
if (oldResolvedEntry == null) {
result.onCreate(Entries.resolve(currentDir.getPath(), newEntry));
} else {
result.onUpdate(
oldResolvedEntry, Entries.resolve(currentDir.getPath(), newEntry));
}
return result;
} else {
addDirectory(currentDir, typedPath, result);
return result;
}
} else {
final CachedDirectoryImpl dir = currentDir.subdirectories.get(p);
if (dir == null && currentDir.depth > 0) {
addDirectory(
currentDir,
TypedPaths.getDelegate(currentDir.getPath().resolve(p), typedPath),
result);
}
currentDir = dir;
}
}
} else if (typedPath.isDirectory()) {
final List> oldEntries = listEntries(getMaxDepth(), AllPass);
init();
final List> newEntries = listEntries(getMaxDepth(), AllPass);
MapOps.diffDirectoryEntries(oldEntries, newEntries, result);
} else {
final Entry oldEntry = getEntry();
final TypedPath tp = TypedPaths.getDelegate(getTypedPath().expanded(), typedPath);
final Entry newEntry = Entries.get(tp, converter, tp);
_cacheEntry.set(newEntry);
result.onUpdate(oldEntry, getEntry());
}
} finally {
this.subdirectories.unlock();
}
}
return result;
}
private Either, CachedDirectoryImpl> findImpl(final List parts) {
final Iterator it = parts.iterator();
CachedDirectoryImpl currentDir = this;
Either, CachedDirectoryImpl> result = null;
while (it.hasNext() && currentDir != null && result == null) {
final Path p = it.next();
if (!it.hasNext()) {
final CachedDirectoryImpl subdir = currentDir.subdirectories.get(p);
if (subdir != null) {
result = Either.right(subdir);
} else {
final Entry entry = currentDir.files.get(p);
if (entry != null) result = Either.left(Entries.resolve(currentDir.getPath(), entry));
}
} else {
currentDir = currentDir.subdirectories.get(p);
}
}
return result;
}
private Either, CachedDirectoryImpl> find(final Path path) {
if (path.equals(this.getPath())) {
return Either.right(this);
} else if (!path.isAbsolute()) {
return findImpl(parts(path));
} else if (path.startsWith(this.getPath())) {
return findImpl(parts(this.getPath().relativize(path)));
} else {
return null;
}
}
@SuppressWarnings("unchecked")
private void listImpl(
final int maxDepth,
final Filter super R> filter,
final List result,
final ListTransformer function) {
if (this.depth < 0 || maxDepth < 0) {
result.add(function.apply(this.getEntry()));
} else {
if (subdirectories.lock()) {
try {
final Collection> files = new ArrayList<>(this.files.values());
final Collection> subdirectories =
new ArrayList<>(this.subdirectories.values());
final Iterator> filesIterator = files.iterator();
while (filesIterator.hasNext()) {
final Entry entry = filesIterator.next();
final R resolved = function.apply(Entries.resolve(getPath(), entry));
if (filter.accept(resolved)) result.add(resolved);
}
final Iterator> subdirIterator = subdirectories.iterator();
while (subdirIterator.hasNext()) {
final CachedDirectoryImpl subdir = subdirIterator.next();
final Entry entry = subdir.getEntry();
final R resolved = function.apply(Entries.resolve(getPath(), entry));
if (filter.accept(resolved)) result.add(resolved);
if (maxDepth > 0) subdir.listImpl(maxDepth - 1, filter, result, function);
}
} finally {
subdirectories.unlock();
}
}
}
}
private List> removeImpl(final List parts) {
final List> result = new ArrayList<>();
if (this.subdirectories.lock()) {
try {
final Iterator it = parts.iterator();
CachedDirectoryImpl currentDir = this;
while (it.hasNext() && currentDir != null) {
final Path p = it.next();
if (!it.hasNext()) {
final Entry entry = currentDir.files.remove(p);
if (entry != null) {
result.add(Entries.resolve(currentDir.getPath(), entry));
} else {
final CachedDirectoryImpl dir = currentDir.subdirectories.remove(p);
if (dir != null) {
result.addAll(dir.listEntries(Integer.MAX_VALUE, AllPass));
result.add(dir.getEntry());
}
}
} else {
currentDir = currentDir.subdirectories.get(p);
}
}
} finally {
this.subdirectories.unlock();
}
}
return result;
}
CachedDirectoryImpl init() throws IOException {
return init(typedPath.getPath());
}
private CachedDirectoryImpl init(final Path realPath) throws IOException {
if (subdirectories.lock()) {
try {
subdirectories.clear();
files.clear();
if (depth >= 0
&& (!this.getPath().startsWith(realPath) || this.getPath().equals(realPath))) {
final Iterator it =
fileTreeView.list(this.getPath(), 0, pathFilter).iterator();
while (it.hasNext()) {
final TypedPath file = it.next();
final Path path = file.getPath();
final Path expandedPath = file.expanded();
final Path key = this.typedPath.getPath().relativize(path).getFileName();
if (file.isDirectory()) {
if (depth > 0) {
if (!file.isSymbolicLink() || !isLoop(path, expandedPath)) {
final CachedDirectoryImpl dir =
new CachedDirectoryImpl<>(
file, converter, subdirectoryDepth(), pathFilter, fileTreeView);
try {
dir.init();
subdirectories.put(key, dir);
} catch (final IOException e) {
if (Files.exists(dir.getPath())) {
subdirectories.put(key, dir);
}
}
} else {
subdirectories.put(
key,
new CachedDirectoryImpl<>(file, converter, -1, pathFilter, fileTreeView));
}
} else {
files.put(key, Entries.get(TypedPaths.getDelegate(key, file), converter, file));
}
} else {
files.put(key, Entries.get(TypedPaths.getDelegate(key, file), converter, file));
}
}
}
} finally {
subdirectories.unlock();
}
}
return this;
}
}