001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.io.monitor;
018
019import java.io.File;
020import java.io.FileFilter;
021import java.io.IOException;
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Comparator;
026import java.util.List;
027import java.util.Objects;
028import java.util.concurrent.CopyOnWriteArrayList;
029import java.util.stream.Stream;
030
031import org.apache.commons.io.FileUtils;
032import org.apache.commons.io.IOCase;
033import org.apache.commons.io.build.AbstractOriginSupplier;
034import org.apache.commons.io.comparator.NameFileComparator;
035import org.apache.commons.io.filefilter.TrueFileFilter;
036
037/**
038 * FileAlterationObserver represents the state of files below a root directory, checking the file system and notifying listeners of create, change or delete
039 * events.
040 * <p>
041 * To use this implementation:
042 * </p>
043 * <ul>
044 * <li>Create {@link FileAlterationListener} implementation(s) that process the file/directory create, change and delete events</li>
045 * <li>Register the listener(s) with a {@link FileAlterationObserver} for the appropriate directory.</li>
046 * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or run manually.</li>
047 * </ul>
048 * <h2>Basic Usage</h2> Create a {@link FileAlterationObserver} for the directory and register the listeners:
049 *
050 * <pre>
051 *      File directory = new File(FileUtils.current(), "src");
052 *      FileAlterationObserver observer = new FileAlterationObserver(directory);
053 *      observer.addListener(...);
054 *      observer.addListener(...);
055 * </pre>
056 * <p>
057 * To manually observe a directory, initialize the observer and invoked the {@link #checkAndNotify()} method as required:
058 * </p>
059 *
060 * <pre>
061 *      // initialize
062 *      observer.init();
063 *      ...
064 *      // invoke as required
065 *      observer.checkAndNotify();
066 *      ...
067 *      observer.checkAndNotify();
068 *      ...
069 *      // finished
070 *      observer.finish();
071 * </pre>
072 * <p>
073 * Alternatively, register the observer(s) with a {@link FileAlterationMonitor}, which creates a new thread, invoking the observer at the specified interval:
074 * </p>
075 *
076 * <pre>
077 *      long interval = ...
078 *      FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
079 *      monitor.addObserver(observer);
080 *      monitor.start();
081 *      ...
082 *      monitor.stop();
083 * </pre>
084 *
085 * <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
086 * that are of interest. This makes it more efficient and reduces the noise from <em>unwanted</em> file system events.
087 * <p>
088 * <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>
089 * implementations for this purpose.
090 * </p>
091 * <p>
092 * 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
093 * {@link FileAlterationObserver} in the following way:
094 * </p>
095 *
096 * <pre>
097 *      // Create a FileFilter
098 *      IOFileFilter directories = FileFilterUtils.and(
099 *                                      FileFilterUtils.directoryFileFilter(),
100 *                                      HiddenFileFilter.VISIBLE);
101 *      IOFileFilter files       = FileFilterUtils.and(
102 *                                      FileFilterUtils.fileFileFilter(),
103 *                                      FileFilterUtils.suffixFileFilter(".java"));
104 *      IOFileFilter filter = FileFilterUtils.or(directories, files);
105 *
106 *      // Create the File system observer and register File Listeners
107 *      FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter);
108 *      observer.addListener(...);
109 *      observer.addListener(...);
110 * </pre>
111 *
112 * <h2>FileEntry</h2>
113 * <p>
114 * {@link FileEntry} represents the state of a file or directory, capturing {@link File} attributes at a point in time. Custom implementations of
115 * {@link FileEntry} can be used to capture additional properties that the basic implementation does not support. The {@link FileEntry#refresh(File)} method is
116 * 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.
117 * </p>
118 * <h2>Deprecating Serialization</h2>
119 * <p>
120 * <em>Serialization is deprecated and will be removed in 3.0.</em>
121 * </p>
122 *
123 * @see FileAlterationListener
124 * @see FileAlterationMonitor
125 * @since 2.0
126 */
127public class FileAlterationObserver implements Serializable {
128
129    /**
130     * Builds instances of {@link FileAlterationObserver}.
131     *
132     * @since 2.18.0
133     */
134    public static final class Builder extends AbstractOriginSupplier<FileAlterationObserver, Builder> {
135
136        private FileEntry rootEntry;
137        private FileFilter fileFilter;
138        private IOCase ioCase;
139
140        private Builder() {
141            // empty
142        }
143
144        /**
145         * Gets a new {@link FileAlterationObserver} instance.
146         */
147        @Override
148        public FileAlterationObserver get() throws IOException {
149            return new FileAlterationObserver(rootEntry != null ? rootEntry : new FileEntry(checkOrigin().getFile()), fileFilter, toComparator(ioCase));
150        }
151
152        /**
153         * Sets the file filter or null if none.
154         *
155         * @param fileFilter file filter or null if none.
156         * @return This instance.
157         */
158        public Builder setFileFilter(final FileFilter fileFilter) {
159            this.fileFilter = fileFilter;
160            return asThis();
161        }
162
163        /**
164         * Sets what case sensitivity to use comparing file names, null means system sensitive.
165         *
166         * @param ioCase what case sensitivity to use comparing file names, null means system sensitive.
167         * @return This instance.
168         */
169        public Builder setIOCase(final IOCase ioCase) {
170            this.ioCase = ioCase;
171            return asThis();
172        }
173
174        /**
175         * Sets the root directory to observe.
176         *
177         * @param rootEntry the root directory to observe.
178         * @return This instance.
179         */
180        public Builder setRootEntry(final FileEntry rootEntry) {
181            this.rootEntry = rootEntry;
182            return asThis();
183        }
184
185    }
186
187    private static final long serialVersionUID = 1185122225658782848L;
188
189    /**
190     * Creates a new builder.
191     *
192     * @return a new builder.
193     * @since 2.18.0
194     */
195    public static Builder builder() {
196        return new Builder();
197    }
198
199    private static Comparator<File> toComparator(final IOCase ioCase) {
200        switch (IOCase.value(ioCase, IOCase.SYSTEM)) {
201        case SYSTEM:
202            return NameFileComparator.NAME_SYSTEM_COMPARATOR;
203        case INSENSITIVE:
204            return NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
205        default:
206            return NameFileComparator.NAME_COMPARATOR;
207        }
208    }
209
210    /**
211     * List of listeners.
212     */
213    private transient final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>();
214
215    /**
216     * The root directory to observe.
217     */
218    private final FileEntry rootEntry;
219
220    /**
221     * The file filter or null if none.
222     */
223    private transient final FileFilter fileFilter;
224
225    /**
226     * Compares file names.
227     */
228    private final Comparator<File> comparator;
229
230    /**
231     * Constructs an observer for the specified directory.
232     *
233     * @param directory the directory to observe.
234     * @deprecated Use {@link #builder()}.
235     */
236    @Deprecated
237    public FileAlterationObserver(final File directory) {
238        this(directory, null);
239    }
240
241    /**
242     * Constructs an observer for the specified directory and file filter.
243     *
244     * @param directory  The directory to observe.
245     * @param fileFilter The file filter or null if none.
246     * @deprecated Use {@link #builder()}.
247     */
248    @Deprecated
249    public FileAlterationObserver(final File directory, final FileFilter fileFilter) {
250        this(directory, fileFilter, null);
251    }
252
253    /**
254     * Constructs an observer for the specified directory, file filter and file comparator.
255     *
256     * @param directory  The directory to observe.
257     * @param fileFilter The file filter or null if none.
258     * @param ioCase     What case sensitivity to use comparing file names, null means system sensitive.
259     * @deprecated Use {@link #builder()}.
260     */
261    @Deprecated
262    public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase ioCase) {
263        this(new FileEntry(directory), fileFilter, ioCase);
264    }
265
266    /**
267     * Constructs an observer for the specified directory, file filter and file comparator.
268     *
269     * @param rootEntry  The root directory to observe.
270     * @param fileFilter The file filter or null if none.
271     * @param comparator How to compare files.
272     */
273    private FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final Comparator<File> comparator) {
274        Objects.requireNonNull(rootEntry, "rootEntry");
275        Objects.requireNonNull(rootEntry.getFile(), "rootEntry.getFile()");
276        this.rootEntry = rootEntry;
277        this.fileFilter = fileFilter != null ? fileFilter : TrueFileFilter.INSTANCE;
278        this.comparator = Objects.requireNonNull(comparator, "comparator");
279    }
280
281    /**
282     * Constructs an observer for the specified directory, file filter and file comparator.
283     *
284     * @param rootEntry  The root directory to observe.
285     * @param fileFilter The file filter or null if none.
286     * @param ioCase     What case sensitivity to use comparing file names, null means system sensitive.
287     */
288    protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase ioCase) {
289        this(rootEntry, fileFilter, toComparator(ioCase));
290    }
291
292    /**
293     * Constructs an observer for the specified directory.
294     *
295     * @param directoryName the name of the directory to observe.
296     * @deprecated Use {@link #builder()}.
297     */
298    @Deprecated
299    public FileAlterationObserver(final String directoryName) {
300        this(new File(directoryName));
301    }
302
303    /**
304     * Constructs an observer for the specified directory and file filter.
305     *
306     * @param directoryName the name of the directory to observe.
307     * @param fileFilter    The file filter or null if none.
308     * @deprecated Use {@link #builder()}.
309     */
310    @Deprecated
311    public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) {
312        this(new File(directoryName), fileFilter);
313    }
314
315    /**
316     * Constructs an observer for the specified directory, file filter and file comparator.
317     *
318     * @param directoryName the name of the directory to observe.
319     * @param fileFilter    The file filter or null if none.
320     * @param ioCase        what case sensitivity to use comparing file names, null means system sensitive.
321     * @deprecated Use {@link #builder()}.
322     */
323    @Deprecated
324    public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase ioCase) {
325        this(new File(directoryName), fileFilter, ioCase);
326    }
327
328    /**
329     * Adds a file system listener.
330     *
331     * @param listener The file system listener.
332     */
333    public void addListener(final FileAlterationListener listener) {
334        if (listener != null) {
335            listeners.add(listener);
336        }
337    }
338
339    /**
340     * Compares two file lists for files which have been created, modified or deleted.
341     *
342     * @param parentEntry     The parent entry.
343     * @param previousEntries The original list of file entries.
344     * @param currentEntries  The current list of files entries.
345     */
346    private void checkAndFire(final FileEntry parentEntry, final FileEntry[] previousEntries, final File[] currentEntries) {
347        int c = 0;
348        final FileEntry[] actualEntries = currentEntries.length > 0 ? new FileEntry[currentEntries.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
349        for (final FileEntry previousEntry : previousEntries) {
350            while (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) > 0) {
351                actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
352                fireOnCreate(actualEntries[c]);
353                c++;
354            }
355            if (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) == 0) {
356                fireOnChange(previousEntry, currentEntries[c]);
357                checkAndFire(previousEntry, previousEntry.getChildren(), listFiles(currentEntries[c]));
358                actualEntries[c] = previousEntry;
359                c++;
360            } else {
361                checkAndFire(previousEntry, previousEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
362                fireOnDelete(previousEntry);
363            }
364        }
365        for (; c < currentEntries.length; c++) {
366            actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
367            fireOnCreate(actualEntries[c]);
368        }
369        parentEntry.setChildren(actualEntries);
370    }
371
372    /**
373     * Checks whether the file and its children have been created, modified or deleted.
374     */
375    public void checkAndNotify() {
376
377        // fire onStart()
378        listeners.forEach(listener -> listener.onStart(this));
379
380        // fire directory/file events
381        final File rootFile = rootEntry.getFile();
382        if (rootFile.exists()) {
383            checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
384        } else if (rootEntry.isExists()) {
385            checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
386        }
387        // Else: Didn't exist and still doesn't
388
389        // fire onStop()
390        listeners.forEach(listener -> listener.onStop(this));
391    }
392
393    /**
394     * Creates a new file entry for the specified file.
395     *
396     * @param parent The parent file entry.
397     * @param file   The file to wrap.
398     * @return A new file entry.
399     */
400    private FileEntry createFileEntry(final FileEntry parent, final File file) {
401        final FileEntry entry = parent.newChildInstance(file);
402        entry.refresh(file);
403        entry.setChildren(listFileEntries(file, entry));
404        return entry;
405    }
406
407    /**
408     * Final processing.
409     *
410     * @throws Exception if an error occurs.
411     */
412    @SuppressWarnings("unused") // Possibly thrown from subclasses.
413    public void destroy() throws Exception {
414        // noop
415    }
416
417    /**
418     * Fires directory/file change events to the registered listeners.
419     *
420     * @param entry The previous file system entry.
421     * @param file  The current file.
422     */
423    private void fireOnChange(final FileEntry entry, final File file) {
424        if (entry.refresh(file)) {
425            listeners.forEach(listener -> {
426                if (entry.isDirectory()) {
427                    listener.onDirectoryChange(file);
428                } else {
429                    listener.onFileChange(file);
430                }
431            });
432        }
433    }
434
435    /**
436     * Fires directory/file created events to the registered listeners.
437     *
438     * @param entry The file entry.
439     */
440    private void fireOnCreate(final FileEntry entry) {
441        listeners.forEach(listener -> {
442            if (entry.isDirectory()) {
443                listener.onDirectoryCreate(entry.getFile());
444            } else {
445                listener.onFileCreate(entry.getFile());
446            }
447        });
448        Stream.of(entry.getChildren()).forEach(this::fireOnCreate);
449    }
450
451    /**
452     * Fires directory/file delete events to the registered listeners.
453     *
454     * @param entry The file entry.
455     */
456    private void fireOnDelete(final FileEntry entry) {
457        listeners.forEach(listener -> {
458            if (entry.isDirectory()) {
459                listener.onDirectoryDelete(entry.getFile());
460            } else {
461                listener.onFileDelete(entry.getFile());
462            }
463        });
464    }
465
466    Comparator<File> getComparator() {
467        return comparator;
468    }
469
470    /**
471     * Returns the directory being observed.
472     *
473     * @return the directory being observed.
474     */
475    public File getDirectory() {
476        return rootEntry.getFile();
477    }
478
479    /**
480     * Returns the fileFilter.
481     *
482     * @return the fileFilter.
483     * @since 2.1
484     */
485    public FileFilter getFileFilter() {
486        return fileFilter;
487    }
488
489    /**
490     * Returns the set of registered file system listeners.
491     *
492     * @return The file system listeners
493     */
494    public Iterable<FileAlterationListener> getListeners() {
495        return new ArrayList<>(listeners);
496    }
497
498    /**
499     * Initializes the observer.
500     *
501     * @throws Exception if an error occurs.
502     */
503    @SuppressWarnings("unused") // Possibly thrown from subclasses.
504    public void initialize() throws Exception {
505        rootEntry.refresh(rootEntry.getFile());
506        rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry));
507    }
508
509    /**
510     * Lists the file entries in {@code file}.
511     *
512     * @param file  The directory to list.
513     * @param entry the parent entry.
514     * @return The child file entries.
515     */
516    private FileEntry[] listFileEntries(final File file, final FileEntry entry) {
517        return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new);
518    }
519
520    /**
521     * Lists the contents of a directory.
522     *
523     * @param directory The directory to list.
524     * @return the directory contents or a zero length array if the empty or the file is not a directory
525     */
526    private File[] listFiles(final File directory) {
527        return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY;
528    }
529
530    /**
531     * Removes a file system listener.
532     *
533     * @param listener The file system listener.
534     */
535    public void removeListener(final FileAlterationListener listener) {
536        if (listener != null) {
537            listeners.removeIf(listener::equals);
538        }
539    }
540
541    private File[] sort(final File[] files) {
542        if (files == null) {
543            return FileUtils.EMPTY_FILE_ARRAY;
544        }
545        if (files.length > 1) {
546            Arrays.sort(files, comparator);
547        }
548        return files;
549    }
550
551    /**
552     * Returns a String representation of this observer.
553     *
554     * @return a String representation of this observer.
555     */
556    @Override
557    public String toString() {
558        final StringBuilder builder = new StringBuilder();
559        builder.append(getClass().getSimpleName());
560        builder.append("[file='");
561        builder.append(getDirectory().getPath());
562        builder.append('\'');
563        builder.append(", ");
564        builder.append(fileFilter.toString());
565        builder.append(", listeners=");
566        builder.append(listeners.size());
567        builder.append("]");
568        return builder.toString();
569    }
570
571}