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