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

org.xnio.nio.WatchServiceFileSystemWatcher Maven / Gradle / Ivy

There is a newer version: 3.8.16.Final
Show newest version
/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2013 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * 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.xnio.nio;

import org.xnio.FileChangeCallback;
import org.xnio.FileChangeEvent;
import org.xnio.FileSystemWatcher;
import org.xnio.IoUtils;

import java.io.File;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
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.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 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 org.xnio.nio.Log.log;

/**
 * File system watcher service based on JDK7 {@link WatchService}. Instantiating this class will create a new thread,
 * that will run until {@link #close()} is called.
 *
 * @author Stuart Douglas
 */
class WatchServiceFileSystemWatcher implements FileSystemWatcher, Runnable {

    private static final AtomicInteger threadIdCounter = new AtomicInteger(0);
    public static final String THREAD_NAME = "xnio-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;

    WatchServiceFileSystemWatcher(final String name, final boolean daemon) {
        try {
            watchService = FileSystems.getDefault().newWatchService();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        watchThread = new Thread(this, THREAD_NAME + "[" + name + "]-" + threadIdCounter);
        watchThread.setDaemon(daemon);
        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) {
                            final List results = new ArrayList();
                            List> events = key.pollEvents();
                            final Set addedFiles = new HashSet();
                            final Set deletedFiles = new HashSet();
                            for (WatchEvent event : events) {
                                Path eventPath = (Path) event.context();
                                File targetFile = ((Path) key.watchable()).resolve(eventPath).toFile();
                                FileChangeEvent.Type type;

                                if (event.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
                                    type = FileChangeEvent.Type.ADDED;
                                    addedFiles.add(targetFile);
                                    if (targetFile.isDirectory()) {
                                        try {
                                            addWatchedDirectory(pathData, targetFile);
                                        } catch (IOException e) {
                                            log.debugf(e, "Could not add watched directory %s", targetFile);
                                        }
                                    }
                                } else if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY) {
                                    type = FileChangeEvent.Type.MODIFIED;
                                } else if (event.kind() == StandardWatchEventKinds.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
                            Iterator it = results.iterator();
                            while (it.hasNext()) {
                                FileChangeEvent event = it.next();
                                if (event.getType() == FileChangeEvent.Type.MODIFIED) {
                                    if (addedFiles.contains(event.getFile()) &&
                                            deletedFiles.contains(event.getFile())) {
                                        // XNIO-344
                                        // All file change events (ADDED, REMOVED and MODIFIED) occurred here.
                                        // This happens when an updated file is moved from the different
                                        // filesystems or the directory having different project quota on Linux.
                                        // ADDED and REMOVED events will be removed in the latter conditional branching.
                                        // So, this MODIFIED event needs to be kept for the file change notification.
                                        continue;
                                    }
                                    if (addedFiles.contains(event.getFile()) ||
                                            deletedFiles.contains(event.getFile())) {
                                        it.remove();
                                    }
                                } else if (event.getType() == FileChangeEvent.Type.ADDED) {
                                    if (deletedFiles.contains(event.getFile())) {
                                        it.remove();
                                    }
                                } else if (event.getType() == FileChangeEvent.Type.REMOVED) {
                                    if (addedFiles.contains(event.getFile())) {
                                        it.remove();
                                    }
                                }
                            }

                            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;
            }
        }
    }

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

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

    private void addWatchedDirectory(PathData data, File dir) throws IOException {
        Path path = Paths.get(dir.toURI());
        WatchKey key = path.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY);
        pathDataByKey.put(key, data);
        data.keys.add(key);
    }

    @Override
    public synchronized void unwatchPath(File 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();
        IoUtils.safeClose(watchService);
    }


    private static Map doScan(File file) {
        final Map results = new HashMap();

        final Deque toScan = new ArrayDeque();
        toScan.add(file);
        while (!toScan.isEmpty()) {
            File next = toScan.pop();
            if (next.isDirectory()) {
                results.put(next, next.lastModified());
                File[] list = next.listFiles();
                if (list != null) {
                    for (File f : list) {
                        toScan.push(new File(f.getAbsolutePath()));
                    }
                }
            }
        }
        return results;
    }

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

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

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


}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy