001/* 002 * Copyright 2012-2017 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.boot.devtools.filewatch; 018 019import java.io.File; 020import java.io.FileFilter; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.LinkedHashMap; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Set; 030import java.util.concurrent.atomic.AtomicInteger; 031 032import org.springframework.util.Assert; 033 034/** 035 * Watches specific folders for file changes. 036 * 037 * @author Andy Clement 038 * @author Phillip Webb 039 * @since 1.3.0 040 * @see FileChangeListener 041 */ 042public class FileSystemWatcher { 043 044 private static final long DEFAULT_POLL_INTERVAL = 1000; 045 046 private static final long DEFAULT_QUIET_PERIOD = 400; 047 048 private final List<FileChangeListener> listeners = new ArrayList<FileChangeListener>(); 049 050 private final boolean daemon; 051 052 private final long pollInterval; 053 054 private final long quietPeriod; 055 056 private final AtomicInteger remainingScans = new AtomicInteger(-1); 057 058 private final Map<File, FolderSnapshot> folders = new HashMap<File, FolderSnapshot>(); 059 060 private Thread watchThread; 061 062 private FileFilter triggerFilter; 063 064 private final Object monitor = new Object(); 065 066 /** 067 * Create a new {@link FileSystemWatcher} instance. 068 */ 069 public FileSystemWatcher() { 070 this(true, DEFAULT_POLL_INTERVAL, DEFAULT_QUIET_PERIOD); 071 } 072 073 /** 074 * Create a new {@link FileSystemWatcher} instance. 075 * @param daemon if a daemon thread used to monitor changes 076 * @param pollInterval the amount of time to wait between checking for changes 077 * @param quietPeriod the amount of time required after a change has been detected to 078 * ensure that updates have completed 079 */ 080 public FileSystemWatcher(boolean daemon, long pollInterval, long quietPeriod) { 081 Assert.isTrue(pollInterval > 0, "PollInterval must be positive"); 082 Assert.isTrue(quietPeriod > 0, "QuietPeriod must be positive"); 083 Assert.isTrue(pollInterval > quietPeriod, 084 "PollInterval must be greater than QuietPeriod"); 085 this.daemon = daemon; 086 this.pollInterval = pollInterval; 087 this.quietPeriod = quietPeriod; 088 } 089 090 /** 091 * Add listener for file change events. Cannot be called after the watcher has been 092 * {@link #start() started}. 093 * @param fileChangeListener the listener to add 094 */ 095 public void addListener(FileChangeListener fileChangeListener) { 096 Assert.notNull(fileChangeListener, "FileChangeListener must not be null"); 097 synchronized (this.monitor) { 098 checkNotStarted(); 099 this.listeners.add(fileChangeListener); 100 } 101 } 102 103 /** 104 * Add source folders to monitor. Cannot be called after the watcher has been 105 * {@link #start() started}. 106 * @param folders the folders to monitor 107 */ 108 public void addSourceFolders(Iterable<File> folders) { 109 Assert.notNull(folders, "Folders must not be null"); 110 synchronized (this.monitor) { 111 for (File folder : folders) { 112 addSourceFolder(folder); 113 } 114 } 115 } 116 117 /** 118 * Add a source folder to monitor. Cannot be called after the watcher has been 119 * {@link #start() started}. 120 * @param folder the folder to monitor 121 */ 122 public void addSourceFolder(File folder) { 123 Assert.notNull(folder, "Folder must not be null"); 124 Assert.isTrue(folder.isDirectory(), 125 "Folder '" + folder + "' must exist and must" + " be a directory"); 126 synchronized (this.monitor) { 127 checkNotStarted(); 128 this.folders.put(folder, null); 129 } 130 } 131 132 /** 133 * Set an optional {@link FileFilter} used to limit the files that trigger a change. 134 * @param triggerFilter a trigger filter or null 135 */ 136 public void setTriggerFilter(FileFilter triggerFilter) { 137 synchronized (this.monitor) { 138 this.triggerFilter = triggerFilter; 139 } 140 } 141 142 private void checkNotStarted() { 143 synchronized (this.monitor) { 144 Assert.state(this.watchThread == null, "FileSystemWatcher already started"); 145 } 146 } 147 148 /** 149 * Start monitoring the source folder for changes. 150 */ 151 public void start() { 152 synchronized (this.monitor) { 153 saveInitialSnapshots(); 154 if (this.watchThread == null) { 155 Map<File, FolderSnapshot> localFolders = new HashMap<File, FolderSnapshot>(); 156 localFolders.putAll(this.folders); 157 this.watchThread = new Thread(new Watcher(this.remainingScans, 158 new ArrayList<FileChangeListener>(this.listeners), 159 this.triggerFilter, this.pollInterval, this.quietPeriod, 160 localFolders)); 161 this.watchThread.setName("File Watcher"); 162 this.watchThread.setDaemon(this.daemon); 163 this.watchThread.start(); 164 } 165 } 166 } 167 168 private void saveInitialSnapshots() { 169 for (File folder : this.folders.keySet()) { 170 this.folders.put(folder, new FolderSnapshot(folder)); 171 } 172 } 173 174 /** 175 * Stop monitoring the source folders. 176 */ 177 public void stop() { 178 stopAfter(0); 179 } 180 181 /** 182 * Stop monitoring the source folders. 183 * @param remainingScans the number of remaining scans 184 */ 185 void stopAfter(int remainingScans) { 186 Thread thread = null; 187 synchronized (this.monitor) { 188 thread = this.watchThread; 189 if (thread != null) { 190 this.remainingScans.set(remainingScans); 191 if (remainingScans <= 0) { 192 thread.interrupt(); 193 } 194 } 195 this.watchThread = null; 196 } 197 if (thread != null && Thread.currentThread() != thread) { 198 try { 199 thread.join(); 200 } 201 catch (InterruptedException ex) { 202 Thread.currentThread().interrupt(); 203 } 204 } 205 } 206 207 private static final class Watcher implements Runnable { 208 209 private final AtomicInteger remainingScans; 210 211 private final List<FileChangeListener> listeners; 212 213 private final FileFilter triggerFilter; 214 215 private final long pollInterval; 216 217 private final long quietPeriod; 218 219 private Map<File, FolderSnapshot> folders; 220 221 private Watcher(AtomicInteger remainingScans, List<FileChangeListener> listeners, 222 FileFilter triggerFilter, long pollInterval, long quietPeriod, 223 Map<File, FolderSnapshot> folders) { 224 this.remainingScans = remainingScans; 225 this.listeners = listeners; 226 this.triggerFilter = triggerFilter; 227 this.pollInterval = pollInterval; 228 this.quietPeriod = quietPeriod; 229 this.folders = folders; 230 } 231 232 @Override 233 public void run() { 234 int remainingScans = this.remainingScans.get(); 235 while (remainingScans > 0 || remainingScans == -1) { 236 try { 237 if (remainingScans > 0) { 238 this.remainingScans.decrementAndGet(); 239 } 240 scan(); 241 } 242 catch (InterruptedException ex) { 243 Thread.currentThread().interrupt(); 244 } 245 remainingScans = this.remainingScans.get(); 246 } 247 }; 248 249 private void scan() throws InterruptedException { 250 Thread.sleep(this.pollInterval - this.quietPeriod); 251 Map<File, FolderSnapshot> previous; 252 Map<File, FolderSnapshot> current = this.folders; 253 do { 254 previous = current; 255 current = getCurrentSnapshots(); 256 Thread.sleep(this.quietPeriod); 257 } 258 while (isDifferent(previous, current)); 259 if (isDifferent(this.folders, current)) { 260 updateSnapshots(current.values()); 261 } 262 } 263 264 private boolean isDifferent(Map<File, FolderSnapshot> previous, 265 Map<File, FolderSnapshot> current) { 266 if (!previous.keySet().equals(current.keySet())) { 267 return true; 268 } 269 for (Map.Entry<File, FolderSnapshot> entry : previous.entrySet()) { 270 FolderSnapshot previousFolder = entry.getValue(); 271 FolderSnapshot currentFolder = current.get(entry.getKey()); 272 if (!previousFolder.equals(currentFolder, this.triggerFilter)) { 273 return true; 274 } 275 } 276 return false; 277 } 278 279 private Map<File, FolderSnapshot> getCurrentSnapshots() { 280 Map<File, FolderSnapshot> snapshots = new LinkedHashMap<File, FolderSnapshot>(); 281 for (File folder : this.folders.keySet()) { 282 snapshots.put(folder, new FolderSnapshot(folder)); 283 } 284 return snapshots; 285 } 286 287 private void updateSnapshots(Collection<FolderSnapshot> snapshots) { 288 Map<File, FolderSnapshot> updated = new LinkedHashMap<File, FolderSnapshot>(); 289 Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>(); 290 for (FolderSnapshot snapshot : snapshots) { 291 FolderSnapshot previous = this.folders.get(snapshot.getFolder()); 292 updated.put(snapshot.getFolder(), snapshot); 293 ChangedFiles changedFiles = previous.getChangedFiles(snapshot, 294 this.triggerFilter); 295 if (!changedFiles.getFiles().isEmpty()) { 296 changeSet.add(changedFiles); 297 } 298 } 299 if (!changeSet.isEmpty()) { 300 fireListeners(Collections.unmodifiableSet(changeSet)); 301 } 302 this.folders = updated; 303 } 304 305 private void fireListeners(Set<ChangedFiles> changeSet) { 306 for (FileChangeListener listener : this.listeners) { 307 listener.onChange(changeSet); 308 } 309 } 310 311 } 312 313}