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.IOException; 022import java.io.Serializable; 023import java.util.ArrayList; 024import java.util.Arrays; 025import java.util.Comparator; 026import java.util.List; 027import java.util.Objects; 028import java.util.concurrent.CopyOnWriteArrayList; 029import java.util.stream.Stream; 030 031import org.apache.commons.io.FileUtils; 032import org.apache.commons.io.IOCase; 033import org.apache.commons.io.build.AbstractOriginSupplier; 034import org.apache.commons.io.comparator.NameFileComparator; 035import org.apache.commons.io.filefilter.TrueFileFilter; 036 037/** 038 * FileAlterationObserver represents the state of files below a root directory, checking the file system and notifying listeners of create, change or delete 039 * events. 040 * <p> 041 * To use this implementation: 042 * </p> 043 * <ul> 044 * <li>Create {@link FileAlterationListener} implementation(s) that process the file/directory create, change and delete events</li> 045 * <li>Register the listener(s) with a {@link FileAlterationObserver} for the appropriate directory.</li> 046 * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or run manually.</li> 047 * </ul> 048 * <h2>Basic Usage</h2> Create a {@link FileAlterationObserver} for the directory and register the listeners: 049 * 050 * <pre> 051 * File directory = new File(FileUtils.current(), "src"); 052 * FileAlterationObserver observer = new FileAlterationObserver(directory); 053 * observer.addListener(...); 054 * observer.addListener(...); 055 * </pre> 056 * <p> 057 * To manually observe a directory, initialize the observer and invoked the {@link #checkAndNotify()} method as required: 058 * </p> 059 * 060 * <pre> 061 * // initialize 062 * observer.init(); 063 * ... 064 * // invoke as required 065 * observer.checkAndNotify(); 066 * ... 067 * observer.checkAndNotify(); 068 * ... 069 * // finished 070 * observer.finish(); 071 * </pre> 072 * <p> 073 * Alternatively, register the observer(s) with a {@link FileAlterationMonitor}, which creates a new thread, invoking the observer at the specified interval: 074 * </p> 075 * 076 * <pre> 077 * long interval = ... 078 * FileAlterationMonitor monitor = new FileAlterationMonitor(interval); 079 * monitor.addObserver(observer); 080 * monitor.start(); 081 * ... 082 * monitor.stop(); 083 * </pre> 084 * 085 * <h2>File Filters</h2> This implementation can monitor portions of the file system by using {@link FileFilter}s to observe only the files and/or directories 086 * that are of interest. This makes it more efficient and reduces the noise from <em>unwanted</em> file system events. 087 * <p> 088 * <a href="https://commons.apache.org/io/">Commons IO</a> has a good range of useful, ready-made <a href="../filefilter/package-summary.html">File Filter</a> 089 * implementations for this purpose. 090 * </p> 091 * <p> 092 * For example, to only observe 1) visible directories and 2) files with a ".java" suffix in a root directory called "src" you could set up a 093 * {@link FileAlterationObserver} in the following way: 094 * </p> 095 * 096 * <pre> 097 * // Create a FileFilter 098 * IOFileFilter directories = FileFilterUtils.and( 099 * FileFilterUtils.directoryFileFilter(), 100 * HiddenFileFilter.VISIBLE); 101 * IOFileFilter files = FileFilterUtils.and( 102 * FileFilterUtils.fileFileFilter(), 103 * FileFilterUtils.suffixFileFilter(".java")); 104 * IOFileFilter filter = FileFilterUtils.or(directories, files); 105 * 106 * // Create the File system observer and register File Listeners 107 * FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter); 108 * observer.addListener(...); 109 * observer.addListener(...); 110 * </pre> 111 * 112 * <h2>FileEntry</h2> 113 * <p> 114 * {@link FileEntry} represents the state of a file or directory, capturing {@link File} attributes at a point in time. Custom implementations of 115 * {@link FileEntry} can be used to capture additional properties that the basic implementation does not support. The {@link FileEntry#refresh(File)} method is 116 * used to determine if a file or directory has changed since the last check and stores the current state of the {@link File}'s properties. 117 * </p> 118 * <h2>Deprecating Serialization</h2> 119 * <p> 120 * <em>Serialization is deprecated and will be removed in 3.0.</em> 121 * </p> 122 * 123 * @see FileAlterationListener 124 * @see FileAlterationMonitor 125 * @since 2.0 126 */ 127public class FileAlterationObserver implements Serializable { 128 129 /** 130 * Builds instances of {@link FileAlterationObserver}. 131 * 132 * @since 2.18.0 133 */ 134 public static final class Builder extends AbstractOriginSupplier<FileAlterationObserver, Builder> { 135 136 private FileEntry rootEntry; 137 private FileFilter fileFilter; 138 private IOCase ioCase; 139 140 private Builder() { 141 // empty 142 } 143 144 /** 145 * Gets a new {@link FileAlterationObserver} instance. 146 */ 147 @Override 148 public FileAlterationObserver get() throws IOException { 149 return new FileAlterationObserver(rootEntry != null ? rootEntry : new FileEntry(checkOrigin().getFile()), fileFilter, toComparator(ioCase)); 150 } 151 152 /** 153 * Sets the file filter or null if none. 154 * 155 * @param fileFilter file filter or null if none. 156 * @return This instance. 157 */ 158 public Builder setFileFilter(final FileFilter fileFilter) { 159 this.fileFilter = fileFilter; 160 return asThis(); 161 } 162 163 /** 164 * Sets what case sensitivity to use comparing file names, null means system sensitive. 165 * 166 * @param ioCase what case sensitivity to use comparing file names, null means system sensitive. 167 * @return This instance. 168 */ 169 public Builder setIOCase(final IOCase ioCase) { 170 this.ioCase = ioCase; 171 return asThis(); 172 } 173 174 /** 175 * Sets the root directory to observe. 176 * 177 * @param rootEntry the root directory to observe. 178 * @return This instance. 179 */ 180 public Builder setRootEntry(final FileEntry rootEntry) { 181 this.rootEntry = rootEntry; 182 return asThis(); 183 } 184 185 } 186 187 private static final long serialVersionUID = 1185122225658782848L; 188 189 /** 190 * Creates a new builder. 191 * 192 * @return a new builder. 193 * @since 2.18.0 194 */ 195 public static Builder builder() { 196 return new Builder(); 197 } 198 199 private static Comparator<File> toComparator(final IOCase ioCase) { 200 switch (IOCase.value(ioCase, IOCase.SYSTEM)) { 201 case SYSTEM: 202 return NameFileComparator.NAME_SYSTEM_COMPARATOR; 203 case INSENSITIVE: 204 return NameFileComparator.NAME_INSENSITIVE_COMPARATOR; 205 default: 206 return NameFileComparator.NAME_COMPARATOR; 207 } 208 } 209 210 /** 211 * List of listeners. 212 */ 213 private transient final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>(); 214 215 /** 216 * The root directory to observe. 217 */ 218 private final FileEntry rootEntry; 219 220 /** 221 * The file filter or null if none. 222 */ 223 private transient final FileFilter fileFilter; 224 225 /** 226 * Compares file names. 227 */ 228 private final Comparator<File> comparator; 229 230 /** 231 * Constructs an observer for the specified directory. 232 * 233 * @param directory the directory to observe. 234 * @deprecated Use {@link #builder()}. 235 */ 236 @Deprecated 237 public FileAlterationObserver(final File directory) { 238 this(directory, null); 239 } 240 241 /** 242 * Constructs an observer for the specified directory and file filter. 243 * 244 * @param directory The directory to observe. 245 * @param fileFilter The file filter or null if none. 246 * @deprecated Use {@link #builder()}. 247 */ 248 @Deprecated 249 public FileAlterationObserver(final File directory, final FileFilter fileFilter) { 250 this(directory, fileFilter, null); 251 } 252 253 /** 254 * Constructs an observer for the specified directory, file filter and file comparator. 255 * 256 * @param directory The directory to observe. 257 * @param fileFilter The file filter or null if none. 258 * @param ioCase What case sensitivity to use comparing file names, null means system sensitive. 259 * @deprecated Use {@link #builder()}. 260 */ 261 @Deprecated 262 public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase ioCase) { 263 this(new FileEntry(directory), fileFilter, ioCase); 264 } 265 266 /** 267 * Constructs an observer for the specified directory, file filter and file comparator. 268 * 269 * @param rootEntry The root directory to observe. 270 * @param fileFilter The file filter or null if none. 271 * @param comparator How to compare files. 272 */ 273 private FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final Comparator<File> comparator) { 274 Objects.requireNonNull(rootEntry, "rootEntry"); 275 Objects.requireNonNull(rootEntry.getFile(), "rootEntry.getFile()"); 276 this.rootEntry = rootEntry; 277 this.fileFilter = fileFilter != null ? fileFilter : TrueFileFilter.INSTANCE; 278 this.comparator = Objects.requireNonNull(comparator, "comparator"); 279 } 280 281 /** 282 * Constructs an observer for the specified directory, file filter and file comparator. 283 * 284 * @param rootEntry The root directory to observe. 285 * @param fileFilter The file filter or null if none. 286 * @param ioCase What case sensitivity to use comparing file names, null means system sensitive. 287 */ 288 protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase ioCase) { 289 this(rootEntry, fileFilter, toComparator(ioCase)); 290 } 291 292 /** 293 * Constructs an observer for the specified directory. 294 * 295 * @param directoryName the name of the directory to observe. 296 * @deprecated Use {@link #builder()}. 297 */ 298 @Deprecated 299 public FileAlterationObserver(final String directoryName) { 300 this(new File(directoryName)); 301 } 302 303 /** 304 * Constructs an observer for the specified directory and file filter. 305 * 306 * @param directoryName the name of the directory to observe. 307 * @param fileFilter The file filter or null if none. 308 * @deprecated Use {@link #builder()}. 309 */ 310 @Deprecated 311 public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) { 312 this(new File(directoryName), fileFilter); 313 } 314 315 /** 316 * Constructs an observer for the specified directory, file filter and file comparator. 317 * 318 * @param directoryName the name of the directory to observe. 319 * @param fileFilter The file filter or null if none. 320 * @param ioCase what case sensitivity to use comparing file names, null means system sensitive. 321 * @deprecated Use {@link #builder()}. 322 */ 323 @Deprecated 324 public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase ioCase) { 325 this(new File(directoryName), fileFilter, ioCase); 326 } 327 328 /** 329 * Adds a file system listener. 330 * 331 * @param listener The file system listener. 332 */ 333 public void addListener(final FileAlterationListener listener) { 334 if (listener != null) { 335 listeners.add(listener); 336 } 337 } 338 339 /** 340 * Compares two file lists for files which have been created, modified or deleted. 341 * 342 * @param parentEntry The parent entry. 343 * @param previousEntries The original list of file entries. 344 * @param currentEntries The current list of files entries. 345 */ 346 private void checkAndFire(final FileEntry parentEntry, final FileEntry[] previousEntries, final File[] currentEntries) { 347 int c = 0; 348 final FileEntry[] actualEntries = currentEntries.length > 0 ? new FileEntry[currentEntries.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY; 349 for (final FileEntry previousEntry : previousEntries) { 350 while (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) > 0) { 351 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]); 352 fireOnCreate(actualEntries[c]); 353 c++; 354 } 355 if (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) == 0) { 356 fireOnChange(previousEntry, currentEntries[c]); 357 checkAndFire(previousEntry, previousEntry.getChildren(), listFiles(currentEntries[c])); 358 actualEntries[c] = previousEntry; 359 c++; 360 } else { 361 checkAndFire(previousEntry, previousEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 362 fireOnDelete(previousEntry); 363 } 364 } 365 for (; c < currentEntries.length; c++) { 366 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]); 367 fireOnCreate(actualEntries[c]); 368 } 369 parentEntry.setChildren(actualEntries); 370 } 371 372 /** 373 * Checks whether the file and its children have been created, modified or deleted. 374 */ 375 public void checkAndNotify() { 376 377 // fire onStart() 378 listeners.forEach(listener -> listener.onStart(this)); 379 380 // fire directory/file events 381 final File rootFile = rootEntry.getFile(); 382 if (rootFile.exists()) { 383 checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile)); 384 } else if (rootEntry.isExists()) { 385 checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 386 } 387 // Else: Didn't exist and still doesn't 388 389 // fire onStop() 390 listeners.forEach(listener -> listener.onStop(this)); 391 } 392 393 /** 394 * Creates a new file entry for the specified file. 395 * 396 * @param parent The parent file entry. 397 * @param file The file to wrap. 398 * @return A new file entry. 399 */ 400 private FileEntry createFileEntry(final FileEntry parent, final File file) { 401 final FileEntry entry = parent.newChildInstance(file); 402 entry.refresh(file); 403 entry.setChildren(listFileEntries(file, entry)); 404 return entry; 405 } 406 407 /** 408 * Final processing. 409 * 410 * @throws Exception if an error occurs. 411 */ 412 @SuppressWarnings("unused") // Possibly thrown from subclasses. 413 public void destroy() throws Exception { 414 // noop 415 } 416 417 /** 418 * Fires directory/file change events to the registered listeners. 419 * 420 * @param entry The previous file system entry. 421 * @param file The current file. 422 */ 423 private void fireOnChange(final FileEntry entry, final File file) { 424 if (entry.refresh(file)) { 425 listeners.forEach(listener -> { 426 if (entry.isDirectory()) { 427 listener.onDirectoryChange(file); 428 } else { 429 listener.onFileChange(file); 430 } 431 }); 432 } 433 } 434 435 /** 436 * Fires directory/file created events to the registered listeners. 437 * 438 * @param entry The file entry. 439 */ 440 private void fireOnCreate(final FileEntry entry) { 441 listeners.forEach(listener -> { 442 if (entry.isDirectory()) { 443 listener.onDirectoryCreate(entry.getFile()); 444 } else { 445 listener.onFileCreate(entry.getFile()); 446 } 447 }); 448 Stream.of(entry.getChildren()).forEach(this::fireOnCreate); 449 } 450 451 /** 452 * Fires directory/file delete events to the registered listeners. 453 * 454 * @param entry The file entry. 455 */ 456 private void fireOnDelete(final FileEntry entry) { 457 listeners.forEach(listener -> { 458 if (entry.isDirectory()) { 459 listener.onDirectoryDelete(entry.getFile()); 460 } else { 461 listener.onFileDelete(entry.getFile()); 462 } 463 }); 464 } 465 466 Comparator<File> getComparator() { 467 return comparator; 468 } 469 470 /** 471 * Returns the directory being observed. 472 * 473 * @return the directory being observed. 474 */ 475 public File getDirectory() { 476 return rootEntry.getFile(); 477 } 478 479 /** 480 * Returns the fileFilter. 481 * 482 * @return the fileFilter. 483 * @since 2.1 484 */ 485 public FileFilter getFileFilter() { 486 return fileFilter; 487 } 488 489 /** 490 * Returns the set of registered file system listeners. 491 * 492 * @return The file system listeners 493 */ 494 public Iterable<FileAlterationListener> getListeners() { 495 return new ArrayList<>(listeners); 496 } 497 498 /** 499 * Initializes the observer. 500 * 501 * @throws Exception if an error occurs. 502 */ 503 @SuppressWarnings("unused") // Possibly thrown from subclasses. 504 public void initialize() throws Exception { 505 rootEntry.refresh(rootEntry.getFile()); 506 rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry)); 507 } 508 509 /** 510 * Lists the file entries in {@code file}. 511 * 512 * @param file The directory to list. 513 * @param entry the parent entry. 514 * @return The child file entries. 515 */ 516 private FileEntry[] listFileEntries(final File file, final FileEntry entry) { 517 return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new); 518 } 519 520 /** 521 * Lists the contents of a directory. 522 * 523 * @param directory The directory to list. 524 * @return the directory contents or a zero length array if the empty or the file is not a directory 525 */ 526 private File[] listFiles(final File directory) { 527 return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY; 528 } 529 530 /** 531 * Removes a file system listener. 532 * 533 * @param listener The file system listener. 534 */ 535 public void removeListener(final FileAlterationListener listener) { 536 if (listener != null) { 537 listeners.removeIf(listener::equals); 538 } 539 } 540 541 private File[] sort(final File[] files) { 542 if (files == null) { 543 return FileUtils.EMPTY_FILE_ARRAY; 544 } 545 if (files.length > 1) { 546 Arrays.sort(files, comparator); 547 } 548 return files; 549 } 550 551 /** 552 * Returns a String representation of this observer. 553 * 554 * @return a String representation of this observer. 555 */ 556 @Override 557 public String toString() { 558 final StringBuilder builder = new StringBuilder(); 559 builder.append(getClass().getSimpleName()); 560 builder.append("[file='"); 561 builder.append(getDirectory().getPath()); 562 builder.append('\''); 563 builder.append(", "); 564 builder.append(fileFilter.toString()); 565 builder.append(", listeners="); 566 builder.append(listeners.size()); 567 builder.append("]"); 568 return builder.toString(); 569 } 570 571}