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.ArrayList;
23  import java.util.Arrays;
24  import java.util.Comparator;
25  import java.util.List;
26  import java.util.Objects;
27  import java.util.concurrent.CopyOnWriteArrayList;
28  import java.util.stream.Stream;
29  
30  import org.apache.commons.io.FileUtils;
31  import org.apache.commons.io.IOCase;
32  import org.apache.commons.io.comparator.NameFileComparator;
33  import org.apache.commons.io.filefilter.TrueFileFilter;
34  
35  /**
36   * FileAlterationObserver represents the state of files below a root directory, checking the file system and notifying listeners of create, change or delete
37   * events.
38   * <p>
39   * To use this implementation:
40   * </p>
41   * <ul>
42   * <li>Create {@link FileAlterationListener} implementation(s) that process the file/directory create, change and delete events</li>
43   * <li>Register the listener(s) with a {@link FileAlterationObserver} for the appropriate directory.</li>
44   * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or run manually.</li>
45   * </ul>
46   * <h2>Basic Usage</h2> Create a {@link FileAlterationObserver} for the directory and register the listeners:
47   * <pre>
48   *      File directory = new File(FileUtils.current(), "src");
49   *      FileAlterationObserver observer = new FileAlterationObserver(directory);
50   *      observer.addListener(...);
51   *      observer.addListener(...);
52   * </pre>
53   * <p>
54   * To manually observe a directory, initialize the observer and invoked the {@link #checkAndNotify()} method as required:
55   * </p>
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   * <p>
69   * Alternatively, register the observer(s) with a {@link FileAlterationMonitor}, which creates a new thread, invoking the observer at the specified interval:
70   * </p>
71   * <pre>
72   *      long interval = ...
73   *      FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
74   *      monitor.addObserver(observer);
75   *      monitor.start();
76   *      ...
77   *      monitor.stop();
78   * </pre>
79   * <h2>File Filters</h2> This implementation can monitor portions of the file system by using {@link FileFilter}s to observe only the files and/or directories
80   * that are of interest. This makes it more efficient and reduces the noise from <i>unwanted</i> file system events.
81   * <p>
82   * <a href="https://commons.apache.org/io/">Commons IO</a> has a good range of useful, ready-made <a href="../filefilter/package-summary.html">File Filter</a>
83   * implementations for this purpose.
84   * </p>
85   * <p>
86   * For example, to only observe 1) visible directories and 2) files with a ".java" suffix in a root directory called "src" you could set up a
87   * {@link FileAlterationObserver} in the following way:
88   * </p>
89   * <pre>
90   *      // Create a FileFilter
91   *      IOFileFilter directories = FileFilterUtils.and(
92   *                                      FileFilterUtils.directoryFileFilter(),
93   *                                      HiddenFileFilter.VISIBLE);
94   *      IOFileFilter files       = FileFilterUtils.and(
95   *                                      FileFilterUtils.fileFileFilter(),
96   *                                      FileFilterUtils.suffixFileFilter(".java"));
97   *      IOFileFilter filter = FileFilterUtils.or(directories, files);
98   *
99   *      // Create the File system observer and register File Listeners
100  *      FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter);
101  *      observer.addListener(...);
102  *      observer.addListener(...);
103  * </pre>
104  * <h2>FileEntry</h2>
105  * <p>
106  * {@link FileEntry} represents the state of a file or directory, capturing {@link File} attributes at a point in time. Custom
107  * implementations of {@link FileEntry} can be used to capture additional properties that the basic implementation does not support. The
108  * {@link FileEntry#refresh(File)} method is used to determine if a file or directory has changed since the last check and stores the current state of the
109  * {@link File}'s properties.
110  * </p>
111  * <h2>Deprecating Serialization</h2>
112  * <p>
113  * <em>Serialization is deprecated and will be removed in 3.0.</em>
114  * </p>
115  *
116  * @see FileAlterationListener
117  * @see FileAlterationMonitor
118  * @since 2.0
119  */
120 public class FileAlterationObserver implements Serializable {
121 
122     private static final long serialVersionUID = 1185122225658782848L;
123 
124     private static Comparator<File> toComparator(final IOCase ioCase) {
125         switch (IOCase.value(ioCase, IOCase.SYSTEM)) {
126         case SYSTEM:
127             return NameFileComparator.NAME_SYSTEM_COMPARATOR;
128         case INSENSITIVE:
129             return NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
130         default:
131             return NameFileComparator.NAME_COMPARATOR;
132         }
133     }
134 
135     /**
136      * List of listeners.
137      */
138     private transient final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>();
139 
140     /**
141      * The root directory to observe.
142      */
143     private final FileEntry rootEntry;
144 
145     /**
146      * The file filter or null if none.
147      */
148     private transient final FileFilter fileFilter;
149 
150     /**
151      * Compares file names.
152      */
153     private final Comparator<File> comparator;
154 
155     /**
156      * Constructs an observer for the specified directory.
157      *
158      * @param directory the directory to observe.
159      */
160     public FileAlterationObserver(final File directory) {
161         this(directory, null);
162     }
163 
164     /**
165      * Constructs an observer for the specified directory and file filter.
166      *
167      * @param directory  the directory to observe.
168      * @param fileFilter The file filter or null if none.
169      */
170     public FileAlterationObserver(final File directory, final FileFilter fileFilter) {
171         this(directory, fileFilter, null);
172     }
173 
174     /**
175      * Constructs an observer for the specified directory, file filter and file comparator.
176      *
177      * @param directory  the directory to observe.
178      * @param fileFilter The file filter or null if none.
179      * @param ioCase     what case sensitivity to use comparing file names, null means system sensitive.
180      */
181     public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase ioCase) {
182         this(new FileEntry(directory), fileFilter, ioCase);
183     }
184 
185     /**
186      * Constructs an observer for the specified directory, file filter and file comparator.
187      *
188      * @param rootEntry  the root directory to observe.
189      * @param fileFilter The file filter or null if none.
190      * @param comparator how to compare files.
191      */
192     private FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final Comparator<File> comparator) {
193         Objects.requireNonNull(rootEntry, "rootEntry");
194         Objects.requireNonNull(rootEntry.getFile(), "rootEntry.getFile()");
195         this.rootEntry = rootEntry;
196         this.fileFilter = fileFilter != null ? fileFilter : TrueFileFilter.INSTANCE;
197         this.comparator = Objects.requireNonNull(comparator, "comparator");
198     }
199 
200     /**
201      * Constructs an observer for the specified directory, file filter and file comparator.
202      *
203      * @param rootEntry  the root directory to observe.
204      * @param fileFilter The file filter or null if none.
205      * @param ioCase     what case sensitivity to use comparing file names, null means system sensitive.
206      */
207     protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase ioCase) {
208         this(rootEntry, fileFilter, toComparator(ioCase));
209     }
210 
211     /**
212      * Constructs an observer for the specified directory.
213      *
214      * @param directoryName the name of the directory to observe.
215      */
216     public FileAlterationObserver(final String directoryName) {
217         this(new File(directoryName));
218     }
219 
220     /**
221      * Constructs an observer for the specified directory and file filter.
222      *
223      * @param directoryName the name of the directory to observe.
224      * @param fileFilter    The file filter or null if none.
225      */
226     public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) {
227         this(new File(directoryName), fileFilter);
228     }
229 
230     /**
231      * Constructs an observer for the specified directory, file filter and file comparator.
232      *
233      * @param directoryName the name of the directory to observe.
234      * @param fileFilter    The file filter or null if none.
235      * @param ioCase        what case sensitivity to use comparing file names, null means system sensitive.
236      */
237     public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase ioCase) {
238         this(new File(directoryName), fileFilter, ioCase);
239     }
240 
241     /**
242      * Adds a file system listener.
243      *
244      * @param listener The file system listener.
245      */
246     public void addListener(final FileAlterationListener listener) {
247         if (listener != null) {
248             listeners.add(listener);
249         }
250     }
251 
252     /**
253      * Compares two file lists for files which have been created, modified or deleted.
254      *
255      * @param parentEntry     The parent entry.
256      * @param previousEntries The original list of file entries.
257      * @param currentEntries  The current list of files entries.
258      */
259     private void checkAndFire(final FileEntry parentEntry, final FileEntry[] previousEntries, final File[] currentEntries) {
260         int c = 0;
261         final FileEntry[] actualEntries = currentEntries.length > 0 ? new FileEntry[currentEntries.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
262         for (final FileEntry previousEntry : previousEntries) {
263             while (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) > 0) {
264                 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
265                 fireOnCreate(actualEntries[c]);
266                 c++;
267             }
268             if (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) == 0) {
269                 fireOnChange(previousEntry, currentEntries[c]);
270                 checkAndFire(previousEntry, previousEntry.getChildren(), listFiles(currentEntries[c]));
271                 actualEntries[c] = previousEntry;
272                 c++;
273             } else {
274                 checkAndFire(previousEntry, previousEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
275                 fireOnDelete(previousEntry);
276             }
277         }
278         for (; c < currentEntries.length; c++) {
279             actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
280             fireOnCreate(actualEntries[c]);
281         }
282         parentEntry.setChildren(actualEntries);
283     }
284 
285     /**
286      * Checks whether the file and its children have been created, modified or deleted.
287      */
288     public void checkAndNotify() {
289 
290         // fire onStart()
291         listeners.forEach(listener -> listener.onStart(this));
292 
293         // fire directory/file events
294         final File rootFile = rootEntry.getFile();
295         if (rootFile.exists()) {
296             checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
297         } else if (rootEntry.isExists()) {
298             checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
299         }
300         // Else: Didn't exist and still doesn't
301 
302         // fire onStop()
303         listeners.forEach(listener -> listener.onStop(this));
304     }
305 
306     /**
307      * Creates a new file entry for the specified file.
308      *
309      * @param parent The parent file entry.
310      * @param file   The file to wrap.
311      * @return A new file entry.
312      */
313     private FileEntry createFileEntry(final FileEntry parent, final File file) {
314         final FileEntry entry = parent.newChildInstance(file);
315         entry.refresh(file);
316         entry.setChildren(listFileEntries(file, entry));
317         return entry;
318     }
319 
320     /**
321      * Final processing.
322      *
323      * @throws Exception if an error occurs.
324      */
325     @SuppressWarnings("unused") // Possibly thrown from subclasses.
326     public void destroy() throws Exception {
327         // noop
328     }
329 
330     /**
331      * Fires directory/file change events to the registered listeners.
332      *
333      * @param entry The previous file system entry.
334      * @param file  The current file.
335      */
336     private void fireOnChange(final FileEntry entry, final File file) {
337         if (entry.refresh(file)) {
338             listeners.forEach(listener -> {
339                 if (entry.isDirectory()) {
340                     listener.onDirectoryChange(file);
341                 } else {
342                     listener.onFileChange(file);
343                 }
344             });
345         }
346     }
347 
348     /**
349      * Fires directory/file created events to the registered listeners.
350      *
351      * @param entry The file entry.
352      */
353     private void fireOnCreate(final FileEntry entry) {
354         listeners.forEach(listener -> {
355             if (entry.isDirectory()) {
356                 listener.onDirectoryCreate(entry.getFile());
357             } else {
358                 listener.onFileCreate(entry.getFile());
359             }
360         });
361         Stream.of(entry.getChildren()).forEach(this::fireOnCreate);
362     }
363 
364     /**
365      * Fires directory/file delete events to the registered listeners.
366      *
367      * @param entry The file entry.
368      */
369     private void fireOnDelete(final FileEntry entry) {
370         listeners.forEach(listener -> {
371             if (entry.isDirectory()) {
372                 listener.onDirectoryDelete(entry.getFile());
373             } else {
374                 listener.onFileDelete(entry.getFile());
375             }
376         });
377     }
378 
379     /**
380      * Returns the directory being observed.
381      *
382      * @return the directory being observed.
383      */
384     public File getDirectory() {
385         return rootEntry.getFile();
386     }
387 
388     /**
389      * Returns the fileFilter.
390      *
391      * @return the fileFilter.
392      * @since 2.1
393      */
394     public FileFilter getFileFilter() {
395         return fileFilter;
396     }
397 
398     /**
399      * Returns the set of registered file system listeners.
400      *
401      * @return The file system listeners
402      */
403     public Iterable<FileAlterationListener> getListeners() {
404         return new ArrayList<>(listeners);
405     }
406 
407     /**
408      * Initializes the observer.
409      *
410      * @throws Exception if an error occurs.
411      */
412     @SuppressWarnings("unused") // Possibly thrown from subclasses.
413     public void initialize() throws Exception {
414         rootEntry.refresh(rootEntry.getFile());
415         rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry));
416     }
417 
418     /**
419      * Lists the file entries in {@code file}.
420      *
421      * @param file  The directory to list.
422      * @param entry the parent entry.
423      * @return The child file entries.
424      */
425     private FileEntry[] listFileEntries(final File file, final FileEntry entry) {
426         return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new);
427     }
428 
429     /**
430      * Lists the contents of a directory.
431      *
432      * @param directory The directory to list.
433      * @return the directory contents or a zero length array if the empty or the file is not a directory
434      */
435     private File[] listFiles(final File directory) {
436         return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY;
437     }
438 
439     /**
440      * Removes a file system listener.
441      *
442      * @param listener The file system listener.
443      */
444     public void removeListener(final FileAlterationListener listener) {
445         if (listener != null) {
446             listeners.removeIf(listener::equals);
447         }
448     }
449 
450     private File[] sort(final File[] files) {
451         if (files == null) {
452             return FileUtils.EMPTY_FILE_ARRAY;
453         }
454         if (files.length > 1) {
455             Arrays.sort(files, comparator);
456         }
457         return files;
458     }
459 
460     /**
461      * Returns a String representation of this observer.
462      *
463      * @return a String representation of this observer.
464      */
465     @Override
466     public String toString() {
467         final StringBuilder builder = new StringBuilder();
468         builder.append(getClass().getSimpleName());
469         builder.append("[file='");
470         builder.append(getDirectory().getPath());
471         builder.append('\'');
472         builder.append(", ");
473         builder.append(fileFilter.toString());
474         builder.append(", listeners=");
475         builder.append(listeners.size());
476         builder.append("]");
477         return builder.toString();
478     }
479 
480 }