Skip to content

Commit 81d2126

Browse files
authored
Merge pull request #10 from SWAT-engineering/feat/refactor-interface-name
Refactor interface name
2 parents 3e4ac38 + 211181d commit 81d2126

File tree

12 files changed

+243
-21
lines changed

12 files changed

+243
-21
lines changed

.codecov.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
coverage:
2+
range: "80..100"
3+
precision: 1
4+
status:
5+
project:
6+
default:
7+
target: auto
8+
threshold: 5% # allow a bit of coverage drop
9+
base: auto
10+
patch:
11+
default:
12+
target: 50% # have at least 50% of test coverage
13+
threshold: 10%

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Start using java-watch:
3737
var directory = Path.of("tmp", "test-dir");
3838
var watcherSetup = Watcher.watch(directory, WatchScope.PATH_AND_CHILDREN)
3939
.withExecutor(Executors.newCachedThreadPool()) // optionally configure a custom thread pool
40-
.onEvent(watchEvent -> {
40+
.on(watchEvent -> {
4141
System.err.println(watchEvent);
4242
});
4343

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
<exclude>target/**</exclude>
130130
<exclude>.vscode/**</exclude>
131131
<exclude>.editorconfig</exclude>
132+
<exclude>.codecov.yml</exclude>
132133
</excludes>
133134
</licenseSet>
134135
</licenseSets>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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;
28+
29+
/**
30+
* A visit like interface that allows you to only override the functions you are interested in
31+
*/
32+
public interface WatchEventListener {
33+
default void onCreated(WatchEvent ev) { }
34+
default void onModified(WatchEvent ev) { }
35+
default void onDeleted(WatchEvent ev) { }
36+
default void onOverflow(WatchEvent ev) { }
37+
}

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

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public class Watcher {
5454
private final Path path;
5555
private volatile Executor executor = CompletableFuture::runAsync;
5656

57-
private static final Consumer<WatchEvent> NULL_HANDLER = p -> {};
58-
private volatile Consumer<WatchEvent> eventHandler = NULL_HANDLER;
57+
private static final Consumer<WatchEvent> EMPTY_HANDLER = p -> {};
58+
private volatile Consumer<WatchEvent> eventHandler = EMPTY_HANDLER;
5959

6060

6161
private Watcher(WatchScope scope, Path path) {
@@ -99,13 +99,44 @@ public static Watcher watch(Path path, WatchScope scope) {
9999
* @param eventHandler a callback that handles the watch event, will be called once per event.
100100
* @return this for optional method chaining
101101
*/
102-
public Watcher onEvent(Consumer<WatchEvent> eventHandler) {
102+
public Watcher on(Consumer<WatchEvent> eventHandler) {
103+
if (this.eventHandler != EMPTY_HANDLER) {
104+
throw new IllegalArgumentException("on handler cannot be set more than once");
105+
}
103106
this.eventHandler = eventHandler;
104107
return this;
105108
}
106109

107110
/**
108-
* Optionally configure the executor in which the {@link #onEvent(Consumer)} callbacks are scheduled.
111+
* Convenience variant of {@link #on(Consumer)}, which allows you to only respond to certain events
112+
*/
113+
public Watcher on(WatchEventListener listener) {
114+
if (this.eventHandler != EMPTY_HANDLER) {
115+
throw new IllegalArgumentException("on handler cannot be set more than once");
116+
}
117+
this.eventHandler = ev -> {
118+
switch (ev.getKind()) {
119+
case CREATED:
120+
listener.onCreated(ev);
121+
break;
122+
case DELETED:
123+
listener.onDeleted(ev);
124+
break;
125+
case MODIFIED:
126+
listener.onModified(ev);
127+
break;
128+
case OVERFLOW:
129+
listener.onOverflow(ev);
130+
break;
131+
default:
132+
throw new IllegalArgumentException("Unexpected kind: " + ev.getKind());
133+
}
134+
};
135+
return this;
136+
}
137+
138+
/**
139+
* Optionally configure the executor in which the {@link #on(Consumer)} callbacks are scheduled.
109140
* If not defined, every task will be scheduled on the {@link java.util.concurrent.ForkJoinPool#commonPool()}.
110141
* @param callbackHandler worker pool to use
111142
* @return this for optional method chaining
@@ -119,10 +150,10 @@ public Watcher withExecutor(Executor callbackHandler) {
119150
* Start watch the path for events.
120151
* @return a subscription for the watch, when closed, new events will stop being registered to the worker pool.
121152
* @throws IOException in case the starting of the watcher caused an underlying IO exception
122-
* @throws IllegalStateException the watchers is not configured correctly (for example, missing {@link #onEvent(Consumer)}, or a watcher is started twice)
153+
* @throws IllegalStateException the watchers is not configured correctly (for example, missing {@link #on(Consumer)}, or a watcher is started twice)
123154
*/
124155
public ActiveWatch start() throws IOException {
125-
if (this.eventHandler == NULL_HANDLER) {
156+
if (this.eventHandler == EMPTY_HANDLER) {
126157
throw new IllegalStateException("There is no onEvent handler defined");
127158
}
128159
switch (scope) {
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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;
28+
29+
import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
30+
31+
import java.io.IOException;
32+
import java.nio.file.Files;
33+
34+
import org.awaitility.Awaitility;
35+
import org.junit.jupiter.api.AfterEach;
36+
import org.junit.jupiter.api.BeforeAll;
37+
import org.junit.jupiter.api.BeforeEach;
38+
import org.junit.jupiter.api.Test;
39+
40+
public class APIErrorsTests {
41+
42+
private TestDirectory testDir;
43+
44+
@BeforeEach
45+
void setup() throws IOException {
46+
testDir = new TestDirectory();
47+
}
48+
49+
@AfterEach
50+
void cleanup() {
51+
if (testDir != null) {
52+
testDir.close();
53+
}
54+
}
55+
56+
@BeforeAll
57+
static void setupEverything() {
58+
Awaitility.setDefaultTimeout(TestHelper.NORMAL_WAIT);
59+
}
60+
61+
@Test
62+
void noDuplicateEvents() {
63+
assertThrowsExactly(IllegalArgumentException.class, () ->
64+
Watcher
65+
.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_CHILDREN)
66+
.on(System.out::println)
67+
.on(System.err::println)
68+
);
69+
}
70+
71+
@Test
72+
void onlyDirectoryWatchingOnDirectories() {
73+
assertThrowsExactly(IllegalArgumentException.class, () ->
74+
Watcher
75+
.watch(testDir.getTestFiles().get(0), WatchScope.PATH_AND_CHILDREN)
76+
);
77+
}
78+
79+
@Test
80+
void doNotStartWithoutEventHandler() {
81+
assertThrowsExactly(IllegalStateException.class, () ->
82+
Watcher
83+
.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_CHILDREN)
84+
.start()
85+
);
86+
}
87+
88+
@Test
89+
void noRelativePaths() {
90+
var relativePath = testDir.getTestDirectory().resolve("d1").relativize(testDir.getTestDirectory());
91+
92+
assertThrowsExactly(IllegalArgumentException.class, () ->
93+
Watcher
94+
.watch(relativePath, WatchScope.PATH_AND_CHILDREN)
95+
.start()
96+
);
97+
}
98+
99+
@Test
100+
void nonExistingDirectory() throws IOException {
101+
var nonExistingDir = testDir.getTestDirectory().resolve("testd1");
102+
Files.createDirectory(nonExistingDir);
103+
var w = Watcher.watch(nonExistingDir, WatchScope.PATH_AND_CHILDREN);
104+
Files.delete(nonExistingDir);
105+
assertThrowsExactly(IllegalStateException.class, w::start);
106+
}
107+
108+
109+
}

src/test/java/engineering/swat/watch/DeleteLockTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ private static void recursiveDelete(Path target) throws IOException {
7777
}
7878

7979
private void deleteAndVerify(Path target, WatchScope scope) throws IOException {
80-
try (var watch = Watcher.watch(target, scope).onEvent(ev -> {}).start()) {
80+
try (var watch = Watcher.watch(target, scope).on(ev -> {}).start()) {
8181
recursiveDelete(target);
8282
assertFalse(Files.exists(target), "The file/directory shouldn't exist anymore");
8383
}

src/test/java/engineering/swat/watch/RecursiveWatchTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ void newDirectoryWithFilesChangesDetected() throws IOException {
7474
var created = new AtomicBoolean(false);
7575
var changed = new AtomicBoolean(false);
7676
var watchConfig = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_ALL_DESCENDANTS)
77-
.onEvent(ev -> {
77+
.on(ev -> {
7878
logger.debug("Event received: {}", ev);
7979
if (ev.calculateFullPath().equals(target.get())) {
8080
switch (ev.getKind()) {
@@ -106,7 +106,7 @@ void correctRelativePathIsReported() throws IOException {
106106
Path relative = Path.of("a","b", "c", "d.txt");
107107
var seen = new AtomicBoolean(false);
108108
var watcher = Watcher.watch(testDir.getTestDirectory(), WatchScope.PATH_AND_ALL_DESCENDANTS)
109-
.onEvent(ev -> {
109+
.on(ev -> {
110110
logger.debug("Seen event: {}", ev);
111111
if (ev.getRelativePath().equals(relative)) {
112112
seen.set(true);
@@ -131,7 +131,7 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedExc
131131
.orElseThrow();
132132
var seen = new AtomicBoolean(false);
133133
var watchConfig = Watcher.watch(target.getParent(), WatchScope.PATH_AND_CHILDREN)
134-
.onEvent(ev -> {
134+
.on(ev -> {
135135
if (ev.getKind() == Kind.DELETED && ev.calculateFullPath().equals(target)) {
136136
seen.set(true);
137137
}

src/test/java/engineering/swat/watch/SingleDirectoryTests.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedExc
6868
var seenDelete = new AtomicBoolean(false);
6969
var seenCreate = new AtomicBoolean(false);
7070
var watchConfig = Watcher.watch(target.getParent(), WatchScope.PATH_AND_CHILDREN)
71-
.onEvent(ev -> {
71+
.on(ev -> {
7272
if (ev.getKind() == Kind.DELETED && ev.calculateFullPath().equals(target)) {
7373
seenDelete.set(true);
7474
}
@@ -89,4 +89,35 @@ void deleteOfFileInDirectoryShouldBeVisible() throws IOException, InterruptedExc
8989
.untilTrue(seenCreate);
9090
}
9191
}
92+
93+
@Test
94+
void alternativeAPITest() throws IOException, InterruptedException {
95+
var target = testDir.getTestFiles().get(0);
96+
var seenDelete = new AtomicBoolean(false);
97+
var seenCreate = new AtomicBoolean(false);
98+
var watchConfig = Watcher.watch(target.getParent(), WatchScope.PATH_AND_CHILDREN)
99+
.on(new WatchEventListener() {
100+
@Override
101+
public void onCreated(WatchEvent ev) {
102+
seenCreate.set(true);
103+
}
104+
105+
@Override
106+
public void onDeleted(WatchEvent ev) {
107+
seenDelete.set(true);
108+
}
109+
});
110+
try (var watch = watchConfig.start()) {
111+
112+
// Delete the file
113+
Files.delete(target);
114+
await("File deletion should generate delete event")
115+
.untilTrue(seenDelete);
116+
117+
// Re-create it again
118+
Files.writeString(target, "Hello World");
119+
await("File creation should generate create event")
120+
.untilTrue(seenCreate);
121+
}
122+
}
92123
}

src/test/java/engineering/swat/watch/SingleFileTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ void singleFileShouldNotTriggerOnOtherFilesInSameDir() throws IOException, Inter
6666
var seen = new AtomicBoolean(false);
6767
var others = new AtomicBoolean(false);
6868
var watchConfig = Watcher.watch(target, WatchScope.PATH_ONLY)
69-
.onEvent(ev -> {
69+
.on(ev -> {
7070
if (ev.calculateFullPath().equals(target)) {
7171
seen.set(true);
7272
}
@@ -95,7 +95,7 @@ void singleFileThatMonitorsOnlyADirectory() throws IOException, InterruptedExcep
9595
var seen = new AtomicBoolean(false);
9696
var others = new AtomicBoolean(false);
9797
var watchConfig = Watcher.watch(target, WatchScope.PATH_ONLY)
98-
.onEvent(ev -> {
98+
.on(ev -> {
9999
if (ev.calculateFullPath().equals(target)) {
100100
seen.set(true);
101101
}

0 commit comments

Comments
 (0)