Skip to content

Commit fd68764

Browse files
authored
Merge pull request #27 from SWAT-engineering/improved-overflow-support/jdk-file-tree-watch
Improved overflow support: Recursive directory watches
2 parents 9b0ba39 + 3f80e77 commit fd68764

File tree

11 files changed

+486
-363
lines changed

11 files changed

+486
-363
lines changed

src/main/java/engineering/swat/watch/WatchEvent.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,16 @@ public enum Kind {
6868
private final Path rootPath;
6969
private final Path relativePath;
7070

71+
private static final Path EMPTY_PATH = Path.of("");
72+
7173
public WatchEvent(Kind kind, Path rootPath) {
7274
this(kind, rootPath, null);
7375
}
7476

7577
public WatchEvent(Kind kind, Path rootPath, @Nullable Path relativePath) {
7678
this.kind = kind;
7779
this.rootPath = rootPath;
78-
this.relativePath = relativePath == null ? Path.of("") : relativePath;
80+
this.relativePath = relativePath == null ? EMPTY_PATH : relativePath;
7981
}
8082

8183
public Kind getKind() {
@@ -105,6 +107,20 @@ public Path calculateFullPath() {
105107
return rootPath.resolve(relativePath);
106108
}
107109

110+
/**
111+
* @return The file name of the full path of this event, or {@code null} if
112+
* it has zero elements (cf. {@link Path#getFileName()}), but without
113+
* calculating the full path. This method is equivalent to, but more
114+
* efficient than, {@code calculateFullPath().getFileName()}.
115+
*/
116+
public @Nullable Path getFileName() {
117+
var fileName = relativePath.getFileName();
118+
if (fileName == null || fileName.equals(EMPTY_PATH)) {
119+
fileName = rootPath.getFileName();
120+
}
121+
return fileName;
122+
}
123+
108124
@Override
109125
public String toString() {
110126
return String.format("WatchEvent[%s, %s, %s]", this.rootPath, this.kind, this.relativePath);

src/main/java/engineering/swat/watch/Watcher.java

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,15 @@
3434
import java.util.concurrent.Executor;
3535
import java.util.function.BiConsumer;
3636
import java.util.function.Consumer;
37+
import java.util.function.Predicate;
3738

3839
import org.apache.logging.log4j.LogManager;
3940
import org.apache.logging.log4j.Logger;
4041

4142
import engineering.swat.watch.impl.EventHandlingWatch;
4243
import engineering.swat.watch.impl.jdk.JDKDirectoryWatch;
44+
import engineering.swat.watch.impl.jdk.JDKFileTreeWatch;
4345
import engineering.swat.watch.impl.jdk.JDKFileWatch;
44-
import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatch;
4546
import engineering.swat.watch.impl.overflows.IndexingRescanner;
4647
import engineering.swat.watch.impl.overflows.MemorylessRescanner;
4748

@@ -61,6 +62,8 @@ public class Watcher {
6162

6263
private static final BiConsumer<EventHandlingWatch, WatchEvent> EMPTY_HANDLER = (w, e) -> {};
6364
private volatile BiConsumer<EventHandlingWatch, WatchEvent> eventHandler = EMPTY_HANDLER;
65+
private static final Predicate<WatchEvent> TRUE_FILTER = e -> true;
66+
private volatile Predicate<WatchEvent> eventFilter = TRUE_FILTER;
6467

6568
private Watcher(Path path, WatchScope scope) {
6669
this.path = path;
@@ -138,6 +141,22 @@ public Watcher on(WatchEventListener listener) {
138141
return this;
139142
}
140143

144+
/**
145+
* Configures the event filter to determine which events should be passed to
146+
* the event handler. By default (without calling this method), all events
147+
* are passed. This method must be called at most once.
148+
* @param predicate The predicate to determine an event should be kept
149+
* ({@code true}) or dropped ({@code false})
150+
* @return {@code this} (to support method chaining)
151+
*/
152+
Watcher filter(Predicate<WatchEvent> predicate) {
153+
if (this.eventFilter != TRUE_FILTER) {
154+
throw new IllegalArgumentException("filter cannot be set more than once");
155+
}
156+
this.eventFilter = predicate;
157+
return this;
158+
}
159+
141160
/**
142161
* Optionally configure the executor in which the {@link #on(Consumer)} callbacks are scheduled.
143162
* If not defined, every task will be scheduled on the {@link java.util.concurrent.ForkJoinPool#commonPool()}.
@@ -180,26 +199,26 @@ public ActiveWatch start() throws IOException {
180199

181200
switch (scope) {
182201
case PATH_AND_CHILDREN: {
183-
var result = new JDKDirectoryWatch(path, executor, h);
202+
var result = new JDKDirectoryWatch(path, executor, h, eventFilter);
184203
result.open();
185204
return result;
186205
}
187206
case PATH_AND_ALL_DESCENDANTS: {
188207
try {
189-
var result = new JDKDirectoryWatch(path, executor, eventHandler, true);
208+
var result = new JDKDirectoryWatch(path, executor, h, eventFilter, true);
190209
result.open();
191210
return result;
192211
} catch (Throwable ex) {
193212
// no native support, use the simulation
194213
logger.debug("Not possible to register the native watcher, using fallback for {}", path);
195214
logger.trace(ex);
196-
var result = new JDKRecursiveDirectoryWatch(path, executor, eventHandler);
215+
var result = new JDKFileTreeWatch(path, executor, h, eventFilter);
197216
result.open();
198217
return result;
199218
}
200219
}
201220
case PATH_ONLY: {
202-
var result = new JDKFileWatch(path, executor, h);
221+
var result = new JDKFileWatch(path, executor, h, eventFilter);
203222
result.open();
204223
return result;
205224
}

src/main/java/engineering/swat/watch/impl/jdk/JDKBaseWatch.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.concurrent.Executor;
3333
import java.util.concurrent.atomic.AtomicBoolean;
3434
import java.util.function.BiConsumer;
35+
import java.util.function.Predicate;
3536

3637
import org.apache.logging.log4j.LogManager;
3738
import org.apache.logging.log4j.Logger;
@@ -46,12 +47,17 @@ public abstract class JDKBaseWatch implements EventHandlingWatch {
4647
protected final Path path;
4748
protected final Executor exec;
4849
protected final BiConsumer<EventHandlingWatch, WatchEvent> eventHandler;
50+
protected final Predicate<WatchEvent> eventFilter;
4951
protected final AtomicBoolean started = new AtomicBoolean();
5052

51-
protected JDKBaseWatch(Path path, Executor exec, BiConsumer<EventHandlingWatch, WatchEvent> eventHandler) {
53+
protected JDKBaseWatch(Path path, Executor exec,
54+
BiConsumer<EventHandlingWatch, WatchEvent> eventHandler,
55+
Predicate<WatchEvent> eventFilter) {
56+
5257
this.path = path;
5358
this.exec = exec;
5459
this.eventHandler = eventHandler;
60+
this.eventFilter = eventFilter;
5561
}
5662

5763
public void open() throws IOException {
@@ -99,7 +105,7 @@ protected WatchEvent translate(java.nio.file.WatchEvent<?> jdkEvent) {
99105
return event;
100106
}
101107

102-
private WatchEvent.Kind translate(java.nio.file.WatchEvent.Kind<?> jdkKind) {
108+
protected WatchEvent.Kind translate(java.nio.file.WatchEvent.Kind<?> jdkKind) {
103109
if (jdkKind == StandardWatchEventKinds.ENTRY_CREATE) {
104110
return WatchEvent.Kind.CREATED;
105111
}
@@ -119,12 +125,14 @@ private WatchEvent.Kind translate(java.nio.file.WatchEvent.Kind<?> jdkKind) {
119125
// -- EventHandlingWatch --
120126

121127
@Override
122-
public void handleEvent(WatchEvent e) {
123-
eventHandler.accept(this, e);
128+
public Path getPath() {
129+
return path;
124130
}
125131

126132
@Override
127-
public Path getPath() {
128-
return path;
133+
public void handleEvent(WatchEvent e) {
134+
if (eventFilter.test(e)) {
135+
eventHandler.accept(this, e);
136+
}
129137
}
130138
}

src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.util.List;
3333
import java.util.concurrent.Executor;
3434
import java.util.function.BiConsumer;
35+
import java.util.function.Predicate;
3536

3637
import org.apache.logging.log4j.LogManager;
3738
import org.apache.logging.log4j.Logger;
@@ -47,19 +48,30 @@ public class JDKDirectoryWatch extends JDKBaseWatch {
4748
private final Logger logger = LogManager.getLogger();
4849
private final boolean nativeRecursive;
4950
private volatile @MonotonicNonNull Closeable bundledJDKWatcher;
51+
private volatile boolean closed = false;
5052

5153
private static final BundledSubscription<SubscriptionKey, List<java.nio.file.WatchEvent<?>>>
5254
BUNDLED_JDK_WATCHERS = new BundledSubscription<>(JDKPoller::register);
5355

54-
public JDKDirectoryWatch(Path directory, Executor exec, BiConsumer<EventHandlingWatch, WatchEvent> eventHandler) {
55-
this(directory, exec, eventHandler, false);
56+
public JDKDirectoryWatch(Path directory, Executor exec,
57+
BiConsumer<EventHandlingWatch, WatchEvent> eventHandler,
58+
Predicate<WatchEvent> eventFilter) {
59+
60+
this(directory, exec, eventHandler, eventFilter, false);
5661
}
5762

58-
public JDKDirectoryWatch(Path directory, Executor exec, BiConsumer<EventHandlingWatch, WatchEvent> eventHandler, boolean nativeRecursive) {
59-
super(directory, exec, eventHandler);
63+
public JDKDirectoryWatch(Path directory, Executor exec,
64+
BiConsumer<EventHandlingWatch, WatchEvent> eventHandler,
65+
Predicate<WatchEvent> eventFilter, boolean nativeRecursive) {
66+
67+
super(directory, exec, eventHandler, eventFilter);
6068
this.nativeRecursive = nativeRecursive;
6169
}
6270

71+
public boolean isClosed() {
72+
return closed;
73+
}
74+
6375
private void handleJDKEvents(List<java.nio.file.WatchEvent<?>> events) {
6476
exec.execute(() -> {
6577
for (var ev : events) {
@@ -80,10 +92,18 @@ public WatchScope getScope() {
8092
return nativeRecursive ? WatchScope.PATH_AND_ALL_DESCENDANTS : WatchScope.PATH_AND_CHILDREN;
8193
}
8294

95+
@Override
96+
public void handleEvent(WatchEvent e) {
97+
if (!closed) {
98+
super.handleEvent(e);
99+
}
100+
}
101+
83102
@Override
84103
public synchronized void close() throws IOException {
85-
if (bundledJDKWatcher != null) {
104+
if (!closed && bundledJDKWatcher != null) {
86105
logger.trace("Closing watch for: {}", this.path);
106+
closed = true;
87107
bundledJDKWatcher.close();
88108
}
89109
}

0 commit comments

Comments
 (0)