In this blog post, we are looking at an older feature of Java that has been introduced in Java 7 (2011): The ability to monitor directories for changes.
The WatchService
brings an event-driven mechanism to inform your program when changes have been made in a directory.
The WatchService emits the following events:
-
ENTRY_CREATE: A directory entry has been created. This can be a new file or a new directory.
-
ENTRY_DELETE: A file or directory has been deleted. This event is also generated when an entry is renamed out of the observed directory.
-
ENTRY_MODIFY: An entry has been changed. For example, if new data is written to a file or if a file attribute is changed.
-
OVERFLOW: Indicates that events may have been lost.
The following example shows an application monitoring the directory e:/watchme
and writing a message to the console each time a change is happening.
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {
Path path = Paths.get("e:/watchme");
path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.OVERFLOW);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> watchEvent : key.pollEvents()) {
Kind<?> kind = watchEvent.kind();
Path context = (Path) watchEvent.context();
System.out.printf("%s:%s\n", context, kind);
}
boolean valid = key.reset();
if (!valid) {
break;
}
}
}
The program first gets an instance of WatchService
with newWatchService()
. Then it gets a reference to the directory we want to observe, registers the path in
the watcher and specifies which events it is interested in. You can watch multiple directories at the same time by calling register()
for each directory.
Inside an infinite loop the application asks for a WatchKey
with the take()
method. This method blocks the calling thread until a WatchKey
is available.
Alternatively, an application can call poll()
which does not block the calling thread and either returns a WatchKey
immediately or null if no event is available.
The WatchKey
is only a signal indicating that one or more events have been received. To access the events that have arrived the program has to call pollEvents()
on the key which
returns a list of all received WatchEvent
.
The application can now check the type of event (ENTRY_CREATE
, ENTRY_DELETE
, ENTRY_MODIFY
, OVERFLOW
) with kind()
.
context()
returns the Path object which triggered the event.
It is important that after processing the events, the WatchKey is reset with reset()
.
If this is forgotten, then the key does not receive any further events! reset()
returns either true or false in case the WatchKey
is no longer valid.
Now the output of the program can be observed while performing file operations in the monitored directory.
When we create a text file e:/watchme/text.txt
an ENTRY_CREATE
event is emitted. If we open the text file in an editor and make changes, an ENTRY_MODIFY
event will be sent.
If we rename the file, an ENTRY_DELETE
and an ENTRY_CREATE
event will be sent.
Tree monitoring ¶
Note that in the previous example, only the directory e:/watchme
is monitored. The program will be informed with an ENTRY_CREATE
event if we create a subdirectory e:/watchme/subdir
but as soon as we create another subdirectory e:/watchme/subdir/anothersubdir
the program will only get an ENTRY_MODIFY
event for e:/watchme/subdir
but no create event for e:/watchme/subdir/anothersubdir
.
And if we create a file e:/watchme/subdir/anothersubdir/text.txt
the program will not be notified at all.
To monitor an entire directory tree, we have to extend our program. First, we register the root directory and all subdirectories with another feature of NIO.2:
Files.walkFileTree()
.
With this method and the FileVisitor
interface, we can easily traverse directory trees.
In this example, the implementation of the visitor is simple. Every visited directory is registered in the WatchService
.
private static void registerTree(WatchService watchService, Path start) throws IOException {
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
WatchKey key = dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);
watchKeyToPathMap.put(key, dir);
return FileVisitResult.CONTINUE;
}
});
}
The second change affects the event loop, where we have to check whether a new directory has been created or whether an entire directory tree has been copied to the monitored directory. In both cases, the new directory or directories will be registered in the WatchService
.
private static void watch(WatchService watchService, Path start) throws IOException, InterruptedException {
registerTree(watchService, start);
while (true) {
WatchKey key = watchService.take();
for (WatchEvent<?> watchEvent : key.pollEvents()) {
Kind<?> kind = watchEvent.kind();
Path eventPath = (Path) watchEvent.context();
Path directory = watchKeyToPathMap.get(key);
// Path directory = (Path) key.watchable(); //problems with renames
Path child = directory.resolve(eventPath);
if (kind == StandardWatchEventKinds.ENTRY_CREATE && Files.isDirectory(child)) {
registerTree(watchService, child);
}
System.out.printf("%s:%s\n", child, kind);
}
boolean valid = key.reset();
if (!valid) {
watchKeyToPathMap.remove(key);
if (watchKeyToPathMap.isEmpty()) {
break;
}
}
}
}
The program stores each registered WatchKey
in a Map that maps from the WatchKey
to the path. The problem is that watchEvent.context()
only returns the path relative to the registered directory, but for the registration of the new directories, the complete path is needed. WatchKey.watchable()
would return the path of the registered directory but does not work properly if a directory is renamed, which is mentioned in the JavaDoc.
This concludes the short tour about the WatchService
.