/**
* FreeDesktopSearch - A Search Engine for your Desktop
* Copyright (C) 2013 Mirko Sertic
*
* 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 de.mirkosertic.desktopsearch;
import org.apache.log4j.Logger;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
public class DirectoryWatcher {
private static final Logger LOGGER = Logger.getLogger(DirectoryWatcher.class);
public static final int DEFAULT_WAIT_FOR_ACTION = 5;
private static class ActionTimer {
private WatchEvent.Kind kind;
private int waitForAction;
public ActionTimer(WatchEvent.Kind aKind, int aWaitForAction) {
kind = aKind;
waitForAction = aWaitForAction;
}
public void reset(WatchEvent.Kind aKind, int aWaitForAction) {
kind = aKind;
waitForAction = aWaitForAction;
}
public boolean runOneCycle() {
return (--waitForAction <= 0);
}
}
private final WatchService watchService;
private final Thread watcherThread;
private final Map<Path, ActionTimer> fileTimers;
private final int waitForAction;
private final Timer actionTimer;
private final DirectoryListener directoryListener;
private final Configuration.CrawlLocation filesystemLocation;
private final ExecutorPool executorPool;
public DirectoryWatcher(WatchServiceCache aWatchServiceCache, Configuration.CrawlLocation aFileSystemLocation, int aWaitForAction, DirectoryListener aDirectoryListener, ExecutorPool aExecutorPool) throws IOException {
executorPool = aExecutorPool;
fileTimers = new HashMap<>();
waitForAction = aWaitForAction;
directoryListener = aDirectoryListener;
filesystemLocation = aFileSystemLocation;
Path thePath = aFileSystemLocation.getDirectory().toPath();
watchService = aWatchServiceCache.getWatchServiceFor(thePath);
watcherThread = new Thread("WatcherThread-"+thePath) {
@Override
public void run() {
while(!isInterrupted()) {
try {
WatchKey theKey = watchService.take();
Path theParent = (Path) theKey.watchable();
theKey.pollEvents().stream().forEach(theEvent -> {
if (theEvent.kind() == StandardWatchEventKinds.OVERFLOW) {
LOGGER.warn("Overflow for " + theEvent.context() + " count = " + theEvent.count());
// Overflow events are not handled
} else {
Path thePath = theParent.resolve((Path) theEvent.context());
LOGGER.debug(theEvent.kind() + " for " + theEvent.context() + " count = " + theEvent.count());
publishActionFor(thePath, theEvent.kind());
}
});
theKey.reset();
Thread.sleep(10000);
} catch (InterruptedException e) {
LOGGER.debug("Has been interrupted");
}
}
}
};
actionTimer = new Timer();
}
private void publishActionFor(Path aPath, WatchEvent.Kind aKind ) {
synchronized (fileTimers) {
ActionTimer theTimer = fileTimers.get(aPath);
if (theTimer == null) {
fileTimers.put(aPath, new ActionTimer(aKind, waitForAction));
} else {
theTimer.reset(aKind, waitForAction);
}
}
}
private void actionCountDown() {
synchronized (fileTimers) {
Set<Path> theKeysToRemove = new HashSet<>();
fileTimers.entrySet().stream().forEach(theEntry -> {
if (theEntry.getValue().runOneCycle()) {
theKeysToRemove.add(theEntry.getKey());
if (!Files.isDirectory(theEntry.getKey())) {
if (theEntry.getValue().kind == StandardWatchEventKinds.ENTRY_CREATE) {
directoryListener.fileCreatedOrModified(filesystemLocation, theEntry.getKey());
}
if (theEntry.getValue().kind == StandardWatchEventKinds.ENTRY_DELETE) {
directoryListener.fileDeleted(filesystemLocation, theEntry.getKey());
}
if (theEntry.getValue().kind == StandardWatchEventKinds.ENTRY_MODIFY) {
directoryListener.fileCreatedOrModified(filesystemLocation, theEntry.getKey());
}
} else {
try {
if (theEntry.getValue().kind == StandardWatchEventKinds.ENTRY_CREATE) {
registerWatcher(theEntry.getKey());
}
if (theEntry.getValue().kind == StandardWatchEventKinds.ENTRY_MODIFY) {
registerWatcher(theEntry.getKey());
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
});
theKeysToRemove.forEach(fileTimers::remove);
}
}
private void registerWatcher(Path aDirectory) throws IOException {
LOGGER.info("New watchable directory detected : " + aDirectory);
aDirectory.register(watchService, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
}
public DirectoryWatcher startWatching() {
Thread theRegisterWatchers = new Thread("Registering Watchers") {
@Override
public void run() {
try {
Files.walk(filesystemLocation.getDirectory().toPath()).forEach(path -> {
if (Files.isDirectory(path)) {
LOGGER.info("Registering watches for " + path);
try {
registerWatcher(path);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
} catch (IOException e) {
LOGGER.error("Error registering file watcher", e);
}
}
};
theRegisterWatchers.start();
watcherThread.start();
actionTimer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
actionCountDown();
}
}, 1000, 1000);
return this;
}
public void stopWatching() {
actionTimer.cancel();
watcherThread.interrupt();
}
public void crawl() throws IOException {
Path thePath = filesystemLocation.getDirectory().toPath();
Files.walk(thePath).forEach(aPath -> {
if (!Files.isDirectory(aPath)) {
executorPool.execute(() -> directoryListener.fileFoundByCrawler(filesystemLocation, aPath));
}
});
}
}