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