package com.narrowtux.fmm.io.dirwatch;
import java.io.IOException;
import java.nio.file.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static java.nio.file.StandardWatchEventKinds.*;
/**
* A simple class which can monitor files and notify interested parties
* (i.e. listeners) of file changes.
*
* This class is kept lean by only keeping methods that are actually being
* called.
*/
public class SimpleDirectoryWatchService implements DirectoryWatchService, Runnable {
private static final Logger LOGGER = Logger.getLogger(SimpleDirectoryWatchService.class.getName());
private final WatchService mWatchService;
private final AtomicBoolean mIsRunning;
private final ConcurrentMap<WatchKey, Path> mWatchKeyToDirPathMap;
private final ConcurrentMap<Path, Set<OnFileChangeListener>> mDirPathToListenersMap;
private final ConcurrentMap<OnFileChangeListener, Set<PathMatcher>> mListenerToFilePatternsMap;
/**
* A simple no argument constructor for creating a <code>SimpleDirectoryWatchService</code>.
*
* @throws IOException If an I/O error occurs.
*/
private SimpleDirectoryWatchService() throws IOException {
mWatchService = FileSystems.getDefault().newWatchService();
mIsRunning = new AtomicBoolean(false);
mWatchKeyToDirPathMap = newConcurrentMap();
mDirPathToListenersMap = newConcurrentMap();
mListenerToFilePatternsMap = newConcurrentMap();
}
public static SimpleDirectoryWatchService getInstance() {
return SingletonHolder.INSTANCE;
}
@SuppressWarnings("unchecked")
private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>)event;
}
private static <K, V> ConcurrentMap<K, V> newConcurrentMap() {
return new ConcurrentHashMap<>();
}
private static <T> Set<T> newConcurrentSet() {
return Collections.newSetFromMap(newConcurrentMap());
}
public static PathMatcher matcherForGlobExpression(String globPattern) {
return FileSystems.getDefault().getPathMatcher("glob:" + globPattern);
}
public static boolean matches(Path input, PathMatcher pattern) {
return pattern.matches(input);
}
public static boolean matchesAny(Path input, Set<PathMatcher> patterns) {
for (PathMatcher pattern : patterns) {
if (matches(input, pattern)) {
return true;
}
}
return false;
}
private Path getDirPath(WatchKey key) {
return mWatchKeyToDirPathMap.get(key);
}
private Set<OnFileChangeListener> getListeners(Path dir) {
return mDirPathToListenersMap.get(dir);
}
private Set<PathMatcher> getPatterns(OnFileChangeListener listener) {
return mListenerToFilePatternsMap.get(listener);
}
private Set<OnFileChangeListener> matchedListeners(Path dir, Path file) {
return getListeners(dir)
.stream()
.filter(listener -> matchesAny(file, getPatterns(listener)))
.collect(Collectors.toSet());
}
private void notifyListeners(WatchKey key) {
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind eventKind = event.kind();
// Overflow occurs when the watch event queue is overflown
// with events.
if (eventKind.equals(OVERFLOW)) {
// TODO: Notify all listeners.
return;
}
WatchEvent<Path> pathEvent = cast(event);
Path file = pathEvent.context();
if (eventKind.equals(ENTRY_CREATE)) {
matchedListeners(getDirPath(key), file)
.forEach(listener -> listener.onFileCreate(file.toString()));
} else if (eventKind.equals(ENTRY_MODIFY)) {
matchedListeners(getDirPath(key), file)
.forEach(listener -> listener.onFileModify(file.toString()));
} else if (eventKind.equals(ENTRY_DELETE)) {
matchedListeners(getDirPath(key), file)
.forEach(listener -> listener.onFileDelete(file.toString()));
}
}
}
/**
* {@inheritDoc}
*/
@Override
public void register(OnFileChangeListener listener, String dirPath, String... globPatterns)
throws IOException {
Path dir = Paths.get(dirPath);
if (!Files.isDirectory(dir)) {
throw new IllegalArgumentException(dirPath + " is not a directory.");
}
if (!mDirPathToListenersMap.containsKey(dir)) {
// May throw
WatchKey key = dir.register(
mWatchService, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE
);
mWatchKeyToDirPathMap.put(key, dir);
mDirPathToListenersMap.put(dir, newConcurrentSet());
}
getListeners(dir).add(listener);
Set<PathMatcher> patterns = newConcurrentSet();
for (String globPattern : globPatterns) {
patterns.add(matcherForGlobExpression(globPattern));
}
if (patterns.isEmpty()) {
patterns.add(matcherForGlobExpression("*")); // Match everything if no filter is found
}
mListenerToFilePatternsMap.put(listener, patterns);
LOGGER.info("Watching files matching " + Arrays.toString(globPatterns)
+ " under " + dirPath + " for changes.");
}
/**
* Start this <code>SimpleDirectoryWatchService</code> instance by spawning a new thread.
*
* @see #stop()
*/
public void start() {
if (mIsRunning.compareAndSet(false, true)) {
Thread runnerThread = new Thread(this, DirectoryWatchService.class.getSimpleName());
runnerThread.start();
}
}
/**
* Stop this <code>SimpleDirectoryWatchService</code> thread.
* The killing happens lazily, giving the running thread an opportunity
* to finish the work at hand.
*
* @see #start()
*/
public void stop() {
// Kill thread lazily
mIsRunning.set(false);
}
/**
* {@inheritDoc}
*/
@Override
public void run() {
LOGGER.info("Starting file watcher service.");
while (mIsRunning.get()) {
WatchKey key;
try {
key = mWatchService.take();
} catch (InterruptedException e) {
LOGGER.info(
DirectoryWatchService.class.getSimpleName()
+ " service interrupted."
);
break;
}
if (null == getDirPath(key)) {
LOGGER.severe("Watch key not recognized.");
continue;
}
notifyListeners(key);
// Reset key to allow further events for this key to be processed.
boolean valid = key.reset();
if (!valid) {
mWatchKeyToDirPathMap.remove(key);
if (mWatchKeyToDirPathMap.isEmpty()) {
break;
}
}
}
mIsRunning.set(false);
LOGGER.info("Stopping file watcher service.");
}
private static class SingletonHolder {
/** The singleton instance. */
private static final SimpleDirectoryWatchService INSTANCE;
static {
try {
INSTANCE = new SimpleDirectoryWatchService();
} catch (IOException|UnsupportedOperationException e) {
throw new ExceptionInInitializerError("Unable to start "
+ DirectoryWatchService.class.getSimpleName() + " instance.");
}
}
}
}