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 *      https://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.AbstractOrigin;
034import org.apache.commons.io.build.AbstractOriginSupplier;
035import org.apache.commons.io.comparator.NameFileComparator;
036import org.apache.commons.io.filefilter.TrueFileFilter;
037
038/**
039 * FileAlterationObserver represents the state of files below a root directory, checking the file system and notifying listeners of create, change or delete
040 * events.
041 * <p>
042 * To use this implementation:
043 * </p>
044 * <ul>
045 * <li>Create {@link FileAlterationListener} implementation(s) that process the file/directory create, change and delete events</li>
046 * <li>Register the listener(s) with a {@link FileAlterationObserver} for the appropriate directory.</li>
047 * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or run manually.</li>
048 * </ul>
049 * <h2>Basic Usage</h2> Create a {@link FileAlterationObserver} for the directory and register the listeners:
050 *
051 * <pre>
052 *      File directory = new File(FileUtils.current(), "src");
053 *      FileAlterationObserver observer = new FileAlterationObserver(directory);
054 *      observer.addListener(...);
055 *      observer.addListener(...);
056 * </pre>
057 * <p>
058 * To manually observe a directory, initialize the observer and invoked the {@link #checkAndNotify()} method as required:
059 * </p>
060 *
061 * <pre>
062 *      // initialize
063 *      observer.init();
064 *      ...
065 *      // invoke as required
066 *      observer.checkAndNotify();
067 *      ...
068 *      observer.checkAndNotify();
069 *      ...
070 *      // finished
071 *      observer.finish();
072 * </pre>
073 * <p>
074 * Alternatively, register the observer(s) with a {@link FileAlterationMonitor}, which creates a new thread, invoking the observer at the specified interval:
075 * </p>
076 *
077 * <pre>
078 *      long interval = ...
079 *      FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
080 *      monitor.addObserver(observer);
081 *      monitor.start();
082 *      ...
083 *      monitor.stop();
084 * </pre>
085 *
086 * <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
087 * that are of interest. This makes it more efficient and reduces the noise from <em>unwanted</em> file system events.
088 * <p>
089 * <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>
090 * implementations for this purpose.
091 * </p>
092 * <p>
093 * 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
094 * {@link FileAlterationObserver} in the following way:
095 * </p>
096 *
097 * <pre>
098 *      // Create a FileFilter
099 *      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 */
128public 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}