概要

WatchServiceを使用してディレクトリの変更を監視し、ファイルの追加削除をJTableに表示します。

サンプルコード

Path dir = Paths.get(System.getProperty("java.io.tmpdir"));
SecondaryLoop loop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop();
Thread worker = new Thread(() -> {
  try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
    dir.register(watcher,
        StandardWatchEventKinds.ENTRY_CREATE,
        StandardWatchEventKinds.ENTRY_DELETE);
    append("register: " + dir);
    processEvents(dir, watcher);
    loop.exit();
  } catch (IOException ex) {
    throw new UncheckedIOException(ex);
  }
});
worker.start();
if (!loop.enter()) {
  append("Error");
}

// Watching a Directory for Changes (The Java™ Tutorials > Essential Classes > Basic I/O)
// https://docs.oracle.com/javase/tutorial/essential/io/notification.html
// Process all events for keys queued to the watcher
public void processEvents(Path dir, WatchService watcher) {
  for (;;) {
    // wait for key to be signaled
    WatchKey key;
    try {
      key = watcher.take();
    } catch (InterruptedException ex) {
      EventQueue.invokeLater(() -> append("Interrupted"));
      return;
    }

    for (WatchEvent<?> event: key.pollEvents()) {
      WatchEvent.Kind<?> kind = event.kind();

      // This key is registered only for ENTRY_CREATE events,
      // but an OVERFLOW event can occur regardless if events
      // are lost or discarded.
      if (kind == StandardWatchEventKinds.OVERFLOW) {
        continue;
      }

      // The filename is the context of the event.
      @SuppressWarnings("unchecked")
      WatchEvent<Path> ev = (WatchEvent<Path>) event;
      Path filename = ev.context();

      Path child = dir.resolve(filename);
      EventQueue.invokeLater(() -> {
        append(String.format("%s: %s", kind, child));
        updateTable(kind, child);
      });
    }

    // Reset the key -- this step is critical if you want to
    // receive further watch events.  If the key is no longer valid,
    // the directory is inaccessible so exit the loop.
    boolean valid = key.reset();
    if (!valid) {
      break;
    }
  }
}

public void updateTable(WatchEvent.Kind<?> kind, Path child) {
  if (kind == StandardWatchEventKinds.ENTRY_CREATE) {
    model.addPath(child);
  } else if (kind == StandardWatchEventKinds.ENTRY_DELETE) {
    for (int i = 0; i < model.getRowCount(); i++) {
      Object value = model.getValueAt(i, 2);
      String path = Objects.toString(value, "");
      if (path.equals(child.toString())) {
        deleteRowSet.add(i);
        // model.removeRow(i);
        break;
      }
    }
    sorter.setRowFilter(new RowFilter<TableModel, Integer>() {
      @Override
      public boolean include(Entry<? extends TableModel, ? extends Integer> entry) {
        return !deleteRowSet.contains(entry.getIdentifier());
      }
    });
  }
}
View in GitHub: Java, Kotlin

解説

上記のサンプルでは、SecondaryLoopで作成したEDTとは別のスレッドでWatchServiceを起動し、System.getProperty("java.io.tmpdir")で取得した一時ディレクトリの変更を監視しています。

  • createTempFileボタンで一時ファイルを作成した場合:
    • WatchServiceで更新を取得し、StandardWatchEventKinds.ENTRY_CREATEの場合はJTablePathを行として追加
  • JTableに表示されている一時ファイルを表す行がJPopupMenuから削除された場合:
    • Files.delete(...)メソッドで一時ディレクトリから削除
      • この段階ではJTableから行の削除は実行しない
    • WatchServiceStandardWatchEventKinds.ENTRY_DELETEを検出したらRowFilterで削除された一時ファイルを表す行を非表示に設定
      • DefaultTableModel#removeRow(...)で削除すると例外が発生する場合がある(再現できない?)
  • 別アプリケーションで一時ファイルが削除された場合:
    • WatchServiceStandardWatchEventKinds.ENTRY_DELETEを検出したらRowFilterで削除された一時ファイルを表す行を非表示に設定
  • このサンプルアプリケーションを終了した場合:
    • HierarchyListenerでパネルの破棄を検出したらWatchServiceを使用しているスレッドにinterrupt()メソッドで割り込みInterruptedExceptionを発生させる
    • InterruptedExceptionをキャッチしたらWatchServiceの監視ループを抜けてSecondaryLoop#exit()メソッドを実行してセカンダリループも抜ける

参考リンク

Watching a Directory for Changes (The Java™ Tutorials > Essential Classes > Basic I/O)

コメント