com.swoval.files.FileCacheDirectoryTree Maven / Gradle / Ivy
package com.swoval.files;
import static com.swoval.files.PathWatchers.Event.Kind.Create;
import static com.swoval.files.PathWatchers.Event.Kind.Delete;
import static com.swoval.files.PathWatchers.Event.Kind.Error;
import static com.swoval.files.PathWatchers.Event.Kind.Modify;
import static com.swoval.functional.Filters.AllPass;
import com.swoval.files.FileTreeDataViews.CacheObserver;
import com.swoval.files.FileTreeDataViews.Converter;
import com.swoval.files.FileTreeDataViews.Entry;
import com.swoval.files.FileTreeDataViews.ObservableCache;
import com.swoval.files.FileTreeRepositoryImpl.Callback;
import com.swoval.files.FileTreeViews.Observer;
import com.swoval.files.PathWatchers.Event;
import com.swoval.files.PathWatchers.Event.Kind;
import com.swoval.functional.Filter;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
class FileCacheDirectories extends LockableMap> {
FileCacheDirectories(final ReentrantLock lock) {
super(new HashMap>(), lock);
}
}
class FileCachePendingFiles extends Lockable {
private final Set pendingFiles = new HashSet<>();
FileCachePendingFiles(final ReentrantLock reentrantLock) {
super(reentrantLock);
}
void clear() {
if (lock()) {
try {
pendingFiles.clear();
} finally {
unlock();
}
}
}
boolean add(final Path path) {
if (lock()) {
try {
return pendingFiles.add(path);
} finally {
unlock();
}
} else {
return false;
}
}
boolean remove(final Path path) {
if (lock()) {
try {
return pendingFiles.remove(path);
} finally {
unlock();
}
} else {
return false;
}
}
}
class FileCacheDirectoryTree implements ObservableCache, FileTreeDataView {
private final DirectoryRegistry directoryRegistry = new DirectoryRegistryImpl();
private final Filter filter = DirectoryRegistries.toTypedPathFilter(directoryRegistry);
private final Converter converter;
private final CacheObservers observers = new CacheObservers<>();
private final Executor callbackExecutor;
private final boolean followLinks;
private final AtomicBoolean closed = new AtomicBoolean(false);
final SymlinkWatcher symlinkWatcher;
FileCacheDirectoryTree(
final Converter converter,
final Executor callbackExecutor,
final SymlinkWatcher symlinkWatcher) {
this.converter = converter;
this.callbackExecutor = callbackExecutor;
this.symlinkWatcher = symlinkWatcher;
this.followLinks = symlinkWatcher != null;
if (symlinkWatcher != null) {
symlinkWatcher.addObserver(
new Observer() {
@Override
public void onError(final Throwable t) {
t.printStackTrace(System.err);
}
@Override
public void onNext(final Event event) {
handleEvent(event.getTypedPath());
}
});
}
final ReentrantLock reentrantLock = new ReentrantLock();
pendingFiles = new FileCachePendingFiles(reentrantLock);
directories = new FileCacheDirectories<>(reentrantLock);
}
private final FileCacheDirectories directories;
private final FileCachePendingFiles pendingFiles;
private final DirectoryRegistry READ_ONLY_DIRECTORY_REGISTRY =
new DirectoryRegistry() {
@Override
public void close() {}
@Override
public boolean addDirectory(final Path path, final int maxDepth) {
return false;
}
@Override
public int maxDepthFor(final Path path) {
return directoryRegistry.maxDepthFor(path);
}
@Override
public Map registered() {
return directoryRegistry.registered();
}
@Override
public void removeDirectory(final Path path) {}
@Override
public boolean acceptPrefix(final Path path) {
return directoryRegistry.acceptPrefix(path);
}
@Override
public boolean accept(final Path path) {
return directoryRegistry.accept(path);
}
};
DirectoryRegistry readOnlyDirectoryRegistry() {
return READ_ONLY_DIRECTORY_REGISTRY;
}
void unregister(final Path path) {
if (directories.lock()) {
try {
directoryRegistry.removeDirectory(path);
if (!directoryRegistry.accept(path)) {
final CachedDirectory dir = find(path);
if (dir != null) {
if (dir.getPath().equals(path)) {
directories.remove(path);
} else {
dir.remove(path);
}
}
}
} finally {
directories.unlock();
}
}
}
private CachedDirectory find(final Path path) {
CachedDirectory foundDir = null;
final List> dirs = directories.values();
Collections.sort(
dirs,
new Comparator>() {
@Override
public int compare(final CachedDirectory left, final CachedDirectory right) {
// Descending order so that we find the most specific path
return right.getPath().compareTo(left.getPath());
}
});
final Iterator> it = dirs.iterator();
while (it.hasNext() && foundDir == null) {
final CachedDirectory dir = it.next();
if (path.startsWith(dir.getPath())) {
if (dir.getMaxDepth() == Integer.MAX_VALUE || path.equals(dir.getPath())) {
foundDir = dir;
} else {
int depth = dir.getPath().relativize(path).getNameCount() - 1;
if (depth <= dir.getMaxDepth()) {
foundDir = dir;
}
}
}
}
return foundDir;
}
private void runCallbacks(final List callbacks) {
if (!callbacks.isEmpty() && !closed.get()) {
callbackExecutor.run(
new Runnable() {
@Override
public void run() {
Collections.sort(callbacks);
final Iterator it = callbacks.iterator();
while (it.hasNext()) {
it.next().run();
}
}
});
}
}
@SuppressWarnings("EmptyCatchBlock")
void handleEvent(final TypedPath typedPath) {
final List symlinks = new ArrayList<>();
final List callbacks = new ArrayList<>();
if (!closed.get() && directories.lock()) {
try {
final Path path = typedPath.getPath();
if (typedPath.exists()) {
final CachedDirectory dir = find(typedPath.getPath());
if (dir != null) {
try {
final TypedPath updatePath =
(followLinks || !typedPath.isSymbolicLink())
? typedPath
: TypedPaths.get(typedPath.getPath(), Entries.LINK | Entries.FILE);
dir.update(updatePath).observe(callbackObserver(callbacks, symlinks));
} catch (final IOException e) {
handleDelete(path, callbacks, symlinks);
}
} else if (pendingFiles.remove(path)) {
try {
CachedDirectory cachedDirectory;
try {
cachedDirectory = newCachedDirectory(path, directoryRegistry.maxDepthFor(path));
} catch (final NotDirectoryException nde) {
cachedDirectory = newCachedDirectory(path, -1);
}
final CachedDirectory previous = directories.put(path, cachedDirectory);
if (previous != null) previous.close();
addCallback(
callbacks,
symlinks,
cachedDirectory.getEntry(),
null,
cachedDirectory.getEntry(),
Create,
null);
final Iterator> it =
cachedDirectory.listEntries(cachedDirectory.getMaxDepth(), AllPass).iterator();
while (it.hasNext()) {
final FileTreeDataViews.Entry entry = it.next();
addCallback(callbacks, symlinks, entry, null, entry, Create, null);
}
} catch (final IOException e) {
pendingFiles.add(path);
}
}
} else {
handleDelete(path, callbacks, symlinks);
}
} finally {
directories.unlock();
}
final Iterator it = symlinks.iterator();
while (it.hasNext()) {
final TypedPath tp = it.next();
final Path path = tp.getPath();
if (symlinkWatcher != null) {
if (tp.exists()) {
try {
symlinkWatcher.addSymlink(path, directoryRegistry.maxDepthFor(path));
} catch (final IOException e) {
observers.onError(e);
}
} else {
symlinkWatcher.remove(path);
}
}
}
runCallbacks(callbacks);
}
}
private void handleDelete(
final Path path, final List callbacks, final List symlinks) {
final List>> removeIterators = new ArrayList<>();
final Iterator> directoryIterator =
new ArrayList<>(directories.values()).iterator();
while (directoryIterator.hasNext()) {
final CachedDirectory dir = directoryIterator.next();
if (path.startsWith(dir.getPath())) {
final List> updates =
path.equals(dir.getPath())
? dir.listEntries(Integer.MAX_VALUE, AllPass)
: dir.remove(path);
final Iterator it = directoryRegistry.registered().keySet().iterator();
while (it.hasNext()) {
if (it.next().equals(path)) {
pendingFiles.add(path);
}
}
if (dir.getPath().equals(path)) {
directories.remove(path);
updates.add(dir.getEntry());
}
removeIterators.add(updates.iterator());
}
}
final Iterator>> it = removeIterators.iterator();
while (it.hasNext()) {
final Iterator> removeIterator = it.next();
while (removeIterator.hasNext()) {
final FileTreeDataViews.Entry entry = Entries.setExists(removeIterator.next(), false);
addCallback(callbacks, symlinks, entry, entry, null, Delete, null);
}
}
}
@Override
public void close() {
if (closed.compareAndSet(false, true) && directories.lock()) {
try {
callbackExecutor.close();
if (symlinkWatcher != null) symlinkWatcher.close();
directories.clear();
observers.close();
directoryRegistry.close();
pendingFiles.clear();
} finally {
directories.unlock();
}
}
}
CachedDirectory register(
final Path path, final int maxDepth, final PathWatcher watcher)
throws IOException {
if (directoryRegistry.addDirectory(path, maxDepth) && directories.lock()) {
try {
watcher.register(path, maxDepth);
final List> dirs = new ArrayList<>(directories.values());
Collections.sort(
dirs,
new Comparator>() {
@Override
public int compare(final CachedDirectory left, final CachedDirectory right) {
return left.getPath().compareTo(right.getPath());
}
});
final Iterator> it = dirs.iterator();
CachedDirectory existing = null;
while (it.hasNext() && existing == null) {
final CachedDirectory dir = it.next();
if (path.startsWith(dir.getPath())) {
final int depth = dir.getPath().relativize(path).getNameCount() - 1;
if (dir.getMaxDepth() == Integer.MAX_VALUE || dir.getMaxDepth() - depth > maxDepth) {
existing = dir;
}
}
}
CachedDirectory dir;
if (existing == null) {
try {
try {
dir = newCachedDirectory(path, maxDepth);
} catch (final NotDirectoryException e) {
dir = newCachedDirectory(path, -1);
}
directories.put(path, dir);
} catch (final NoSuchFileException e) {
pendingFiles.add(path);
dir = newCachedDirectory(path, -1);
}
} else {
existing.update(TypedPaths.get(path));
dir = existing;
}
cleanupDirectories(path, maxDepth);
return dir;
} finally {
directories.unlock();
}
} else {
return null;
}
}
private void cleanupDirectories(final Path path, final int maxDepth) {
final Iterator> it = directories.values().iterator();
final List toRemove = new ArrayList<>();
while (it.hasNext()) {
final CachedDirectory dir = it.next();
if (dir.getPath().startsWith(path) && !dir.getPath().equals(path)) {
if (maxDepth == Integer.MAX_VALUE) {
toRemove.add(dir.getPath());
} else {
int depth = path.relativize(dir.getPath()).getNameCount();
if (maxDepth - depth >= dir.getMaxDepth()) {
toRemove.add(dir.getPath());
}
}
}
}
final Iterator removeIterator = toRemove.iterator();
while (removeIterator.hasNext()) {
directories.remove(removeIterator.next());
}
}
@SuppressWarnings("EmptyCatchBlock")
private void addCallback(
final List callbacks,
final List symlinks,
final FileTreeDataViews.Entry entry,
final FileTreeDataViews.Entry oldEntry,
final FileTreeDataViews.Entry newEntry,
final Kind kind,
final IOException ioException) {
final TypedPath typedPath = entry == null ? null : entry.getTypedPath();
if (typedPath != null && typedPath.isSymbolicLink()) {
symlinks.add(typedPath);
}
callbacks.add(
new Callback(typedPath == null ? Paths.get("") : typedPath.getPath()) {
@Override
public void run() {
try {
if (ioException != null) {
observers.onError(ioException);
} else if (kind.equals(Create)) {
observers.onCreate(newEntry);
} else if (kind.equals(Delete)) {
observers.onDelete(Entries.setExists(oldEntry, false));
} else if (kind.equals(Modify)) {
observers.onUpdate(oldEntry, newEntry);
}
} catch (final Exception e) {
e.printStackTrace();
}
}
});
}
@Override
public int addObserver(final Observer super Entry> observer) {
return observers.addObserver(observer);
}
@Override
public void removeObserver(final int handle) {
observers.removeObserver(handle);
}
@Override
public int addCacheObserver(final CacheObserver observer) {
return observers.addCacheObserver(observer);
}
@Override
public List> listEntries(
final Path path, final int maxDepth, final Filter super Entry> filter) {
if (directories.lock()) {
try {
final CachedDirectory dir = find(path);
if (dir == null) {
return Collections.emptyList();
} else {
if (dir.getPath().equals(path) && dir.getMaxDepth() == -1) {
List> result = new ArrayList<>();
result.add(dir.getEntry());
return result;
} else {
final int depth = directoryRegistry.maxDepthFor(path);
return dir.listEntries(path, depth < maxDepth ? depth : maxDepth, filter);
}
}
} finally {
directories.unlock();
}
} else {
return Collections.emptyList();
}
}
private CacheObserver callbackObserver(
final List callbacks, final List symlinks) {
return new CacheObserver() {
@Override
public void onCreate(final FileTreeDataViews.Entry newEntry) {
addCallback(callbacks, symlinks, newEntry, null, newEntry, Create, null);
}
@Override
public void onDelete(final FileTreeDataViews.Entry oldEntry) {
addCallback(callbacks, symlinks, oldEntry, oldEntry, null, Delete, null);
}
@Override
public void onUpdate(
final FileTreeDataViews.Entry oldEntry, final FileTreeDataViews.Entry newEntry) {
addCallback(callbacks, symlinks, oldEntry, oldEntry, newEntry, Modify, null);
}
@Override
public void onError(final IOException exception) {
addCallback(callbacks, symlinks, null, null, null, Error, exception);
}
};
}
@Override
public List list(Path path, int maxDepth, Filter super TypedPath> filter) {
if (directories.lock()) {
try {
final CachedDirectory dir = find(path);
if (dir == null) {
return Collections.emptyList();
} else {
if (dir.getPath().equals(path) && dir.getMaxDepth() == -1) {
List result = new ArrayList<>();
result.add(TypedPaths.getDelegate(dir.getPath(), dir.getTypedPath()));
return result;
} else {
return dir.list(path, maxDepth, filter);
}
}
} finally {
directories.unlock();
}
} else {
return Collections.emptyList();
}
}
private CachedDirectory newCachedDirectory(final Path path, final int depth)
throws IOException {
return new CachedDirectoryImpl<>(
TypedPaths.get(path),
converter,
depth,
filter,
FileTreeViews.getDefault(followLinks, false))
.init();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy