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 file system 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   *      // initialize
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 observer(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="https://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  *
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<>();
126     private final FileEntry rootEntry;
127     private final FileFilter fileFilter;
128     private final Comparator<File> comparator;
129 
130     /**
131      * Constructs 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      * Constructs 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      * Constructs 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      * Constructs 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      * Constructs 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      * Constructs 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      * Returns the directory being observed.
222      *
223      * @return the directory being observed
224      */
225     public File getDirectory() {
226         return rootEntry.getFile();
227     }
228 
229     /**
230      * Returns the fileFilter.
231      *
232      * @return the fileFilter
233      * @since 2.1
234      */
235     public FileFilter getFileFilter() {
236         return fileFilter;
237     }
238 
239     /**
240      * Adds 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      * Removes 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                 // empty
259             }
260         }
261     }
262 
263     /**
264      * Returns the set of registered file system listeners.
265      *
266      * @return The file system listeners
267      */
268     public Iterable<FileAlterationListener> getListeners() {
269         return listeners;
270     }
271 
272     /**
273      * Initializes the observer.
274      *
275      * @throws Exception if an error occurs
276      */
277     @SuppressWarnings("unused") // Possibly thrown from subclasses.
278     public void initialize() throws Exception {
279         rootEntry.refresh(rootEntry.getFile());
280         final FileEntry[] children = doListFiles(rootEntry.getFile(), rootEntry);
281         rootEntry.setChildren(children);
282     }
283 
284     /**
285      * Final processing.
286      *
287      * @throws Exception if an error occurs
288      */
289     @SuppressWarnings("unused") // Possibly thrown from subclasses.
290     public void destroy() throws Exception {
291         // noop
292     }
293 
294     /**
295      * Checks whether the file and its children have been created, modified or deleted.
296      */
297     public void checkAndNotify() {
298 
299         /* fire onStart() */
300         for (final FileAlterationListener listener : listeners) {
301             listener.onStart(this);
302         }
303 
304         /* fire directory/file events */
305         final File rootFile = rootEntry.getFile();
306         if (rootFile.exists()) {
307             checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
308         } else if (rootEntry.isExists()) {
309             checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
310         } else {
311             // Didn't exist and still doesn't
312         }
313 
314         /* fire onStop() */
315         for (final FileAlterationListener listener : listeners) {
316             listener.onStop(this);
317         }
318     }
319 
320     /**
321      * Compares two file lists for files which have been created, modified or deleted.
322      *
323      * @param parent The parent entry
324      * @param previous The original list of files
325      * @param files  The current list of files
326      */
327     private void checkAndNotify(final FileEntry/FileEntry.html#FileEntry">FileEntry parent, final FileEntry[] previous, final File[] files) {
328         int c = 0;
329         final FileEntryy">FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
330         for (final FileEntry entry : previous) {
331             while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
332                 current[c] = createFileEntry(parent, files[c]);
333                 doCreate(current[c]);
334                 c++;
335             }
336             if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
337                 doMatch(entry, files[c]);
338                 checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
339                 current[c] = entry;
340                 c++;
341             } else {
342                 checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
343                 doDelete(entry);
344             }
345         }
346         for (; c < files.length; c++) {
347             current[c] = createFileEntry(parent, files[c]);
348             doCreate(current[c]);
349         }
350         parent.setChildren(current);
351     }
352 
353     /**
354      * Creates a new file entry for the specified file.
355      *
356      * @param parent The parent file entry
357      * @param file The file to create an entry for
358      * @return A new file entry
359      */
360     private FileEntryry.html#FileEntry">FileEntry createFileEntry(final FileEntry parent, final File file) {
361         final FileEntry entry = parent.newChildInstance(file);
362         entry.refresh(file);
363         final FileEntry[] children = doListFiles(file, entry);
364         entry.setChildren(children);
365         return entry;
366     }
367 
368     /**
369      * Lists the files
370      * @param file The file to list files for
371      * @param entry the parent entry
372      * @return The child files
373      */
374     private FileEntry[] doListFiFileEntryng class="jxr_keyword">final File file, final FileEntry entry) {
375         final File[] files = listFiles(file);
376         final FileEntry">FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
377         for (int i = 0; i < files.length; i++) {
378             children[i] = createFileEntry(entry, files[i]);
379         }
380         return children;
381     }
382 
383     /**
384      * Fires directory/file created events to the registered listeners.
385      *
386      * @param entry The file entry
387      */
388     private void doCreate(final FileEntry entry) {
389         for (final FileAlterationListener listener : listeners) {
390             if (entry.isDirectory()) {
391                 listener.onDirectoryCreate(entry.getFile());
392             } else {
393                 listener.onFileCreate(entry.getFile());
394             }
395         }
396         final FileEntry[] children = entry.getChildren();
397         for (final FileEntry aChildren : children) {
398             doCreate(aChildren);
399         }
400     }
401 
402     /**
403      * Fires directory/file change events to the registered listeners.
404      *
405      * @param entry The previous file system entry
406      * @param file The current file
407      */
408     private void doMatch(final FileEntry entry, final File file) {
409         if (entry.refresh(file)) {
410             for (final FileAlterationListener listener : listeners) {
411                 if (entry.isDirectory()) {
412                     listener.onDirectoryChange(file);
413                 } else {
414                     listener.onFileChange(file);
415                 }
416             }
417         }
418     }
419 
420     /**
421      * Fires directory/file delete events to the registered listeners.
422      *
423      * @param entry The file entry
424      */
425     private void doDelete(final FileEntry entry) {
426         for (final FileAlterationListener listener : listeners) {
427             if (entry.isDirectory()) {
428                 listener.onDirectoryDelete(entry.getFile());
429             } else {
430                 listener.onFileDelete(entry.getFile());
431             }
432         }
433     }
434 
435     /**
436      * Lists the contents of a directory
437      *
438      * @param file The file to list the contents of
439      * @return the directory contents or a zero length array if
440      * the empty or the file is not a directory
441      */
442     private File[] listFiles(final File file) {
443         File[] children = null;
444         if (file.isDirectory()) {
445             children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
446         }
447         if (children == null) {
448             children = FileUtils.EMPTY_FILE_ARRAY;
449         }
450         if (comparator != null && children.length > 1) {
451             Arrays.sort(children, comparator);
452         }
453         return children;
454     }
455 
456     /**
457      * Returns a String representation of this observer.
458      *
459      * @return a String representation of this observer
460      */
461     @Override
462     public String toString() {
463         final StringBuilder builder = new StringBuilder();
464         builder.append(getClass().getSimpleName());
465         builder.append("[file='");
466         builder.append(getDirectory().getPath());
467         builder.append('\'');
468         if (fileFilter != null) {
469             builder.append(", ");
470             builder.append(fileFilter.toString());
471         }
472         builder.append(", listeners=");
473         builder.append(listeners.size());
474         builder.append("]");
475         return builder.toString();
476     }
477 
478 }