DefaultRecursiveWatcher.java
/*
* Syncany, www.syncany.org
* Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.syncany.operations.watch;
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.FileVisitor;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
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.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
/**
* The default recursive file watcher monitors a folder (and its sub-folders)
* by registering a watch on each of the sub-folders. This class is used on
* Linux/Unix-based operating systems and uses the Java 7 {@link WatchService}.
*
* <p>The class walks through the file tree and registers to a watch to every sub-folder.
* For new folders, a new watch is registered, and stale watches are removed.
*
* <p>When a file event occurs, a timer is started to wait for the file operations
* to settle. It is reset whenever a new event occurs. When the timer times out,
* an event is thrown through the {@link WatchListener}.
*
* @author Philipp C. Heckel (philipp.heckel@gmail.com)
*/
public class DefaultRecursiveWatcher extends RecursiveWatcher {
private WatchService watchService;
private Map<Path, WatchKey> watchPathKeyMap;
public DefaultRecursiveWatcher(Path root, List<Path> ignorePaths, int settleDelay, WatchListener listener) {
super(root, ignorePaths, settleDelay, listener);
this.watchService = null;
this.watchPathKeyMap = new HashMap<Path, WatchKey>();
}
@Override
public void beforeStart() throws Exception {
watchService = FileSystems.getDefault().newWatchService();
}
@Override
protected void beforePollEventLoop() {
walkTreeAndSetWatches();
}
@Override
protected boolean pollEvents() throws InterruptedException {
// Take events, but don't care what they are!
WatchKey watchKey = watchService.take();
watchKey.pollEvents();
watchKey.reset();
// Events are always relevant; ignored paths are not monitored
return true;
}
@Override
protected void watchEventsOccurred() {
walkTreeAndSetWatches();
unregisterStaleWatches();
}
@Override
public void afterStop() throws IOException {
watchService.close();
}
private synchronized void walkTreeAndSetWatches() {
logger.log(Level.INFO, "Registering new folders at watch service ...");
try {
Files.walkFileTree(root, new FileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (ignorePaths.contains(dir)) {
return FileVisitResult.SKIP_SUBTREE;
}
else {
registerWatch(dir);
return FileVisitResult.CONTINUE;
}
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
});
}
catch (IOException e) {
logger.log(Level.FINE, "IO failed", e);
}
}
private synchronized void unregisterStaleWatches() {
Set<Path> paths = new HashSet<Path>(watchPathKeyMap.keySet());
Set<Path> stalePaths = new HashSet<Path>();
for (Path path : paths) {
if (!Files.exists(path, LinkOption.NOFOLLOW_LINKS)) {
stalePaths.add(path);
}
}
if (stalePaths.size() > 0) {
logger.log(Level.INFO, "Cancelling stale path watches ...");
for (Path stalePath : stalePaths) {
unregisterWatch(stalePath);
}
}
}
private synchronized void registerWatch(Path dir) {
if (!watchPathKeyMap.containsKey(dir)) {
logger.log(Level.INFO, "- Registering " + dir);
try {
WatchKey watchKey = dir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY, OVERFLOW);
watchPathKeyMap.put(dir, watchKey);
}
catch (IOException e) {
logger.log(Level.FINE, "IO Failed", e);
}
}
}
private synchronized void unregisterWatch(Path dir) {
WatchKey watchKey = watchPathKeyMap.get(dir);
if (watchKey != null) {
logger.log(Level.INFO, "- Cancelling " + dir);
watchKey.cancel();
watchPathKeyMap.remove(dir);
}
}
}