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 public void initialize() throws Exception { 278 rootEntry.refresh(rootEntry.getFile()); 279 final FileEntry[] children = doListFiles(rootEntry.getFile(), rootEntry); 280 rootEntry.setChildren(children); 281 } 282 283 /** 284 * Final processing. 285 * 286 * @throws Exception if an error occurs 287 */ 288 public void destroy() throws Exception { 289 // noop 290 } 291 292 /** 293 * Checks whether the file and its children have been created, modified or deleted. 294 */ 295 public void checkAndNotify() { 296 297 /* fire onStart() */ 298 for (final FileAlterationListener listener : listeners) { 299 listener.onStart(this); 300 } 301 302 /* fire directory/file events */ 303 final File rootFile = rootEntry.getFile(); 304 if (rootFile.exists()) { 305 checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile)); 306 } else if (rootEntry.isExists()) { 307 checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 308 } else { 309 // Didn't exist and still doesn't 310 } 311 312 /* fire onStop() */ 313 for (final FileAlterationListener listener : listeners) { 314 listener.onStop(this); 315 } 316 } 317 318 /** 319 * Compares two file lists for files which have been created, modified or deleted. 320 * 321 * @param parent The parent entry 322 * @param previous The original list of files 323 * @param files The current list of files 324 */ 325 private void checkAndNotify(final FileEntry parent, final FileEntry[] previous, final File[] files) { 326 int c = 0; 327 final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 328 for (final FileEntry entry : previous) { 329 while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) { 330 current[c] = createFileEntry(parent, files[c]); 331 doCreate(current[c]); 332 c++; 333 } 334 if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) { 335 doMatch(entry, files[c]); 336 checkAndNotify(entry, entry.getChildren(), listFiles(files[c])); 337 current[c] = entry; 338 c++; 339 } else { 340 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 341 doDelete(entry); 342 } 343 } 344 for (; c < files.length; c++) { 345 current[c] = createFileEntry(parent, files[c]); 346 doCreate(current[c]); 347 } 348 parent.setChildren(current); 349 } 350 351 /** 352 * Creates a new file entry for the specified file. 353 * 354 * @param parent The parent file entry 355 * @param file The file to create an entry for 356 * @return A new file entry 357 */ 358 private FileEntry createFileEntry(final FileEntry parent, final File file) { 359 final FileEntry entry = parent.newChildInstance(file); 360 entry.refresh(file); 361 final FileEntry[] children = doListFiles(file, entry); 362 entry.setChildren(children); 363 return entry; 364 } 365 366 /** 367 * Lists the files 368 * @param file The file to list files for 369 * @param entry the parent entry 370 * @return The child files 371 */ 372 private FileEntry[] doListFiles(final File file, final FileEntry entry) { 373 final File[] files = listFiles(file); 374 final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 375 for (int i = 0; i < files.length; i++) { 376 children[i] = createFileEntry(entry, files[i]); 377 } 378 return children; 379 } 380 381 /** 382 * Fires directory/file created events to the registered listeners. 383 * 384 * @param entry The file entry 385 */ 386 private void doCreate(final FileEntry entry) { 387 for (final FileAlterationListener listener : listeners) { 388 if (entry.isDirectory()) { 389 listener.onDirectoryCreate(entry.getFile()); 390 } else { 391 listener.onFileCreate(entry.getFile()); 392 } 393 } 394 final FileEntry[] children = entry.getChildren(); 395 for (final FileEntry aChildren : children) { 396 doCreate(aChildren); 397 } 398 } 399 400 /** 401 * Fires directory/file change events to the registered listeners. 402 * 403 * @param entry The previous file system entry 404 * @param file The current file 405 */ 406 private void doMatch(final FileEntry entry, final File file) { 407 if (entry.refresh(file)) { 408 for (final FileAlterationListener listener : listeners) { 409 if (entry.isDirectory()) { 410 listener.onDirectoryChange(file); 411 } else { 412 listener.onFileChange(file); 413 } 414 } 415 } 416 } 417 418 /** 419 * Fires directory/file delete events to the registered listeners. 420 * 421 * @param entry The file entry 422 */ 423 private void doDelete(final FileEntry entry) { 424 for (final FileAlterationListener listener : listeners) { 425 if (entry.isDirectory()) { 426 listener.onDirectoryDelete(entry.getFile()); 427 } else { 428 listener.onFileDelete(entry.getFile()); 429 } 430 } 431 } 432 433 /** 434 * Lists the contents of a directory 435 * 436 * @param file The file to list the contents of 437 * @return the directory contents or a zero length array if 438 * the empty or the file is not a directory 439 */ 440 private File[] listFiles(final File file) { 441 File[] children = null; 442 if (file.isDirectory()) { 443 children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter); 444 } 445 if (children == null) { 446 children = FileUtils.EMPTY_FILE_ARRAY; 447 } 448 if (comparator != null && children.length > 1) { 449 Arrays.sort(children, comparator); 450 } 451 return children; 452 } 453 454 /** 455 * Returns a String representation of this observer. 456 * 457 * @return a String representation of this observer 458 */ 459 @Override 460 public String toString() { 461 final StringBuilder builder = new StringBuilder(); 462 builder.append(getClass().getSimpleName()); 463 builder.append("[file='"); 464 builder.append(getDirectory().getPath()); 465 builder.append('\''); 466 if (fileFilter != null) { 467 builder.append(", "); 468 builder.append(fileFilter.toString()); 469 } 470 builder.append(", listeners="); 471 builder.append(listeners.size()); 472 builder.append("]"); 473 return builder.toString(); 474 } 475 476}