Coverage Report - org.apache.commons.io.monitor.FileAlterationObserver
 
Classes in this File Line Coverage Branch Coverage Complexity
FileAlterationObserver
91%
113/123
85%
60/70
2.609
 
 1  
 /*
 2  
  * Licensed to the Apache Software Foundation (ASF) under one or more
 3  
  * contributor license agreements.  See the NOTICE file distributed with
 4  
  * this work for additional information regarding copyright ownership.
 5  
  * The ASF licenses this file to You under the Apache License, Version 2.0
 6  
  * (the "License"); you may not use this file except in compliance with
 7  
  * the License.  You may obtain a copy of the License at
 8  
  *
 9  
  *      http://www.apache.org/licenses/LICENSE-2.0
 10  
  *
 11  
  * Unless required by applicable law or agreed to in writing, software
 12  
  * distributed under the License is distributed on an "AS IS" BASIS,
 13  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  
  * See the License for the specific language governing permissions and
 15  
  * limitations under the License.
 16  
  */
 17  
 package org.apache.commons.io.monitor;
 18  
 
 19  
 import java.io.File;
 20  
 import java.io.FileFilter;
 21  
 import java.io.Serializable;
 22  
 import java.util.Arrays;
 23  
 import java.util.Comparator;
 24  
 import java.util.List;
 25  
 import java.util.concurrent.CopyOnWriteArrayList;
 26  
 
 27  
 import org.apache.commons.io.FileUtils;
 28  
 import org.apache.commons.io.IOCase;
 29  
 import org.apache.commons.io.comparator.NameFileComparator;
 30  
 
 31  
 /**
 32  
  * FileAlterationObserver represents the state of files below a root directory,
 33  
  * checking the filesystem and notifying listeners of create, change or
 34  
  * delete events.
 35  
  * <p>
 36  
  * To use this implementation:
 37  
  * <ul>
 38  
  *   <li>Create {@link FileAlterationListener} implementation(s) that process
 39  
  *      the file/directory create, change and delete events</li>
 40  
  *   <li>Register the listener(s) with a {@link FileAlterationObserver} for
 41  
  *       the appropriate directory.</li>
 42  
  *   <li>Either register the observer(s) with a {@link FileAlterationMonitor} or
 43  
  *       run manually.</li>
 44  
  * </ul>
 45  
  *
 46  
  * <h2>Basic Usage</h2>
 47  
  * Create a {@link FileAlterationObserver} for the directory and register the listeners:
 48  
  * <pre>
 49  
  *      File directory = new File(new File("."), "src");
 50  
  *      FileAlterationObserver observer = new FileAlterationObserver(directory);
 51  
  *      observer.addListener(...);
 52  
  *      observer.addListener(...);
 53  
  * </pre>
 54  
  * To manually observe a directory, initialize the observer and invoked the
 55  
  * {@link #checkAndNotify()} method as required:
 56  
  * <pre>
 57  
  *      // intialize
 58  
  *      observer.init();
 59  
  *      ...
 60  
  *      // invoke as required
 61  
  *      observer.checkAndNotify();
 62  
  *      ...
 63  
  *      observer.checkAndNotify();
 64  
  *      ...
 65  
  *      // finished
 66  
  *      observer.finish();
 67  
  * </pre>
 68  
  * Alternatively, register the oberver(s) with a {@link FileAlterationMonitor},
 69  
  * which creates a new thread, invoking the observer at the specified interval:
 70  
  * <pre>
 71  
  *      long interval = ...
 72  
  *      FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
 73  
  *      monitor.addObserver(observer);
 74  
  *      monitor.start();
 75  
  *      ...
 76  
  *      monitor.stop();
 77  
  * </pre>
 78  
  *
 79  
  * <h2>File Filters</h2>
 80  
  * This implementation can monitor portions of the file system
 81  
  * by using {@link FileFilter}s to observe only the files and/or directories
 82  
  * that are of interest. This makes it more efficient and reduces the
 83  
  * noise from <i>unwanted</i> file system events.
 84  
  * <p>
 85  
  * <a href="http://commons.apache.org/io/">Commons IO</a> has a good range of
 86  
  * useful, ready made 
 87  
  * <a href="../filefilter/package-summary.html">File Filter</a>
 88  
  * implementations for this purpose.
 89  
  * <p>
 90  
  * For example, to only observe 1) visible directories and 2) files with a ".java" suffix
 91  
  * in a root directory called "src" you could set up a {@link FileAlterationObserver} in the following
 92  
  * way:
 93  
  * <pre>
 94  
  *      // Create a FileFilter
 95  
  *      IOFileFilter directories = FileFilterUtils.and(
 96  
  *                                      FileFilterUtils.directoryFileFilter(),
 97  
  *                                      HiddenFileFilter.VISIBLE);
 98  
  *      IOFileFilter files       = FileFilterUtils.and(
 99  
  *                                      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 1686747 2015-06-21 18:44:49Z krosenvold $
 120  
  * @since 2.0
 121  
  */
 122  
 public class FileAlterationObserver implements Serializable {
 123  
 
 124  
     private static final long serialVersionUID = 1185122225658782848L;
 125  16
     private final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<FileAlterationListener>();
 126  
     private final FileEntry rootEntry;
 127  
     private final FileFilter fileFilter;
 128  
     private final Comparator<File> comparator;
 129  
 
 130  
     /**
 131  
      * Construct 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  2
         this(new File(directoryName));
 137  2
     }
 138  
 
 139  
     /**
 140  
      * Construct 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  0
         this(new File(directoryName), fileFilter);
 147  0
     }
 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  0
         this(new File(directoryName), fileFilter, caseSensitivity);
 160  0
     }
 161  
 
 162  
     /**
 163  
      * Construct an observer for the specified directory.
 164  
      *
 165  
      * @param directory the directory to observe
 166  
      */
 167  
     public FileAlterationObserver(final File directory) {
 168  3
         this(directory, null);
 169  3
     }
 170  
 
 171  
     /**
 172  
      * Construct 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  16
         this(directory, fileFilter, null);
 179  16
     }
 180  
 
 181  
     /**
 182  
      * Construct 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  16
         this(new FileEntry(directory), fileFilter, caseSensitivity);
 191  16
     }
 192  
 
 193  
     /**
 194  
      * Construct 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  16
                                      final IOCase caseSensitivity) {
 203  16
         if (rootEntry == null) {
 204  0
             throw new IllegalArgumentException("Root entry is missing");
 205  
         }
 206  16
         if (rootEntry.getFile() == null) {
 207  0
             throw new IllegalArgumentException("Root directory is missing");
 208  
         }
 209  16
         this.rootEntry = rootEntry;
 210  16
         this.fileFilter = fileFilter;
 211  16
         if (caseSensitivity == null || caseSensitivity.equals(IOCase.SYSTEM)) {
 212  16
             this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR;
 213  0
         } else if (caseSensitivity.equals(IOCase.INSENSITIVE)) {
 214  0
             this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
 215  
         } else {
 216  0
             this.comparator = NameFileComparator.NAME_COMPARATOR;
 217  
         }
 218  16
     }
 219  
 
 220  
     /**
 221  
      * Return the directory being observed.
 222  
      *
 223  
      * @return the directory being observed
 224  
      */
 225  
     public File getDirectory() {
 226  3
         return rootEntry.getFile();
 227  
     }
 228  
 
 229  
     /**
 230  
      * Return the fileFilter.
 231  
      *
 232  
      * @return the fileFilter
 233  
      * @since 2.1
 234  
      */
 235  
     public FileFilter getFileFilter() {
 236  0
         return fileFilter;
 237  
     }
 238  
 
 239  
     /**
 240  
      * Add a file system listener.
 241  
      *
 242  
      * @param listener The file system listener
 243  
      */
 244  
     public void addListener(final FileAlterationListener listener) {
 245  26
         if (listener != null) {
 246  25
             listeners.add(listener);
 247  
         }
 248  26
     }
 249  
 
 250  
     /**
 251  
      * Remove a file system listener.
 252  
      *
 253  
      * @param listener The file system listener
 254  
      */
 255  
     public void removeListener(final FileAlterationListener listener) {
 256  2
         if (listener != null) {
 257  2
             while (listeners.remove(listener)) {
 258  
             }
 259  
         }
 260  2
     }
 261  
 
 262  
     /**
 263  
      * Returns the set of registered file system listeners.
 264  
      *
 265  
      * @return The file system listeners
 266  
      */
 267  
     public Iterable<FileAlterationListener> getListeners() {
 268  4
         return listeners;
 269  
     }
 270  
 
 271  
     /**
 272  
      * Initialize the observer.
 273  
      *
 274  
      * @throws Exception if an error occurs
 275  
      */
 276  
     public void initialize() throws Exception {
 277  14
         rootEntry.refresh(rootEntry.getFile());
 278  14
         final FileEntry[] children = doListFiles(rootEntry.getFile(), rootEntry);
 279  14
         rootEntry.setChildren(children);
 280  14
     }
 281  
 
 282  
     /**
 283  
      * Final processing.
 284  
      *
 285  
      * @throws Exception if an error occurs
 286  
      */
 287  
     public void destroy() throws Exception {
 288  2
     }
 289  
 
 290  
     /**
 291  
      * Check whether the file and its chlidren have been created, modified or deleted.
 292  
      */
 293  
     public void checkAndNotify() {
 294  
 
 295  
         /* fire onStart() */
 296  39
         for (final FileAlterationListener listener : listeners) {
 297  78
             listener.onStart(this);
 298  78
         }
 299  
 
 300  
         /* fire directory/file events */
 301  39
         final File rootFile = rootEntry.getFile();
 302  39
         if (rootFile.exists()) {
 303  38
             checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
 304  1
         } else if (rootEntry.isExists()) {
 305  1
             checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
 306  
         } else {
 307  
             // Didn't exist and still doesn't
 308  
         }
 309  
 
 310  
         /* fire onStop() */
 311  39
         for (final FileAlterationListener listener : listeners) {
 312  78
             listener.onStop(this);
 313  78
         }
 314  39
     }
 315  
 
 316  
     /**
 317  
      * Compare two file lists for files which have been created, modified or deleted.
 318  
      *
 319  
      * @param parent The parent entry
 320  
      * @param previous The original list of files
 321  
      * @param files  The current list of files
 322  
      */
 323  
     private void checkAndNotify(final FileEntry parent, final FileEntry[] previous, final File[] files) {
 324  126
         int c = 0;
 325  126
         final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
 326  213
         for (final FileEntry entry : previous) {
 327  89
             while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
 328  2
                 current[c] = createFileEntry(parent, files[c]);
 329  2
                 doCreate(current[c]);
 330  2
                 c++;
 331  
             }
 332  87
             if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
 333  74
                 doMatch(entry, files[c]);
 334  74
                 checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
 335  74
                 current[c] = entry;
 336  74
                 c++;
 337  
             } else {
 338  13
                 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
 339  13
                 doDelete(entry);
 340  
             }
 341  
         }
 342  146
         for (; c < files.length; c++) {
 343  10
             current[c] = createFileEntry(parent, files[c]);
 344  10
             doCreate(current[c]);
 345  
         }
 346  126
         parent.setChildren(current);
 347  126
     }
 348  
 
 349  
     /**
 350  
      * Create a new file entry for the specified file.
 351  
      *
 352  
      * @param parent The parent file entry
 353  
      * @param file The file to create an entry for
 354  
      * @return A new file entry
 355  
      */
 356  
     private FileEntry createFileEntry(final FileEntry parent, final File file) {
 357  28
         final FileEntry entry = parent.newChildInstance(file);
 358  28
         entry.refresh(file);
 359  28
         final FileEntry[] children = doListFiles(file, entry);
 360  28
         entry.setChildren(children);
 361  28
         return entry;
 362  
     }
 363  
 
 364  
     /**
 365  
      * List the files
 366  
      * @param file The file to list files for
 367  
      * @param entry the parent entry
 368  
      * @return The child files
 369  
      */
 370  
     private FileEntry[] doListFiles(File file, FileEntry entry) {
 371  42
         final File[] files = listFiles(file);
 372  42
         final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
 373  58
         for (int i = 0; i < files.length; i++) {
 374  16
             children[i] = createFileEntry(entry, files[i]);
 375  
         }
 376  42
         return children;
 377  
     }
 378  
 
 379  
     /**
 380  
      * Fire directory/file created events to the registered listeners.
 381  
      *
 382  
      * @param entry The file entry
 383  
      */
 384  
     private void doCreate(final FileEntry entry) {
 385  28
         for (final FileAlterationListener listener : listeners) {
 386  56
             if (entry.isDirectory()) {
 387  12
                 listener.onDirectoryCreate(entry.getFile());
 388  
             } else {
 389  44
                 listener.onFileCreate(entry.getFile());
 390  
             }
 391  56
         }
 392  28
         final FileEntry[] children = entry.getChildren();
 393  44
         for (final FileEntry aChildren : children) {
 394  16
             doCreate(aChildren);
 395  
         }
 396  28
     }
 397  
 
 398  
     /**
 399  
      * Fire directory/file change events to the registered listeners.
 400  
      *
 401  
      * @param entry The previous file system entry
 402  
      * @param file The current file
 403  
      */
 404  
     private void doMatch(final FileEntry entry, final File file) {
 405  74
         if (entry.refresh(file)) {
 406  15
             for (final FileAlterationListener listener : listeners) {
 407  30
                 if (entry.isDirectory()) {
 408  18
                     listener.onDirectoryChange(file);
 409  
                 } else {
 410  12
                     listener.onFileChange(file);
 411  
                 }
 412  30
             }
 413  
         }
 414  74
     }
 415  
 
 416  
     /**
 417  
      * Fire directory/file delete events to the registered listeners.
 418  
      *
 419  
      * @param entry The file entry
 420  
      */
 421  
     private void doDelete(final FileEntry entry) {
 422  13
         for (final FileAlterationListener listener : listeners) {
 423  26
             if (entry.isDirectory()) {
 424  6
                 listener.onDirectoryDelete(entry.getFile());
 425  
             } else {
 426  20
                 listener.onFileDelete(entry.getFile());
 427  
             }
 428  26
         }
 429  13
     }
 430  
 
 431  
     /**
 432  
      * List the contents of a directory
 433  
      *
 434  
      * @param file The file to list the contents of
 435  
      * @return the directory contents or a zero length array if
 436  
      * the empty or the file is not a directory
 437  
      */
 438  
     private File[] listFiles(final File file) {
 439  154
         File[] children = null;
 440  154
         if (file.isDirectory()) {
 441  75
             children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
 442  
         }
 443  154
         if (children == null) {
 444  79
             children = FileUtils.EMPTY_FILE_ARRAY;
 445  
         }
 446  154
         if (comparator != null && children.length > 1) {
 447  21
             Arrays.sort(children, comparator);
 448  
         }
 449  154
         return children;
 450  
     }
 451  
 
 452  
     /**
 453  
      * Provide a String representation of this observer.
 454  
      *
 455  
      * @return a String representation of this observer
 456  
      */
 457  
     @Override
 458  
     public String toString() {
 459  2
         final StringBuilder builder = new StringBuilder();
 460  2
         builder.append(getClass().getSimpleName());
 461  2
         builder.append("[file='");
 462  2
         builder.append(getDirectory().getPath());
 463  2
         builder.append('\'');
 464  2
         if (fileFilter != null) {
 465  1
             builder.append(", ");
 466  1
             builder.append(fileFilter.toString());
 467  
         }
 468  2
         builder.append(", listeners=");
 469  2
         builder.append(listeners.size());
 470  2
         builder.append("]");
 471  2
         return builder.toString();
 472  
     }
 473  
 
 474  
 }