33import static java .nio .file .StandardWatchEventKinds .ENTRY_CREATE ;
44import static java .nio .file .StandardWatchEventKinds .ENTRY_DELETE ;
55import static java .nio .file .StandardWatchEventKinds .ENTRY_MODIFY ;
6- import static java .nio .file .StandardWatchEventKinds .OVERFLOW ;
76
87import java .io .IOException ;
98import java .io .UncheckedIOException ;
9+ import java .net .URI ;
1010import java .nio .file .ClosedWatchServiceException ;
1111import java .nio .file .FileSystems ;
12- import java .nio .file .Files ;
1312import java .nio .file .Path ;
14- import java .nio .file .Paths ;
13+ import java .nio .file .WatchEvent ;
1514import java .nio .file .WatchKey ;
1615import java .nio .file .WatchService ;
1716import java .time .Duration ;
1817import java .util .Collection ;
1918import java .util .HashMap ;
19+ import java .util .List ;
2020import java .util .Map ;
21- import java .util .Optional ;
2221import java .util .Set ;
23- import java .util .concurrent .Callable ;
2422import java .util .concurrent .ConcurrentHashMap ;
25- import java .util .function .Function ;
23+ import java .util .function .Consumer ;
2624import lombok .extern .slf4j .Slf4j ;
27- import org .jetbrains .annotations .NotNull ;
2825import org .springframework .util .Assert ;
2926
3027@ Slf4j
@@ -33,83 +30,62 @@ public final class MultiFileWatcher implements AutoCloseable {
3330 private static final long DEBOUNCE_MS = Duration .ofMillis (1000 ).toMillis ();
3431
3532 private final WatchService watchService = FileSystems .getDefault ().newWatchService ();
36- private final Set <Path > watchedFiles = ConcurrentHashMap .newKeySet ();
33+ private final Set <URI > watchedFiles = ConcurrentHashMap .newKeySet ();
3734 private final Map <WatchKey , Path > watchDirsByKey = new HashMap <>();
38- private final Function <Path , Void > reloader ;
35+ private final Consumer <Path > reloader ;
3936
4037 private long lastTriggerAt = 0 ;
4138
42- public MultiFileWatcher (Collection <Path > filesToWatch , Function <Path , Void > reloader ) throws IOException {
39+ public MultiFileWatcher (Collection <Path > filesToWatch , Consumer <Path > reloader ) throws IOException {
4340 Assert .notNull (reloader , "reloader must not be null" );
4441 this .reloader = reloader ;
4542
4643 if (filesToWatch .isEmpty ()) {
4744 log .warn ("No files to watch, aborting" );
4845 }
4946
47+
48+ List <Path > directories = filesToWatch .stream ().map (Path ::getParent ).distinct ().toList ();
5049 watchedFiles .addAll (filesToWatch .stream ()
51- .map (p -> {
52- try {
53- return Files .exists (p ) ? p .toRealPath () : p .toAbsolutePath ().normalize ();
54- } catch (IOException e ) {
55- return p .toAbsolutePath ().normalize ();
56- }
57- })
58- .toList ());
50+ .map (p -> p .toAbsolutePath ().normalize ())
51+ .map (Path ::toUri )
52+ .toList ()
53+ );
5954
6055 if (watchedFiles .isEmpty ()) {
6156 log .warn ("No files to watch resolved, aborting" );
6257 return ;
6358 }
6459
6560 log .debug ("Going to watch {} files" , watchedFiles .size ());
66- log .trace ("Watching files: {}" , watchedFiles .stream ().map (Path ::toString ).toList ());
61+ log .trace ("Watching files: {}" , watchedFiles .stream ().map (URI ::toString ).toList ());
6762
68- watchedFiles .stream ()
69- // .map(getParentPath())
70- .distinct ()
71- .forEach (file -> {
63+ directories
64+ .forEach (dir -> {
7265 try {
73- var key = getParentPath ().apply (file ).register (watchService ,
74- ENTRY_MODIFY ,
75- ENTRY_CREATE , ENTRY_DELETE // watch these for atomic replacements
76- );
77- watchDirsByKey .put (key , file );
66+ WatchKey key = dir .register (watchService , ENTRY_MODIFY , ENTRY_CREATE , ENTRY_DELETE );
67+ watchDirsByKey .put (key , dir );
7868 } catch (IOException e ) {
7969 throw new UncheckedIOException (e );
8070 }
8171 });
8272
83- log .trace ("Watching directories: {}" , watchDirsByKey .values ().stream ().map (a -> getParentPath ().apply (a )).map (Path ::toString ).toList ());
73+ // log.trace("Watching directories: {}", watchDirsByKey.values().stream().map(a -> getParentPath().apply(a)).map(Path::toString).toList());
8474 }
8575
8676 public void watchLoop () {
8777 while (true ) {
8878 try {
8979 var key = watchService .take ();
90- var dir = getParentPath ().apply (watchDirsByKey .get (key ));
91- if (dir == null ) {
92- continue ;
93- }
94-
95- var hit = key .pollEvents ()
96- .stream ()
97- .filter (ev -> ev .kind () != OVERFLOW )
98- .map (ev -> dir .resolve ((Path ) ev .context ()).normalize ().toAbsolutePath ())
99- .anyMatch (this ::matchesTarget );
100-
101- if (hit && shouldTrigger ()) {
102- var filePath = watchDirsByKey .get (key ); // TODO
103- reloader .apply (filePath );
104- }
105-
106- if (!key .reset ()) {
107- watchDirsByKey .remove (key );
108- }
109- if (watchDirsByKey .isEmpty ()) {
110- break ;
80+ Path dir = watchDirsByKey .get (key );
81+ for (WatchEvent <?> event : key .pollEvents ()) {
82+ Path relativePath = (Path ) event .context ();
83+ Path path = dir .resolve (relativePath );
84+ if (watchedFiles .contains (path .toAbsolutePath ().normalize ().toUri ())) {
85+ reloader .accept (path );
86+ }
11187 }
112-
88+ key . reset ();
11389 } catch (InterruptedException e ) {
11490 Thread .currentThread ().interrupt ();
11591 break ;
@@ -123,36 +99,9 @@ public void watchLoop() {
12399 }
124100 }
125101
126- private boolean matchesTarget (Path changed ) {
127- if (watchedFiles .contains (changed )) {
128- return true ;
129- }
130- try {
131- return watchedFiles .contains (changed .toRealPath ());
132- } catch (IOException ignored ) {
133- return false ;
134- }
135- }
136-
137- private boolean shouldTrigger () {
138- var now = System .currentTimeMillis ();
139-
140- if (now - lastTriggerAt < DEBOUNCE_MS ) {
141- return false ;
142- }
143-
144- lastTriggerAt = now ;
145- return true ;
146- }
147-
148102 @ Override
149103 public void close () throws IOException {
150104 watchService .close ();
151105 }
152-
153- @ NotNull
154- private static Function <Path , Path > getParentPath () {
155- return p -> Optional .ofNullable (p .getParent ()).orElse (Path .of ("." ));
156- }
157106}
158107
0 commit comments