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}