Created
July 12, 2016 00:54
-
-
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)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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