    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.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        /**
    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}