Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for path contexts + misc improvements and fixes #58

Merged
merged 10 commits into from
Dec 29, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions core/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a maven build?

I'm happy to discuss alternative build systems, but I think it makes more sense to discuss this in a separate issue before we make any changes. Ideally we should choose a single build system and stick with it for everything, and not try to build the project with multiple build systems.

Copy link
Contributor Author

@mdproctor mdproctor Nov 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't, you can exclude it. It's just easier for me, as it was what I was familiar with, and works well in my IDE. I put it there, as it may make things easier for other people too, who don't have all the SBT/Scala stuff.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I see your point. It just hard to make sure things are consistent between the maven and sbt builds, so it ultimately ends up creating more issues for us to fix.

I am eventually thinking about splitting out the better-files extension into a completely separate library (with separate release cycle) so maybe that's the right time to consider the maven stuff.

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.methvin</groupId>
<artifactId>directory-watcher-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>directory-watcher-core</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-utils</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.0-jre</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.5.0</version>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

</project>
12 changes: 10 additions & 2 deletions core/src/main/java/io/methvin/watcher/DirectoryChangeEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import java.nio.file.WatchEvent;
import java.util.Objects;

public final class DirectoryChangeEvent {
public final class DirectoryChangeEvent<C> {
public enum EventType {

/* A new file was created */
Expand Down Expand Up @@ -47,11 +47,13 @@ public WatchEvent.Kind<?> getWatchEventKind() {
private final EventType eventType;
private final Path path;
private final int count;
private final C context;

public DirectoryChangeEvent(EventType eventType, Path path, int count) {
public DirectoryChangeEvent(EventType eventType, Path path, int count, C context) {
gmethvin marked this conversation as resolved.
Show resolved Hide resolved
this.eventType = eventType;
this.path = path;
this.count = count;
this.context = context;
}

public EventType eventType() {
Expand All @@ -66,6 +68,10 @@ public int count() {
return count;
}

public C context() {
return context;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -88,6 +94,8 @@ public String toString() {
+ path
+ ", count="
+ count
+ ", context="
+ context
+ '}';
}
}
96 changes: 72 additions & 24 deletions core/src/main/java/io/methvin/watcher/DirectoryWatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
Expand All @@ -35,14 +38,15 @@
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;

public class DirectoryWatcher {
public class DirectoryWatcher<C> {

/**
* A builder for a {@link DirectoryWatcher}. Use {@code DirectoryWatcher.builder()} to get a new
* instance.
*/
public static final class Builder {
public static final class Builder<C> {
gmethvin marked this conversation as resolved.
Show resolved Hide resolved
private List<Path> paths = Collections.emptyList();
private Map<Path, C> contexts = new HashMap<>();
gmethvin marked this conversation as resolved.
Show resolved Hide resolved
private DirectoryChangeListener listener = (event -> {});
private Logger logger = null;
private FileHasher fileHasher = FileHasher.DEFAULT_FILE_HASHER;
Expand All @@ -56,6 +60,14 @@ public Builder paths(List<Path> paths) {
return this;
}


/** Set multiple paths to watch with a context per path. */
public Builder paths(Map<Path, C> contexts) {
paths(new ArrayList<>(contexts.keySet()));
this.contexts = contexts;
return this;
}

/** Set a single path to watch. */
public Builder path(Path path) {
return paths(Collections.singletonList(path));
Expand Down Expand Up @@ -114,7 +126,7 @@ public DirectoryWatcher build() throws IOException {
if (logger == null) {
staticLogger();
}
return new DirectoryWatcher(paths, listener, watchService, fileHasher, logger);
return new DirectoryWatcher(paths, contexts, listener, watchService, fileHasher, logger);
}

private Builder osDefaultWatchService() throws IOException {
Expand Down Expand Up @@ -146,7 +158,8 @@ public static Builder builder() {
private final Logger logger;

private final WatchService watchService;
private final List<Path> paths;
private Map<Path, C> contexts;
private Map<Path, C> registeredContexts;
private final boolean isMac;
private final DirectoryChangeListener listener;
private final Map<Path, HashCode> pathHashes;
Expand All @@ -156,14 +169,19 @@ public static Builder builder() {
private Boolean fileTreeSupported = null;
private FileHasher fileHasher;

private volatile boolean closed;

public DirectoryWatcher(
List<Path> paths,
Map<Path, C> contexts,
DirectoryChangeListener listener,
WatchService watchService,
FileHasher fileHasher,
Logger logger)
throws IOException {
this.paths = paths;
this.closed = false;
this.contexts = contexts;
this.registeredContexts = new HashMap<>(contexts);
this.listener = listener;
this.watchService = watchService;
this.isMac = watchService instanceof MacOSXListeningWatchService;
Expand All @@ -173,7 +191,7 @@ public DirectoryWatcher(
this.logger = logger;

for (Path path : paths) {
registerAll(path);
registerAll(path, contexts.get(path));
}
}

Expand Down Expand Up @@ -225,19 +243,21 @@ public void watch() {
throw new IllegalStateException(
"WatchService returned key [" + key + "] but it was not found in keyRoots!");
}
Path registeredPath = keyRoots.get(key);
C context = registeredContexts.get(registeredPath);
Path childPath = eventPath == null ? null : keyRoots.get(key).resolve(eventPath);
logger.debug("{} [{}]", kind, childPath);
/*
* If a directory is created, and we're watching recursively, then register it and its sub-directories.
*/
if (kind == OVERFLOW) {
listener.onEvent(new DirectoryChangeEvent(EventType.OVERFLOW, childPath, count));
onEvent(count, childPath, EventType.OVERFLOW, context);
} else if (eventPath == null) {
throw new IllegalStateException("WatchService returned a null path for " + kind.name());
} else if (kind == ENTRY_CREATE) {
if (Files.isDirectory(childPath, NOFOLLOW_LINKS)) {
if (!Boolean.TRUE.equals(fileTreeSupported)) {
registerAll(childPath);
registerAll(childPath, context);
}
/*
* Our custom Mac service sends subdirectory changes but the Windows/Linux do not.
Expand All @@ -246,11 +266,11 @@ public void watch() {
if (!isMac) {
PathUtils.recursiveVisitFiles(
childPath,
dir -> notifyCreateEvent(dir, count),
file -> notifyCreateEvent(file, count));
dir -> notifyCreateEvent(dir, count, context),
file -> notifyCreateEvent(file, count, context));
}
}
notifyCreateEvent(childPath, count);
notifyCreateEvent(childPath, count, context);
} else if (kind == ENTRY_MODIFY) {
if (fileHasher != null || Files.isDirectory(childPath)) {
/*
Expand All @@ -267,19 +287,19 @@ public void watch() {

if (newHash != null && !newHash.equals(existingHash)) {
pathHashes.put(childPath, newHash);
listener.onEvent(new DirectoryChangeEvent(EventType.MODIFY, childPath, count));
onEvent(count, childPath, EventType.MODIFY, context);
} else if (newHash == null) {
logger.debug(
"Failed to hash modified file [{}]. It may have been deleted.", childPath);
}
} else {
listener.onEvent(new DirectoryChangeEvent(EventType.MODIFY, childPath, count));
onEvent(count, childPath, EventType.MODIFY, context);
}
} else if (kind == ENTRY_DELETE) {
// we cannot tell if the deletion was on file or folder because path points nowhere
// (file/folder was deleted)
pathHashes.entrySet().removeIf(e -> e.getKey().startsWith(childPath));
listener.onEvent(new DirectoryChangeEvent(EventType.DELETE, childPath, count));
onEvent(count, childPath, EventType.DELETE, context);
}
} catch (Exception e) {
logger.debug("DirectoryWatcher got an exception while watching!", e);
Expand All @@ -290,46 +310,73 @@ public void watch() {
if (!valid) {
logger.debug("WatchKey for [{}] no longer valid; removing.", key.watchable());
// remove the key from the keyRoots
keyRoots.remove(key);
Path registeredPath = keyRoots.remove(key);

// Also remove from the context maps
registeredContexts.remove(registeredPath);
contexts.remove(registeredPath); // it may not be in this one, if it's a nested path. But quicker to remove, than check and remove.

// if there are no more keys left to watch, we can break out
if (keyRoots.isEmpty()) {
logger.debug("No more directories left to watch; terminating watcher.");
break;
}
}
}
try {
close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}

private void onEvent(int count, Path childPath, EventType eventType, C context) throws IOException {
gmethvin marked this conversation as resolved.
Show resolved Hide resolved
listener.onEvent(new DirectoryChangeEvent(eventType, childPath, count, context));
}

public DirectoryChangeListener getListener() {
return listener;
}

public Map<Path, C> getContexts() {
return contexts;
}

public Map<Path, C> getRegisteredContexts() {
return registeredContexts;
}

public void close() throws IOException {
watchService.close();
closed = true;
}

public boolean isClosed() {
return closed;
}

private void registerAll(final Path start) throws IOException {
private void registerAll(final Path start, final C context) throws IOException {
if (!Boolean.FALSE.equals(fileTreeSupported)) {
// Try using FILE_TREE modifier since we aren't certain that it's unsupported
try {
register(start, true);
register(start, true, context);
// We didn't get an UnsupportedOperationException so assume FILE_TREE is supported
fileTreeSupported = true;
} catch (UnsupportedOperationException e) {
// UnsupportedOperationException should only happen if FILE_TREE is unsupported
logger.debug("Assuming ExtendedWatchEventModifier.FILE_TREE is not supported", e);
fileTreeSupported = false;
// If we failed to use the FILE_TREE modifier, try again without
registerAll(start);
registerAll(start, context);
}
} else {
// Since FILE_TREE is unsupported, register root directory and sub-directories
PathUtils.recursiveVisitFiles(start, dir -> register(dir, false), file -> {});
PathUtils.recursiveVisitFiles(start, dir -> register(dir, false, context), file -> {});
}
}

// Internal method to be used by registerAll
private void register(Path directory, boolean useFileTreeModifier) throws IOException {
private void register(Path directory, boolean useFileTreeModifier, C context) throws IOException {
logger.debug("Registering [{}].", directory);
Watchable watchable = isMac ? new WatchablePath(directory) : directory;
WatchEvent.Modifier[] modifiers =
Expand All @@ -340,9 +387,10 @@ private void register(Path directory, boolean useFileTreeModifier) throws IOExce
new WatchEvent.Kind<?>[] {ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY};
WatchKey watchKey = watchable.register(watchService, kinds, modifiers);
keyRoots.put(watchKey, directory);
registeredContexts.put(directory, context);
}

private void notifyCreateEvent(Path path, int count) throws IOException {
private void notifyCreateEvent(Path path, int count, C context) throws IOException {
if (fileHasher != null || Files.isDirectory(path)) {
HashCode newHash = PathUtils.hash(fileHasher, path);
if (newHash == null) {
Expand All @@ -353,19 +401,19 @@ private void notifyCreateEvent(Path path, int count) throws IOException {
} else {
logger.debug("Failed to hash created file [{}]. It may be locked.", path);
logger.debug("{} [{}]", EventType.CREATE, path);
listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count));
onEvent(count, path, EventType.CREATE, context);
}
} else {
// Notify for the file create if not already notified
if (!pathHashes.containsKey(path)) {
logger.debug("{} [{}]", EventType.CREATE, path);
listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count));
onEvent(count, path, EventType.CREATE, context);
pathHashes.put(path, newHash);
}
}
} else {
logger.debug("{} [{}]", EventType.CREATE, path);
listener.onEvent(new DirectoryChangeEvent(EventType.CREATE, path, count));
onEvent(count, path, EventType.CREATE, context);
}
}
}
Loading