0

I'm pretty new to JavaFX and I'm currently writing a simple GUI application that should react to file creation in a specified directory. So far, I have used WatchService in classic Java apps, but I'm not sure how to make the GUI app react to WatchService. I could maybe run a WatchService in different thread and when the file creation happens, I could set a flag of some sort to which the main GUI class will react.

A6SE
  • 177
  • 1
  • 11
  • Your proposed solution sounds reasonable to me. I suggest you implement it, If you get into difficulty, you can post your code here and hopefully get help with it. – Abra Sep 17 '19 at 18:24
  • Ok, thank you. I'm going to try this idea, I'll post the update. – A6SE Sep 17 '19 at 18:28

1 Answers1

2

Here is a JavaFX service which will use the WatchService to watch for events on another thread.

The sample app demonstrating the service listens for changes which the WatchService has detected and records them in the list. But you could filter the change events and do whatever you like when you receive them (such as opening a stage as you wish to do). If you wished, you could even translate the detected events to JavaFX events, though an implementation for that is outside the scope of this answer.

ScheduledService implementation of the directory WatchService

The implementation of the WatchService was based upon a combination of an Oracle tutorial on watching a file directory and a JavaFX ScheduledService.

import javafx.beans.property.*;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Task;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;

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

/**
 * Watch a directory (or tree) for changes to files.
 */
public class WatchDirService extends ScheduledService<List<WatchEvent<Path>>> {

    private final WatchService watcher;
    private final Map<WatchKey,Path> keys;
    private boolean trace;

    private ReadOnlyStringWrapper dir = new ReadOnlyStringWrapper(this, "dir");
    public final String getDir() { return dir.get(); }
    public final ReadOnlyStringProperty dirProperty() { return dir.getReadOnlyProperty(); }

    private ReadOnlyBooleanWrapper recursive = new ReadOnlyBooleanWrapper(this, "recursive");
    public boolean isRecursive() { return recursive.get(); }
    public ReadOnlyBooleanProperty recursiveProperty() { return recursive; }

    @SuppressWarnings("unchecked")
    private static <T> WatchEvent<T> cast(WatchEvent<?> event) {
        return (WatchEvent<T>)event;
    }

    /**
     * Creates a WatchService and registers the given directory
     */
    public WatchDirService(Path dir, boolean recursive) throws IOException {
        this.watcher = FileSystems.getDefault().newWatchService();
        this.keys = new HashMap<>();
        this.dir.set(dir.toString());
        this.recursive.set(recursive);

        if (recursive) {
            System.out.format("Scanning %s ...\n", dir);
            registerAll(dir);
            System.out.println("Done.");
        } else {
            register(dir);
        }

        // enable trace after initial registration
        this.trace = true;
    }

    /**
     * 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
            {
                register(dir);
                return FileVisitResult.CONTINUE;
            }
        });
    }

    @Override
    protected Task<List<WatchEvent<Path>>> createTask() {
        return new WatchTask();
    }

    class WatchTask extends Task<List<WatchEvent<Path>>> {
        @Override
        protected List<WatchEvent<Path>> call() {
            // wait for key to be signalled
            WatchKey key;
            try {
                key = watcher.take();
            } catch (InterruptedException x) {
                if (isCancelled()) {
                    updateMessage("Cancelled");
                }

                return Collections.emptyList();
            }

            Path dir = keys.get(key);
            if (dir == null) {
                System.err.println("WatchKey not recognized");
                return Collections.emptyList();
            }

            List<WatchEvent<Path>> interestingEvents = new ArrayList<>();
            for (WatchEvent<?> event: key.pollEvents()) {
                WatchEvent.Kind kind = event.kind();

                if (kind == OVERFLOW) {
                    continue;
                }

                // Context for directory entry event is the file name of entry
                WatchEvent<Path> pathWatchEvent = cast(event);
                Path name = pathWatchEvent.context();
                Path child = dir.resolve(name);

                interestingEvents.add(pathWatchEvent);

                // if directory is created, and watching recursively, then
                // register it and its sub-directories
                if (recursive.get() && (kind == ENTRY_CREATE)) {
                    try {
                        if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
                            registerAll(child);
                        }
                    } catch (IOException x) {
                        System.err.println("Unable to register created directory for watching: " + child);
                    }
                }
            }

            // reset key and remove from set if directory no longer accessible
            boolean valid = key.reset();
            if (!valid) {
                keys.remove(key);


                // if all directories are inaccessible
                // even the root watch directory
                // might wight want to cancel the service.
                if (keys.isEmpty()) {
                    System.out.println("No directories being watched");
                }
            }

            return Collections.unmodifiableList(
                    interestingEvents
            );
        }
    }
}

For simplicity, the input parameters for the JavaFX WatchDirService are coded as readonly properties. This means if you wanted to change the watch from recursive to non-recursive or vice-versa or change the directory being watched, you would have to cancel the existing service and create a new one with the new settings. It is probably possible to have the properties read-write, so that the existing running service could be modified to watch different directories, but getting that to work seems a little tricky to me, so I didn't try it.

I was quite pleasantly surprised how well the integration of the WatchService and the JavaFX ScheduledService seems to work both in terms of how it is implemented, how it is used by the application and how it performs.

Basic usage of a directory monitoring WatchService

And a sample app demonstrating its use (detects changes in the root of the user's home directory):

watchapp image

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;

public class WatchDirApp extends Application {
    private static final String WATCH_DIR = System.getProperty("user.home");
    private WatchDirService watchDirService;

    @Override
    public void init() throws Exception {
        watchDirService = new WatchDirService(
                Paths.get(WATCH_DIR),
                false
        );
    }

    @Override
    public void start(Stage stage) throws Exception {
        ListView<String> events = new ListView<>();

        watchDirService.start();
        watchDirService.valueProperty().addListener((observable, previousEvents, newEvents) -> {
            if (newEvents != null) {
                newEvents.forEach(event ->
                        events.getItems().add(eventToString(event))
                );
            }
        });

        Scene scene = new Scene(new StackPane(events));
        stage.setScene(scene);
        stage.show();
    }

    private String eventToString(WatchEvent<Path> event) {
        return event.kind() + ":" + event.context() + ":n=" + event.count();
    }

    @Override
    public void stop() throws Exception {
        watchDirService.cancel();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

Example with Event Filtering

Here is a more extensive sample of using the WatchService from JavaFX, which will watch for creation of new text files in the user's home directory (or modification of existing files) and launch a new window to display the text in the files.

The example demonstrates filtering the events generated by the watch service so that different actions can be taken based upon the event (for example view the text file in a new window or update the contents of an existing window when the file is modified).

When running the example it can be noted that there is a delay of a second or two between the action which generates the event (e.g. saving a new text file) and the update to the UI (e.g. displaying the newly saved text file). This is because the watch service doesn't notify change events in real time (at least on my machine, which runs OS X), instead it notifies of the change events with a slight delay.

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

import java.io.IOException;
import java.nio.file.*;
import java.nio.file.WatchEvent;
import java.util.HashMap;
import java.util.Map;

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

public class WatchedTextFileViewerApp extends Application {
    private static final String TEXT_FILE_EXTENSION = ".txt";

    private static final String WATCH_DIR = System.getProperty("user.home");
    private WatchDirService watchDirService;

    private Stage appStage;
    private Map<Path, FileViewer> fileViewers = new HashMap<>();

    @Override
    public void init() throws Exception {
        watchDirService = new WatchDirService(
                Paths.get(WATCH_DIR),
                false
        );
    }

    @Override
    public void start(Stage stage) throws Exception {
        this.appStage = stage;
        ListView<String> events = new ListView<>();

        watchDirService.start();
        watchDirService.valueProperty().addListener((observable, previousEvents, newEvents) -> {
            if (newEvents != null) {
                newEvents.forEach(event ->
                        events.getItems().add(eventToString(event))
                );

                newEvents.stream()
                        .filter(event -> ENTRY_CREATE.equals(event.kind()) && isForTextFile(event.context()))
                        .forEach(event -> view(event.context()));

                newEvents.stream()
                        .filter(event -> ENTRY_MODIFY.equals(event.kind()) && isForTextFile(event.context()))
                        .forEach(event -> refresh(event.context()));
            }
        });

        Scene scene = new Scene(new StackPane(events));
        stage.setScene(scene);
        stage.show();
    }

    @Override
    public void stop() throws Exception {
        watchDirService.cancel();
    }

    private boolean isForTextFile(Path path) {
        return path != null
                && !Files.isDirectory(path)
                && path.toString().endsWith(TEXT_FILE_EXTENSION);
    }

    private FileViewer view(Path path) {
        FileViewer fileViewer = new FileViewer(appStage, path);
        fileViewers.put(path, fileViewer);
        fileViewer.show();

        return fileViewer;
    }

    private void refresh(Path path) {
        FileViewer fileViewer = fileViewers.get(path);
        if (fileViewer == null) {
            fileViewer = view(path);
        }

        fileViewer.refreshText();
        fileViewer.show();
    }

    private String eventToString(WatchEvent<Path> event) {
        return event.kind() + ":" + event.context() + ":n=" + event.count();
    }

    private static class FileViewer extends Stage {
        private TextArea textArea = new TextArea();
        private Path path;

        FileViewer(Stage owner, Path path) {
            this.path = Paths.get(WATCH_DIR).resolve(path);

            initOwner(owner);
            setTitle(path.toString());
            textArea.setEditable(false);

            refreshText();

            setScene(new Scene(textArea));
        }

        void refreshText() {
            try {
                textArea.setText(String.join("\n", Files.readAllLines(path)));
            } catch (IOException e) {
                System.err.println("Unable to read the content of: " + path);
            }
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}
jewelsea
  • 150,031
  • 14
  • 366
  • 406