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