001/*
002 * Stallion Core: A Modern Web Framework
003 *
004 * Copyright (C) 2015 - 2016 Stallion Software LLC.
005 *
006 * This program is free software: you can redistribute it and/or modify it under the terms of the
007 * GNU General Public License as published by the Free Software Foundation, either version 2 of
008 * the License, or (at your option) any later version. This program is distributed in the hope that
009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public
011 * License for more details. You should have received a copy of the GNU General Public License
012 * along with this program.  If not, see <http://www.gnu.org/licenses/gpl-2.0.html>.
013 *
014 *
015 *
016 */
017
018package io.stallion.fileSystem;
019
020import com.sun.nio.file.SensitivityWatchEventModifier;
021import io.stallion.services.Log;
022import org.apache.commons.io.FileUtils;
023import org.apache.commons.io.filefilter.DirectoryFileFilter;
024import org.apache.commons.lang3.StringUtils;
025
026import java.io.File;
027import java.io.FileFilter;
028import java.io.IOException;
029import java.nio.file.*;
030import java.util.Collection;
031import java.util.HashMap;
032import java.util.List;
033import java.util.Map;
034
035import static io.stallion.utils.Literals.list;
036import static io.stallion.utils.Literals.safeLoop;
037import static java.nio.file.StandardWatchEventKinds.*;
038
039/**
040 * A side thread that watches the file system, and responds to file change
041 * events, and calls the registered watch event handler.
042 *
043 *
044 */
045public class FileSystemWatcherRunner implements Runnable {
046    private WatchService watcher;
047    private Boolean shouldRun = true;
048    private Map<String, IWatchEventHandler> watchedByPath = new HashMap<>();
049
050
051    public FileSystemWatcherRunner() {
052        this(false);
053    }
054
055    public FileSystemWatcherRunner(boolean isCodeWatcher) {
056
057        try {
058            watcher = FileSystems.getDefault().newWatchService();
059        } catch (IOException e) {
060            throw new RuntimeException(e);
061        }
062    }
063
064    @Override
065    public void run() {
066        try {
067            doRun();
068        } catch (Exception exc) {
069            System.err.print(exc);
070        }
071        Log.info("FileSystemWatcher run method is complete.");
072    }
073
074    public void registerWatcher(IWatchEventHandler handler) {
075
076        Log.fine("Watch folder {0} handler={1}", handler.getWatchedFolder(), handler.getClass().getSimpleName());
077        registerWatcherForFolder(handler, handler.getWatchedFolder());
078        if (handler.getWatchTree()) {
079            List<File> directories = list(new File(handler.getWatchedFolder()));
080            for(int x: safeLoop(100000)) {
081                if (directories.size() == 0) {
082                    break;
083                }
084                File directory = directories.remove(0);
085                //Collection<File> subdirectories = FileUtils.listFiles(
086                //        directory,
087                //        DirectoryFileFilter.DIRECTORY,
088                //        DirectoryFileFilter.DIRECTORY
089                //);
090                File[] subdirectories = directory.listFiles((FileFilter) DirectoryFileFilter.INSTANCE);
091                if (subdirectories != null && subdirectories.length > 0) {
092                    for (File dir : subdirectories) {
093                        Log.finer("Register recursive watcher: " + dir.getAbsolutePath());
094                        directories.add(dir);
095                        registerWatcherForFolder(handler, dir.getAbsolutePath());
096                    }
097                }
098            }
099        }
100        watchedByPath.put(handler.getWatchedFolder(), handler);
101    }
102
103    private void registerWatcherForFolder(IWatchEventHandler handler, String folder) {
104        Path itemsDir = FileSystems.getDefault().getPath(folder);
105        try {
106            if (new File(itemsDir.toString()).isDirectory()) {
107                itemsDir.register(watcher, new WatchEvent.Kind[]{
108                        StandardWatchEventKinds.ENTRY_MODIFY,
109                        StandardWatchEventKinds.ENTRY_CREATE,
110                        StandardWatchEventKinds.ENTRY_DELETE
111                }, SensitivityWatchEventModifier.HIGH);
112                Log.fine("Folder registered with watcher {0}", folder);
113            }
114        } catch (IOException e) {
115            throw new RuntimeException(e);
116        }
117    }
118
119    private void doRun() {
120        while (shouldRun) {
121            Log.fine("Running the file system watcher.");
122            WatchKey key;
123            try {
124                key = watcher.take();
125            } catch (InterruptedException x) {
126                Log.warn("Interuppted the watcher!!!");
127                try {
128                    Thread.sleep(1000);
129                } catch (InterruptedException e) {
130                    Log.info("Exit watcher run method.");
131                    return;
132                }
133                continue;
134            }
135            Log.fine("Watch event key taken. Runner instance is {0}", this.hashCode());
136
137            for (WatchEvent<?> event : key.pollEvents()) {
138
139                WatchEvent.Kind<?> kind = event.kind();
140                Log.fine("Event is " + kind);
141                // This key is registered only
142                // for ENTRY_CREATE events,
143                // but an OVERFLOW event can
144                // occur regardless if events
145                // are lost or discarded.
146                if (kind == OVERFLOW) {
147                    continue;
148                }
149
150                // The filename is the
151                // context of the event.
152                WatchEvent<Path> ev = (WatchEvent<Path>) event;
153                Path filename = ev.context();
154
155                // Ignore emacs autosave files
156                if (filename.toString().contains(".#")) {
157                    continue;
158                }
159                Log.finer("Changed file is {0}", filename);
160                Path directory = (Path)key.watchable();
161                Log.finer("Changed directory is {0}", directory);
162                Path fullPath = directory.resolve(filename);
163                Log.fine("Changed path is {0}", fullPath);
164                Boolean handlerFound = false;
165                for (IWatchEventHandler handler: watchedByPath.values()) {
166                    Log.finer("Checking matching handler {0} {1}", handler.getInternalHandlerLabel(), handler.getWatchedFolder());
167                    // Ignore private files
168                    if (filename.getFileName().startsWith(".")) {
169                        continue;
170                    }
171                    if ((handler.getWatchedFolder().equals(directory.toAbsolutePath().toString())
172                           || (handler.getWatchTree() && directory.startsWith(handler.getWatchedFolder())))
173                        && (StringUtils.isEmpty(handler.getExtension()) || fullPath.toString().endsWith(handler.getExtension()))
174                    ) {
175                        String relativePath = filename.getFileName().toString();
176                        Log.info("Handling {0} with watcher {1} for folder {2}", filename, handler.getClass().getName(), handler.getWatchedFolder());
177                        try {
178                            handler.handle(relativePath, fullPath.toString(), kind, event);
179                            handlerFound = true;
180                        } catch(Exception e) {
181                            Log.exception(e, "Exception processing path={0} handler={1}", relativePath, handler.getClass().getName());
182                        }
183                    }
184                }
185                if (!handlerFound) {
186                    Log.info("No handler found for {0}", fullPath);
187                }
188            }
189            // Reset the key -- this step is critical if you want to
190            // receive further watch events.  If the key is no longer valid,
191            // the directory is inaccessible so exit the loop.
192            boolean valid = key.reset();
193            if (!valid) {
194                Log.warn("Key invalid! Exit watch.");
195                break;
196            }
197        }
198    }
199
200    public void shutdown() {
201        setShouldRun(false);
202        try {
203            if (watcher != null) {
204                watcher.close();
205            }
206            watcher = null;
207        } catch (IOException e) {
208            throw new RuntimeException(e);
209        }
210        watchedByPath = new HashMap<>();
211    }
212    public Boolean getShouldRun() {
213        return shouldRun;
214    }
215
216    public void setShouldRun(Boolean shouldRun) {
217        this.shouldRun = shouldRun;
218    }
219
220}