All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.morimekta.util.FileWatcher Maven / Gradle / Ivy

Go to download

Utilities helping with reading writing and keeping various data formats, including JSON, binary data and formatted text.

There is a newer version: 3.7.1
Show newest version
package net.morimekta.util;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import static com.sun.nio.file.SensitivityWatchEventModifier.HIGH;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

/**
 * File watcher helper for use with simple callbacks.
 */
public class FileWatcher implements AutoCloseable {
    @FunctionalInterface
    public interface Watcher {
        void onFileUpdate(File file);
    }

    public FileWatcher() {
        this(newWatchService(),
             Executors.newSingleThreadExecutor(),
             Executors.newSingleThreadExecutor());
    }

    // @VisibleForTesting
    protected FileWatcher(WatchService watchService,
                          ExecutorService watcherExecutor,
                          ExecutorService callbackExecutor) {
        this.watchers = new LinkedList<>();
        this.watchDirKeys = new HashMap<>();
        this.watchKeyDirs = new HashMap<>();
        this.watchedFiles = Collections.synchronizedSet(new HashSet<>());
        this.watchService = watchService;
        this.watcherExecutor = watcherExecutor;
        this.callbackExecutor = callbackExecutor;
        this.watcherExecutor.submit(this::watchFilesTask);
    }

    public void addWatcher(Watcher watcher) {
        if (watcher == null) {
            throw new IllegalArgumentException("Null watcher added");
        }

        synchronized (watchers) {
            if (watcherExecutor.isShutdown()) {
                throw new IllegalStateException("Adding watcher on closed FileWatcher");
            }
            watchers.add(watcher);
        }
    }

    public boolean removeWatcher(Watcher watcher) {
        if (watcher == null) {
            throw new IllegalArgumentException("Null watcher removed");
        }

        synchronized (watchers) {
            return watchers.remove(watcher);
        }
    }

    public void startWatching(File file) {
        try {
            if (file == null) {
                throw new IllegalArgumentException("Null file argument");
            }
            if (watcherExecutor.isShutdown()) {
                throw new IllegalStateException("Starts to watch on closed FileWatcher");
            }

            file = file.getCanonicalFile()
                       .getAbsoluteFile();

            if (watchedFiles.contains(file.toString())) {
                // We're already watching this file, do nothing.
                return;
            }

            File parent = file.getParentFile();
            if (!watchDirKeys.containsKey(parent.toString())) {

                Path dirPath = Paths.get(parent.getAbsolutePath());
                WatchKey key = dirPath.register(watchService, new WatchEvent.Kind[]{ENTRY_MODIFY, ENTRY_CREATE}, HIGH);
                watchDirKeys.put(parent.toString(), key);
                watchKeyDirs.put(key, parent.toString());
            }

            watchedFiles.add(file.toString());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void stopWatching(File file) {
        try {
            if (file == null) {
                throw new IllegalArgumentException("Null file argument");
            }
            file = file.getCanonicalFile()
                       .getAbsoluteFile();

            watchedFiles.remove(file.toString());
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public void close() throws IOException {
        synchronized (watchers) {
            watchers.clear();
        }
        watcherExecutor.shutdown();
        watchService.close();
    }

    /**
     * Handle the watch service event loop.
     */
    private void watchFilesTask() {
        while (!watcherExecutor.isShutdown()) {
            try {
                WatchKey key = watchService.take();
                String parent = watchKeyDirs.get(key);
                if (parent == null) {
                    key.reset();
                    continue;
                }

                Set updates = new TreeSet<>();
                for (WatchEvent ev : key.pollEvents()) {
                    WatchEvent.Kind kind = ev.kind();

                    // Only file modification is interesting.
                    if (kind != ENTRY_MODIFY && kind != ENTRY_CREATE) {
                        if (kind == OVERFLOW) {
                            LOGGER.warn("Overflow event, file updates may have been lost");
                        }
                        continue;
                    }

                    @SuppressWarnings("unchecked")
                    WatchEvent event = (WatchEvent) ev;

                    File file = new File(parent, event.context().toString());
                    if (watchedFiles.contains(file.toString())) {
                        LOGGER.trace("Watched file " + file + " event " + kind);
                        updates.add(file);
                    }
                }
                // Ready the key again so it vil signal more events.
                key.reset();

                if (updates.size() > 0) {
                    List tmp = new LinkedList<>();
                    synchronized (watchers) {
                        tmp.addAll(watchers);
                    }
                    callbackExecutor.submit(() -> {
                        for (final Watcher watcher : tmp) {
                            for (final File file : updates) {
                                watcher.onFileUpdate(file);
                            }
                        }
                    });
                }
            } catch (InterruptedException interruptedEx) {
                LOGGER.error("Interrupted in service file watch thread: " + interruptedEx.getMessage(), interruptedEx);
                return;
            }
        }
    }

    private static WatchService newWatchService() {
        try {
            return FileSystems.getDefault().newWatchService();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(FileWatcher.class);

    private final LinkedList   watchers;
    private final Map watchDirKeys;
    private final Map watchKeyDirs;
    private final Set           watchedFiles;
    private final ExecutorService       callbackExecutor;
    private final ExecutorService       watcherExecutor;
    private final WatchService          watchService;
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy