The javadoc for StandardWatchEventKinds.ENTRY_MODIFY
says:
Directory entry modified. When a directory is registered for this event the
WatcherServices reports events twice because the underlying file is updated twice. Once for the content and once for the file modified time. These events happen within a short time span. To solve this, sleep between the poll()
or take()
calls and the key.pollEvents()
call. For example:
@Override
@SuppressWarnings( "SleepWhileInLoop" )
public void run() {
setListening( true );
while( isListening() ) {
try {
final WatchKey key = getWatchService().take();
final Path path = get( key );
// Prevent receiving two separate ENTRY_MODIFY events: file modified
// and timestamp updated. Instead, receive one ENTRY_MODIFY event
// with two counts.
Thread.sleep( 50 );
for( final WatchEvent<?> event : key.pollEvents() ) {
final Path changed = path.resolve( (Path)event.context() );
if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
System.out.println( "Changed: " + changed );
}
}
if( !key.reset() ) {
ignore( path );
}
} catch( IOException | InterruptedException ex ) {
// Stop eavesdropping.
setListening( false );
}
}
}
Calling sleep()
helps eliminate the double calls. The delay might have to be as high as three seconds.
Here is a full implementation using timestamps
to avoid firing multiple events:
import java.io.File;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.HashMap;
import java.util.Map;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.*;
public abstract class DirectoryWatcher
{
private WatchService watcher;
private Map<WatchKey, Path> keys;
private Map<Path, Long> fileTimeStamps;
private boolean recursive;
private boolean trace = true;
@SuppressWarnings("unchecked")
private static <T> WatchEvent<T> cast(WatchEvent<?> event)
{
return (WatchEvent<T>) event;
}
/**
* Register the given directory with the WatchService
*/
private void register(Path directory) throws IOException
{
WatchKey watchKey = directory.register(watcher, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
addFileTimeStamps(directory);
if (trace)
{
Path existingFilePath = keys.get(watchKey);
if (existingFilePath == null)
{
System.out.format("register: %s\n", directory);
} else
{
if (!directory.equals(existingFilePath))
{
System.out.format("update: %s -> %s\n", existingFilePath, directory);
}
}
}
keys.put(watchKey, directory);
}
private void addFileTimeStamps(Path directory)
{
File[] files = directory.toFile().listFiles();
if (files != null)
{
for (File file : files)
{
if (file.isFile())
{
fileTimeStamps.put(file.toPath(), file.lastModified());
}
}
}
}
/**
* Register the given directory, and all its sub-directories, with the
* WatchService.
*/
private void registerAll(Path directory) throws IOException
{
Files.walkFileTree(directory, new SimpleFileVisitor<Path>()
{
@Override
public FileVisitResult preVisitDirectory(Path currentDirectory, BasicFileAttributes attrs)
throws IOException
{
register(currentDirectory);
return FileVisitResult.CONTINUE;
}
});
}
/**
* Creates a WatchService and registers the given directory
*/
DirectoryWatcher(Path directory, boolean recursive) throws IOException
{
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new HashMap<>();
fileTimeStamps = new HashMap<>();
this.recursive = recursive;
if (recursive)
{
System.out.format("Scanning %s ...\n", directory);
registerAll(directory);
System.out.println("Done.");
} else
{
register(directory);
}
// enable trace after initial registration
this.trace = true;
}
/**
* Process all events for keys queued to the watcher
*/
void processEvents() throws InterruptedException, IOException
{
while (true)
{
WatchKey key = watcher.take();
Path dir = keys.get(key);
if (dir == null)
{
System.err.println("WatchKey not recognized!!");
continue;
}
for (WatchEvent<?> event : key.pollEvents())
{
WatchEvent.Kind watchEventKind = event.kind();
// TBD - provide example of how OVERFLOW event is handled
if (watchEventKind == OVERFLOW)
{
continue;
}
// Context for directory entry event is the file name of entry
WatchEvent<Path> watchEvent = cast(event);
Path fileName = watchEvent.context();
Path filePath = dir.resolve(fileName);
long oldFileModifiedTimeStamp = fileTimeStamps.get(filePath);
long newFileModifiedTimeStamp = filePath.toFile().lastModified();
if (newFileModifiedTimeStamp > oldFileModifiedTimeStamp)
{
fileTimeStamps.remove(filePath);
onEventOccurred();
fileTimeStamps.put(filePath, filePath.toFile().lastModified());
}
if (recursive && watchEventKind == ENTRY_CREATE)
{
if (Files.isDirectory(filePath, NOFOLLOW_LINKS))
{
registerAll(filePath);
}
}
break;
}
boolean valid = key.reset();
if (!valid)
{
keys.remove(key);
if (keys.isEmpty())
{
break;
}
}
}
}
public abstract void onEventOccurred();
}
Extend the class and implement the onEventOccurred()
method.
One of my goto solutions for problems like this is to simply queue up the unique event resources and delay processing for an acceptable amount of time. In this case I maintain a Set<String>
that contains every file name derived from each event that arrives. Using a Set<>
ensures that duplicates don't get added and, therefore, will only be processed once (per delay period).
Each time an interesting event arrives I add the file name to the Set<>
and restart my delay timer. When things settle down and the delay period elapses, I proceed to processing the files.
The addFileToProcess() and processFiles() methods are 'synchronized' to ensure that no ConcurrentModificationExceptions are thrown.
This simplified/standalone example is a derivative of Oracle's WatchDir.java:
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.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
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.Iterator;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
public class DirectoryWatcherService implements Runnable {
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>)event;
}
/*
* Wait this long after an event before processing the files.
*/
private final int DELAY = 500;
/*
* Use a SET to prevent duplicates from being added when multiple events on the
* same file arrive in quick succession.
*/
HashSet<String> filesToReload = new HashSet<String>();
/*
* Keep a map that will be used to resolve WatchKeys to the parent directory
* so that we can resolve the full path to an event file.
*/
private final Map<WatchKey,Path> keys;
Timer processDelayTimer = null;
private volatile Thread server;
private boolean trace = false;
private WatchService watcher = null;
public DirectoryWatcherService(Path dir, boolean recursive)
throws IOException {
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new HashMap<WatchKey,Path>();
if (recursive) {
registerAll(dir);
} else {
register(dir);
}
// enable trace after initial registration
this.trace = true;
}
private synchronized void addFileToProcess(String filename) {
boolean alreadyAdded = filesToReload.add(filename) == false;
System.out.println("Queuing file for processing: "
+ filename + (alreadyAdded?"(already queued)":""));
if (processDelayTimer != null) {
processDelayTimer.cancel();
}
processDelayTimer = new Timer();
processDelayTimer.schedule(new TimerTask() {
@Override
public void run() {
processFiles();
}
}, DELAY);
}
private synchronized void processFiles() {
/*
* Iterate over the set of file to be processed
*/
for (Iterator<String> it = filesToReload.iterator(); it.hasNext();) {
String filename = it.next();
/*
* Sometimes you just have to do what you have to do...
*/
System.out.println("Processing file: " + filename);
/*
* Remove this file from the set.
*/
it.remove();
}
}
/**
* Register the given directory with the WatchService
*/
private void register(Path dir) throws IOException {
WatchKey key = dir.register(watcher, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
if (trace) {
Path prev = keys.get(key);
if (prev == null) {
System.out.format("register: %s\n", dir);
} else {
if (!dir.equals(prev)) {
System.out.format("update: %s -> %s\n", prev, dir);
}
}
}
keys.put(key, dir);
}
/**
* Register the given directory, and all its sub-directories, with the
* WatchService.
*/
private void registerAll(final Path start) throws IOException {
// register directory and sub-directories
Files.walkFileTree(start, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException
{
if (dir.getFileName().toString().startsWith(".")) {
return FileVisitResult.SKIP_SUBTREE;
}
register(dir);
return FileVisitResult.CONTINUE;
}
});
}
@SuppressWarnings("unchecked")
@Override
public void run() {
Thread thisThread = Thread.currentThread();
while (server == thisThread) {
try {
// wait for key to be signaled
WatchKey key;
try {
key = watcher.take();
} catch (InterruptedException x) {
return;
}
Path dir = keys.get(key);
if (dir == null) {
continue;
}
for (WatchEvent<?> event: key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == OVERFLOW) {
continue;
}
if (kind == ENTRY_MODIFY) {
WatchEvent<Path> ev = (WatchEvent<Path>)event;
Path name = ev.context();
Path child = dir.resolve(name);
String filename = child.toAbsolutePath().toString();
addFileToProcess(filename);
}
}
key.reset();
} catch (Exception e) {
e.printStackTrace();
}
}
}
public void start() {
server = new Thread(this);
server.setName("Directory Watcher Service");
server.start();
}
public void stop() {
Thread moribund = server;
server = null;
if (moribund != null) {
moribund.interrupt();
}
}
public static void main(String[] args) {
if (args==null || args.length == 0) {
System.err.println("You need to provide a path to watch!");
System.exit(-1);
}
Path p = Paths.get(args[0]);
if (!Files.isDirectory(p)) {
System.err.println(p + " is not a directory!");
System.exit(-1);
}
DirectoryWatcherService watcherService;
try {
watcherService = new DirectoryWatcherService(p, true);
watcherService.start();
} catch (IOException e) {
System.err.println(e.getMessage());
}
}
}
I modified WatchDir.java to receive only human-made modifications. Comparing .lastModified()
of a file.
long lastModi=0; //above for loop
if(kind==ENTRY_CREATE){
System.out.format("%s: %s\n", event.kind().name(), child);
}else if(kind==ENTRY_MODIFY){
if(child.toFile().lastModified() - lastModi > 1000){
System.out.format("%s: %s\n", event.kind().name(), child);
}
}else if(kind==ENTRY_DELETE){
System.out.format("%s: %s\n", event.kind().name(), child);
}
lastModi=child.toFile().lastModified();
I tried this and it's working perfectly :
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import static java.nio.file.StandardWatchEventKinds.*;
public class FileWatcher implements Runnable, AutoCloseable {
private final WatchService service;
private final Map<Path, WatchTarget> watchTargets = new HashMap<>();
private final List<FileListener> fileListeners = new CopyOnWriteArrayList<>();
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
private final Lock w = lock.writeLock();
private final AtomicBoolean running = new AtomicBoolean(false);
public FileWatcher() throws IOException {
service = FileSystems.getDefault().newWatchService();
}
@Override
public void run() {
if (running.compareAndSet(false, true)) {
while (running.get()) {
WatchKey key;
try {
key = service.take();
} catch (Throwable e) {
break;
}
if (key.isValid()) {
r.lock();
try {
key.pollEvents().stream()
.filter(e -> e.kind() != OVERFLOW)
.forEach(e -> watchTargets.values().stream()
.filter(t -> t.isInterested(e))
.forEach(t -> fireOnEvent(t.path, e.kind())));
} finally {
r.unlock();
}
if (!key.reset()) {
break;
}
}
}
running.set(false);
}
}
public boolean registerPath(Path path, boolean updateIfExists, WatchEvent.Kind... eventKinds) {
w.lock();
try {
WatchTarget target = watchTargets.get(path);
if (!updateIfExists && target != null) {
return false;
}
Path parent = path.getParent();
if (parent != null) {
if (target == null) {
watchTargets.put(path, new WatchTarget(path, eventKinds));
parent.register(service, eventKinds);
} else {
target.setEventKinds(eventKinds);
}
return true;
}
} catch (Throwable e) {
e.printStackTrace();
} finally {
w.unlock();
}
return false;
}
public void addFileListener(FileListener fileListener) {
fileListeners.add(fileListener);
}
public void removeFileListener(FileListener fileListener) {
fileListeners.remove(fileListener);
}
private void fireOnEvent(Path path, WatchEvent.Kind eventKind) {
for (FileListener fileListener : fileListeners) {
fileListener.onEvent(path, eventKind);
}
}
public boolean isRunning() {
return running.get();
}
@Override
public void close() throws IOException {
running.set(false);
w.lock();
try {
service.close();
} finally {
w.unlock();
}
}
private final class WatchTarget {
private final Path path;
private final Path fileName;
private final Set<String> eventNames = new HashSet<>();
private final Event lastEvent = new Event();
private WatchTarget(Path path, WatchEvent.Kind[] eventKinds) {
this.path = path;
this.fileName = path.getFileName();
setEventKinds(eventKinds);
}
private void setEventKinds(WatchEvent.Kind[] eventKinds) {
eventNames.clear();
for (WatchEvent.Kind k : eventKinds) {
eventNames.add(k.name());
}
}
private boolean isInterested(WatchEvent e) {
long now = System.currentTimeMillis();
String name = e.kind().name();
if (e.context().equals(fileName) && eventNames.contains(name)) {
if (lastEvent.name == null || !lastEvent.name.equals(name) || now - lastEvent.when > 100) {
lastEvent.name = name;
lastEvent.when = now;
return true;
}
}
return false;
}
@Override
public int hashCode() {
return path.hashCode();
}
@Override
public boolean equals(Object obj) {
return obj == this || obj != null && obj instanceof WatchTarget && Objects.equals(path, ((WatchTarget) obj).path);
}
}
private final class Event {
private String name;
private long when;
}
public static void main(String[] args) throws IOException, InterruptedException {
FileWatcher watcher = new FileWatcher();
if (watcher.registerPath(Paths.get("filename"), false, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE)) {
watcher.addFileListener((path, eventKind) -> System.out.println(path + " -> " + eventKind.name()));
new Thread(watcher).start();
System.in.read();
}
watcher.close();
System.exit(0);
}
}
FileListener :
import java.nio.file.Path;
import java.nio.file.WatchEvent;
public interface FileListener {
void onEvent(Path path, WatchEvent.Kind eventKind);
}
I solved this problem by defining a global boolean variable named "modifySolver" which be false by default. You can handle this problem as I show bellow:
else if (eventKind.equals (ENTRY_MODIFY))
{
if (event.count() == 2)
{
getListener(getDirPath(key)).onChange (FileChangeType.MODIFY, file.toString ());
}
/*capture first modify event*/
else if ((event.count() == 1) && (!modifySolver))
{
getListener(getDirPath(key)).onChange (FileChangeType.MODIFY, file.toString ());
modifySolver = true;
}
/*discard the second modify event*/
else if ((event.count() == 1) && (modifySolver))
{
modifySolver = false;
}
}