001 /* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 package org.apache.commons.io.monitor; 018 019 import java.io.File; 020 import java.io.FileFilter; 021 import java.io.Serializable; 022 import java.util.Arrays; 023 import java.util.Comparator; 024 import java.util.List; 025 import java.util.concurrent.CopyOnWriteArrayList; 026 027 import org.apache.commons.io.FileUtils; 028 import org.apache.commons.io.IOCase; 029 import org.apache.commons.io.comparator.NameFileComparator; 030 031 /** 032 * FileAlterationObserver represents the state of files below a root directory, 033 * checking the filesystem and notifying listeners of create, change or 034 * delete events. 035 * <p> 036 * To use this implementation: 037 * <ul> 038 * <li>Create {@link FileAlterationListener} implementation(s) that process 039 * the file/directory create, change and delete events</li> 040 * <li>Register the listener(s) with a {@link FileAlterationObserver} for 041 * the appropriate directory.</li> 042 * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or 043 * run manually.</li> 044 * </ul> 045 * 046 * <h2>Basic Usage</h2> 047 * Create a {@link FileAlterationObserver} for the directory and register the listeners: 048 * <pre> 049 * File directory = new File(new File("."), "src"); 050 * FileAlterationObserver observer = new FileAlterationObserver(directory); 051 * observer.addListener(...); 052 * observer.addListener(...); 053 * </pre> 054 * To manually observe a directory, initialize the observer and invoked the 055 * {@link #checkAndNotify()} method as required: 056 * <pre> 057 * // intialize 058 * observer.init(); 059 * ... 060 * // invoke as required 061 * observer.checkAndNotify(); 062 * ... 063 * observer.checkAndNotify(); 064 * ... 065 * // finished 066 * observer.finish(); 067 * </pre> 068 * Alternatively, register the oberver(s) with a {@link FileAlterationMonitor}, 069 * which creates a new thread, invoking the observer at the specified interval: 070 * <pre> 071 * long interval = ... 072 * FileAlterationMonitor monitor = new FileAlterationMonitor(interval); 073 * monitor.addObserver(observer); 074 * monitor.start(); 075 * ... 076 * monitor.stop(); 077 * </pre> 078 * 079 * <h2>File Filters</h2> 080 * This implementation can monitor portions of the file system 081 * by using {@link FileFilter}s to observe only the files and/or directories 082 * that are of interest. This makes it more efficient and reduces the 083 * noise from <i>unwanted</i> file system events. 084 * <p> 085 * <a href="http://commons.apache.org/io/">Commons IO</a> has a good range of 086 * useful, ready made 087 * <a href="../filefilter/package-summary.html">File Filter</a> 088 * implementations for this purpose. 089 * <p> 090 * For example, to only observe 1) visible directories and 2) files with a ".java" suffix 091 * in a root directory called "src" you could set up a {@link FileAlterationObserver} in the following 092 * way: 093 * <pre> 094 * // Create a FileFilter 095 * IOFileFilter directories = FileFilterUtils.and( 096 * FileFilterUtils.directoryFileFilter(), 097 * HiddenFileFilter.VISIBLE); 098 * IOFileFilter files = FileFilterUtils.and( 099 * FileFilterUtils.fileFileFilter(), 100 * FileFilterUtils.suffixFileFilter(".java")); 101 * IOFileFilter filter = FileFilterUtils.or(directories, files); 102 * 103 * // Create the File system observer and register File Listeners 104 * FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter); 105 * observer.addListener(...); 106 * observer.addListener(...); 107 * </pre> 108 * 109 * <h2>FileEntry</h2> 110 * {@link FileEntry} represents the state of a file or directory, capturing 111 * {@link File} attributes at a point in time. Custom implementations of 112 * {@link FileEntry} can be used to capture additional properties that the 113 * basic implementation does not support. The {@link FileEntry#refresh(File)} 114 * method is used to determine if a file or directory has changed since the last 115 * check and stores the current state of the {@link File}'s properties. 116 * 117 * @see FileAlterationListener 118 * @see FileAlterationMonitor 119 * @version $Id: FileAlterationObserver.java 1304052 2012-03-22 20:55:29Z ggregory $ 120 * @since 2.0 121 */ 122 public class FileAlterationObserver implements Serializable { 123 124 private final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<FileAlterationListener>(); 125 private final FileEntry rootEntry; 126 private final FileFilter fileFilter; 127 private final Comparator<File> comparator; 128 129 /** 130 * Construct an observer for the specified directory. 131 * 132 * @param directoryName the name of the directory to observe 133 */ 134 public FileAlterationObserver(String directoryName) { 135 this(new File(directoryName)); 136 } 137 138 /** 139 * Construct an observer for the specified directory and file filter. 140 * 141 * @param directoryName the name of the directory to observe 142 * @param fileFilter The file filter or null if none 143 */ 144 public FileAlterationObserver(String directoryName, FileFilter fileFilter) { 145 this(new File(directoryName), fileFilter); 146 } 147 148 /** 149 * Construct an observer for the specified directory, file filter and 150 * file comparator. 151 * 152 * @param directoryName the name of the directory to observe 153 * @param fileFilter The file filter or null if none 154 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 155 */ 156 public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) { 157 this(new File(directoryName), fileFilter, caseSensitivity); 158 } 159 160 /** 161 * Construct an observer for the specified directory. 162 * 163 * @param directory the directory to observe 164 */ 165 public FileAlterationObserver(File directory) { 166 this(directory, (FileFilter)null); 167 } 168 169 /** 170 * Construct an observer for the specified directory and file filter. 171 * 172 * @param directory the directory to observe 173 * @param fileFilter The file filter or null if none 174 */ 175 public FileAlterationObserver(File directory, FileFilter fileFilter) { 176 this(directory, fileFilter, (IOCase)null); 177 } 178 179 /** 180 * Construct an observer for the specified directory, file filter and 181 * file comparator. 182 * 183 * @param directory the directory to observe 184 * @param fileFilter The file filter or null if none 185 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 186 */ 187 public FileAlterationObserver(File directory, FileFilter fileFilter, IOCase caseSensitivity) { 188 this(new FileEntry(directory), fileFilter, caseSensitivity); 189 } 190 191 /** 192 * Construct an observer for the specified directory, file filter and 193 * file comparator. 194 * 195 * @param rootEntry the root directory to observe 196 * @param fileFilter The file filter or null if none 197 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 198 */ 199 protected FileAlterationObserver(FileEntry rootEntry, FileFilter fileFilter, IOCase caseSensitivity) { 200 if (rootEntry == null) { 201 throw new IllegalArgumentException("Root entry is missing"); 202 } 203 if (rootEntry.getFile() == null) { 204 throw new IllegalArgumentException("Root directory is missing"); 205 } 206 this.rootEntry = rootEntry; 207 this.fileFilter = fileFilter; 208 if (caseSensitivity == null || caseSensitivity.equals(IOCase.SYSTEM)) { 209 this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR; 210 } else if (caseSensitivity.equals(IOCase.INSENSITIVE)) { 211 this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR; 212 } else { 213 this.comparator = NameFileComparator.NAME_COMPARATOR; 214 } 215 } 216 217 /** 218 * Return the directory being observed. 219 * 220 * @return the directory being observed 221 */ 222 public File getDirectory() { 223 return rootEntry.getFile(); 224 } 225 226 /** 227 * Return the fileFilter. 228 * 229 * @return the fileFilter 230 * @since 2.1 231 */ 232 public FileFilter getFileFilter() { 233 return fileFilter; 234 } 235 236 /** 237 * Add a file system listener. 238 * 239 * @param listener The file system listener 240 */ 241 public void addListener(final FileAlterationListener listener) { 242 if (listener != null) { 243 listeners.add(listener); 244 } 245 } 246 247 /** 248 * Remove a file system listener. 249 * 250 * @param listener The file system listener 251 */ 252 public void removeListener(final FileAlterationListener listener) { 253 if (listener != null) { 254 while (listeners.remove(listener)) { 255 } 256 } 257 } 258 259 /** 260 * Returns the set of registered file system listeners. 261 * 262 * @return The file system listeners 263 */ 264 public Iterable<FileAlterationListener> getListeners() { 265 return listeners; 266 } 267 268 /** 269 * Initialize the observer. 270 * 271 * @throws Exception if an error occurs 272 */ 273 public void initialize() throws Exception { 274 rootEntry.refresh(rootEntry.getFile()); 275 File[] files = listFiles(rootEntry.getFile()); 276 FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 277 for (int i = 0; i < files.length; i++) { 278 children[i] = createFileEntry(rootEntry, files[i]); 279 } 280 rootEntry.setChildren(children); 281 } 282 283 /** 284 * Final processing. 285 * 286 * @throws Exception if an error occurs 287 */ 288 public void destroy() throws Exception { 289 } 290 291 /** 292 * Check whether the file and its chlidren have been created, modified or deleted. 293 */ 294 public void checkAndNotify() { 295 296 /* fire onStart() */ 297 for (FileAlterationListener listener : listeners) { 298 listener.onStart(this); 299 } 300 301 /* fire directory/file events */ 302 File rootFile = rootEntry.getFile(); 303 if (rootFile.exists()) { 304 checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile)); 305 } else if (rootEntry.isExists()) { 306 checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 307 } else { 308 // Didn't exist and still doesn't 309 } 310 311 /* fire onStop() */ 312 for (FileAlterationListener listener : listeners) { 313 listener.onStop(this); 314 } 315 } 316 317 /** 318 * Compare two file lists for files which have been created, modified or deleted. 319 * 320 * @param parent The parent entry 321 * @param previous The original list of files 322 * @param files The current list of files 323 */ 324 private void checkAndNotify(FileEntry parent, FileEntry[] previous, File[] files) { 325 int c = 0; 326 FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 327 for (FileEntry entry : previous) { 328 while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) { 329 current[c] = createFileEntry(parent, files[c]); 330 doCreate(current[c]); 331 c++; 332 } 333 if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) { 334 doMatch(entry, files[c]); 335 checkAndNotify(entry, entry.getChildren(), listFiles(files[c])); 336 current[c] = entry; 337 c++; 338 } else { 339 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 340 doDelete(entry); 341 } 342 } 343 for (; c < files.length; c++) { 344 current[c] = createFileEntry(parent, files[c]); 345 doCreate(current[c]); 346 } 347 parent.setChildren(current); 348 } 349 350 /** 351 * Create a new file entry for the specified file. 352 * 353 * @param parent The parent file entry 354 * @param file The file to create an entry for 355 * @return A new file entry 356 */ 357 private FileEntry createFileEntry(FileEntry parent, File file) { 358 FileEntry entry = parent.newChildInstance(file); 359 entry.refresh(file); 360 File[] files = listFiles(file); 361 FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 362 for (int i = 0; i < files.length; i++) { 363 children[i] = createFileEntry(entry, files[i]); 364 } 365 entry.setChildren(children); 366 return entry; 367 } 368 369 /** 370 * Fire directory/file created events to the registered listeners. 371 * 372 * @param entry The file entry 373 */ 374 private void doCreate(FileEntry entry) { 375 for (FileAlterationListener listener : listeners) { 376 if (entry.isDirectory()) { 377 listener.onDirectoryCreate(entry.getFile()); 378 } else { 379 listener.onFileCreate(entry.getFile()); 380 } 381 } 382 FileEntry[] children = entry.getChildren(); 383 for (FileEntry aChildren : children) { 384 doCreate(aChildren); 385 } 386 } 387 388 /** 389 * Fire directory/file change events to the registered listeners. 390 * 391 * @param entry The previous file system entry 392 * @param file The current file 393 */ 394 private void doMatch(FileEntry entry, File file) { 395 if (entry.refresh(file)) { 396 for (FileAlterationListener listener : listeners) { 397 if (entry.isDirectory()) { 398 listener.onDirectoryChange(file); 399 } else { 400 listener.onFileChange(file); 401 } 402 } 403 } 404 } 405 406 /** 407 * Fire directory/file delete events to the registered listeners. 408 * 409 * @param entry The file entry 410 */ 411 private void doDelete(FileEntry entry) { 412 for (FileAlterationListener listener : listeners) { 413 if (entry.isDirectory()) { 414 listener.onDirectoryDelete(entry.getFile()); 415 } else { 416 listener.onFileDelete(entry.getFile()); 417 } 418 } 419 } 420 421 /** 422 * List the contents of a directory 423 * 424 * @param file The file to list the contents of 425 * @return the directory contents or a zero length array if 426 * the empty or the file is not a directory 427 */ 428 private File[] listFiles(File file) { 429 File[] children = null; 430 if (file.isDirectory()) { 431 children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter); 432 } 433 if (children == null) { 434 children = FileUtils.EMPTY_FILE_ARRAY; 435 } 436 if (comparator != null && children.length > 1) { 437 Arrays.sort(children, comparator); 438 } 439 return children; 440 } 441 442 /** 443 * Provide a String representation of this observer. 444 * 445 * @return a String representation of this observer 446 */ 447 @Override 448 public String toString() { 449 StringBuilder builder = new StringBuilder(); 450 builder.append(getClass().getSimpleName()); 451 builder.append("[file='"); 452 builder.append(getDirectory().getPath()); 453 builder.append('\''); 454 if (fileFilter != null) { 455 builder.append(", "); 456 builder.append(fileFilter.toString()); 457 } 458 builder.append(", listeners="); 459 builder.append(listeners.size()); 460 builder.append("]"); 461 return builder.toString(); 462 } 463 464 }