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.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         /**
146          * Gets a new {@link FileAlterationObserver} instance.
147          *
148          * @throws IOException                   if an I/O error occurs converting to an {@link File} using {@link AbstractOrigin#getFile()}.
149          * @see #getUnchecked()
150          */
151         @Override
152         public FileAlterationObserver get() throws IOException {
153             return new FileAlterationObserver(rootEntry != null ? rootEntry : new FileEntry(checkOrigin().getFile()), fileFilter, toComparator(ioCase));
154         }
155 
156         /**
157          * Sets the file filter or null if none.
158          *
159          * @param fileFilter file filter or null if none.
160          * @return This instance.
161          */
162         public Builder setFileFilter(final FileFilter fileFilter) {
163             this.fileFilter = fileFilter;
164             return asThis();
165         }
166 
167         /**
168          * Sets what case sensitivity to use comparing file names, null means system sensitive.
169          *
170          * @param ioCase what case sensitivity to use comparing file names, null means system sensitive.
171          * @return This instance.
172          */
173         public Builder setIOCase(final IOCase ioCase) {
174             this.ioCase = ioCase;
175             return asThis();
176         }
177 
178         /**
179          * Sets the root directory to observe.
180          *
181          * @param rootEntry the root directory to observe.
182          * @return This instance.
183          */
184         public Builder setRootEntry(final FileEntry rootEntry) {
185             this.rootEntry = rootEntry;
186             return asThis();
187         }
188 
189     }
190 
191     private static final long serialVersionUID = 1185122225658782848L;
192 
193     /**
194      * Creates a new builder.
195      *
196      * @return a new builder.
197      * @since 2.18.0
198      */
199     public static Builder builder() {
200         return new Builder();
201     }
202 
203     private static Comparator<File> toComparator(final IOCase ioCase) {
204         switch (IOCase.value(ioCase, IOCase.SYSTEM)) {
205         case SYSTEM:
206             return NameFileComparator.NAME_SYSTEM_COMPARATOR;
207         case INSENSITIVE:
208             return NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
209         default:
210             return NameFileComparator.NAME_COMPARATOR;
211         }
212     }
213 
214     /**
215      * List of listeners.
216      */
217     private final transient List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>();
218 
219     /**
220      * The root directory to observe.
221      */
222     private final FileEntry rootEntry;
223 
224     /**
225      * The file filter or null if none.
226      */
227     private final transient FileFilter fileFilter;
228 
229     /**
230      * Compares file names.
231      */
232     private final Comparator<File> comparator;
233 
234     /**
235      * Constructs an observer for the specified directory.
236      *
237      * @param directory the directory to observe.
238      * @deprecated Use {@link #builder()}.
239      */
240     @Deprecated
241     public FileAlterationObserver(final File directory) {
242         this(directory, null);
243     }
244 
245     /**
246      * Constructs an observer for the specified directory and file filter.
247      *
248      * @param directory  The directory to observe.
249      * @param fileFilter The file filter or null if none.
250      * @deprecated Use {@link #builder()}.
251      */
252     @Deprecated
253     public FileAlterationObserver(final File directory, final FileFilter fileFilter) {
254         this(directory, fileFilter, null);
255     }
256 
257     /**
258      * Constructs an observer for the specified directory, file filter and file comparator.
259      *
260      * @param directory  The directory to observe.
261      * @param fileFilter The file filter or null if none.
262      * @param ioCase     What case sensitivity to use comparing file names, null means system sensitive.
263      * @deprecated Use {@link #builder()}.
264      */
265     @Deprecated
266     public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase ioCase) {
267         this(new FileEntry(directory), fileFilter, ioCase);
268     }
269 
270     /**
271      * Constructs an observer for the specified directory, file filter and file comparator.
272      *
273      * @param rootEntry  The root directory to observe.
274      * @param fileFilter The file filter or null if none.
275      * @param comparator How to compare files.
276      */
277     private FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final Comparator<File> comparator) {
278         Objects.requireNonNull(rootEntry, "rootEntry");
279         Objects.requireNonNull(rootEntry.getFile(), "rootEntry.getFile()");
280         this.rootEntry = rootEntry;
281         this.fileFilter = fileFilter != null ? fileFilter : TrueFileFilter.INSTANCE;
282         this.comparator = Objects.requireNonNull(comparator, "comparator");
283     }
284 
285     /**
286      * Constructs an observer for the specified directory, file filter and file comparator.
287      *
288      * @param rootEntry  The root directory to observe.
289      * @param fileFilter The file filter or null if none.
290      * @param ioCase     What case sensitivity to use comparing file names, null means system sensitive.
291      */
292     protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase ioCase) {
293         this(rootEntry, fileFilter, toComparator(ioCase));
294     }
295 
296     /**
297      * Constructs an observer for the specified directory.
298      *
299      * @param directoryName the name of the directory to observe.
300      * @deprecated Use {@link #builder()}.
301      */
302     @Deprecated
303     public FileAlterationObserver(final String directoryName) {
304         this(new File(directoryName));
305     }
306 
307     /**
308      * Constructs an observer for the specified directory and file filter.
309      *
310      * @param directoryName the name of the directory to observe.
311      * @param fileFilter    The file filter or null if none.
312      * @deprecated Use {@link #builder()}.
313      */
314     @Deprecated
315     public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) {
316         this(new File(directoryName), fileFilter);
317     }
318 
319     /**
320      * Constructs an observer for the specified directory, file filter and file comparator.
321      *
322      * @param directoryName the name of the directory to observe.
323      * @param fileFilter    The file filter or null if none.
324      * @param ioCase        what case sensitivity to use comparing file names, null means system sensitive.
325      * @deprecated Use {@link #builder()}.
326      */
327     @Deprecated
328     public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase ioCase) {
329         this(new File(directoryName), fileFilter, ioCase);
330     }
331 
332     /**
333      * Adds a file system listener.
334      *
335      * @param listener The file system listener.
336      */
337     public void addListener(final FileAlterationListener listener) {
338         if (listener != null) {
339             listeners.add(listener);
340         }
341     }
342 
343     /**
344      * Compares two file lists for files which have been created, modified or deleted.
345      *
346      * @param parentEntry     The parent entry.
347      * @param previousEntries The original list of file entries.
348      * @param currentEntries  The current list of files entries.
349      */
350     private void checkAndFire(final FileEntry parentEntry, final FileEntry[] previousEntries, final File[] currentEntries) {
351         int c = 0;
352         final FileEntry[] actualEntries = currentEntries.length > 0 ? new FileEntry[currentEntries.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
353         for (final FileEntry previousEntry : previousEntries) {
354             while (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) > 0) {
355                 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
356                 fireOnCreate(actualEntries[c]);
357                 c++;
358             }
359             if (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) == 0) {
360                 fireOnChange(previousEntry, currentEntries[c]);
361                 checkAndFire(previousEntry, previousEntry.getChildren(), listFiles(currentEntries[c]));
362                 actualEntries[c] = previousEntry;
363                 c++;
364             } else {
365                 checkAndFire(previousEntry, previousEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
366                 fireOnDelete(previousEntry);
367             }
368         }
369         for (; c < currentEntries.length; c++) {
370             actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
371             fireOnCreate(actualEntries[c]);
372         }
373         parentEntry.setChildren(actualEntries);
374     }
375 
376     /**
377      * Checks whether the file and its children have been created, modified or deleted.
378      */
379     public void checkAndNotify() {
380 
381         // fire onStart()
382         listeners.forEach(listener -> listener.onStart(this));
383 
384         // fire directory/file events
385         final File rootFile = rootEntry.getFile();
386         if (rootFile.exists()) {
387             checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
388         } else if (rootEntry.isExists()) {
389             checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
390         }
391         // Else: Didn't exist and still doesn't
392 
393         // fire onStop()
394         listeners.forEach(listener -> listener.onStop(this));
395     }
396 
397     /**
398      * Creates a new file entry for the specified file.
399      *
400      * @param parent The parent file entry.
401      * @param file   The file to wrap.
402      * @return A new file entry.
403      */
404     private FileEntry createFileEntry(final FileEntry parent, final File file) {
405         final FileEntry entry = parent.newChildInstance(file);
406         entry.refresh(file);
407         entry.setChildren(listFileEntries(file, entry));
408         return entry;
409     }
410 
411     /**
412      * Final processing.
413      *
414      * @throws Exception if an error occurs.
415      */
416     @SuppressWarnings("unused") // Possibly thrown from subclasses.
417     public void destroy() throws Exception {
418         // noop
419     }
420 
421     /**
422      * Fires directory/file change events to the registered listeners.
423      *
424      * @param entry The previous file system entry.
425      * @param file  The current file.
426      */
427     private void fireOnChange(final FileEntry entry, final File file) {
428         if (entry.refresh(file)) {
429             listeners.forEach(listener -> {
430                 if (entry.isDirectory()) {
431                     listener.onDirectoryChange(file);
432                 } else {
433                     listener.onFileChange(file);
434                 }
435             });
436         }
437     }
438 
439     /**
440      * Fires directory/file created events to the registered listeners.
441      *
442      * @param entry The file entry.
443      */
444     private void fireOnCreate(final FileEntry entry) {
445         listeners.forEach(listener -> {
446             if (entry.isDirectory()) {
447                 listener.onDirectoryCreate(entry.getFile());
448             } else {
449                 listener.onFileCreate(entry.getFile());
450             }
451         });
452         Stream.of(entry.getChildren()).forEach(this::fireOnCreate);
453     }
454 
455     /**
456      * Fires directory/file delete events to the registered listeners.
457      *
458      * @param entry The file entry.
459      */
460     private void fireOnDelete(final FileEntry entry) {
461         listeners.forEach(listener -> {
462             if (entry.isDirectory()) {
463                 listener.onDirectoryDelete(entry.getFile());
464             } else {
465                 listener.onFileDelete(entry.getFile());
466             }
467         });
468     }
469 
470     Comparator<File> getComparator() {
471         return comparator;
472     }
473 
474     /**
475      * Returns the directory being observed.
476      *
477      * @return the directory being observed.
478      */
479     public File getDirectory() {
480         return rootEntry.getFile();
481     }
482 
483     /**
484      * Returns the fileFilter.
485      *
486      * @return the fileFilter.
487      * @since 2.1
488      */
489     public FileFilter getFileFilter() {
490         return fileFilter;
491     }
492 
493     /**
494      * Returns the set of registered file system listeners.
495      *
496      * @return The file system listeners
497      */
498     public Iterable<FileAlterationListener> getListeners() {
499         return new ArrayList<>(listeners);
500     }
501 
502     /**
503      * Initializes the observer.
504      *
505      * @throws Exception if an error occurs.
506      */
507     @SuppressWarnings("unused") // Possibly thrown from subclasses.
508     public void initialize() throws Exception {
509         rootEntry.refresh(rootEntry.getFile());
510         rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry));
511     }
512 
513     /**
514      * Lists the file entries in {@code file}.
515      *
516      * @param file  The directory to list.
517      * @param entry the parent entry.
518      * @return The child file entries.
519      */
520     private FileEntry[] listFileEntries(final File file, final FileEntry entry) {
521         return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new);
522     }
523 
524     /**
525      * Lists the contents of a directory.
526      *
527      * @param directory The directory to list.
528      * @return the directory contents or a zero length array if the empty or the file is not a directory
529      */
530     private File[] listFiles(final File directory) {
531         return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY;
532     }
533 
534     /**
535      * Removes a file system listener.
536      *
537      * @param listener The file system listener.
538      */
539     public void removeListener(final FileAlterationListener listener) {
540         if (listener != null) {
541             listeners.removeIf(listener::equals);
542         }
543     }
544 
545     private File[] sort(final File[] files) {
546         if (files == null) {
547             return FileUtils.EMPTY_FILE_ARRAY;
548         }
549         if (files.length > 1) {
550             Arrays.sort(files, comparator);
551         }
552         return files;
553     }
554 
555     /**
556      * Returns a String representation of this observer.
557      *
558      * @return a String representation of this observer.
559      */
560     @Override
561     public String toString() {
562         final StringBuilder builder = new StringBuilder();
563         builder.append(getClass().getSimpleName());
564         builder.append("[file='");
565         builder.append(getDirectory().getPath());
566         builder.append('\'');
567         builder.append(", ");
568         builder.append(fileFilter.toString());
569         builder.append(", listeners=");
570         builder.append(listeners.size());
571         builder.append("]");
572         return builder.toString();
573     }
574 
575 }