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 1415850 2012-11-30 20:51:39Z ggregory $
120  * @since 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(final 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(final String directoryName, final 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(final String directoryName, final FileFilter fileFilter, final 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(final 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(final File directory, final 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(final File directory, final FileFilter fileFilter, final 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(final FileEntry rootEntry, final FileFilter fileFilter, final 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      * Return the fileFilter.
228      *
229      * @return the fileFilter
230      * @since 2.1
231      */
232     public FileFilter getFileFilter() {
233         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         if (listener != null) {
243             listeners.add(listener);
244         }
245     }
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         if (listener != null) {
254             while (listeners.remove(listener)) {
255             }
256         }
257     }
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         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         rootEntry.refresh(rootEntry.getFile());
275         final File[] files = listFiles(rootEntry.getFile());
276         final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
277         for (int i = 0; i < files.length; i++) {
278             children[i] = createFileEntry(rootEntry, files[i]);
279         }
280         rootEntry.setChildren(children);
281     }
282 
283     /**
284      * Final processing.
285      *
286      * @throws Exception if an error occurs
287      */
288     public void destroy() throws Exception {
289     }
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         for (final FileAlterationListener listener : listeners) {
298             listener.onStart(this);
299         }
300 
301         /* fire directory/file events */
302         final File rootFile = rootEntry.getFile();
303         if (rootFile.exists()) {
304             checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
305         } else if (rootEntry.isExists()) {
306             checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
307         } else {
308             // Didn't exist and still doesn't
309         }
310 
311         /* fire onStop() */
312         for (final FileAlterationListener listener : listeners) {
313             listener.onStop(this);
314         }
315     }
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         int c = 0;
326         final FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
327         for (final FileEntry entry : previous) {
328             while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
329                 current[c] = createFileEntry(parent, files[c]);
330                 doCreate(current[c]);
331                 c++;
332             }
333             if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
334                 doMatch(entry, files[c]);
335                 checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
336                 current[c] = entry;
337                 c++;
338             } else {
339                 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
340                 doDelete(entry);
341             }
342         }
343         for (; c < files.length; c++) {
344             current[c] = createFileEntry(parent, files[c]);
345             doCreate(current[c]);
346         }
347         parent.setChildren(current);
348     }
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         final FileEntry entry = parent.newChildInstance(file);
359         entry.refresh(file);
360         final File[] files = listFiles(file);
361         final FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
362         for (int i = 0; i < files.length; i++) {
363             children[i] = createFileEntry(entry, files[i]);
364         }
365         entry.setChildren(children);
366         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         for (final FileAlterationListener listener : listeners) {
376             if (entry.isDirectory()) {
377                 listener.onDirectoryCreate(entry.getFile());
378             } else {
379                 listener.onFileCreate(entry.getFile());
380             }
381         }
382         final FileEntry[] children = entry.getChildren();
383         for (final FileEntry aChildren : children) {
384             doCreate(aChildren);
385         }
386     }
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         if (entry.refresh(file)) {
396             for (final FileAlterationListener listener : listeners) {
397                 if (entry.isDirectory()) {
398                     listener.onDirectoryChange(file);
399                 } else {
400                     listener.onFileChange(file);
401                 }
402             }
403         }
404     }
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         for (final FileAlterationListener listener : listeners) {
413             if (entry.isDirectory()) {
414                 listener.onDirectoryDelete(entry.getFile());
415             } else {
416                 listener.onFileDelete(entry.getFile());
417             }
418         }
419     }
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         File[] children = null;
430         if (file.isDirectory()) {
431             children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
432         }
433         if (children == null) {
434             children = FileUtils.EMPTY_FILE_ARRAY;
435         }
436         if (comparator != null && children.length > 1) {
437             Arrays.sort(children, comparator);
438         }
439         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         final StringBuilder builder = new StringBuilder();
450         builder.append(getClass().getSimpleName());
451         builder.append("[file='");
452         builder.append(getDirectory().getPath());
453         builder.append('\'');
454         if (fileFilter != null) {
455             builder.append(", ");
456             builder.append(fileFilter.toString());
457         }
458         builder.append(", listeners=");
459         builder.append(listeners.size());
460         builder.append("]");
461         return builder.toString();
462     }
463 
464 }