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.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 /** 146 * Gets a new {@link FileAlterationObserver} instance. 147 * 148 * @throws IOException if an I/O error occurs converting to an {@link File} using {@link AbstractOrigin#getFile()}. 149 * @see #getUnchecked() 150 */ 151 @Override 152 public FileAlterationObserver get() throws IOException { 153 return new FileAlterationObserver(rootEntry != null ? rootEntry : new FileEntry(checkOrigin().getFile()), fileFilter, toComparator(ioCase)); 154 } 155 156 /** 157 * Sets the file filter or null if none. 158 * 159 * @param fileFilter file filter or null if none. 160 * @return This instance. 161 */ 162 public Builder setFileFilter(final FileFilter fileFilter) { 163 this.fileFilter = fileFilter; 164 return asThis(); 165 } 166 167 /** 168 * Sets what case sensitivity to use comparing file names, null means system sensitive. 169 * 170 * @param ioCase what case sensitivity to use comparing file names, null means system sensitive. 171 * @return This instance. 172 */ 173 public Builder setIOCase(final IOCase ioCase) { 174 this.ioCase = ioCase; 175 return asThis(); 176 } 177 178 /** 179 * Sets the root directory to observe. 180 * 181 * @param rootEntry the root directory to observe. 182 * @return This instance. 183 */ 184 public Builder setRootEntry(final FileEntry rootEntry) { 185 this.rootEntry = rootEntry; 186 return asThis(); 187 } 188 189 } 190 191 private static final long serialVersionUID = 1185122225658782848L; 192 193 /** 194 * Creates a new builder. 195 * 196 * @return a new builder. 197 * @since 2.18.0 198 */ 199 public static Builder builder() { 200 return new Builder(); 201 } 202 203 private static Comparator<File> toComparator(final IOCase ioCase) { 204 switch (IOCase.value(ioCase, IOCase.SYSTEM)) { 205 case SYSTEM: 206 return NameFileComparator.NAME_SYSTEM_COMPARATOR; 207 case INSENSITIVE: 208 return NameFileComparator.NAME_INSENSITIVE_COMPARATOR; 209 default: 210 return NameFileComparator.NAME_COMPARATOR; 211 } 212 } 213 214 /** 215 * List of listeners. 216 */ 217 private final transient List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>(); 218 219 /** 220 * The root directory to observe. 221 */ 222 private final FileEntry rootEntry; 223 224 /** 225 * The file filter or null if none. 226 */ 227 private final transient FileFilter fileFilter; 228 229 /** 230 * Compares file names. 231 */ 232 private final Comparator<File> comparator; 233 234 /** 235 * Constructs an observer for the specified directory. 236 * 237 * @param directory the directory to observe. 238 * @deprecated Use {@link #builder()}. 239 */ 240 @Deprecated 241 public FileAlterationObserver(final File directory) { 242 this(directory, null); 243 } 244 245 /** 246 * Constructs an observer for the specified directory and file filter. 247 * 248 * @param directory The directory to observe. 249 * @param fileFilter The file filter or null if none. 250 * @deprecated Use {@link #builder()}. 251 */ 252 @Deprecated 253 public FileAlterationObserver(final File directory, final FileFilter fileFilter) { 254 this(directory, fileFilter, null); 255 } 256 257 /** 258 * Constructs an observer for the specified directory, file filter and file comparator. 259 * 260 * @param directory The directory to observe. 261 * @param fileFilter The file filter or null if none. 262 * @param ioCase What case sensitivity to use comparing file names, null means system sensitive. 263 * @deprecated Use {@link #builder()}. 264 */ 265 @Deprecated 266 public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase ioCase) { 267 this(new FileEntry(directory), fileFilter, ioCase); 268 } 269 270 /** 271 * Constructs an observer for the specified directory, file filter and file comparator. 272 * 273 * @param rootEntry The root directory to observe. 274 * @param fileFilter The file filter or null if none. 275 * @param comparator How to compare files. 276 */ 277 private FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final Comparator<File> comparator) { 278 Objects.requireNonNull(rootEntry, "rootEntry"); 279 Objects.requireNonNull(rootEntry.getFile(), "rootEntry.getFile()"); 280 this.rootEntry = rootEntry; 281 this.fileFilter = fileFilter != null ? fileFilter : TrueFileFilter.INSTANCE; 282 this.comparator = Objects.requireNonNull(comparator, "comparator"); 283 } 284 285 /** 286 * Constructs an observer for the specified directory, file filter and file comparator. 287 * 288 * @param rootEntry The root directory to observe. 289 * @param fileFilter The file filter or null if none. 290 * @param ioCase What case sensitivity to use comparing file names, null means system sensitive. 291 */ 292 protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase ioCase) { 293 this(rootEntry, fileFilter, toComparator(ioCase)); 294 } 295 296 /** 297 * Constructs an observer for the specified directory. 298 * 299 * @param directoryName the name of the directory to observe. 300 * @deprecated Use {@link #builder()}. 301 */ 302 @Deprecated 303 public FileAlterationObserver(final String directoryName) { 304 this(new File(directoryName)); 305 } 306 307 /** 308 * Constructs an observer for the specified directory and file filter. 309 * 310 * @param directoryName the name of the directory to observe. 311 * @param fileFilter The file filter or null if none. 312 * @deprecated Use {@link #builder()}. 313 */ 314 @Deprecated 315 public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) { 316 this(new File(directoryName), fileFilter); 317 } 318 319 /** 320 * Constructs an observer for the specified directory, file filter and file comparator. 321 * 322 * @param directoryName the name of the directory to observe. 323 * @param fileFilter The file filter or null if none. 324 * @param ioCase what case sensitivity to use comparing file names, null means system sensitive. 325 * @deprecated Use {@link #builder()}. 326 */ 327 @Deprecated 328 public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase ioCase) { 329 this(new File(directoryName), fileFilter, ioCase); 330 } 331 332 /** 333 * Adds a file system listener. 334 * 335 * @param listener The file system listener. 336 */ 337 public void addListener(final FileAlterationListener listener) { 338 if (listener != null) { 339 listeners.add(listener); 340 } 341 } 342 343 /** 344 * Compares two file lists for files which have been created, modified or deleted. 345 * 346 * @param parentEntry The parent entry. 347 * @param previousEntries The original list of file entries. 348 * @param currentEntries The current list of files entries. 349 */ 350 private void checkAndFire(final FileEntry parentEntry, final FileEntry[] previousEntries, final File[] currentEntries) { 351 int c = 0; 352 final FileEntry[] actualEntries = currentEntries.length > 0 ? new FileEntry[currentEntries.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY; 353 for (final FileEntry previousEntry : previousEntries) { 354 while (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) > 0) { 355 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]); 356 fireOnCreate(actualEntries[c]); 357 c++; 358 } 359 if (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) == 0) { 360 fireOnChange(previousEntry, currentEntries[c]); 361 checkAndFire(previousEntry, previousEntry.getChildren(), listFiles(currentEntries[c])); 362 actualEntries[c] = previousEntry; 363 c++; 364 } else { 365 checkAndFire(previousEntry, previousEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 366 fireOnDelete(previousEntry); 367 } 368 } 369 for (; c < currentEntries.length; c++) { 370 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]); 371 fireOnCreate(actualEntries[c]); 372 } 373 parentEntry.setChildren(actualEntries); 374 } 375 376 /** 377 * Checks whether the file and its children have been created, modified or deleted. 378 */ 379 public void checkAndNotify() { 380 381 // fire onStart() 382 listeners.forEach(listener -> listener.onStart(this)); 383 384 // fire directory/file events 385 final File rootFile = rootEntry.getFile(); 386 if (rootFile.exists()) { 387 checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile)); 388 } else if (rootEntry.isExists()) { 389 checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 390 } 391 // Else: Didn't exist and still doesn't 392 393 // fire onStop() 394 listeners.forEach(listener -> listener.onStop(this)); 395 } 396 397 /** 398 * Creates a new file entry for the specified file. 399 * 400 * @param parent The parent file entry. 401 * @param file The file to wrap. 402 * @return A new file entry. 403 */ 404 private FileEntry createFileEntry(final FileEntry parent, final File file) { 405 final FileEntry entry = parent.newChildInstance(file); 406 entry.refresh(file); 407 entry.setChildren(listFileEntries(file, entry)); 408 return entry; 409 } 410 411 /** 412 * Final processing. 413 * 414 * @throws Exception if an error occurs. 415 */ 416 @SuppressWarnings("unused") // Possibly thrown from subclasses. 417 public void destroy() throws Exception { 418 // noop 419 } 420 421 /** 422 * Fires directory/file change events to the registered listeners. 423 * 424 * @param entry The previous file system entry. 425 * @param file The current file. 426 */ 427 private void fireOnChange(final FileEntry entry, final File file) { 428 if (entry.refresh(file)) { 429 listeners.forEach(listener -> { 430 if (entry.isDirectory()) { 431 listener.onDirectoryChange(file); 432 } else { 433 listener.onFileChange(file); 434 } 435 }); 436 } 437 } 438 439 /** 440 * Fires directory/file created events to the registered listeners. 441 * 442 * @param entry The file entry. 443 */ 444 private void fireOnCreate(final FileEntry entry) { 445 listeners.forEach(listener -> { 446 if (entry.isDirectory()) { 447 listener.onDirectoryCreate(entry.getFile()); 448 } else { 449 listener.onFileCreate(entry.getFile()); 450 } 451 }); 452 Stream.of(entry.getChildren()).forEach(this::fireOnCreate); 453 } 454 455 /** 456 * Fires directory/file delete events to the registered listeners. 457 * 458 * @param entry The file entry. 459 */ 460 private void fireOnDelete(final FileEntry entry) { 461 listeners.forEach(listener -> { 462 if (entry.isDirectory()) { 463 listener.onDirectoryDelete(entry.getFile()); 464 } else { 465 listener.onFileDelete(entry.getFile()); 466 } 467 }); 468 } 469 470 Comparator<File> getComparator() { 471 return comparator; 472 } 473 474 /** 475 * Returns the directory being observed. 476 * 477 * @return the directory being observed. 478 */ 479 public File getDirectory() { 480 return rootEntry.getFile(); 481 } 482 483 /** 484 * Returns the fileFilter. 485 * 486 * @return the fileFilter. 487 * @since 2.1 488 */ 489 public FileFilter getFileFilter() { 490 return fileFilter; 491 } 492 493 /** 494 * Returns the set of registered file system listeners. 495 * 496 * @return The file system listeners 497 */ 498 public Iterable<FileAlterationListener> getListeners() { 499 return new ArrayList<>(listeners); 500 } 501 502 /** 503 * Initializes the observer. 504 * 505 * @throws Exception if an error occurs. 506 */ 507 @SuppressWarnings("unused") // Possibly thrown from subclasses. 508 public void initialize() throws Exception { 509 rootEntry.refresh(rootEntry.getFile()); 510 rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry)); 511 } 512 513 /** 514 * Lists the file entries in {@code file}. 515 * 516 * @param file The directory to list. 517 * @param entry the parent entry. 518 * @return The child file entries. 519 */ 520 private FileEntry[] listFileEntries(final File file, final FileEntry entry) { 521 return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new); 522 } 523 524 /** 525 * Lists the contents of a directory. 526 * 527 * @param directory The directory to list. 528 * @return the directory contents or a zero length array if the empty or the file is not a directory 529 */ 530 private File[] listFiles(final File directory) { 531 return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY; 532 } 533 534 /** 535 * Removes a file system listener. 536 * 537 * @param listener The file system listener. 538 */ 539 public void removeListener(final FileAlterationListener listener) { 540 if (listener != null) { 541 listeners.removeIf(listener::equals); 542 } 543 } 544 545 private File[] sort(final File[] files) { 546 if (files == null) { 547 return FileUtils.EMPTY_FILE_ARRAY; 548 } 549 if (files.length > 1) { 550 Arrays.sort(files, comparator); 551 } 552 return files; 553 } 554 555 /** 556 * Returns a String representation of this observer. 557 * 558 * @return a String representation of this observer. 559 */ 560 @Override 561 public String toString() { 562 final StringBuilder builder = new StringBuilder(); 563 builder.append(getClass().getSimpleName()); 564 builder.append("[file='"); 565 builder.append(getDirectory().getPath()); 566 builder.append('\''); 567 builder.append(", "); 568 builder.append(fileFilter.toString()); 569 builder.append(", listeners="); 570 builder.append(listeners.size()); 571 builder.append("]"); 572 return builder.toString(); 573 } 574 575}