View Javadoc
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     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         this(new File(directoryName));
137     }
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         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      * Construct 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      * 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         this(directory, fileFilter, null);
179     }
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         this(new FileEntry(directory), fileFilter, caseSensitivity);
191     }
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                                      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      * Return the directory being observed.
222      *
223      * @return the directory being observed
224      */
225     public File getDirectory() {
226         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         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         if (listener != null) {
246             listeners.add(listener);
247         }
248     }
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         if (listener != null) {
257             while (listeners.remove(listener)) {
258             }
259         }
260     }
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         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         rootEntry.refresh(rootEntry.getFile());
278         final FileEntry[] children = doListFiles(rootEntry.getFile(), rootEntry);
279         rootEntry.setChildren(children);
280     }
281 
282     /**
283      * Final processing.
284      *
285      * @throws Exception if an error occurs
286      */
287     public void destroy() throws Exception {
288     }
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         for (final FileAlterationListener listener : listeners) {
297             listener.onStart(this);
298         }
299 
300         /* fire directory/file events */
301         final File rootFile = rootEntry.getFile();
302         if (rootFile.exists()) {
303             checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
304         } else if (rootEntry.isExists()) {
305             checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
306         } else {
307             // Didn't exist and still doesn't
308         }
309 
310         /* fire onStop() */
311         for (final FileAlterationListener listener : listeners) {
312             listener.onStop(this);
313         }
314     }
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         int c = 0;
325         final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
326         for (final FileEntry entry : previous) {
327             while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
328                 current[c] = createFileEntry(parent, files[c]);
329                 doCreate(current[c]);
330                 c++;
331             }
332             if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
333                 doMatch(entry, files[c]);
334                 checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
335                 current[c] = entry;
336                 c++;
337             } else {
338                 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
339                 doDelete(entry);
340             }
341         }
342         for (; c < files.length; c++) {
343             current[c] = createFileEntry(parent, files[c]);
344             doCreate(current[c]);
345         }
346         parent.setChildren(current);
347     }
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         final FileEntry entry = parent.newChildInstance(file);
358         entry.refresh(file);
359         final FileEntry[] children = doListFiles(file, entry);
360         entry.setChildren(children);
361         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         final File[] files = listFiles(file);
372         final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
373         for (int i = 0; i < files.length; i++) {
374             children[i] = createFileEntry(entry, files[i]);
375         }
376         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         for (final FileAlterationListener listener : listeners) {
386             if (entry.isDirectory()) {
387                 listener.onDirectoryCreate(entry.getFile());
388             } else {
389                 listener.onFileCreate(entry.getFile());
390             }
391         }
392         final FileEntry[] children = entry.getChildren();
393         for (final FileEntry aChildren : children) {
394             doCreate(aChildren);
395         }
396     }
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         if (entry.refresh(file)) {
406             for (final FileAlterationListener listener : listeners) {
407                 if (entry.isDirectory()) {
408                     listener.onDirectoryChange(file);
409                 } else {
410                     listener.onFileChange(file);
411                 }
412             }
413         }
414     }
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         for (final FileAlterationListener listener : listeners) {
423             if (entry.isDirectory()) {
424                 listener.onDirectoryDelete(entry.getFile());
425             } else {
426                 listener.onFileDelete(entry.getFile());
427             }
428         }
429     }
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         File[] children = null;
440         if (file.isDirectory()) {
441             children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
442         }
443         if (children == null) {
444             children = FileUtils.EMPTY_FILE_ARRAY;
445         }
446         if (comparator != null && children.length > 1) {
447             Arrays.sort(children, comparator);
448         }
449         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         final StringBuilder builder = new StringBuilder();
460         builder.append(getClass().getSimpleName());
461         builder.append("[file='");
462         builder.append(getDirectory().getPath());
463         builder.append('\'');
464         if (fileFilter != null) {
465             builder.append(", ");
466             builder.append(fileFilter.toString());
467         }
468         builder.append(", listeners=");
469         builder.append(listeners.size());
470         builder.append("]");
471         return builder.toString();
472     }
473 
474 }