    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}