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