com.swoval.files.NioPathWatcher 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.Modify;
import static com.swoval.functional.Filters.AllPass;
import static java.util.Map.Entry;
import com.swoval.files.FileTreeDataViews.CacheObserver;
import com.swoval.files.FileTreeDataViews.Converter;
import com.swoval.files.FileTreeViews.Observer;
import com.swoval.files.PathWatchers.Event;
import com.swoval.files.PathWatchers.Event.Kind;
import com.swoval.files.PathWatchers.Overflow;
import com.swoval.functional.Consumer;
import com.swoval.functional.Either;
import com.swoval.functional.Filter;
import com.swoval.runtime.Platform;
import java.io.IOException;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
class RootDirectories extends LockableMap> {}
/** Provides a PathWatcher that is backed by a {@link java.nio.file.WatchService}. */
class NioPathWatcher implements PathWatcher, AutoCloseable {
private final AtomicBoolean closed = new AtomicBoolean(false);
private final Observers observers = new Observers<>();
private final RootDirectories rootDirectories = new RootDirectories();
private final DirectoryRegistry directoryRegistry;
private final Converter converter;
private CacheObserver updateCacheObserver(final List events) {
return new CacheObserver() {
@Override
@SuppressWarnings("EmptyCatchBlock")
public void onCreate(final FileTreeDataViews.Entry newEntry) {
events.add(new Event(newEntry, Create));
try {
final Iterator it =
FileTreeViews.list(
newEntry.getPath(),
0,
new Filter() {
@Override
public boolean accept(final TypedPath typedPath) {
return directoryRegistry.accept(typedPath.getPath());
}
})
.iterator();
while (it.hasNext()) {
final TypedPath tp = it.next();
events.add(new Event(tp, Create));
}
} catch (final IOException e) {
// This likely means the directory was deleted, which should be handle by the downstream
// NioPathWatcherService.
}
}
@Override
public void onDelete(final FileTreeDataViews.Entry oldEntry) {
if (oldEntry.getValue().isRight()) {
oldEntry.getValue().get().close();
}
events.add(new Event(oldEntry, Delete));
}
@Override
public void onUpdate(
final FileTreeDataViews.Entry oldEntry,
final FileTreeDataViews.Entry newEntry) {}
@Override
public void onError(final IOException exception) {}
};
}
private final NioPathWatcherService service;
NioPathWatcher(
final DirectoryRegistry directoryRegistry, final RegisterableWatchService watchService)
throws InterruptedException {
this.directoryRegistry = directoryRegistry;
this.service =
new NioPathWatcherService(
new Consumer>() {
@Override
public void accept(final Either either) {
if (!closed.get()) {
if (either.isRight()) {
final Event event = either.get();
handleEvent(event);
} else {
handleOverflow(Either.leftProjection(either).getValue());
}
}
}
},
watchService);
this.converter =
new Converter() {
@Override
public WatchedDirectory apply(final TypedPath typedPath) {
return typedPath.isDirectory()
? Either.getOrElse(
service.register(typedPath.getPath()), WatchedDirectories.INVALID)
: WatchedDirectories.INVALID;
}
};
}
/**
* Similar to register, but tracks all of the new files found in the directory. It polls the
* directory until the contents stop changing to ensure that a callback is fired for each path in
* the newly created directory (up to the maxDepth). The assumption is that once the callback is
* fired for the path, it is safe to assume that no event for a new file in the directory is
* missed. Without the polling, it would be possible that a new file was created in the directory
* before we registered it with the watch service. If this happened, then no callback would be
* invoked for that file.
*
* @param typedPath The newly created directory to add
*/
void add(final TypedPath typedPath, final List events) {
if (directoryRegistry.maxDepthFor(typedPath.getPath()) >= 0) {
final CachedDirectory dir = getOrAdd(typedPath.getPath());
if (dir != null) {
update(dir, typedPath, events);
}
}
}
@Override
public Either register(final Path path, final int maxDepth) {
final int existingMaxDepth = directoryRegistry.maxDepthFor(path);
boolean result = existingMaxDepth < maxDepth;
final TypedPath typedPath = TypedPaths.get(path);
final Path realPath = typedPath.toRealPath();
if (result) {
directoryRegistry.addDirectory(typedPath.getPath(), maxDepth);
}
final CachedDirectory dir = getOrAdd(realPath);
final List events = new ArrayList<>();
if (dir != null) {
final List> directories =
dir.listEntries(typedPath.getPath(), -1, AllPass);
if (result || directories.isEmpty() || directories.get(0).getValue().isRight()) {
Path toUpdate = typedPath.getPath();
if (toUpdate != null) update(dir, typedPath, events);
}
}
runCallbacks(events);
return Either.right(result);
}
private CachedDirectory find(final Path rawPath, final List toRemove) {
final Path parent = Platform.isMac() ? rawPath : rawPath.getRoot();
final Path path = parent == null ? rawPath.getRoot() : parent;
assert (path != null);
if (rootDirectories.lock()) {
try {
final Iterator>> it =
rootDirectories.iterator();
CachedDirectory result = null;
while (result == null && it.hasNext()) {
final Entry> entry = it.next();
final Path root = entry.getKey();
if (path.startsWith(root)) {
result = entry.getValue();
} else if (root.startsWith(path) && !path.equals(root)) {
toRemove.add(root);
}
}
return result;
} finally {
rootDirectories.unlock();
}
} else {
return null;
}
}
private CachedDirectory findOrAddRoot(final Path rawPath) {
final Path parent = Platform.isMac() ? rawPath : rawPath.getRoot();
final Path path = parent == null ? rawPath.getRoot() : parent;
assert (path != null);
final List toRemove = new ArrayList<>();
CachedDirectory result = find(rawPath, toRemove);
if (result == null) {
Path toAdd = path;
boolean init = false;
while (!init && toAdd != null) {
try {
result =
new CachedDirectoryImpl<>(
toAdd,
toAdd,
converter,
Integer.MAX_VALUE,
new Filter() {
@Override
public boolean accept(final TypedPath typedPath) {
return typedPath.isDirectory()
&& !typedPath.isSymbolicLink()
&& directoryRegistry.acceptPrefix(typedPath.getPath());
}
},
FileTreeViews.getDefault(false))
.init();
init = true;
rootDirectories.put(toAdd, result);
} catch (final IOException e) {
toAdd = toAdd.getParent();
}
}
}
final Iterator toRemoveIterator = toRemove.iterator();
while (toRemoveIterator.hasNext()) {
rootDirectories.remove(toRemoveIterator.next());
}
return result;
}
private CachedDirectory getOrAdd(final Path path) {
CachedDirectory result = null;
if (rootDirectories.lock()) {
try {
if (!closed.get()) {
result = findOrAddRoot(path);
}
} finally {
rootDirectories.unlock();
}
}
return result;
}
@Override
@SuppressWarnings("EmptyCatchBlock")
public void unregister(final Path path) {
directoryRegistry.removeDirectory(path);
if (rootDirectories.lock()) {
try {
final CachedDirectory dir = find(path, new ArrayList());
if (dir != null) {
final int depth = dir.getPath().relativize(path).getNameCount();
List> toRemove =
dir.listEntries(
depth,
new Filter>() {
@Override
public boolean accept(final FileTreeDataViews.Entry entry) {
return !directoryRegistry.acceptPrefix(entry.getPath());
}
});
final Iterator> it = toRemove.iterator();
while (it.hasNext()) {
final FileTreeDataViews.Entry entry = it.next();
if (!directoryRegistry.acceptPrefix(entry.getPath())) {
final Iterator> toCancel =
dir.remove(entry.getPath()).iterator();
while (toCancel.hasNext()) {
final Either either = toCancel.next().getValue();
if (either.isRight()) either.get().close();
}
}
}
rootDirectories.remove(dir.getPath());
}
} finally {
rootDirectories.unlock();
}
}
}
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
service.close();
rootDirectories.clear();
}
}
@SuppressWarnings("EmptyCatchBlock")
private void update(
final CachedDirectory dir,
final TypedPath typedPath,
final List events) {
try {
dir.update(typedPath).observe(updateCacheObserver(events));
} catch (final NoSuchFileException e) {
dir.remove(typedPath.getPath());
final TypedPath newTypedPath = TypedPaths.get(typedPath.getPath());
events.add(new Event(newTypedPath, newTypedPath.exists() ? Kind.Modify : Kind.Delete));
final CachedDirectory root = rootDirectories.remove(typedPath.getPath());
if (root != null) {
final Iterator> it =
root.listEntries(Integer.MAX_VALUE, AllPass).iterator();
while (it.hasNext()) {
it.next().getValue().get().close();
}
}
} catch (final IOException e) {
}
}
private void handleOverflow(final Overflow overflow) {
final Path path = overflow.getPath();
final List events = new ArrayList<>();
if (rootDirectories.lock()) {
try {
final CachedDirectory root = find(path, new ArrayList());
if (root != null) {
try {
final Iterator it =
FileTreeViews.list(
path,
0,
new Filter() {
@Override
public boolean accept(TypedPath typedPath) {
return typedPath.isDirectory()
&& directoryRegistry.acceptPrefix(typedPath.getPath());
}
})
.iterator();
while (it.hasNext()) {
final TypedPath file = it.next();
add(file, events);
}
} catch (final IOException e) {
final List> removed = root.remove(path);
final Iterator> removedIt =
removed.iterator();
while (removedIt.hasNext()) {
events.add(new Event(Entries.setExists(removedIt.next(), false), Delete));
}
}
}
} finally {
rootDirectories.unlock();
}
}
final TypedPath tp = TypedPaths.get(path);
events.add(new Event(tp, tp.exists() ? Modify : Delete));
runCallbacks(events);
}
private void runCallbacks(final List events) {
final Iterator it = events.iterator();
final Set handled = new HashSet<>();
while (it.hasNext()) {
final Event event = it.next();
if (directoryRegistry.accept(event.getPath()) && handled.add(event.getPath())) {
observers.onNext(new Event(TypedPaths.get(event.getPath()), event.getKind()));
}
}
}
private void handleEvent(final Event event) {
final List events = new ArrayList<>();
if (!closed.get() && rootDirectories.lock()) {
try {
if (directoryRegistry.acceptPrefix(event.getPath())) {
final TypedPath typedPath = TypedPaths.get(event.getPath());
if (!typedPath.exists()) {
final CachedDirectory root = getOrAdd(event.getPath());
if (root != null) {
final boolean isRoot = root.getPath().equals(event.getPath());
final Iterator> it =
isRoot
? root.listEntries(root.getMaxDepth(), AllPass).iterator()
: root.remove(event.getPath()).iterator();
while (it.hasNext()) {
final FileTreeDataViews.Entry entry = it.next();
final Either either = entry.getValue();
if (either.isRight()) {
either.get().close();
}
events.add(new Event(Entries.setExists(entry, false), Kind.Delete));
}
final CachedDirectory parent =
find(event.getPath().getParent(), new ArrayList());
if (parent != null) {
update(parent, parent.getEntry(), events);
}
if (isRoot) {
rootDirectories.remove(root.getPath());
getOrAdd(event.getPath());
}
}
}
events.add(event);
if (typedPath.isDirectory()) {
add(typedPath, events);
}
}
} finally {
rootDirectories.unlock();
}
}
runCallbacks(events);
}
@Override
public int addObserver(final Observer super Event> observer) {
return observers.addObserver(observer);
}
@Override
public void removeObserver(final int handle) {
observers.removeObserver(handle);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy