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

org.fakereplace.integration.filewatcher.WatchServiceFileSystemWatcher Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2016, Stuart Douglas, and individual contributors as indicated
 * by the @authors tag.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.fakereplace.integration.filewatcher;

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 java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;

import com.sun.nio.file.SensitivityWatchEventModifier;

public class WatchServiceFileSystemWatcher implements Runnable, AutoCloseable {

    private static final AtomicInteger threadIdCounter = new AtomicInteger(0);
    private static final int WAIT_TIME = Integer.getInteger("fakereplace.wait-time", 500);
    private static final String THREAD_NAME = "fakereplace-file-watcher";

    private WatchService watchService;
    private final Map files = Collections.synchronizedMap(new HashMap<>());
    private final Map pathDataByKey = Collections.synchronizedMap(new IdentityHashMap<>());

    private volatile boolean stopped = false;
    private final Thread watchThread;

    public WatchServiceFileSystemWatcher() {
        try {
            watchService = FileSystems.getDefault().newWatchService();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        watchThread = new Thread(this, THREAD_NAME + threadIdCounter);
        watchThread.setDaemon(true);
        watchThread.start();
    }

    @Override
    public void run() {
        while (!stopped) {
            try {
                final WatchKey key = watchService.take();
                if (key != null) {
                    try {
                        PathData pathData = pathDataByKey.get(key);
                        if (pathData != null) {
                            List> events = new ArrayList<>(key.pollEvents());
                            final List results = new ArrayList<>();
                            List> latest;
                            do {
                                //we need to wait till nothing has changed in 500ms to make sure we have picked up all the changes
                                Thread.sleep(WAIT_TIME);
                                latest = key.pollEvents();
                                events.addAll(latest);
                            } while (!latest.isEmpty());
                            final Set addedFiles = new HashSet<>();
                            final Set deletedFiles = new HashSet<>();
                            for (WatchEvent event : events) {
                                Path eventPath = (Path) event.context();
                                Path targetFile = ((Path) key.watchable()).resolve(eventPath);
                                FileChangeEvent.Type type;

                                if (event.kind() == ENTRY_CREATE) {
                                    type = FileChangeEvent.Type.ADDED;
                                    addedFiles.add(targetFile);
                                    if (Files.isDirectory(targetFile)) {
                                        try {
                                            addWatchedDirectory(pathData, targetFile);
                                        } catch (IOException e) {
                                            e.printStackTrace();
                                        }
                                    }
                                } else if (event.kind() == ENTRY_MODIFY) {
                                    type = FileChangeEvent.Type.MODIFIED;
                                } else if (event.kind() == ENTRY_DELETE) {
                                    type = FileChangeEvent.Type.REMOVED;
                                    deletedFiles.add(targetFile);
                                } else {
                                    continue;
                                }
                                results.add(new FileChangeEvent(targetFile, type));
                            }
                            key.pollEvents().clear();

                            //now we need to prune the results, to remove duplicates
                            //e.g. if the file is modified after creation we only want to
                            //show the create event
                            final List newEvents = new ArrayList<>();
                            Iterator it = results.iterator();
                            while (it.hasNext()) {
                                FileChangeEvent event = it.next();
                                boolean added = addedFiles.contains(event.getFile());
                                boolean deleted = deletedFiles.contains(event.getFile());
                                if (event.getType() == FileChangeEvent.Type.MODIFIED) {
                                    if (added || deleted) {
                                        it.remove();
                                    }
                                } else if (event.getType() == FileChangeEvent.Type.ADDED) {
                                    if (deleted) {
                                        it.remove();
                                        newEvents.add(new FileChangeEvent(event.getFile(), FileChangeEvent.Type.MODIFIED)); //if it was both deleted and added it was modified
                                    }
                                } else if (event.getType() == FileChangeEvent.Type.REMOVED) {
                                    if (added) {
                                        it.remove();
                                    }
                                }
                            }
                            results.addAll(newEvents);

                            if (!results.isEmpty()) {
                                for (FileChangeCallback callback : pathData.callbacks) {
                                    invokeCallback(callback, results);
                                }
                            }
                        }
                    } finally {
                        //if the key is no longer valid remove it from the files list
                        if (!key.reset()) {
                            files.remove(key.watchable());
                        }
                    }
                }
            } catch (InterruptedException e) {
                //ignore
            } catch (ClosedWatchServiceException cwse) {
                // the watcher service is closed, so no more waiting on events
                // @see https://developer.jboss.org/message/911519
                break;
            }
        }
    }

    public synchronized void watchPath(Path path, FileChangeCallback callback) {
        try {
            PathData data = files.get(path);
            if (data == null) {
                Set allDirectories = doScan(path).keySet();
                data = new PathData(path);
                for (Path dir : allDirectories) {
                    addWatchedDirectory(data, dir);
                }
                files.put(path, data);
            }
            data.callbacks.add(callback);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    private void addWatchedDirectory(PathData data, Path dir) throws IOException {
        WatchKey key = dir.register(watchService, new WatchEvent.Kind[] {ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY}, SensitivityWatchEventModifier.HIGH);
        pathDataByKey.put(key, data);
        data.keys.add(key);
    }

    public synchronized void unwatchPath(Path file, final FileChangeCallback callback) {
        PathData data = files.get(file);
        if (data != null) {
            data.callbacks.remove(callback);
            if (data.callbacks.isEmpty()) {
                files.remove(file);
                for (WatchKey key : data.keys) {
                    key.cancel();
                    pathDataByKey.remove(key);
                }

            }
        }
    }

    @Override
    public void close() throws IOException {
        this.stopped = true;
        watchThread.interrupt();
        if (watchService != null) {
            watchService.close();
        }
    }


    private static Map doScan(Path file) throws IOException {
        final Map results = new HashMap<>();

        final Deque toScan = new ArrayDeque<>();
        toScan.add(file);
        while (!toScan.isEmpty()) {
            Path next = toScan.pop();
            if (Files.isDirectory(next)) {
                results.put(next, Files.getLastModifiedTime(next).toMillis());
                Stream list = Files.list(next);
                if (list != null) {
                    list.forEach((toScan::push));
                }
            }
        }
        return results;
    }

    private static void invokeCallback(FileChangeCallback callback, List results) {
        try {
            callback.handleChanges(results);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private class PathData {
        final Path path;
        final List callbacks = new ArrayList<>();
        final List keys = new ArrayList<>();

        private PathData(Path path) {
            this.path = path;
        }
    }


    /**
     * The event object that is fired when a file system change is detected.
     *
     * @author Stuart Douglas
     */
    public static class FileChangeEvent {

        private final Path file;
        private final Type type;

        /**
         * Construct a new instance.
         *
         * @param file the file which is being watched
         * @param type the type of event that was encountered
         */
        public FileChangeEvent(Path file, Type type) {
            this.file = file;
            this.type = type;
        }

        /**
         * Get the file which was being watched.
         *
         * @return the file which was being watched
         */
        public Path getFile() {
            return file;
        }

        /**
         * Get the type of event.
         *
         * @return the type of event
         */
        public Type getType() {
            return type;
        }

        /**
         * Watched file event types.  More may be added in the future.
         */
        public enum Type {
            /**
             * A file was added in a directory.
             */
            ADDED,
            /**
             * A file was removed from a directory.
             */
            REMOVED,
            /**
             * A file was modified in a directory.
             */
            MODIFIED,
        }

    }

    /**
     * Callback for file system change events
     *
     * @author Stuart Douglas
     */
    public interface FileChangeCallback {

        /**
         * Method that is invoked when file system changes are detected.
         *
         * @param changes the file system changes
         */
        void handleChanges(final Collection changes);

    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy