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 */ 017 package org.apache.commons.io.monitor; 018 019 import java.io.File; 020 import java.io.FileFilter; 021 import java.io.Serializable; 022 import java.util.Arrays; 023 import java.util.Comparator; 024 import java.util.List; 025 import java.util.concurrent.CopyOnWriteArrayList; 026 027 import org.apache.commons.io.FileUtils; 028 import org.apache.commons.io.IOCase; 029 import org.apache.commons.io.comparator.NameFileComparator; 030 031 /** 032 * FileAlterationObserver represents the state of files below a root directory, 033 * checking the filesystem 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 * // intialize 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 oberver(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="http://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 * @version $Id: FileAlterationObserver.java 1022803 2010-10-15 01:09:39Z niallp $ 120 * @since Commons IO 2.0 121 */ 122 public class FileAlterationObserver implements Serializable { 123 124 private final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<FileAlterationListener>(); 125 private final FileEntry rootEntry; 126 private final FileFilter fileFilter; 127 private final Comparator<File> comparator; 128 129 /** 130 * Construct an observer for the specified directory. 131 * 132 * @param directoryName the name of the directory to observe 133 */ 134 public FileAlterationObserver(String directoryName) { 135 this(new File(directoryName)); 136 } 137 138 /** 139 * Construct an observer for the specified directory and file filter. 140 * 141 * @param directoryName the name of the directory to observe 142 * @param fileFilter The file filter or null if none 143 */ 144 public FileAlterationObserver(String directoryName, FileFilter fileFilter) { 145 this(new File(directoryName), fileFilter); 146 } 147 148 /** 149 * Construct an observer for the specified directory, file filter and 150 * file comparator. 151 * 152 * @param directoryName the name of the directory to observe 153 * @param fileFilter The file filter or null if none 154 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 155 */ 156 public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) { 157 this(new File(directoryName), fileFilter, caseSensitivity); 158 } 159 160 /** 161 * Construct an observer for the specified directory. 162 * 163 * @param directory the directory to observe 164 */ 165 public FileAlterationObserver(File directory) { 166 this(directory, (FileFilter)null); 167 } 168 169 /** 170 * Construct an observer for the specified directory and file filter. 171 * 172 * @param directory the directory to observe 173 * @param fileFilter The file filter or null if none 174 */ 175 public FileAlterationObserver(File directory, FileFilter fileFilter) { 176 this(directory, fileFilter, (IOCase)null); 177 } 178 179 /** 180 * Construct an observer for the specified directory, file filter and 181 * file comparator. 182 * 183 * @param directory the directory to observe 184 * @param fileFilter The file filter or null if none 185 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 186 */ 187 public FileAlterationObserver(File directory, FileFilter fileFilter, IOCase caseSensitivity) { 188 this(new FileEntry(directory), fileFilter, caseSensitivity); 189 } 190 191 /** 192 * Construct an observer for the specified directory, file filter and 193 * file comparator. 194 * 195 * @param rootEntry the root directory to observe 196 * @param fileFilter The file filter or null if none 197 * @param caseSensitivity what case sensitivity to use comparing file names, null means system sensitive 198 */ 199 protected FileAlterationObserver(FileEntry rootEntry, FileFilter fileFilter, IOCase caseSensitivity) { 200 if (rootEntry == null) { 201 throw new IllegalArgumentException("Root entry is missing"); 202 } 203 if (rootEntry.getFile() == null) { 204 throw new IllegalArgumentException("Root directory is missing"); 205 } 206 this.rootEntry = rootEntry; 207 this.fileFilter = fileFilter; 208 if (caseSensitivity == null || caseSensitivity.equals(IOCase.SYSTEM)) { 209 this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR; 210 } else if (caseSensitivity.equals(IOCase.INSENSITIVE)) { 211 this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR; 212 } else { 213 this.comparator = NameFileComparator.NAME_COMPARATOR; 214 } 215 } 216 217 /** 218 * Return the directory being observed. 219 * 220 * @return the directory being observed 221 */ 222 public File getDirectory() { 223 return rootEntry.getFile(); 224 } 225 226 /** 227 * Add a file system listener. 228 * 229 * @param listener The file system listener 230 */ 231 public void addListener(final FileAlterationListener listener) { 232 if (listener != null) { 233 listeners.add(listener); 234 } 235 } 236 237 /** 238 * Remove a file system listener. 239 * 240 * @param listener The file system listener 241 */ 242 public void removeListener(final FileAlterationListener listener) { 243 if (listener != null) { 244 while (listeners.remove(listener)) { 245 } 246 } 247 } 248 249 /** 250 * Returns the set of registered file system listeners. 251 * 252 * @return The file system listeners 253 */ 254 public Iterable<FileAlterationListener> getListeners() { 255 return listeners; 256 } 257 258 /** 259 * Initialize the observer. 260 * 261 * @throws Exception if an error occurs 262 */ 263 public void initialize() throws Exception { 264 rootEntry.refresh(rootEntry.getFile()); 265 File[] files = listFiles(rootEntry.getFile()); 266 FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 267 for (int i = 0; i < files.length; i++) { 268 children[i] = createFileEntry(rootEntry, files[i]); 269 } 270 rootEntry.setChildren(children); 271 } 272 273 /** 274 * Final processing. 275 * 276 * @throws Exception if an error occurs 277 */ 278 public void destroy() throws Exception { 279 } 280 281 /** 282 * Check whether the file and its chlidren have been created, modified or deleted. 283 */ 284 public void checkAndNotify() { 285 286 /* fire onStart() */ 287 for (FileAlterationListener listener : listeners) { 288 listener.onStart(this); 289 } 290 291 /* fire directory/file events */ 292 File rootFile = rootEntry.getFile(); 293 if (rootFile.exists()) { 294 checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile)); 295 } else if (rootEntry.isExists()) { 296 checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 297 } else { 298 // Didn't exist and still doesn't 299 } 300 301 /* fire onStop() */ 302 for (FileAlterationListener listener : listeners) { 303 listener.onStop(this); 304 } 305 } 306 307 /** 308 * Compare two file lists for files which have been created, modified or deleted. 309 * 310 * @param parent The parent entry 311 * @param previous The original list of files 312 * @param files The current list of files 313 */ 314 private void checkAndNotify(FileEntry parent, FileEntry[] previous, File[] files) { 315 int c = 0; 316 FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 317 for (FileEntry entry : previous) { 318 while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) { 319 current[c] = createFileEntry(parent, files[c]); 320 doCreate(current[c]); 321 c++; 322 } 323 if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) { 324 doMatch(entry, files[c]); 325 checkAndNotify(entry, entry.getChildren(), listFiles(files[c])); 326 current[c] = entry; 327 c++; 328 } else { 329 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY); 330 doDelete(entry); 331 } 332 } 333 for (; c < files.length; c++) { 334 current[c] = createFileEntry(parent, files[c]); 335 doCreate(current[c]); 336 } 337 parent.setChildren(current); 338 } 339 340 /** 341 * Create a new file entry for the specified file. 342 * 343 * @param parent The parent file entry 344 * @param file The file to create an entry for 345 * @return A new file entry 346 */ 347 private FileEntry createFileEntry(FileEntry parent, File file) { 348 FileEntry entry = parent.newChildInstance(file); 349 entry.refresh(file); 350 File[] files = listFiles(file); 351 FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES; 352 for (int i = 0; i < files.length; i++) { 353 children[i] = createFileEntry(entry, files[i]); 354 } 355 entry.setChildren(children); 356 return entry; 357 } 358 359 /** 360 * Fire directory/file created events to the registered listeners. 361 * 362 * @param entry The file entry 363 */ 364 private void doCreate(FileEntry entry) { 365 for (FileAlterationListener listener : listeners) { 366 if (entry.isDirectory()) { 367 listener.onDirectoryCreate(entry.getFile()); 368 } else { 369 listener.onFileCreate(entry.getFile()); 370 } 371 } 372 FileEntry[] children = entry.getChildren(); 373 for (FileEntry aChildren : children) { 374 doCreate(aChildren); 375 } 376 } 377 378 /** 379 * Fire directory/file change events to the registered listeners. 380 * 381 * @param entry The previous file system entry 382 * @param file The current file 383 */ 384 private void doMatch(FileEntry entry, File file) { 385 if (entry.refresh(file)) { 386 for (FileAlterationListener listener : listeners) { 387 if (entry.isDirectory()) { 388 listener.onDirectoryChange(file); 389 } else { 390 listener.onFileChange(file); 391 } 392 } 393 } 394 } 395 396 /** 397 * Fire directory/file delete events to the registered listeners. 398 * 399 * @param entry The file entry 400 */ 401 private void doDelete(FileEntry entry) { 402 for (FileAlterationListener listener : listeners) { 403 if (entry.isDirectory()) { 404 listener.onDirectoryDelete(entry.getFile()); 405 } else { 406 listener.onFileDelete(entry.getFile()); 407 } 408 } 409 } 410 411 /** 412 * List 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(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 * Provide a String representation of this observer. 434 * 435 * @return a String representation of this observer 436 */ 437 @Override 438 public String toString() { 439 StringBuilder builder = new StringBuilder(); 440 builder.append(getClass().getSimpleName()); 441 builder.append("[file='"); 442 builder.append(getDirectory().getPath()); 443 builder.append('\''); 444 if (fileFilter != null) { 445 builder.append(", "); 446 builder.append(fileFilter.toString()); 447 } 448 builder.append(", listeners="); 449 builder.append(listeners.size()); 450 builder.append("]"); 451 return builder.toString(); 452 } 453 454 }