3535import java .util .concurrent .ConcurrentHashMap ;
3636import java .util .concurrent .CopyOnWriteArrayList ;
3737import java .util .concurrent .TimeUnit ;
38- import java .util .stream .Collectors ;
3938
4039import org .apache .commons .logging .Log ;
4140import org .apache .commons .logging .LogFactory ;
@@ -86,26 +85,18 @@ void watch(Set<Path> paths, Runnable action) {
8685 this .thread = new WatcherThread ();
8786 this .thread .start ();
8887 }
89- Set <Path > actualPaths = new HashSet <>();
88+ Set <Path > registrationPaths = new HashSet <>();
9089 for (Path path : paths ) {
91- actualPaths . add ( resolveSymlinkIfNecessary (path ));
90+ registrationPaths . addAll ( getRegistrationPaths (path ));
9291 }
93- this .thread .register (new Registration (actualPaths , action ));
92+ this .thread .register (new Registration (registrationPaths , action ));
9493 }
9594 catch (IOException ex ) {
9695 throw new UncheckedIOException ("Failed to register paths for watching: " + paths , ex );
9796 }
9897 }
9998 }
10099
101- private static Path resolveSymlinkIfNecessary (Path path ) throws IOException {
102- if (Files .isSymbolicLink (path )) {
103- Path target = path .resolveSibling (Files .readSymbolicLink (path ));
104- return resolveSymlinkIfNecessary (target );
105- }
106- return path ;
107- }
108-
109100 @ Override
110101 public void close () throws IOException {
111102 synchronized (this .lock ) {
@@ -123,6 +114,44 @@ public void close() throws IOException {
123114 }
124115 }
125116
117+ /**
118+ * Retrieves all {@link Path Paths} that should be registered for the specified
119+ * {@link Path}. If the path is a symlink, changes to the symlink should be monitored,
120+ * not just the file it points to. For example, for the given {@code keystore.jks}
121+ * path in the following directory structure:<pre>
122+ * .
123+ * ├── ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f
124+ * │ ├── keystore.jks
125+ * ├── ..data -> ..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f
126+ * ├── keystore.jks -> ..data/keystore.jks
127+ * </pre> the resulting paths would include:
128+ * <ul>
129+ * <li><b>keystore.jks</b></li>
130+ * <li><b>..data/keystore.jks</b></li>
131+ * <li><b>..data</b></li>
132+ * <li><b>..a72e81ff-f0e1-41d8-a19b-068d3d1d4e2f/keystore.jks</b></li>
133+ * </ul>
134+ * @param path the path
135+ * @return all possible {@link Path} instances to be registered
136+ * @throws IOException if an I/O error occurs
137+ */
138+ private static Set <Path > getRegistrationPaths (Path path ) throws IOException {
139+ path = path .toAbsolutePath ();
140+ Set <Path > result = new HashSet <>();
141+ result .add (path );
142+ Path parent = path .getParent ();
143+ if (parent != null && Files .isSymbolicLink (parent )) {
144+ result .add (parent );
145+ Path target = parent .resolveSibling (Files .readSymbolicLink (parent ));
146+ result .addAll (getRegistrationPaths (target .resolve (path .getFileName ())));
147+ }
148+ else if (Files .isSymbolicLink (path )) {
149+ Path target = path .resolveSibling (Files .readSymbolicLink (path ));
150+ result .addAll (getRegistrationPaths (target ));
151+ }
152+ return result ;
153+ }
154+
126155 /**
127156 * The watcher thread used to check for changes.
128157 */
@@ -145,11 +174,15 @@ private void onThreadException(Thread thread, Throwable throwable) {
145174 }
146175
147176 void register (Registration registration ) throws IOException {
177+ Set <Path > directories = new HashSet <>();
148178 for (Path path : registration .paths ()) {
149179 if (!Files .isRegularFile (path ) && !Files .isDirectory (path )) {
150180 throw new IOException ("'%s' is neither a file nor a directory" .formatted (path ));
151181 }
152182 Path directory = Files .isDirectory (path ) ? path : path .getParent ();
183+ directories .add (directory );
184+ }
185+ for (Path directory : directories ) {
153186 WatchKey watchKey = register (directory );
154187 this .registrations .computeIfAbsent (watchKey , (key ) -> new CopyOnWriteArrayList <>()).add (registration );
155188 }
@@ -224,10 +257,6 @@ public void close() throws IOException {
224257 */
225258 private record Registration (Set <Path > paths , Runnable action ) {
226259
227- Registration {
228- paths = paths .stream ().map (Path ::toAbsolutePath ).collect (Collectors .toSet ());
229- }
230-
231260 boolean manages (Path file ) {
232261 Path absolutePath = file .toAbsolutePath ();
233262 return this .paths .contains (absolutePath ) || isInDirectories (absolutePath );
0 commit comments