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