    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}