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 */ 017package org.apache.commons.io.monitor; 018 019import java.io.File; 020import java.io.FileFilter; 021import java.io.Serializable; 022import java.util.Arrays; 023import java.util.Comparator; 024import java.util.List; 025import java.util.concurrent.CopyOnWriteArrayList; 026 027import org.apache.commons.io.FileUtils; 028import org.apache.commons.io.IOCase; 029import 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 1686747 2015-06-21 18:44:49Z krosenvold $ 120 * @since 2.0 121 */ 122public class FileAlterationObserver implements Serializable { 123 124 private static final long serialVersionUID = 1185122225658782848L; 125 private final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<FileAlterationListener>(); 126 private final FileEntry rootEntry; 127 private final FileFilter fileFilter; 128 private final Comparator<File> comparator; 129 130 /** 131 * Construct an observer for the specified directory. 132 * 133 * @param directoryName the name of the directory to observe 134 */ 135 public FileAlterationObserver(final String directoryName) { 136 this(new File(directoryName)); 137 } 138 139 /** 140 * Construct an observer for the specified directory and file filter. 141 * 142 * @param directoryName the name of the directory to observe 143 * @param fileFilter The file filter or null if none 144 */ 145 public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) { 146 this(new File(directoryName), fileFilter); 147 } 148 149 /** 150 * Construct an observer for the specified directory, file filter and 151 * file comparator. 152 * 153 * @param directoryName the name of the directory to observe 154 * @param fileFilter The file filter or null if none 155 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 156 */ 157 public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, 158 final IOCase caseSensitivity) { 159 this(new File(directoryName), fileFilter, caseSensitivity); 160 } 161 162 /** 163 * Construct an observer for the specified directory. 164 * 165 * @param directory the directory to observe 166 */ 167 public FileAlterationObserver(final File directory) { 168 this(directory, null); 169 } 170 171 /** 172 * Construct an observer for the specified directory and file filter. 173 * 174 * @param directory the directory to observe 175 * @param fileFilter The file filter or null if none 176 */ 177 public FileAlterationObserver(final File directory, final FileFilter fileFilter) { 178 this(directory, fileFilter, null); 179 } 180 181 /** 182 * Construct an observer for the specified directory, file filter and 183 * file comparator. 184 * 185 * @param directory the directory to observe 186 * @param fileFilter The file filter or null if none 187 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 188 */ 189 public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase caseSensitivity) { 190 this(new FileEntry(directory), fileFilter, caseSensitivity); 191 } 192 193 /** 194 * Construct an observer for the specified directory, file filter and 195 * file comparator. 196 * 197 * @param rootEntry the root directory to observe 198 * @param fileFilter The file filter or null if none 199 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 200 */ 201 protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, 202 final IOCase caseSensitivity) { 203 if (rootEntry == null) { 204 throw new IllegalArgumentException("Root entry is missing"); 205 } 206 if (rootEntry.getFile() == null) { 207 throw new IllegalArgumentException("Root directory is missing"); 208 } 209 this.rootEntry = rootEntry; 210 this.fileFilter = fileFilter; 211 if (caseSensitivity == null || caseSensitivity.equals(IOCase.SYSTEM)) { 212 this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR; 213 } else if (caseSensitivity.equals(IOCase.INSENSITIVE)) { 214 this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR; 215 } else { 216 this.comparator = NameFileComparator.NAME_COMPARATOR; 217 } 218 } 219 220 /** 221 * Return the directory being observed. 222 * 223 * @return the directory being observed 224 */ 225 public File getDirectory() { 226 return rootEntry.getFile(); 227 } 228 229 /** 230 * Return the fileFilter. 231 * 232 * @return the fileFilter 233 * @since 2.1 234 */ 235 public FileFilter getFileFilter() { 236 return fileFilter; 237 } 238 239 /** 240 * Add a file system listener. 241 * 242 * @param listener The file system listener 243 */ 244 public void addListener(final FileAlterationListener listener) { 245 if (listener != null) { 246 listeners.add(listener); 247 } 248 } 249 250 /** 251 * Remove a file system listener. 252 * 253 * @param listener The file system listener 254 */ 255 public void removeListener(final FileAlterationListener listener) { 256 if (listener != null) { 257 while (listeners.remove(listener)) { 258 } 259 } 260 } 261 262 /** 263 * Returns the set of registered file system listeners. 264 * 265 * @return The file system listeners 266 */ 267 public Iterable<FileAlterationListener> getListeners() { 268 return listeners; 269 } 270 271 /** 272 * Initialize the observer. 273 * 274 * @throws Exception if an error occurs 275 */ 276 public void initialize() throws Exception { 277 rootEntry.refresh(rootEntry.getFile()); 278 final FileEntry[] children = doListFiles(rootEntry.getFile(), rootEntry); 279 rootEntry.setChildren(children); 280 } 281 282 /** 283 * Final processing. 284 * 285 * @throws Exception if an error occurs 286 */ 287 public void destroy() throws Exception { 288 } 289 290 /** 291 * Check whether the file and its chlidren have been created, modified or deleted. 292 */ 293 public void checkAndNotify() { 294 295 /* fire onStart() */ 296 for (final FileAlterationListener listener : listeners) { 297 listener.onStart(this); 298 } 299 300 /* fire directory/file events */ 301 final File rootFile = rootEntry.getFile(); 302 if (rootFile.exists()) { 303 checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile)); 304 } else if (rootEntry.isExists()) { 305 checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 306 } else { 307 // Didn't exist and still doesn't 308 } 309 310 /* fire onStop() */ 311 for (final FileAlterationListener listener : listeners) { 312 listener.onStop(this); 313 } 314 } 315 316 /** 317 * Compare two file lists for files which have been created, modified or deleted. 318 * 319 * @param parent The parent entry 320 * @param previous The original list of files 321 * @param files The current list of files 322 */ 323 private void checkAndNotify(final FileEntry parent, final FileEntry[] previous, final File[] files) { 324 int c = 0; 325 final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 326 for (final FileEntry entry : previous) { 327 while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) { 328 current[c] = createFileEntry(parent, files[c]); 329 doCreate(current[c]); 330 c++; 331 } 332 if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) { 333 doMatch(entry, files[c]); 334 checkAndNotify(entry, entry.getChildren(), listFiles(files[c])); 335 current[c] = entry; 336 c++; 337 } else { 338 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 339 doDelete(entry); 340 } 341 } 342 for (; c < files.length; c++) { 343 current[c] = createFileEntry(parent, files[c]); 344 doCreate(current[c]); 345 } 346 parent.setChildren(current); 347 } 348 349 /** 350 * Create a new file entry for the specified file. 351 * 352 * @param parent The parent file entry 353 * @param file The file to create an entry for 354 * @return A new file entry 355 */ 356 private FileEntry createFileEntry(final FileEntry parent, final File file) { 357 final FileEntry entry = parent.newChildInstance(file); 358 entry.refresh(file); 359 final FileEntry[] children = doListFiles(file, entry); 360 entry.setChildren(children); 361 return entry; 362 } 363 364 /** 365 * List the files 366 * @param file The file to list files for 367 * @param entry the parent entry 368 * @return The child files 369 */ 370 private FileEntry[] doListFiles(File file, FileEntry entry) { 371 final File[] files = listFiles(file); 372 final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 373 for (int i = 0; i < files.length; i++) { 374 children[i] = createFileEntry(entry, files[i]); 375 } 376 return children; 377 } 378 379 /** 380 * Fire directory/file created events to the registered listeners. 381 * 382 * @param entry The file entry 383 */ 384 private void doCreate(final FileEntry entry) { 385 for (final FileAlterationListener listener : listeners) { 386 if (entry.isDirectory()) { 387 listener.onDirectoryCreate(entry.getFile()); 388 } else { 389 listener.onFileCreate(entry.getFile()); 390 } 391 } 392 final FileEntry[] children = entry.getChildren(); 393 for (final FileEntry aChildren : children) { 394 doCreate(aChildren); 395 } 396 } 397 398 /** 399 * Fire directory/file change events to the registered listeners. 400 * 401 * @param entry The previous file system entry 402 * @param file The current file 403 */ 404 private void doMatch(final FileEntry entry, final File file) { 405 if (entry.refresh(file)) { 406 for (final FileAlterationListener listener : listeners) { 407 if (entry.isDirectory()) { 408 listener.onDirectoryChange(file); 409 } else { 410 listener.onFileChange(file); 411 } 412 } 413 } 414 } 415 416 /** 417 * Fire directory/file delete events to the registered listeners. 418 * 419 * @param entry The file entry 420 */ 421 private void doDelete(final FileEntry entry) { 422 for (final FileAlterationListener listener : listeners) { 423 if (entry.isDirectory()) { 424 listener.onDirectoryDelete(entry.getFile()); 425 } else { 426 listener.onFileDelete(entry.getFile()); 427 } 428 } 429 } 430 431 /** 432 * List the contents of a directory 433 * 434 * @param file The file to list the contents of 435 * @return the directory contents or a zero length array if 436 * the empty or the file is not a directory 437 */ 438 private File[] listFiles(final File file) { 439 File[] children = null; 440 if (file.isDirectory()) { 441 children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter); 442 } 443 if (children == null) { 444 children = FileUtils.EMPTY_FILE_ARRAY; 445 } 446 if (comparator != null && children.length > 1) { 447 Arrays.sort(children, comparator); 448 } 449 return children; 450 } 451 452 /** 453 * Provide a String representation of this observer. 454 * 455 * @return a String representation of this observer 456 */ 457 @Override 458 public String toString() { 459 final StringBuilder builder = new StringBuilder(); 460 builder.append(getClass().getSimpleName()); 461 builder.append("[file='"); 462 builder.append(getDirectory().getPath()); 463 builder.append('\''); 464 if (fileFilter != null) { 465 builder.append(", "); 466 builder.append(fileFilter.toString()); 467 } 468 builder.append(", listeners="); 469 builder.append(listeners.size()); 470 builder.append("]"); 471 return builder.toString(); 472 } 473 474}