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