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

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

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

import static com.swoval.functional.Either.getOrElse;
import static com.swoval.functional.Either.leftProjection;
import static java.util.Map.Entry;

import com.swoval.files.FileTreeViews.Observable;
import com.swoval.files.FileTreeViews.Observer;
import com.swoval.files.PathWatchers.Event;
import com.swoval.files.SymlinkWatcher.RegisteredPath;
import com.swoval.functional.Either;
import java.io.IOException;
import java.nio.file.FileSystemLoopException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;

class RegisteredPaths extends LockableMap {
  RegisteredPaths(final ReentrantLock reentrantLock) {
    super(new HashMap(), reentrantLock);
  }
}
/**
 * Monitors symlink targets. The {@link SymlinkWatcher} maintains a mapping of symlink targets to
 * symlink. When the symlink target is modified, the watcher will detect the update and invoke a
 * provided {@link com.swoval.functional.Consumer} for the symlink.
 */
class SymlinkWatcher implements Observable, AutoCloseable {
  private final AtomicBoolean isClosed = new AtomicBoolean(false);
  private final Observers observers = new Observers<>();
  private final Executor callbackExecutor =
      Executor.make("com.swoval.files.SymlinkWather.callback-executor");

  SymlinkWatcher(final PathWatcher watcher) {
    this.watcher = watcher;
    final ReentrantLock reentrantLock = new ReentrantLock();
    watchedSymlinksByDirectory = new RegisteredPaths(reentrantLock);
    watchedSymlinksByTarget = new RegisteredPaths(reentrantLock);
    watcher.addObserver(
        new Observer() {
          @Override
          public void onError(final Throwable t) {}

          @Override
          public void onNext(final Event event) {
            if (!isClosed.get()) {
              final List events = new ArrayList<>();
              final Path path = event.getPath();
              if (watchedSymlinksByTarget.lock()) {
                try {
                  final RegisteredPath registeredPath = find(path, watchedSymlinksByTarget);
                  if (registeredPath != null) {
                    final Path relativized = registeredPath.path.relativize(path);
                    final Iterator it = registeredPath.paths.iterator();
                    while (it.hasNext()) {
                      final Path rawPath = it.next().resolve(relativized);
                      if (!hasLoop(rawPath)) {
                        // final TypedPath typedPath = TypedPaths.get(rawPath);
                        events.add(
                            new Event(TypedPaths.get(rawPath, Entries.UNKNOWN), event.getKind()));
                      }
                    }
                  }
                } finally {
                  watchedSymlinksByTarget.unlock();
                }
              }
              if (!Files.exists(event.getPath())) {
                if (watchedSymlinksByTarget.lock()) {
                  try {
                    watchedSymlinksByTarget.remove(event.getPath());
                    final RegisteredPath registeredPath =
                        watchedSymlinksByDirectory.get(event.getPath());
                    if (registeredPath != null) {
                      registeredPath.paths.remove(event.getPath());
                      if (registeredPath.paths.isEmpty()) {
                        watcher.unregister(event.getPath());
                        watchedSymlinksByDirectory.remove(event.getPath());
                      }
                    }
                  } finally {
                    watchedSymlinksByTarget.unlock();
                  }
                }
              }

              final Iterator it = events.iterator();
              while (it.hasNext()) {
                final Event ev = it.next();
                observers.onNext(new Event(TypedPaths.get(ev.getPath()), ev.getKind()));
              }
            }
          }
        });
  }

  private final RegisteredPaths watchedSymlinksByDirectory;
  private final RegisteredPaths watchedSymlinksByTarget;

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

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

  static final class RegisteredPath implements AutoCloseable {
    public final Path path;
    public final Set paths = new HashSet<>();

    RegisteredPath(final Path path, final Path base) {
      this.path = path;
      paths.add(base);
    }

    @Override
    public void close() {
      paths.clear();
    }
  }

  private RegisteredPath find(final Path path, final RegisteredPaths registeredPaths) {
    final RegisteredPath result = registeredPaths.get(path);
    if (result != null) return result;
    else if (path == null || path.getNameCount() == 0) return null;
    else {
      final Path parent = path.getParent();
      if (parent == null || parent.getNameCount() == 0) return null;
      else return find(parent, registeredPaths);
    }
  }

  @SuppressWarnings("EmptyCatchBlock")
  private boolean hasLoop(final Path path) {
    boolean result = false;
    final Path parent = path.getParent();
    try {
      final Path realPath = parent.toRealPath();
      result = parent.startsWith(realPath) && !parent.equals(realPath);
    } catch (final IOException e) {
    }
    return result;
  }

  /*
   * This declaration must go below the constructor for javascript codegen.
   */
  private final PathWatcher watcher;

  @Override
  @SuppressWarnings("EmptyCatchBlock")
  public void close() {
    if (isClosed.compareAndSet(false, true)) {
      final Iterator targetIt = watchedSymlinksByTarget.values().iterator();
      while (targetIt.hasNext()) {
        targetIt.next().paths.clear();
      }
      watchedSymlinksByTarget.clear();
      final Iterator dirIt = watchedSymlinksByDirectory.values().iterator();
      while (dirIt.hasNext()) {
        dirIt.next().paths.clear();
      }
      watchedSymlinksByDirectory.clear();
      watcher.close();
      callbackExecutor.close();
    }
  }

  /**
   * Start monitoring a symlink. As long as the target exists, this method will check if the parent
   * directory of the target is being monitored. If the parent isn't being registered, we register
   * it with the watch service. We add the target symlink to the set of symlinks watched in the
   * parent directory. We also add the base symlink to the set of watched symlinks for this
   * particular target.
   *
   * @param path The symlink base file.
   */
  @SuppressWarnings("EmptyCatchBlock")
  void addSymlink(final Path path, final int maxDepth) throws IOException {
    if (!isClosed.get()) {
      try {
        final Path realPath = path.toRealPath();
        if (path.startsWith(realPath) && !path.equals(realPath)) {
          throw new FileSystemLoopException(path.toString());
        } else {
          if (watchedSymlinksByTarget.lock()) {
            try {
              final RegisteredPath targetRegistrationPath = watchedSymlinksByTarget.get(realPath);
              if (targetRegistrationPath == null) {
                final RegisteredPath registeredPath = watchedSymlinksByDirectory.get(realPath);
                if (registeredPath == null) {
                  final Either result = watcher.register(realPath, maxDepth);
                  if (getOrElse(result, false)) {
                    watchedSymlinksByDirectory.put(realPath, new RegisteredPath(path, realPath));
                    watchedSymlinksByTarget.put(realPath, new RegisteredPath(realPath, path));
                  } else if (result.isLeft()) {
                    throw leftProjection(result).getValue();
                  }
                }
              } else {
                targetRegistrationPath.paths.add(path);
              }
            } finally {
              watchedSymlinksByTarget.unlock();
            }
          }
        }
      } catch (final IOException e) {
        throw e;
      }
    }
  }

  /**
   * Removes the symlink from monitoring. If there are no remaining targets in the parent directory,
   * then we remove the parent directory from monitoring.
   *
   * @param path The symlink base to stop monitoring
   */
  void remove(final Path path) {
    if (!isClosed.get()) {
      if (watchedSymlinksByTarget.lock()) {
        try {
          Path target = null;
          final Iterator> it = watchedSymlinksByTarget.iterator();
          while (it.hasNext() && target == null) {
            final Entry entry = it.next();
            if (entry.getValue().paths.remove(path)) {
              target = entry.getKey();
            }
          }
          if (target != null) {
            final RegisteredPath targetRegisteredPath = watchedSymlinksByTarget.get(target);
            if (targetRegisteredPath != null) {
              targetRegisteredPath.paths.remove(path);
              if (targetRegisteredPath.paths.isEmpty()) {
                watchedSymlinksByTarget.remove(target);
                final RegisteredPath registeredPath = watchedSymlinksByDirectory.get(target);
                if (registeredPath != null) {
                  registeredPath.paths.remove(target);
                  if (registeredPath.paths.isEmpty()) {
                    watcher.unregister(target);
                    watchedSymlinksByDirectory.remove(target);
                  }
                }
              }
            }
          }
        } finally {
          watchedSymlinksByTarget.unlock();
        }
      }
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy