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

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

There is a newer version: 2.1.12
Show newest version
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.FileSystems;
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.getTypedPath(), Create));
        try {
          final Iterator it =
              FileTreeViews.list(
                      newEntry.getTypedPath().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.getTypedPath(), 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 Path absolutePath = path.isAbsolute() ? path : path.toAbsolutePath();
    final int existingMaxDepth = directoryRegistry.maxDepthFor(absolutePath);
    boolean result = existingMaxDepth < maxDepth;
    final TypedPath typedPath = TypedPaths.get(absolutePath);
    Path realPath;
    try {
      realPath = absolutePath.toRealPath();
    } catch (final IOException e) {
      realPath = absolutePath.toAbsolutePath();
    }
    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 path = rawPath == null ? getRoot() : rawPath;
    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 Path getRoot() {
    /* This may not make sense on windows which has multiple root directories, but at least it
     * will return something.
     */
    final Iterator it = FileSystems.getDefault().getRootDirectories().iterator();
    return it.next();
  }

  private CachedDirectory findOrAddRoot(final Path rawPath) {
    final List toRemove = new ArrayList<>();
    CachedDirectory result = find(rawPath, toRemove);
    if (result == null) {
      /*
       * We want to monitor the parent in case the file is deleted.
       */
      final Path parent = rawPath.getParent();
      Path path = parent == null ? getRoot() : parent;
      assert (path != null);
      boolean init = false;
      while (!init && path != null) {
        try {
          result =
              new CachedDirectoryImpl<>(
                      TypedPaths.get(path),
                      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, false))
                  .init();
          init = true;
          rootDirectories.put(path, result);
        } catch (final IOException e) {
          path = path.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) {
    final Path absolutePath = path.isAbsolute() ? path : path.toAbsolutePath();
    directoryRegistry.removeDirectory(absolutePath);
    if (rootDirectories.lock()) {
      try {
        final CachedDirectory dir = find(absolutePath, new ArrayList());
        if (dir != null) {
          final int depth = dir.getPath().relativize(absolutePath).getNameCount();
          List> toRemove =
              dir.listEntries(
                  depth,
                  new Filter>() {
                    @Override
                    public boolean accept(final FileTreeDataViews.Entry entry) {
                      return !directoryRegistry.acceptPrefix(entry.getTypedPath().getPath());
                    }
                  });
          final Iterator> it = toRemove.iterator();
          while (it.hasNext()) {
            final FileTreeDataViews.Entry entry = it.next();
            if (!directoryRegistry.acceptPrefix(entry.getTypedPath().getPath())) {
              final Iterator> toCancel =
                  dir.remove(entry.getTypedPath().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).getTypedPath(), 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();
      final Path path = event.getTypedPath().getPath();
      if (directoryRegistry.accept(path) && handled.add(path)) {
        observers.onNext(new Event(TypedPaths.get(path), event.getKind()));
      }
    }
  }

  private void handleEvent(final Event event) {
    final List events = new ArrayList<>();
    if (!closed.get() && rootDirectories.lock()) {
      try {
        if (directoryRegistry.acceptPrefix(event.getTypedPath().getPath())) {
          final TypedPath typedPath = TypedPaths.get(event.getTypedPath().getPath());
          if (!typedPath.exists()) {
            final CachedDirectory root = getOrAdd(typedPath.getPath());
            if (root != null) {
              final boolean isRoot = root.getPath().equals(typedPath.getPath());
              final Iterator> it =
                  isRoot
                      ? root.listEntries(root.getMaxDepth(), AllPass).iterator()
                      : root.remove(typedPath.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).getTypedPath(), Kind.Delete));
              }
              final CachedDirectory parent =
                  find(typedPath.getPath().getParent(), new ArrayList());
              if (parent != null) {
                update(parent, parent.getEntry().getTypedPath(), events);
              }
              if (isRoot) {
                rootDirectories.remove(root.getPath());
                getOrAdd(typedPath.getPath());
              }
            }
          }
          events.add(event);
          if (typedPath.isDirectory()) {
            add(typedPath, events);
          }
        }
      } finally {
        rootDirectories.unlock();
      }
    }
    runCallbacks(events);
  }

  @Override
  public int addObserver(final Observer observer) {
    return observers.addObserver(observer);
  }

  @Override
  public void removeObserver(final int handle) {
    observers.removeObserver(handle);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy