Watching files with WatchService

Java introduces more and more event-driven features. One of them is WatchService which allows to handle files-related events.

This article begins by an introduction to WatchService. This part explores main components of this interface. The second part presents, under the form of several test-cases, the implementation and the features in almost real world.

WatchService in Java

Watching files can be a very useful feature if we want to, for example, allow dynamic updates in the application. This feature is natively provided with Spring Framework, but it's good to keep in mind that alternative solutions exists. One of them is java.nio.file.WatchService. However, unlike Spring's reloadable message bundle, WatchService can only watch the changes of a directory and not a file. It must be registered to specific Path object through static register(WatchService, WatchEvent.Kind) method.

As we can observe through register, Java has some other objects related to directory changes watching. The first one is the interface java.nio.file.WatchEvent. Its implementations define listened actions in given directory. In commonly used implementation, java.nio.file.StandardWatchEventKinds, we can find events triggered when: something is created, modified or removed in the directory. There is also an event called OVERFLOW which is triggered only when some event has been lost or discarded. Each event contains following information: context (for example created file in observed directory), kind (type of event, create, delete, modification) and count (how much time given event was repeated).

Another useful interface in WatchService use is java.nio.file.WatchKey. It represents the object for which observation was registered. This object is typically an implementation of Watchable interface and one the sample implementations is java.nio.file.Path interface. An aspect of validity is associated with WatchKey. When its Watchable object becomes inaccessible, for example when observed directory is deleted, WatchKey state passes to invalid. It's translated by false value returned by reset() method which must be mandatory called after each events consumption. Without it, no more new events will be consumed.

Example of WatchService

After this introduction we can start to write test cases illustrating several of presumptions:

public class FileWatchTest {

  private static final String TEST_PATH = FileWatchTest.class.getClassLoader().getResource("").getPath();
  private static final String TEST_DIR = TEST_PATH+"/watchservice";
  private static final String TMP_FILE = TEST_DIR+"/tmp_to_remove";

  @Before
  public void initDirectories() throws IOException {
    Files.createDirectory(Paths.get(TEST_DIR));
  }

  @After
  public void cleanDirectories() throws IOException {
    if (Files.exists(Paths.get(TMP_FILE))) {
      Files.delete(Paths.get(TMP_FILE));
    }
    if (Files.exists(Paths.get(TEST_DIR))) {
      Files.delete(Paths.get(TEST_DIR));
    }
  }

  @Test
  public void should_correctly_exist_when_watched_directory_becomes_not_accessible() throws IOException, InterruptedException {
    WatchService watchService = FileSystems.getDefault().newWatchService();
    Path dir = Paths.get(TEST_DIR);
    dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
        StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
    CountDownLatch countDownLatch = new CountDownLatch(1);
    DirectoryWatcher dirWatcher = new DirectoryWatcher(watchService, countDownLatch);
    new Thread(dirWatcher).start();
    new Thread(() -> {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      try {
        Files.delete(Paths.get(TEST_DIR));
      } catch (IOException e) {
        e.printStackTrace();
      }
    }).start();
    countDownLatch.await(3, TimeUnit.SECONDS);

    assertThat(dirWatcher.wasInvalidKey()).isTrue();
  }

  @Test
  public void should_correctly_notify_about_files_manipulation_in_testing_directory() throws IOException, InterruptedException {
    WatchService watchService = FileSystems.getDefault().newWatchService();
    Path dir = Paths.get(TEST_DIR);
    dir.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,
        StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
    CountDownLatch countDownLatch = new CountDownLatch(1);
    final int OPERATIONS_LIMIT = 3;
    DirectoryWatcher dirWatcher = new DirectoryWatcher(watchService, countDownLatch);
    new Thread(dirWatcher).start();
    new Thread(() -> {
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      try {
        for (int i = 0; i < OPERATIONS_LIMIT; i++) {
          String fileName = TEST_DIR+"/"+System.currentTimeMillis();
          Path filePath = Paths.get(fileName);
          Files.createFile(filePath);
          Files.write(filePath, "Message".getBytes());
          Files.delete(Paths.get(fileName));
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }).start();
    countDownLatch.await(5, TimeUnit.SECONDS);

    assertThat(dirWatcher.getCreates()).isEqualTo(OPERATIONS_LIMIT);
    // Modifications concerns all writes in directory, so creates as well as deletes
    assertThat(dirWatcher.getModifications()).isGreaterThan(OPERATIONS_LIMIT);
    assertThat(dirWatcher.getDeletes()).isEqualTo(OPERATIONS_LIMIT);
  }

  @Test(expected = NotDirectoryException.class)
  public void should_correctly_watch_single_file_only_for_delete_event() throws IOException, InterruptedException {
    WatchService watchService = FileSystems.getDefault().newWatchService();
    Files.createFile(Paths.get(TMP_FILE));

    // This should provoke NotDirectoryException because only directories are allowed to be watched
    Paths.get(TMP_FILE).register(watchService, StandardWatchEventKinds.ENTRY_DELETE);
  }

  private static final class DirectoryWatcher implements Runnable {

    private WatchService watchService;

    private CountDownLatch countDownLatch;

    private boolean invalidKey;

    private Map<WatchEvent.Kind<Path>, Integer> stats = Maps.newHashMap(
      ImmutableMap.<WatchEvent.Kind<Path>, Integer>of(StandardWatchEventKinds.ENTRY_CREATE, 0,
        StandardWatchEventKinds.ENTRY_MODIFY, 0,
        StandardWatchEventKinds.ENTRY_DELETE, 0));

    public DirectoryWatcher(WatchService watchService, CountDownLatch countDownLatch) {
      this.watchService = watchService;
      this.countDownLatch = countDownLatch;
    }

    @Override
    public void run() {
      while (true) {
        WatchKey watchKey;
        try {
          watchKey = watchService.poll(1, TimeUnit.SECONDS);
          if (watchKey != null) {
            for (WatchEvent<?> event : watchKey.pollEvents()) {
              stats.put((WatchEvent.Kind<Path>)event.kind(), stats.get(event.kind())+1);
            }
            // is mandatory, otherwise it blocks further notifications for the file (example: edit the same file twice,
            // without reset() only the first change will be notified)
            if (!watchKey.reset()) {
              invalidKey = true;
              // check if reset was correctly made, otherwise it can mean that the directory is not accessible anymore
              countDownLatch.countDown();
              break;
            }
          }
        } catch (InterruptedException e) {
          e.printStackTrace();
          countDownLatch.countDown();
        }
      }
    }

    public boolean wasInvalidKey() {
      return invalidKey;
    }

    public int getCreates() {
      return stats.get(StandardWatchEventKinds.ENTRY_CREATE);
    }

    public int getModifications() {
      return stats.get(StandardWatchEventKinds.ENTRY_MODIFY);
    }

    public int getDeletes() {
      return stats.get(StandardWatchEventKinds.ENTRY_DELETE);
    }

  }

}

WatchService is very interesting feature to deal with filesystem events. However as we could see, it applies only to directories, so listing for changes in particular files could require to write some code in additionally to already existent one. But beside that, WatchService is pretty easy to implement and explore. There are only some traps to avoid, such as not forget to call reset() method on WatchKey.

If you liked it, you should read: