package org.peerbox.watchservice; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; import static java.nio.file.StandardWatchEventKinds.OVERFLOW; import java.io.IOException; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.SimpleFileVisitor; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributes; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class FolderWatchService extends AbstractWatchService { private static final Logger logger = LoggerFactory.getLogger(FolderWatchService.class); private static final long CLEANUP_TASK_DELAY = 5000; private Thread fileEventProcessor; private WatchService watcher; private final Map<WatchKey, Path> watchKeyToPath; private Timer timer; public FolderWatchService() { super(); this.watchKeyToPath = new HashMap<WatchKey, Path>(); } protected void onStarted() throws IOException { watcher = FileSystems.getDefault().newWatchService(); logger.info("Scanning folder: {} ...", getFolderToWatch()); watchKeyToPath.clear(); registerFoldersRecursive(getFolderToWatch()); logger.info("Scanning done."); fileEventProcessor = new Thread(new FolderWatchEventProcessor()); fileEventProcessor.setName("WatchServiceFileEventProcessor"); fileEventProcessor.start(); logger.info("Watch Service started."); } protected void onStopped() throws Exception { // event processor thread if (fileEventProcessor != null) { fileEventProcessor.interrupt(); fileEventProcessor = null; } // cancel all watch keys and clear key map watchKeyToPath.entrySet().forEach(entry -> { entry.getKey().cancel(); logger.trace("Canceled watchkey: '{}'", entry.getValue()); }); watchKeyToPath.clear(); // Java watch service if (watcher != null) { watcher.close(); watcher = null; } // cleanup task / timer if (timer != null) { timer.cancel(); timer = null; } logger.info("Watch Service stopped."); } private synchronized void registerFolder(final Path folder) throws IOException { // FIXME: containsValue has bad performance in case of many folders. // maybe bidirectional (e.g. BiMap from Guava) map would be an option. if (!watchKeyToPath.containsValue(folder)) { logger.info("Register folder: {}", folder); WatchKey key = folder.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW); watchKeyToPath.put(key, folder); } } private synchronized void registerFoldersRecursive(final Path folder) throws IOException { // register recursively all folders and subfolders Files.walkFileTree(folder, new RegisterFolderVisitor()); } private synchronized void unregisterFolder(WatchKey folderKey) { folderKey.cancel(); Path folder = watchKeyToPath.remove(folderKey); logger.info("Unregister folder: {}", folder); } private synchronized void cleanupFolderRegistrations() { // find watch keys of deleted folders Set<WatchKey> keySet = new HashSet<>(watchKeyToPath.keySet()); Set<WatchKey> keysToCancel = new HashSet<>(); for (WatchKey key : keySet) { Path folder = watchKeyToPath.get(key); if (folder != null && !Files.exists(folder, NOFOLLOW_LINKS)) { keysToCancel.add(key); } } // unregister folders (cancel watch key) for (WatchKey key : keysToCancel) { unregisterFolder(key); } } private synchronized void scheduleCleanupTask() { // cancel previous task if existing if (timer != null) { timer.cancel(); timer = null; } timer = new Timer(getClass().getName()); timer.schedule(new CleanupFolderRegistrationsTask(), CLEANUP_TASK_DELAY); } @SuppressWarnings("unchecked") private static <T> WatchEvent<T> castWatchEvent(WatchEvent<?> event) { return (WatchEvent<T>) event; } private class FolderWatchEventProcessor implements Runnable { @Override public void run() { processEvents(); } private void processEvents() { for (;;) { // wait for key to be signaled WatchKey key = null; try { key = watcher.take(); } catch (InterruptedException iex) { logger.trace("Folder Watch Event Processor interrupted (stop watching folder)."); return; } Path dir = watchKeyToPath.get(key); if (dir == null) { logger.error("WatchKey not recognized!!"); continue; } for (WatchEvent<?> event : key.pollEvents()) { // FIXME: how to handle this event? // means Watcher lost some events due to too many events if (event.kind() == OVERFLOW) { logger.warn("OVERFLOW of WatchService - some eventy may be lost."); continue; } @SuppressWarnings("unchecked") Kind<Path> kind = (Kind<Path>) event.kind(); // Context for directory entry event is the file name of entry WatchEvent<Path> ev = castWatchEvent(event); Path name = ev.context(); Path child = dir.resolve(name); // if directory is created, and watching recursively, then // register it and its sub-directories if (kind == ENTRY_CREATE) { try { if (Files.isDirectory(child, NOFOLLOW_LINKS)) { registerFoldersRecursive(child); } } catch (IOException ioex) { // registration of new folder failed. logger.warn("Could not register new folder: {}", child, ioex); } } // print out event // logger.debug("{}: {}", event.kind().name(), child); handleEvent(kind, child); } // reset key and remove from set if directory no longer accessible boolean valid = key.reset(); if (!valid) { unregisterFolder(key); // all directories are inaccessible if (watchKeyToPath.isEmpty()) { logger.info("No more paths to watch, exit event processing loop."); break; } } // schedule a cleanup task that iterates over all directories and updates watch keys (adds or // removes them) scheduleCleanupTask(); // watch service was stopped in the meantime if(!isRunning.get()) { return; } } } /** * Precondition: Event and child must not be null. * @param kind type of the event (create, modify, ...) * @param source Identifies the related file. */ private void handleEvent(Kind<Path> kind, Path source) { try { if(PathUtils.isFileHidden(source)){ return; } if (kind.equals(ENTRY_CREATE)) { addNotifyEvent(new NotifyFileCreated(source)); } else if (kind.equals(ENTRY_MODIFY)) { addNotifyEvent(new NotifyFileModified(source)); } else if (kind.equals(ENTRY_DELETE)) { addNotifyEvent(new NotifyFileDeleted(source)); } else if (kind.equals(OVERFLOW)) { // error - overflow... should not happen here (continue if such an event occurs). // handled already logger.warn("Overflow event from watch service. Too many events?"); } else { logger.warn("Unknown event received"); } } catch (InterruptedException iex) { // put into queue failed logger.info("Handling event interrupted.", iex); } } } private class CleanupFolderRegistrationsTask extends TimerTask { @Override public void run() { try { logger.info("Running cleanup for registered folders."); registerFoldersRecursive(getFolderToWatch()); cleanupFolderRegistrations(); } catch (IOException e) { logger.warn("Could not register folders ({})", e.getMessage(), e); } } } private class RegisterFolderVisitor extends SimpleFileVisitor<Path> { @Override public FileVisitResult preVisitDirectory(Path folder, BasicFileAttributes attrs) { try { registerFolder(folder); } catch (IOException ioex) { logger.warn("Could not register folder: {} ({})", folder, ioex.getMessage(), ioex); } return FileVisitResult.CONTINUE; } } }