Skip to content

Instantly share code, notes, and snippets.

@liluxdev
Created July 12, 2016 00:54
Show Gist options
  • Save liluxdev/fd706add3b8ad64a0df7ff2705b48d16 to your computer and use it in GitHub Desktop.
Save liluxdev/fd706add3b8ad64a0df7ff2705b48d16 to your computer and use it in GitHub Desktop.
Java 1.7+: Watching a Directory for File Changes (Requires Java 1.8+ to run because this code uses some features from Java 8 as well)
package <package>;
import <package>.Service;
import java.io.IOException;
/**
* Interface definition of a simple directory watch service.
*
* Implementations of this interface allow interested parties to <em>listen</em>
* to file system events coming from a specific directory.
*/
public interface DirectoryWatchService extends Service {
@Override
void start(); /* Suppress Exception */
/**
* Notifies the implementation of <em>this</em> interface that <code>dirPath</code>
* should be monitored for file system events. If the changed file matches any
* of the <code>globPatterns</code>, <code>listener</code> should be notified.
*
* @param listener The listener.
* @param dirPath The directory path.
* @param globPatterns Zero or more file patterns to be matched against file names.
* If none provided, matches <em>any</em> file.
* @throws IOException If <code>dirPath</code> is not a directory.
*/
void register(OnFileChangeListener listener, String dirPath, String... globPatterns)
throws IOException;
/**
* Interface definition for a callback to be invoked when a file under
* watch is changed.
*/
interface OnFileChangeListener {
/**
* Called when the file is created.
* @param filePath The file path.
*/
default void onFileCreate(String filePath) {}
/**
* Called when the file is modified.
* @param filePath The file path.
*/
default void onFileModify(String filePath) {}
/**
* Called when the file is deleted.
* @param filePath The file path.
*/
default void onFileDelete(String filePath) {}
}
}
The MIT License (MIT)
Copyright (c) 2015, Hindol Adhya
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
import <package>.SimpleDirectoryWatchService; // Replace <package> with your package name
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class Main {
private static final Logger LOGGER = LogManager.getLogger(Main.class);
public static void main(String[] args) {
try {
DirectoryWatchService watchService = new SimpleDirectoryWatchService(); // May throw
watchService.register( // May throw
new DirectoryWatchService.OnFileChangeListener() {
@Override
public void onFileCreate(String filePath) {
// File created
}
@Override
public void onFileModify(String filePath) {
// File modified
}
@Override
public void onFileDelete(String filePath) {
// File deleted
}
},
<directory>, // Directory to watch
<file-glob-pattern-1>, // E.g. "*.log"
<file-glob-pattern-2>, // E.g. "input-?.txt"
<file-glob-pattern-3>, // E.g. "config.ini"
... // As many patterns as you like
);
watchService.start();
} catch (IOException e) {
LOGGER.error("Unable to register file change listener for " + fileName);
}
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
watchService.stop();
LOGGER.error("Main thread interrupted.");
break;
}
}
}
}
package <package>;
/**
* Interface definition for services.
*/
public interface Service {
/**
* Starts the service. This method blocks until the service has completely started.
*/
void start() throws Exception;
/**
* Stops the service. This method blocks until the service has completely shut down.
*/
void stop();
}
package <package>;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
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.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 = LogManager.getLogger(SimpleDirectoryWatchService.class);
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.
*/
public SimpleDirectoryWatchService() throws IOException {
mWatchService = FileSystems.getDefault().newWatchService();
mIsRunning = new AtomicBoolean(false);
mWatchKeyToDirPathMap = newConcurrentMap();
mDirPathToListenersMap = newConcurrentMap();
mListenerToFilePatternsMap = newConcurrentMap();
}
@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()
*/
@Override
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()
*/
@Override
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.error("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.");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment