// ===================================================================== // // Copyright (C) 2012 - 2016, Philip Graf // // All rights reserved. This program and the accompanying materials // are made available under the terms of the Eclipse Public License v1.0 // which accompanies this distribution, and is available at // http://www.eclipse.org/legal/epl-v10.html // // ===================================================================== package ch.acanda.eclipse.pmd.file; 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.ClosedWatchServiceException; import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.WatchEvent; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.HashMap; import java.util.Map; import ch.acanda.eclipse.pmd.PMDPlugin; import com.google.common.base.Optional; import com.google.common.collect.HashMultimap; import com.google.common.collect.Multimap; /** * Watches files and notifies registered listeners when they have changed. * * @author Philip Graf */ public final class FileWatcher { private final WatchService watchService; /** * Maps an absolute directory path to its watch key. */ private final Map<Path, WatchKey> watchKeys; /** * Maps an absolute file path to its listeners. */ private final Multimap<Path, FileChangedListener> listeners; /** * Maps an absolute directory path to its absolute file paths that are being watched. */ private final Multimap<Path, Path> watchedFiles; private Optional<WatcherThread> watcherThread = Optional.absent(); public FileWatcher() throws IOException { watchService = FileSystems.getDefault().newWatchService(); watchKeys = new HashMap<>(); listeners = HashMultimap.create(); watchedFiles = HashMultimap.create(); } public Subscription subscribe(final Path file, final FileChangedListener listener) throws IOException { final Path absoluteFile = file.toAbsolutePath(); listeners.put(absoluteFile, listener); final Path absoluteDirectory = file.getParent(); watchedFiles.put(absoluteDirectory, absoluteFile); if (!watchKeys.containsKey(absoluteDirectory)) { final WatchKey watchKey = absoluteDirectory.register(watchService, ENTRY_MODIFY, ENTRY_DELETE); watchKeys.put(absoluteDirectory, watchKey); if (watchKeys.size() == 1) { startWatcher(); } } return new Subscription() { @Override public void cancel() { listeners.remove(absoluteFile, listener); watchedFiles.remove(absoluteDirectory, absoluteFile); if (!watchedFiles.containsKey(absoluteDirectory)) { watchKeys.remove(absoluteDirectory); if (watchKeys.isEmpty()) { stopWatcher(); } } } }; } private void startWatcher() { final WatcherThread watcher = new WatcherThread(); watcherThread = Optional.of(watcher); watcher.start(); } private void stopWatcher() { if (watcherThread.isPresent()) { watcherThread.get().interrupt(); watcherThread = Optional.absent(); } } private final class WatcherThread extends Thread { public WatcherThread() { super("eclipse-pmd RuleSetWatcher"); } @Override public void run() { try { while (true) { final WatchKey watchKey = watchService.take(); if (watchKey.isValid()) { final Path directory = (Path) watchKey.watchable(); for (final WatchEvent<?> event : watchKey.pollEvents()) { if (event.kind() != OVERFLOW) { final String filename = event.context().toString(); final Path file = directory.resolve(filename); PMDPlugin.getDefault().info(event.kind() + ": " + file); for (final FileChangedListener listener : listeners.get(file)) { listener.fileChanged(file); } } } } watchKey.reset(); } } catch (final InterruptedException | ClosedWatchServiceException e) { PMDPlugin.getDefault().info(getName() + " stopped"); } } } }