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