Can I watch for single file change with WatchService (not the whole directory)?
Asked Answered
A

8

88

When I'm trying to register a file instead of a directory java.nio.file.NotDirectoryException is thrown. Can I listen for a single file change, not the whole directory?

Alpaca answered 27/4, 2013 at 10:57 Comment(1)
Javadoc: "In this release, this path locates a directory that exists." ==> so the answer is "no, you cannot register a file". Then: "The directory is registered with the watch service so that entries in the directory can be watched." ==> so registering a directory actually watches for events on directory entries, not on the directory itself. The name of the events remind of what they are related to, they start with ENTRY_, like "ENTRY_MODIFY - entry in directory was modified". The selected answer provides the details for using the event.Acute
P
114

Just filter the events for the file you want in the directory:

final Path path = FileSystems.getDefault().getPath(System.getProperty("user.home"), "Desktop");
System.out.println(path);
try (final WatchService watchService = FileSystems.getDefault().newWatchService()) {
    final WatchKey watchKey = path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
    while (true) {
        final WatchKey wk = watchService.take();
        for (WatchEvent<?> event : wk.pollEvents()) {
            //we only register "ENTRY_MODIFY" so the context is always a Path.
            final Path changed = (Path) event.context();
            System.out.println(changed);
            if (changed.endsWith("myFile.txt")) {
                System.out.println("My file has changed");
            }
        }
        // reset the key
        boolean valid = wk.reset();
        if (!valid) {
            System.out.println("Key has been unregisterede");
        }
    }
}

Here we check whether the changed file is "myFile.txt", if it is then do whatever.

Pricket answered 27/4, 2013 at 11:22 Comment(8)
I met text editor (krViewer) which don't modify file, but create tmp file and on Save just replace file, so no MODIFY message issued and i had to chech CREATE also.Aba
Is there any chance hat WatchService listen for files which exists currently in directory? So I start the project and files are there exactly. So far, only way to INITIALIZE WatchService was changing name of existing files, oraz just creating one.Orchard
Don't forget to test if the event is OVERFLOW. You don't need to register for this. Example here.Premarital
The unspoken trap here is that some platforms' implementations of the watcher can lock the directory you put the watcher on.Oswaldooswalt
final WatchKey watchKey - what's this for? This variable doesn't seem to be used later.Brachy
Make sure you don't try to watch for a file in a resource directory. It feels like its working but its not because the file changes, but the resource doesn't reload automatically.Joselynjoseph
regarding to @Brachy question, I was also wonder what is that variable for, so the only reasonable answer for me is, that code above was taken from something bigger and modified a little just for that example and that part final WatchKey watchKey = wasn't delete. The most important is that the path was registered with watchservice path.register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);Electrotechnics
in case anybody gets the modification event twice, here are some workarounds to solve this: https://mcmap.net/q/196872/-java-7-watchservice-ignoring-multiple-occurrences-of-the-same-eventParham
B
28

Other answers are right that you must watch a directory and filter for your particular file. However, you probably want a thread running in the background. The accepted answer can block indefinitely on watchService.take(); and doesn't close the WatchService. A solution suitable for a separate thread might look like:

public class FileWatcher extends Thread {
    private final File file;
    private AtomicBoolean stop = new AtomicBoolean(false);

    public FileWatcher(File file) {
        this.file = file;
    }

    public boolean isStopped() { return stop.get(); }
    public void stopThread() { stop.set(true); }

    public void doOnChange() {
        // Do whatever action you want here
    }

    @Override
    public void run() {
        try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
            Path path = file.toPath().getParent();
            path.register(watcher, StandardWatchEventKinds.ENTRY_MODIFY);
            while (!isStopped()) {
                WatchKey key;
                try { key = watcher.poll(25, TimeUnit.MILLISECONDS); }
                catch (InterruptedException e) { return; }
                if (key == null) { Thread.yield(); continue; }

                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();

                    @SuppressWarnings("unchecked")
                    WatchEvent<Path> ev = (WatchEvent<Path>) event;
                    Path filename = ev.context();

                    if (kind == StandardWatchEventKinds.OVERFLOW) {
                        Thread.yield();
                        continue;
                    } else if (kind == java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY
                            && filename.toString().equals(file.getName())) {
                        doOnChange();
                    }
                    boolean valid = key.reset();
                    if (!valid) { break; }
                }
                Thread.yield();
            }
        } catch (Throwable e) {
            // Log or rethrow the error
        }
    }
}

I tried working from the accepted answer and this article. You should be able to use this thread with new FileWatcher(new File("/home/me/myfile")).start() and stop it by calling stopThread() on the thread.

Burnejones answered 2/1, 2015 at 3:30 Comment(3)
Remember that non-daemon threads prevent the JVM from exiting, so call setDaemon(boolean) prior to run() depending on how you want your app to behave.Langer
in case anybody gets the modification event twice, here are some workarounds to solve this: https://mcmap.net/q/196872/-java-7-watchservice-ignoring-multiple-occurrences-of-the-same-eventParham
You might also want to reduce the priority of the thread.Symptomatic
A
21

No it isn't possible to register a file, the watch service doesn't work this way. But registering a directory actually watches changes on the directory children (the files and sub-directories), not the changes on the directory itself.

If you want to watch a file, then you register the containing directory with the watch service. Path.register() documentation says:

WatchKey java.nio.file.Path.register(WatchService watcher, Kind[] events, Modifier... modifiers) throws IOException

Registers the file located by this path with a watch service.

In this release, this path locates a directory that exists. The directory is registered with the watch service so that entries in the directory can be watched

Then you need to process events on entries, and detect those related to the file you are interested in, by checking the context value of the event. The context value represents the name of the entry (actually the path of the entry relatively to the path of its parent, which is exactly the child name). You have an example here.

Acute answered 25/6, 2014 at 6:27 Comment(0)
F
11

Apache offers a FileWatchDog class with a doOnChange method.

private class SomeWatchFile extends FileWatchdog {

    protected SomeWatchFile(String filename) {
        super(filename);
    }

    @Override
    protected void doOnChange() {
        fileChanged= true;
    }

}

And where ever you want you can start this thread:

SomeWatchFile someWatchFile = new SomeWatchFile (path);
someWatchFile.start();

The FileWatchDog class polls a file's lastModified() timestamp. The native WatchService from Java NIO is more efficient, since notifications are immediate.

Feticide answered 17/2, 2014 at 9:57 Comment(2)
Would be nice to know from which library the FileWatchdog class is coming from ?Tradein
from log4j - org.apache.log4j.helpersFeticide
L
9

You cannot watch an individual file directly but you can filter out what you don't need.

Here is my FileWatcher class implementation:

import java.io.File;
import java.nio.file.*;
import java.nio.file.WatchEvent.Kind;

import static java.nio.file.StandardWatchEventKinds.*;

public abstract class FileWatcher
{
    private Path folderPath;
    private String watchFile;

    public FileWatcher(String watchFile)
    {
        Path filePath = Paths.get(watchFile);

        boolean isRegularFile = Files.isRegularFile(filePath);

        if (!isRegularFile)
        {
            // Do not allow this to be a folder since we want to watch files
            throw new IllegalArgumentException(watchFile + " is not a regular file");
        }

        // This is always a folder
        folderPath = filePath.getParent();

        // Keep this relative to the watched folder
        this.watchFile = watchFile.replace(folderPath.toString() + File.separator, "");
    }

    public void watchFile() throws Exception
    {
        // We obtain the file system of the Path
        FileSystem fileSystem = folderPath.getFileSystem();

        // We create the new WatchService using the try-with-resources block
        try (WatchService service = fileSystem.newWatchService())
        {
            // We watch for modification events
            folderPath.register(service, ENTRY_MODIFY);

            // Start the infinite polling loop
            while (true)
            {
                // Wait for the next event
                WatchKey watchKey = service.take();

                for (WatchEvent<?> watchEvent : watchKey.pollEvents())
                {
                    // Get the type of the event
                    Kind<?> kind = watchEvent.kind();

                    if (kind == ENTRY_MODIFY)
                    {
                        Path watchEventPath = (Path) watchEvent.context();

                        // Call this if the right file is involved
                        if (watchEventPath.toString().equals(watchFile))
                        {
                            onModified();
                        }
                    }
                }

                if (!watchKey.reset())
                {
                    // Exit if no longer valid
                    break;
                }
            }
        }
    }

    public abstract void onModified();
}

To use this, you just have to extend and implement the onModified() method like so:

import java.io.File;

public class MyFileWatcher extends FileWatcher
{
    public MyFileWatcher(String watchFile)
    {
        super(watchFile);
    }

    @Override
    public void onModified()
    {
        System.out.println("Modified!");
    }
}

Finally, start watching the file:

String watchFile = System.getProperty("user.home") + File.separator + "Desktop" + File.separator + "Test.txt";
FileWatcher fileWatcher = new MyFileWatcher(watchFile);
fileWatcher.watchFile();
Ledbetter answered 12/10, 2016 at 16:24 Comment(2)
clean working code! explanatory and well-written answer!Parkins
Actually it's missing the OVERFLOW case, so not a complete solution yetOswaldooswalt
A
7

Not sure about others, but I groan at the amount of code needed to watch a single file for changes using the basic WatchService API. It has to be simpler!

Here are a couple of alternatives using third party libraries:

Arkwright answered 30/4, 2015 at 12:17 Comment(1)
The second link is still a lot of code around Java Watcher APIReady
V
5

I have created a wrapper around Java 1.7's WatchService that allows registering a directory and any number of glob patterns. This class will take care of the filtering and only emit events you are interested in.

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(); // The actual watcher runs on a new thread
} catch (IOException e) {
    LOGGER.error("Unable to register file change listener for " + fileName);
}

Complete code is in this repo.

Venom answered 23/6, 2015 at 14:7 Comment(0)
L
0

I extended the solution by BullyWiiPlaza a bit, for integration with javafx.concurrent, e.g. javafx.concurrent.Taskand javafx.concurrent.Service. Also I added possibility to track multiple files. Task:

import javafx.concurrent.Task;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.nio.file.*;
import java.util.*;

import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;

@Slf4j
public abstract class FileWatcherTask extends Task<Void> {

    static class Entry {
        private final Path folderPath;
        private final String watchFile;

        Entry(Path folderPath, String watchFile) {
            this.folderPath = folderPath;
            this.watchFile = watchFile;
        }

        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Entry entry = (Entry) o;
            return Objects.equals(folderPath, entry.folderPath) && Objects.equals(watchFile, entry.watchFile);
        }

        @Override
        public int hashCode() {
            return Objects.hash(folderPath, watchFile);
        }
    }

    private final List<Entry> entryList;

    private final Map<WatchKey, Entry> watchKeyEntryMap;

    public FileWatcherTask(Iterable<String> watchFiles) {
        this.entryList = new ArrayList<>();
        this.watchKeyEntryMap = new LinkedHashMap<>();
        for (String watchFile : watchFiles) {
            Path filePath = Paths.get(watchFile);
            boolean isRegularFile = Files.isRegularFile(filePath);
            if (!isRegularFile) {
                // Do not allow this to be a folder since we want to watch files
                throw new IllegalArgumentException(watchFile + " is not a regular file");
            }
            // This is always a folder
            Path folderPath = filePath.getParent();
            // Keep this relative to the watched folder
            watchFile = watchFile.replace(folderPath.toString() + File.separator, "");
            Entry entry = new Entry(folderPath, watchFile);
            entryList.add(entry);
            log.debug("Watcher initialized for {} entries. ({})", entryList.size(), entryList.stream().map(e -> e.watchFile + "-" + e.folderPath).findFirst().orElse("<>"));
        }
    }

    public FileWatcherTask(String... watchFiles) {
        this(Arrays.asList(watchFiles));
    }

    public void watchFile() throws Exception {
        // We obtain the file system of the Path
        // FileSystem fileSystem = folderPath.getFileSystem();
        // TODO: use the actual file system instead of default
        FileSystem fileSystem = FileSystems.getDefault();

        // We create the new WatchService using the try-with-resources block
        try (WatchService service = fileSystem.newWatchService()) {
            log.debug("Watching filesystem {}", fileSystem);
            for (Entry e : entryList) {
                // We watch for modification events
                WatchKey key = e.folderPath.register(service, ENTRY_MODIFY);
                watchKeyEntryMap.put(key, e);
            }

            // Start the infinite polling loop
            while (true) {
                // Wait for the next event
                WatchKey watchKey = service.take();
                for (Entry e : entryList) {
                    // Call this if the right file is involved
                    var hans = watchKeyEntryMap.get(watchKey);
                    if (hans != null) {
                        for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
                            // Get the type of the event
                            WatchEvent.Kind<?> kind = watchEvent.kind();

                            if (kind == ENTRY_MODIFY) {
                                Path watchEventPath = (Path) watchEvent.context();
                                onModified(e.watchFile);
                            }
                            if (!watchKey.reset()) {
                                // Exit if no longer valid
                                log.debug("Watch key {} was reset", watchKey);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }

    @Override
    protected Void call() throws Exception {
        watchFile();
        return null;
    }

    public abstract void onModified(String watchFile);
}

Service:

public abstract class FileWatcherService extends Service<Void> {
    
    private final Iterable<String> files;

    public FileWatcherService(Iterable<String> files) {
        this.files = files;
    }

    @Override
    protected Task<Void> createTask() {
        return new FileWatcherTask(files) {
            @Override
            public void onModified(String watchFile) {
                FileWatcherService.this.onModified(watchFile);
            }
        };
    }
    
    abstract void onModified(String watchFile);
}
Lennon answered 2/6, 2022 at 13:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.