Home | Send Feedback

Directory watching with Java

Published: 1. February 2019  •  java

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:

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;
        }
      }
    }

Watcher.java

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;
      }
    });
  }

TreeWatcher.java

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;
        }
      }
    }

  }

TreeWatcher.java

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.