3333import java .nio .file .attribute .BasicFileAttributes ;
3434import java .nio .file .attribute .FileTime ;
3535import java .util .ArrayDeque ;
36+ import java .util .Collections ;
3637import java .util .Deque ;
3738import java .util .HashSet ;
3839import java .util .Map ;
4243
4344import org .apache .logging .log4j .LogManager ;
4445import org .apache .logging .log4j .Logger ;
46+ import org .checkerframework .checker .nullness .qual .KeyFor ;
47+ import org .checkerframework .checker .nullness .qual .Nullable ;
4548
4649import engineering .swat .watch .WatchEvent ;
4750import engineering .swat .watch .WatchScope ;
4851import engineering .swat .watch .impl .EventHandlingWatch ;
4952
5053public class IndexingRescanner extends MemorylessRescanner {
5154 private final Logger logger = LogManager .getLogger ();
52- private final Map < Path , FileTime > index = new ConcurrentHashMap <> ();
55+ private final Index index = new Index ();
5356
5457 public IndexingRescanner (Executor exec , Path path , WatchScope scope ) {
5558 super (exec );
5659 new Indexer (path , scope ).walkFileTree (); // Make an initial scan to populate the index
5760 }
5861
62+ private static class Index {
63+ private final Map <Path , Map <Path , FileTime >> lastModifiedTimes = new ConcurrentHashMap <>();
64+ // ^^^^ ^^^^
65+ // Parent File name (possibly a directory itself)
66+
67+ public @ Nullable FileTime putLastModifiedTime (Path p , FileTime time ) {
68+ var parent = p .getParent ();
69+ var fileName = p .getFileName ();
70+ if (parent != null && fileName != null ) {
71+ return putLastModifiedTime (parent , fileName , time );
72+ } else {
73+ throw new IllegalArgumentException ("A path key should have both a parent and a file name" );
74+ }
75+ }
76+
77+ public @ Nullable FileTime putLastModifiedTime (Path parent , Path fileName , FileTime time ) {
78+ var nested = lastModifiedTimes .computeIfAbsent (parent , x -> new ConcurrentHashMap <>());
79+ return nested .put (fileName , time );
80+ }
81+
82+ public @ Nullable FileTime getLastModifiedTime (Path p ) {
83+ var parent = p .getParent ();
84+ var fileName = p .getFileName ();
85+ if (parent != null && fileName != null ) {
86+ return getLastModifiedTime (parent , fileName );
87+ } else {
88+ throw new IllegalArgumentException ("A path key should have both a parent and a file name" );
89+ }
90+ }
91+
92+ public @ Nullable FileTime getLastModifiedTime (Path parent , Path fileName ) {
93+ var nested = lastModifiedTimes .get (parent );
94+ return nested == null ? null : nested .get (fileName );
95+ }
96+
97+ public Set <Path > getFileNames (Path parent ) {
98+ var nested = lastModifiedTimes .get (parent );
99+ return nested == null ? Collections .emptySet () : (Set <Path >) nested .keySet ();
100+ }
101+
102+ public @ Nullable FileTime remove (Path p ) {
103+ var parent = p .getParent ();
104+ var fileName = p .getFileName ();
105+ if (parent != null && fileName != null ) {
106+ return remove (parent , fileName );
107+ } else {
108+ throw new IllegalArgumentException ("A path key should have both a parent and a file name" );
109+ }
110+ }
111+
112+ public @ Nullable FileTime remove (Path parent , Path fileName ) {
113+ var nested = lastModifiedTimes .get (parent );
114+ if (nested != null ) {
115+ var removed = nested .remove (fileName );
116+ if (nested .isEmpty ()) {
117+ lastModifiedTimes .remove (parent , nested );
118+ // Note: Between checking `nested` for non-emptiness and
119+ // removing it from `lastModifiedTimes`, other threads may
120+ // have put new entries in it. After the removal, these
121+ // entries are lost, so the index doesn't completely reflect
122+ // the file system anymore, and redundant events may be
123+ // issued. This doesn't break the contract with the client,
124+ // though (because this rescanner still provides an
125+ // over-approximation). Avoiding this race would be costly
126+ // in terms of synchronization.
127+ }
128+ return removed ;
129+ } else {
130+ return null ;
131+ }
132+ }
133+ }
134+
59135 private class Indexer extends BaseFileVisitor {
60136 public Indexer (Path path , WatchScope scope ) {
61137 super (path , scope );
@@ -64,14 +140,14 @@ public Indexer(Path path, WatchScope scope) {
64140 @ Override
65141 public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) throws IOException {
66142 if (!path .equals (dir )) {
67- index .put (dir , attrs .lastModifiedTime ());
143+ index .putLastModifiedTime (dir , attrs .lastModifiedTime ());
68144 }
69145 return FileVisitResult .CONTINUE ;
70146 }
71147
72148 @ Override
73149 public FileVisitResult visitFile (Path file , BasicFileAttributes attrs ) throws IOException {
74- index .put (file , attrs .lastModifiedTime ());
150+ index .putLastModifiedTime (file , attrs .lastModifiedTime ());
75151 return FileVisitResult .CONTINUE ;
76152 }
77153 }
@@ -84,30 +160,32 @@ protected MemorylessRescanner.Generator newGenerator(Path path, WatchScope scope
84160 }
85161
86162 protected class Generator extends MemorylessRescanner .Generator {
87- // Field to keep track of (a stack of) the paths that are visited during
88- // the current rescan (one frame for each nested subdirectory), to
89- // approximate `DELETED` events that happened since the previous rescan.
90- // Instances of this class are supposed to be used non-concurrently, so
91- // no synchronization to access this field is needed.
163+ // Field to keep track of (a stack of sets, of file names, of) the paths
164+ // that are visited during the current rescan (one frame for each nested
165+ // subdirectory), to approximate `DELETED` events that happened since
166+ // the previous rescan. Instances of this class are supposed to be used
167+ // non-concurrently, so no synchronization to access this field is
168+ // needed.
92169 private final Deque <Set <Path >> visited = new ArrayDeque <>();
93170
94171 public Generator (Path path , WatchScope scope ) {
95172 super (path , scope );
96173 this .visited .push (new HashSet <>()); // Initial set for content of `path`
97174 }
98175
99- private < T > void addToPeeked (Deque <Set <T >> deque , T t ) {
176+ private void addToPeeked (Deque <Set <Path >> deque , Path p ) {
100177 var peeked = deque .peek ();
101- if (peeked != null ) {
102- peeked .add (t );
178+ var fileName = p .getFileName ();
179+ if (peeked != null && fileName != null ) {
180+ peeked .add (fileName );
103181 }
104182 }
105183
106184 // -- MemorylessRescanner.Generator --
107185
108186 @ Override
109187 protected void generateEvents (Path path , BasicFileAttributes attrs ) {
110- var lastModifiedTimeOld = index .get (path );
188+ var lastModifiedTimeOld = index .getLastModifiedTime (path );
111189 var lastModifiedTimeNew = attrs .lastModifiedTime ();
112190
113191 // The path isn't indexed yet
@@ -140,12 +218,15 @@ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOEx
140218 // Issue `DELETED` events based on the set of paths visited in `dir`
141219 var visitedInDir = visited .pop ();
142220 if (visitedInDir != null ) {
143- for (var p : index .keySet ()) {
144- if (dir .equals (p .getParent ()) && !visitedInDir .contains (p ) && !Files .exists (p )) {
145- // Note: The third subcondition is needed because the
146- // index may have been updated during the visit. In that
147- // case, `p` might not be in `visitedInDir`, but exist.
148- events .add (new WatchEvent (WatchEvent .Kind .DELETED , p ));
221+ for (var p : index .getFileNames (dir )) {
222+ if (!visitedInDir .contains (p )) {
223+ var fullPath = dir .resolve (p );
224+ // The index may have been updated during the visit, so
225+ // even if `p` isn't contained in `visitedInDir`, by
226+ // now, it might have come into existance.
227+ if (!Files .exists (fullPath )) {
228+ events .add (new WatchEvent (WatchEvent .Kind .DELETED , fullPath ));
229+ }
149230 }
150231 }
151232 }
@@ -169,7 +250,7 @@ public void accept(EventHandlingWatch watch, WatchEvent event) {
169250 case MODIFIED :
170251 try {
171252 var lastModifiedTimeNew = Files .getLastModifiedTime (fullPath );
172- var lastModifiedTimeOld = index .put (fullPath , lastModifiedTimeNew );
253+ var lastModifiedTimeOld = index .putLastModifiedTime (fullPath , lastModifiedTimeNew );
173254
174255 // If a `MODIFIED` event happens for a path that wasn't in
175256 // the index yet, then a `CREATED` event has somehow been
0 commit comments