All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.swoval.files.CachedDirectoryImpl Maven / Gradle / Ivy

There is a newer version: 2.1.12
Show newest version
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 pathFilter; private final LockableMap> subdirectories = new LockableMap<>(); private final Map> files = new HashMap<>(); private interface ListTransformer { R apply(final Entry entry); } CachedDirectoryImpl( final TypedPath typedPath, final Converter converter, final int depth, final Filter 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 filter) { return list(getPath(), maxDepth, filter); } @Override public List list( final Path path, final int maxDepth, final Filter 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> 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> 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 update(typedPath, true); } /** * Updates the CachedDirectory entry for a particular typed typedPath. * * @param typedPath the typedPath to update * @param rescanDirectoriesOnUpdate if true, rescan the entire subtree for this directory. This * can be very expensive. * @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, final boolean rescanDirectoriesOnUpdate) throws IOException { if (pathFilter.accept(typedPath)) { if (typedPath.exists()) { return updateImpl( typedPath.getPath().equals(this.getPath()) ? new ArrayList() : parts(this.getPath().relativize(typedPath.getPath())), typedPath, rescanDirectoriesOnUpdate); } else { final Iterator> it = remove(typedPath.getPath()).iterator(); final Updates result = new Updates<>(); while (it.hasNext()) result.onDelete(it.next()); return result; } } else { return 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("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 void updateDirectory( final CachedDirectoryImpl dir, final Updates result, final Entry entry) { result.onUpdate(dir.getEntry(), entry); dir._cacheEntry.set(entry); } private Updates updateImpl( final List parts, final TypedPath typedPath, final boolean rescanOnDirectoryUpdate) 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.get(p); if (previous == null || rescanOnDirectoryUpdate) { currentDir.subdirectories.put( p, new CachedDirectoryImpl<>( TypedPaths.getDelegate(resolved, typedPath), converter, -1, pathFilter, fileTreeView)); } else { updateDirectory(previous, result, newEntry); } } 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 { final CachedDirectoryImpl previous = currentDir.subdirectories.get(p); if (previous == null || rescanOnDirectoryUpdate) { addDirectory(currentDir, typedPath, result); } else { updateDirectory(previous, result, Entries.get(typedPath, converter, typedPath)); } 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; } } private void listImpl( final int maxDepth, final Filter 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; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy