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