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