Coverage Report - org.apache.commons.io.monitor.FileAlterationObserver
 
Classes in this File Line Coverage Branch Coverage Complexity
FileAlterationObserver
91%
113/124
83%
62/74
2.773
 
 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 1415850 2012-11-30 20:51:39Z ggregory $
 120  
  * @since 2.0
 121  
  */
 122  
 public class FileAlterationObserver implements Serializable {
 123  
 
 124  32
     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(final String directoryName) {
 135  4
         this(new File(directoryName));
 136  4
     }
 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(final String directoryName, final FileFilter fileFilter) {
 145  0
         this(new File(directoryName), fileFilter);
 146  0
     }
 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(final String directoryName, final FileFilter fileFilter, final IOCase caseSensitivity) {
 157  0
         this(new File(directoryName), fileFilter, caseSensitivity);
 158  0
     }
 159  
 
 160  
     /**
 161  
      * Construct an observer for the specified directory.
 162  
      *
 163  
      * @param directory the directory to observe
 164  
      */
 165  
     public FileAlterationObserver(final File directory) {
 166  6
         this(directory, (FileFilter)null);
 167  6
     }
 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(final File directory, final FileFilter fileFilter) {
 176  32
         this(directory, fileFilter, (IOCase)null);
 177  32
     }
 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(final File directory, final FileFilter fileFilter, final IOCase caseSensitivity) {
 188  32
         this(new FileEntry(directory), fileFilter, caseSensitivity);
 189  32
     }
 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  32
     protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase caseSensitivity) {
 200  32
         if (rootEntry == null) {
 201  0
             throw new IllegalArgumentException("Root entry is missing");
 202  
         }
 203  32
         if (rootEntry.getFile() == null) {
 204  0
             throw new IllegalArgumentException("Root directory is missing");
 205  
         }
 206  32
         this.rootEntry = rootEntry;
 207  32
         this.fileFilter = fileFilter;
 208  32
         if (caseSensitivity == null || caseSensitivity.equals(IOCase.SYSTEM)) {
 209  32
             this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR;
 210  0
         } else if (caseSensitivity.equals(IOCase.INSENSITIVE)) {
 211  0
             this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
 212  
         } else {
 213  0
             this.comparator = NameFileComparator.NAME_COMPARATOR;
 214  
         }
 215  32
     }
 216  
 
 217  
     /**
 218  
      * Return the directory being observed.
 219  
      *
 220  
      * @return the directory being observed
 221  
      */
 222  
     public File getDirectory() {
 223  6
         return rootEntry.getFile();
 224  
     }
 225  
 
 226  
     /**
 227  
      * Return the fileFilter.
 228  
      *
 229  
      * @return the fileFilter
 230  
      * @since 2.1
 231  
      */
 232  
     public FileFilter getFileFilter() {
 233  0
         return fileFilter;
 234  
     }
 235  
 
 236  
     /**
 237  
      * Add a file system listener.
 238  
      *
 239  
      * @param listener The file system listener
 240  
      */
 241  
     public void addListener(final FileAlterationListener listener) {
 242  52
         if (listener != null) {
 243  50
             listeners.add(listener);
 244  
         }
 245  52
     }
 246  
 
 247  
     /**
 248  
      * Remove a file system listener.
 249  
      *
 250  
      * @param listener The file system listener
 251  
      */
 252  
     public void removeListener(final FileAlterationListener listener) {
 253  4
         if (listener != null) {
 254  4
             while (listeners.remove(listener)) {
 255  
             }
 256  
         }
 257  4
     }
 258  
 
 259  
     /**
 260  
      * Returns the set of registered file system listeners.
 261  
      *
 262  
      * @return The file system listeners
 263  
      */
 264  
     public Iterable<FileAlterationListener> getListeners() {
 265  8
         return listeners;
 266  
     }
 267  
 
 268  
     /**
 269  
      * Initialize the observer.
 270  
      *
 271  
      * @throws Exception if an error occurs
 272  
      */
 273  
     public void initialize() throws Exception {
 274  28
         rootEntry.refresh(rootEntry.getFile());
 275  28
         final File[] files = listFiles(rootEntry.getFile());
 276  28
         final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
 277  28
         for (int i = 0; i < files.length; i++) {
 278  0
             children[i] = createFileEntry(rootEntry, files[i]);
 279  
         }
 280  28
         rootEntry.setChildren(children);
 281  28
     }
 282  
 
 283  
     /**
 284  
      * Final processing.
 285  
      *
 286  
      * @throws Exception if an error occurs
 287  
      */
 288  
     public void destroy() throws Exception {
 289  4
     }
 290  
 
 291  
     /**
 292  
      * Check whether the file and its chlidren have been created, modified or deleted.
 293  
      */
 294  
     public void checkAndNotify() {
 295  
 
 296  
         /* fire onStart() */
 297  71
         for (final FileAlterationListener listener : listeners) {
 298  142
             listener.onStart(this);
 299  142
         }
 300  
 
 301  
         /* fire directory/file events */
 302  71
         final File rootFile = rootEntry.getFile();
 303  71
         if (rootFile.exists()) {
 304  69
             checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
 305  2
         } else if (rootEntry.isExists()) {
 306  2
             checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
 307  
         } else {
 308  
             // Didn't exist and still doesn't
 309  
         }
 310  
 
 311  
         /* fire onStop() */
 312  71
         for (final FileAlterationListener listener : listeners) {
 313  142
             listener.onStop(this);
 314  142
         }
 315  71
     }
 316  
 
 317  
     /**
 318  
      * Compare two file lists for files which have been created, modified or deleted.
 319  
      *
 320  
      * @param parent The parent entry
 321  
      * @param previous The original list of files
 322  
      * @param files  The current list of files
 323  
      */
 324  
     private void checkAndNotify(final FileEntry parent, final FileEntry[] previous, final File[] files) {
 325  239
         int c = 0;
 326  239
         final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
 327  407
         for (final FileEntry entry : previous) {
 328  172
             while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
 329  4
                 current[c] = createFileEntry(parent, files[c]);
 330  4
                 doCreate(current[c]);
 331  4
                 c++;
 332  
             }
 333  168
             if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
 334  142
                 doMatch(entry, files[c]);
 335  142
                 checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
 336  142
                 current[c] = entry;
 337  142
                 c++;
 338  
             } else {
 339  26
                 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
 340  26
                 doDelete(entry);
 341  
             }
 342  
         }
 343  279
         for (; c < files.length; c++) {
 344  20
             current[c] = createFileEntry(parent, files[c]);
 345  20
             doCreate(current[c]);
 346  
         }
 347  239
         parent.setChildren(current);
 348  239
     }
 349  
 
 350  
     /**
 351  
      * Create a new file entry for the specified file.
 352  
      *
 353  
      * @param parent The parent file entry
 354  
      * @param file The file to create an entry for
 355  
      * @return A new file entry
 356  
      */
 357  
     private FileEntry createFileEntry(final FileEntry parent, final File file) {
 358  56
         final FileEntry entry = parent.newChildInstance(file);
 359  56
         entry.refresh(file);
 360  56
         final File[] files = listFiles(file);
 361  56
         final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
 362  88
         for (int i = 0; i < files.length; i++) {
 363  32
             children[i] = createFileEntry(entry, files[i]);
 364  
         }
 365  56
         entry.setChildren(children);
 366  56
         return entry;
 367  
     }
 368  
 
 369  
     /**
 370  
      * Fire directory/file created events to the registered listeners.
 371  
      *
 372  
      * @param entry The file entry
 373  
      */
 374  
     private void doCreate(final FileEntry entry) {
 375  56
         for (final FileAlterationListener listener : listeners) {
 376  112
             if (entry.isDirectory()) {
 377  24
                 listener.onDirectoryCreate(entry.getFile());
 378  
             } else {
 379  88
                 listener.onFileCreate(entry.getFile());
 380  
             }
 381  112
         }
 382  56
         final FileEntry[] children = entry.getChildren();
 383  88
         for (final FileEntry aChildren : children) {
 384  32
             doCreate(aChildren);
 385  
         }
 386  56
     }
 387  
 
 388  
     /**
 389  
      * Fire directory/file change events to the registered listeners.
 390  
      *
 391  
      * @param entry The previous file system entry
 392  
      * @param file The current file
 393  
      */
 394  
     private void doMatch(final FileEntry entry, final File file) {
 395  142
         if (entry.refresh(file)) {
 396  30
             for (final FileAlterationListener listener : listeners) {
 397  60
                 if (entry.isDirectory()) {
 398  36
                     listener.onDirectoryChange(file);
 399  
                 } else {
 400  24
                     listener.onFileChange(file);
 401  
                 }
 402  60
             }
 403  
         }
 404  142
     }
 405  
 
 406  
     /**
 407  
      * Fire directory/file delete events to the registered listeners.
 408  
      *
 409  
      * @param entry The file entry
 410  
      */
 411  
     private void doDelete(final FileEntry entry) {
 412  26
         for (final FileAlterationListener listener : listeners) {
 413  52
             if (entry.isDirectory()) {
 414  12
                 listener.onDirectoryDelete(entry.getFile());
 415  
             } else {
 416  40
                 listener.onFileDelete(entry.getFile());
 417  
             }
 418  52
         }
 419  26
     }
 420  
 
 421  
     /**
 422  
      * List the contents of a directory
 423  
      *
 424  
      * @param file The file to list the contents of
 425  
      * @return the directory contents or a zero length array if
 426  
      * the empty or the file is not a directory
 427  
      */
 428  
     private File[] listFiles(final File file) {
 429  295
         File[] children = null;
 430  295
         if (file.isDirectory()) {
 431  143
             children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
 432  
         }
 433  295
         if (children == null) {
 434  152
             children = FileUtils.EMPTY_FILE_ARRAY;
 435  
         }
 436  295
         if (comparator != null && children.length > 1) {
 437  42
             Arrays.sort(children, comparator);
 438  
         }
 439  295
         return children;
 440  
     }
 441  
 
 442  
     /**
 443  
      * Provide a String representation of this observer.
 444  
      *
 445  
      * @return a String representation of this observer
 446  
      */
 447  
     @Override
 448  
     public String toString() {
 449  4
         final StringBuilder builder = new StringBuilder();
 450  4
         builder.append(getClass().getSimpleName());
 451  4
         builder.append("[file='");
 452  4
         builder.append(getDirectory().getPath());
 453  4
         builder.append('\'');
 454  4
         if (fileFilter != null) {
 455  2
             builder.append(", ");
 456  2
             builder.append(fileFilter.toString());
 457  
         }
 458  4
         builder.append(", listeners=");
 459  4
         builder.append(listeners.size());
 460  4
         builder.append("]");
 461  4
         return builder.toString();
 462  
     }
 463  
 
 464  
 }