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