Skip to content

Commit e23a194

Browse files
authored
Merge pull request #13 from SWAT-engineering/jdk-base-watcher
Refactor common features of `JDK...Watcher` into common `JDKBaseWatch`
2 parents b07727f + fee22fd commit e23a194

File tree

5 files changed

+199
-153
lines changed

5 files changed

+199
-153
lines changed

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

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@
3737
import org.apache.logging.log4j.LogManager;
3838
import org.apache.logging.log4j.Logger;
3939

40-
import engineering.swat.watch.impl.jdk.JDKDirectoryWatcher;
41-
import engineering.swat.watch.impl.jdk.JDKFileWatcher;
42-
import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatcher;
40+
import engineering.swat.watch.impl.jdk.JDKDirectoryWatch;
41+
import engineering.swat.watch.impl.jdk.JDKFileWatch;
42+
import engineering.swat.watch.impl.jdk.JDKRecursiveDirectoryWatch;
4343

4444
/**
4545
* <p>Watch a path for changes.</p>
@@ -156,35 +156,34 @@ public ActiveWatch start() throws IOException {
156156
if (this.eventHandler == EMPTY_HANDLER) {
157157
throw new IllegalStateException("There is no onEvent handler defined");
158158
}
159+
159160
switch (scope) {
160161
case PATH_AND_CHILDREN: {
161-
var result = new JDKDirectoryWatcher(path, executor, this.eventHandler, false);
162-
result.start();
162+
var result = new JDKDirectoryWatch(path, executor, eventHandler, false);
163+
result.open();
163164
return result;
164165
}
165166
case PATH_AND_ALL_DESCENDANTS: {
166167
try {
167-
var result = new JDKDirectoryWatcher(path, executor, this.eventHandler, true);
168-
result.start();
168+
var result = new JDKDirectoryWatch(path, executor, eventHandler, true);
169+
result.open();
169170
return result;
170171
} catch (Throwable ex) {
171172
// no native support, use the simulation
172173
logger.debug("Not possible to register the native watcher, using fallback for {}", path);
173174
logger.trace(ex);
174-
var result = new JDKRecursiveDirectoryWatcher(path, executor, this.eventHandler);
175-
result.start();
175+
var result = new JDKRecursiveDirectoryWatch(path, executor, eventHandler);
176+
result.open();
176177
return result;
177178
}
178179
}
179180
case PATH_ONLY: {
180-
var result = new JDKFileWatcher(path, executor, this.eventHandler);
181-
result.start();
181+
var result = new JDKFileWatch(path, executor, eventHandler);
182+
result.open();
182183
return result;
183184
}
184-
185185
default:
186186
throw new IllegalStateException("Not supported yet");
187187
}
188188
}
189-
190189
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/*
2+
* BSD 2-Clause License
3+
*
4+
* Copyright (c) 2023, Swat.engineering
5+
*
6+
* Redistribution and use in source and binary forms, with or without
7+
* modification, are permitted provided that the following conditions are met:
8+
*
9+
* 1. Redistributions of source code must retain the above copyright notice, this
10+
* list of conditions and the following disclaimer.
11+
*
12+
* 2. Redistributions in binary form must reproduce the above copyright notice,
13+
* this list of conditions and the following disclaimer in the documentation
14+
* and/or other materials provided with the distribution.
15+
*
16+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17+
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18+
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26+
*/
27+
package engineering.swat.watch.impl.jdk;
28+
29+
import java.io.IOException;
30+
import java.nio.file.Path;
31+
import java.nio.file.StandardWatchEventKinds;
32+
import java.util.concurrent.Executor;
33+
import java.util.concurrent.atomic.AtomicBoolean;
34+
import java.util.function.Consumer;
35+
36+
import org.apache.logging.log4j.LogManager;
37+
import org.apache.logging.log4j.Logger;
38+
import org.checkerframework.checker.nullness.qual.Nullable;
39+
40+
import engineering.swat.watch.ActiveWatch;
41+
import engineering.swat.watch.WatchEvent;
42+
43+
public abstract class JDKBaseWatch implements ActiveWatch {
44+
private final Logger logger = LogManager.getLogger();
45+
46+
protected final Path path;
47+
protected final Executor exec;
48+
protected final Consumer<WatchEvent> eventHandler;
49+
protected final AtomicBoolean started = new AtomicBoolean();
50+
51+
protected JDKBaseWatch(Path path, Executor exec, Consumer<WatchEvent> eventHandler) {
52+
this.path = path;
53+
this.exec = exec;
54+
this.eventHandler = eventHandler;
55+
}
56+
57+
public void open() throws IOException {
58+
try {
59+
if (!startIfFirstTime()) {
60+
throw new IllegalStateException("Could not restart already-started watch for: " + path);
61+
}
62+
logger.debug("Started watch for: {}", path);
63+
} catch (Exception e) {
64+
throw new IOException("Could not start watch for: " + path, e);
65+
}
66+
}
67+
68+
/**
69+
* Starts this watch.
70+
*
71+
* @throws IOException When an I/O exception of some sort has occurred
72+
* (e.g., a nested watch failed to start)
73+
*/
74+
protected abstract void start() throws IOException;
75+
76+
/**
77+
* Starts this watch if it's the first time.
78+
*
79+
* @return `true` iff it's the first time this method is called
80+
* @throws IOException When an I/O exception of some sort has occurred
81+
* (e.g., a nested watch failed to start)
82+
*/
83+
protected boolean startIfFirstTime() throws IOException {
84+
if (started.compareAndSet(false, true)) {
85+
start();
86+
return true;
87+
} else {
88+
return false;
89+
}
90+
}
91+
92+
protected WatchEvent translate(java.nio.file.WatchEvent<?> jdkEvent) {
93+
WatchEvent.Kind kind;
94+
if (jdkEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
95+
kind = WatchEvent.Kind.CREATED;
96+
}
97+
else if (jdkEvent.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
98+
kind = WatchEvent.Kind.MODIFIED;
99+
}
100+
else if (jdkEvent.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
101+
kind = WatchEvent.Kind.DELETED;
102+
}
103+
else if (jdkEvent.kind() == StandardWatchEventKinds.OVERFLOW) {
104+
kind = WatchEvent.Kind.OVERFLOW;
105+
}
106+
else {
107+
throw new IllegalArgumentException("Unexpected watch event: " + jdkEvent);
108+
}
109+
var rootPath = path;
110+
var relativePath = kind == WatchEvent.Kind.OVERFLOW ? Path.of("") : (@Nullable Path)jdkEvent.context();
111+
112+
var event = new WatchEvent(kind, rootPath, relativePath);
113+
logger.trace("Translated: {} to {}", jdkEvent, event);
114+
return event;
115+
}
116+
}

src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatcher.java renamed to src/main/java/engineering/swat/watch/impl/jdk/JDKDirectoryWatch.java

Lines changed: 16 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -29,63 +29,35 @@
2929
import java.io.Closeable;
3030
import java.io.IOException;
3131
import java.nio.file.Path;
32-
import java.nio.file.StandardWatchEventKinds;
3332
import java.util.List;
3433
import java.util.concurrent.Executor;
3534
import java.util.function.Consumer;
3635

3736
import org.apache.logging.log4j.LogManager;
3837
import org.apache.logging.log4j.Logger;
3938
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
40-
import org.checkerframework.checker.nullness.qual.Nullable;
4139

42-
import engineering.swat.watch.ActiveWatch;
4340
import engineering.swat.watch.WatchEvent;
4441
import engineering.swat.watch.impl.util.BundledSubscription;
4542
import engineering.swat.watch.impl.util.SubscriptionKey;
4643

47-
public class JDKDirectoryWatcher implements ActiveWatch {
44+
public class JDKDirectoryWatch extends JDKBaseWatch {
4845
private final Logger logger = LogManager.getLogger();
49-
private final Path directory;
50-
private final Executor exec;
51-
private final Consumer<WatchEvent> eventHandler;
52-
private volatile @MonotonicNonNull Closeable activeWatch;
5346
private final boolean nativeRecursive;
47+
private volatile @MonotonicNonNull Closeable bundledJDKWatcher;
5448

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

58-
public JDKDirectoryWatcher(Path directory, Executor exec, Consumer<WatchEvent> eventHandler) {
52+
public JDKDirectoryWatch(Path directory, Executor exec, Consumer<WatchEvent> eventHandler) {
5953
this(directory, exec, eventHandler, false);
6054
}
6155

62-
public JDKDirectoryWatcher(Path directory, Executor exec, Consumer<WatchEvent> eventHandler, boolean nativeRecursive) {
63-
this.directory = directory;
64-
this.exec = exec;
65-
this.eventHandler = eventHandler;
56+
public JDKDirectoryWatch(Path directory, Executor exec, Consumer<WatchEvent> eventHandler, boolean nativeRecursive) {
57+
super(directory, exec, eventHandler);
6658
this.nativeRecursive = nativeRecursive;
6759
}
6860

69-
70-
synchronized boolean safeStart() throws IOException {
71-
if (activeWatch != null) {
72-
return false;
73-
}
74-
activeWatch = BUNDLED_JDK_WATCHERS.subscribe(new SubscriptionKey(directory, nativeRecursive), this::handleChanges);
75-
return true;
76-
}
77-
78-
public void start() throws IOException {
79-
try {
80-
if (!safeStart()) {
81-
throw new IllegalStateException("Cannot start a watcher twice");
82-
}
83-
logger.debug("Started watch for: {}", directory);
84-
} catch (IOException e) {
85-
throw new IOException("Could not register directory watcher for: " + directory, e);
86-
}
87-
}
88-
8961
private void handleChanges(List<java.nio.file.WatchEvent<?>> events) {
9062
exec.execute(() -> {
9163
for (var ev : events) {
@@ -99,33 +71,20 @@ private void handleChanges(List<java.nio.file.WatchEvent<?>> events) {
9971
});
10072
}
10173

102-
private WatchEvent translate(java.nio.file.WatchEvent<?> ev) {
103-
WatchEvent.Kind kind;
104-
if (ev.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
105-
kind = WatchEvent.Kind.CREATED;
106-
}
107-
else if (ev.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
108-
kind = WatchEvent.Kind.MODIFIED;
109-
}
110-
else if (ev.kind() == StandardWatchEventKinds.ENTRY_DELETE) {
111-
kind = WatchEvent.Kind.DELETED;
112-
}
113-
else if (ev.kind() == StandardWatchEventKinds.OVERFLOW) {
114-
kind = WatchEvent.Kind.OVERFLOW;
115-
}
116-
else {
117-
throw new IllegalArgumentException("Unexpected watch event: " + ev);
118-
}
119-
var path = kind == WatchEvent.Kind.OVERFLOW ? this.directory : (@Nullable Path)ev.context();
120-
logger.trace("Translated: {} to {} at {}", ev, kind, path);
121-
return new WatchEvent(kind, directory, path);
122-
}
74+
// -- JDKBaseWatch --
12375

12476
@Override
12577
public synchronized void close() throws IOException {
126-
if (activeWatch != null) {
127-
logger.trace("Closing watch for: {}", this.directory);
128-
activeWatch.close();
78+
if (bundledJDKWatcher != null) {
79+
logger.trace("Closing watch for: {}", this.path);
80+
bundledJDKWatcher.close();
12981
}
13082
}
83+
84+
@Override
85+
protected synchronized void start() throws IOException {
86+
assert bundledJDKWatcher == null;
87+
var key = new SubscriptionKey(path, nativeRecursive);
88+
bundledJDKWatcher = BUNDLED_JDK_WATCHERS.subscribe(key, this::handleChanges);
89+
}
13190
}

src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatcher.java renamed to src/main/java/engineering/swat/watch/impl/jdk/JDKFileWatch.java

Lines changed: 27 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
*/
2727
package engineering.swat.watch.impl.jdk;
2828

29-
import java.io.Closeable;
3029
import java.io.IOException;
3130
import java.nio.file.Path;
3231
import java.util.concurrent.Executor;
@@ -35,59 +34,35 @@
3534
import org.apache.logging.log4j.LogManager;
3635
import org.apache.logging.log4j.Logger;
3736
import org.checkerframework.checker.nullness.qual.MonotonicNonNull;
37+
import org.checkerframework.checker.nullness.qual.Nullable;
3838

39-
import engineering.swat.watch.ActiveWatch;
4039
import engineering.swat.watch.WatchEvent;
4140

4241
/**
4342
* It's not possible to monitor a single file (or directory), so we have to find a directory watcher, and connect to that
4443
*
4544
* Note that you should take care to call start only once.
4645
*/
47-
public class JDKFileWatcher implements ActiveWatch {
46+
public class JDKFileWatch extends JDKBaseWatch {
4847
private final Logger logger = LogManager.getLogger();
49-
private final Path file;
48+
private final Path parent;
5049
private final Path fileName;
51-
private final Executor exec;
52-
private final Consumer<WatchEvent> eventHandler;
53-
private volatile @MonotonicNonNull Closeable activeWatch;
50+
private volatile @MonotonicNonNull JDKDirectoryWatch parentWatch;
5451

55-
public JDKFileWatcher(Path file, Executor exec, Consumer<WatchEvent> eventHandler) {
56-
this.file = file;
57-
Path filename= file.getFileName();
58-
if (filename == null) {
59-
throw new IllegalArgumentException("Cannot pass in a root path");
60-
}
61-
this.fileName = filename;
62-
this.exec = exec;
63-
this.eventHandler = eventHandler;
64-
}
52+
public JDKFileWatch(Path file, Executor exec, Consumer<WatchEvent> eventHandler) {
53+
super(file, exec, eventHandler);
6554

66-
/**
67-
* Start the file watcher, but only do it once
68-
* @throws IOException
69-
*/
70-
public void start() throws IOException {
71-
try {
72-
var dir = file.getParent();
73-
if (dir == null) {
74-
throw new IllegalArgumentException("cannot watch a single entry that is on the root");
75-
76-
}
77-
assert !dir.equals(file);
78-
JDKDirectoryWatcher parentWatch;
79-
synchronized(this) {
80-
if (activeWatch != null) {
81-
throw new IOException("Cannot start an already started watch");
82-
}
83-
activeWatch = parentWatch = new JDKDirectoryWatcher(dir, exec, this::filter);
84-
parentWatch.start();
85-
}
86-
logger.debug("Started file watch for {} (in reality a watch on {}): {}", file, dir, parentWatch);
55+
var message = "The root path is not a valid path for a file watch";
56+
this.parent = requireNonNull(path.getParent(), message);
57+
this.fileName = requireNonNull(path.getFileName(), message);
58+
assert !parent.equals(path);
59+
}
8760

88-
} catch (IOException e) {
89-
throw new IOException("Could not register file watcher for: " + file, e);
61+
private static Path requireNonNull(@Nullable Path p, String message) {
62+
if (p == null) {
63+
throw new IllegalArgumentException(message);
9064
}
65+
return p;
9166
}
9267

9368
private void filter(WatchEvent event) {
@@ -96,10 +71,20 @@ private void filter(WatchEvent event) {
9671
}
9772
}
9873

74+
// -- JDKBaseWatch --
75+
9976
@Override
10077
public synchronized void close() throws IOException {
101-
if (activeWatch != null) {
102-
activeWatch.close();
78+
if (parentWatch != null) {
79+
parentWatch.close();
10380
}
10481
}
82+
83+
@Override
84+
protected synchronized void start() throws IOException {
85+
assert parentWatch == null;
86+
parentWatch = new JDKDirectoryWatch(parent, exec, this::filter);
87+
parentWatch.open();
88+
logger.debug("File watch (for: {}) is in reality a directory watch (for: {}) with a filter (for: {})", path, parent, fileName);
89+
}
10590
}

0 commit comments

Comments
 (0)