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

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

There is a newer version: 2.1.12
Show newest version
package com.swoval.files;

import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

import com.swoval.files.apple.ClosedFileEventMonitorException;
import com.swoval.files.apple.FileEvent;
import com.swoval.files.apple.FileEventMonitor;
import com.swoval.files.apple.FileEventMonitors;
import com.swoval.files.apple.FileEventMonitors.Handle;
import com.swoval.files.apple.FileEventMonitors.Handles;
import com.swoval.files.apple.Flags.Create;
import com.swoval.functional.Consumer;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.NotDirectoryException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.Watchable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Provides an alternative {@link java.nio.file.WatchService} for mac that uses native file system
 * events rather than polling for file changes.
 */
class MacOSXWatchService implements RegisterableWatchService {
  private static class WatchKeys extends LockableMap {}

  private final int watchLatency;
  private final TimeUnit watchTimeUnit;
  private final int queueSize;
  private final AtomicBoolean open = new AtomicBoolean(true);
  private final WatchKeys registered = new WatchKeys();
  private final LinkedBlockingQueue readyKeys = new LinkedBlockingQueue<>();

  private final Consumer dropEvent =
      new Consumer() {
        @Override
        public void accept(final String s) {
          final Path path = Paths.get(s);
          final WatchKey watchKey = registered.get(path);
          if (watchKey != null) {
            watchKey.handle = Handles.INVALID;
          }
        }
      };
  private final Consumer onFileEvent =
      new Consumer() {
        @Override
        public void accept(final FileEvent fileEvent) {
          final Path path = Paths.get(fileEvent.fileName);
          final WatchKey childKeys = registered.get(path);
          final WatchKey watchKey =
              childKeys == null ? registered.get(path.getParent()) : childKeys;
          final boolean exists = Files.exists(path, LinkOption.NOFOLLOW_LINKS);
          if (watchKey != null) {
            if (fileEvent.mustScanSubDirs()) {
              final Iterator it = watchKey.keys.iterator();
              while (it.hasNext()) {
                final MacOSXWatchKey key = it.next();
                key.addOverflow();
              }
            } else {
              final Iterator it = watchKey.keys.iterator();
              while (it.hasNext()) {
                final MacOSXWatchKey key = it.next();
                if (exists && key.reportModifyEvents()) key.createEvent(ENTRY_MODIFY, path);
                else if (!exists && key.reportDeleteEvents()) key.createEvent(ENTRY_DELETE, path);
              }
            }
          }
        }
      };
  private FileEventMonitor fileEventMonitor;

  /**
   * Creates a new MacOSXWatchService.
   *
   * @param watchLatency the minimum latency between watch events specified in units of 
   *     timeUnit
   * @param timeUnit the time unit the latency is specified with
   * @param queueSize the maximum number of events to queue per watch key
   * @throws InterruptedException if the native file events api initialization is interrupted.
   */
  public MacOSXWatchService(final int watchLatency, final TimeUnit timeUnit, final int queueSize)
      throws InterruptedException {
    this.watchLatency = watchLatency;
    this.queueSize = queueSize;
    this.watchTimeUnit = timeUnit;
    this.fileEventMonitor = FileEventMonitors.get(onFileEvent, dropEvent);
  }

  /**
   * Create a new MacOSXWatchService with a minimum latency of 10 milliseconds and a maximum queue
   * size of 1024 per watch key.
   *
   * @throws InterruptedException if the native file events api initialization is interrupted.
   */
  public MacOSXWatchService() throws InterruptedException {
    // The FsEvents api doesn't seem to report events at lower than 10 millisecond intervals.
    this(10, TimeUnit.MILLISECONDS, 1024);
  }

  @Override
  @SuppressWarnings("EmptyCatchBlock")
  public void close() {
    if (open.compareAndSet(true, false)) {
      final Iterator it = new ArrayList<>(registered.values()).iterator();
      while (it.hasNext()) {
        final WatchKey watchKey = it.next();
        if (watchKey.handle != Handles.INVALID) {
          try {
            fileEventMonitor.stopStream(watchKey.handle);
          } catch (final ClosedFileEventMonitorException e) {
            e.printStackTrace(System.err);
          }
        }
        final Iterator keys = watchKey.keys.iterator();
        while (keys.hasNext()) {
          keys.next().cancel();
        }
        watchKey.keys.clear();
      }
      registered.clear();
      fileEventMonitor.close();
    }
  }

  @Override
  public java.nio.file.WatchKey poll() {
    if (isOpen()) {
      return readyKeys.poll();
    } else {
      throw new ClosedWatchServiceException();
    }
  }

  @Override
  public java.nio.file.WatchKey poll(long timeout, TimeUnit unit) throws InterruptedException {
    if (isOpen()) {
      return readyKeys.poll(timeout, unit);
    } else {
      throw new ClosedWatchServiceException();
    }
  }

  @Override
  public java.nio.file.WatchKey take() throws InterruptedException {
    if (isOpen()) {
      return readyKeys.take();
    } else {
      throw new ClosedWatchServiceException();
    }
  }

  private boolean isOpen() {
    return open.get();
  }

  @Override
  public java.nio.file.WatchKey register(final Path path, final Kind... kinds)
      throws IOException {
    if (isOpen() && registered.lock()) {
      try {
        final Path realPath = path.toRealPath();
        if (!Files.isDirectory(realPath)) throw new NotDirectoryException(realPath.toString());
        final WatchKey watchKey = registered.get(realPath);
        MacOSXWatchKey result;
        if (watchKey == null) {
          final Create flags = new Create().setNoDefer().setFileEvents();
          Handle handle = null;
          final Iterator it = registered.keys().iterator();
          while (it.hasNext() && handle != Handles.INVALID) {
            if (realPath.startsWith(it.next())) {
              handle = Handles.INVALID;
            }
          }
          if (handle != Handles.INVALID) {
            try {
              handle = fileEventMonitor.createStream(realPath, watchLatency, watchTimeUnit, flags);
            } catch (final ClosedFileEventMonitorException e) {
              MacOSXWatchService.this.close();
              throw e;
            }
          }
          result = new MacOSXWatchKey(realPath, queueSize, handle, kinds);
          if (registered.put(realPath, new WatchKey(handle, result)) != null) {
            result.cancel();
            throw new ClosedWatchServiceException();
          }
        } else {
          result = new MacOSXWatchKey(realPath, queueSize, Handles.INVALID, kinds);
          watchKey.keys.add(result);
        }
        return result;
      } finally {
        registered.unlock();
      }
    } else {
      throw new ClosedWatchServiceException();
    }
  }

  private static class Event implements WatchEvent {
    private final WatchEvent.Kind _kind;
    private final int _count;
    private final T _context;

    Event(final WatchEvent.Kind kind, final int count, final T context) {
      _kind = kind;
      _count = count;
      _context = context;
    }

    @Override
    public Kind kind() {
      return _kind;
    }

    @Override
    public int count() {
      return _count;
    }

    @Override
    public T context() {
      return _context;
    }

    @Override
    public String toString() {
      return "Event(" + _context + ", " + _kind + ")";
    }
  }

  class MacOSXWatchKey implements java.nio.file.WatchKey {
    private final ArrayBlockingQueue> events;
    private final AtomicInteger overflow = new AtomicInteger(0);
    private final AtomicBoolean valid = new AtomicBoolean(true);

    public Handle getHandle() {
      return handle;
    }

    public void setHandle(final Handle handle) {
      this.handle = handle;
    }

    private Handle handle;
    private final boolean reportCreateEvents;
    private final boolean reportModifyEvents;

    @SuppressWarnings("unused")
    public boolean reportCreateEvents() {
      return reportCreateEvents;
    }

    boolean reportModifyEvents() {
      return reportModifyEvents;
    }

    boolean reportDeleteEvents() {
      return reportDeleteEvents;
    }

    private final boolean reportDeleteEvents;
    private final Path watchable;

    MacOSXWatchKey(
        final Path watchable,
        final int queueSize,
        final Handle handle,
        final WatchEvent.Kind... kinds) {
      events = new ArrayBlockingQueue<>(queueSize);
      this.handle = handle;
      this.watchable = watchable;
      final Set> kindSet = new HashSet<>();
      int i = 0;
      while (i < kinds.length) {
        kindSet.add(kinds[i]);
        i += 1;
      }
      this.reportCreateEvents = kindSet.contains(ENTRY_CREATE);
      this.reportModifyEvents = kindSet.contains(ENTRY_MODIFY);
      this.reportDeleteEvents = kindSet.contains(ENTRY_DELETE);
    }

    @Override
    public void cancel() {
      valid.set(false);
    }

    @Override
    public Watchable watchable() {
      return watchable;
    }

    @Override
    public boolean isValid() {
      return valid.get();
    }

    @Override
    public List> pollEvents() {
      synchronized (MacOSXWatchKey.this) {
        final int overflowCount = overflow.getAndSet(0);
        final List> result =
            new ArrayList<>(events.size() + overflowCount > 0 ? 1 : 0);
        events.drainTo(result);
        if (overflowCount != 0) {
          result.add(new Event<>(OVERFLOW, overflowCount, watchable));
        }
        return Collections.unmodifiableList(result);
      }
    }

    @Override
    public boolean reset() {
      return true;
    }

    @Override
    public String toString() {
      return "MacOSXWatchKey(" + watchable + ")";
    }

    void addOverflow() {
      events.add(new Event<>(OVERFLOW, 1, null));
      overflow.incrementAndGet();
      if (!readyKeys.contains(this)) {
        readyKeys.offer(this);
      }
    }

    void addEvent(Event event) {
      synchronized (MacOSXWatchKey.this) {
        if (!events.offer(event)) {
          overflow.incrementAndGet();
        } else {
          if (!readyKeys.contains(this)) {
            readyKeys.add(this);
          }
        }
      }
    }

    void createEvent(final WatchEvent.Kind kind, final Path file) {
      Event event = new Event<>(kind, 1, watchable.relativize(file));
      addEvent(event);
    }
  }

  private class WatchKey implements AutoCloseable {
    private Handle handle;
    private final Set keys = new HashSet<>();

    WatchKey(final Handle handle, final MacOSXWatchKey key) {
      this.handle = handle;
      keys.add(key);
    }

    @Override
    public void close() {
      final Iterator it = keys.iterator();
      while (it.hasNext()) {
        it.next().cancel();
      }
      keys.clear();
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy