    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     */
    017    package org.apache.commons.io.monitor;
    018
    019    import java.io.File;
    020    import java.io.FileFilter;
    021    import java.io.Serializable;
    022    import java.util.Arrays;
    023    import java.util.Comparator;
    024    import java.util.List;
    025    import java.util.concurrent.CopyOnWriteArrayList;
    026
    027    import org.apache.commons.io.FileUtils;
    028    import org.apache.commons.io.IOCase;
    029    import org.apache.commons.io.comparator.NameFileComparator;
    030
    031    /**
    032     * FileAlterationObserver represents the state of files below a root directory,
    033     * checking the filesystem 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     *      // intialize
    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 oberver(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="http://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     * @version $Id: FileAlterationObserver.java 1022803 2010-10-15 01:09:39Z niallp $
    120     * @since Commons IO 2.0
    121     */
    122    public class FileAlterationObserver implements Serializable {
    123
    124        private final List<FileAlterationListener> listeners = new CopyOnWriteArrayList<FileAlterationListener>();
    125        private final FileEntry rootEntry;
    126        private final FileFilter fileFilter;
    127        private final Comparator<File> comparator;
    128
    129        /**
    130         * Construct an observer for the specified directory.
    131         *
    132         * @param directoryName the name of the directory to observe
    133         */
    134        public FileAlterationObserver(String directoryName) {
    135            this(new File(directoryName));
    136        }
    137
    138        /**
    139         * Construct an observer for the specified directory and file filter.
    140         *
    141         * @param directoryName the name of the directory to observe
    142         * @param fileFilter The file filter or null if none
    143         */
    144        public FileAlterationObserver(String directoryName, FileFilter fileFilter) {
    145            this(new File(directoryName), fileFilter);
    146        }
    147
    148        /**
    149         * Construct an observer for the specified directory, file filter and
    150         * file comparator.
    151         *
    152         * @param directoryName the name of the directory to observe
    153         * @param fileFilter The file filter or null if none
    154         * @param caseSensitivity  what case sensitivity to use comparing file names, null means system sensitive
    155         */
    156        public FileAlterationObserver(String directoryName, FileFilter fileFilter, IOCase caseSensitivity) {
    157            this(new File(directoryName), fileFilter, caseSensitivity);
    158        }
    159
    160        /**
    161         * Construct an observer for the specified directory.
    162         *
    163         * @param directory the directory to observe
    164         */
    165        public FileAlterationObserver(File directory) {
    166            this(directory, (FileFilter)null);
    167        }
    168
    169        /**
    170         * Construct an observer for the specified directory and file filter.
    171         *
    172         * @param directory the directory to observe
    173         * @param fileFilter The file filter or null if none
    174         */
    175        public FileAlterationObserver(File directory, FileFilter fileFilter) {
    176            this(directory, fileFilter, (IOCase)null);
    177        }
    178
    179        /**
    180         * Construct an observer for the specified directory, file filter and
    181         * file comparator.
    182         *
    183         * @param directory the directory to observe
    184         * @param fileFilter The file filter or null if none
    185         * @param caseSensitivity  what case sensitivity to use comparing file names, null means system sensitive
    186         */
    187        public FileAlterationObserver(File directory, FileFilter fileFilter, IOCase caseSensitivity) {
    188            this(new FileEntry(directory), fileFilter, caseSensitivity);
    189        }
    190
    191        /**
    192         * Construct an observer for the specified directory, file filter and
    193         * file comparator.
    194         *
    195         * @param rootEntry the root directory to observe
    196         * @param fileFilter The file filter or null if none
    197         * @param caseSensitivity  what case sensitivity to use comparing file names, null means system sensitive
    198         */
    199        protected FileAlterationObserver(FileEntry rootEntry, FileFilter fileFilter, IOCase caseSensitivity) {
    200            if (rootEntry == null) {
    201                throw new IllegalArgumentException("Root entry is missing");
    202            }
    203            if (rootEntry.getFile() == null) {
    204                throw new IllegalArgumentException("Root directory is missing");
    205            }
    206            this.rootEntry = rootEntry;
    207            this.fileFilter = fileFilter;
    208            if (caseSensitivity == null || caseSensitivity.equals(IOCase.SYSTEM)) {
    209                this.comparator = NameFileComparator.NAME_SYSTEM_COMPARATOR;
    210            } else if (caseSensitivity.equals(IOCase.INSENSITIVE)) {
    211                this.comparator = NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
    212            } else {
    213                this.comparator = NameFileComparator.NAME_COMPARATOR;
    214            }
    215        }
    216
    217        /**
    218         * Return the directory being observed.
    219         *
    220         * @return the directory being observed
    221         */
    222        public File getDirectory() {
    223            return rootEntry.getFile();
    224        }
    225
    226        /**
    227         * Add a file system listener.
    228         *
    229         * @param listener The file system listener
    230         */
    231        public void addListener(final FileAlterationListener listener) {
    232            if (listener != null) {
    233                listeners.add(listener);
    234            }
    235        }
    236
    237        /**
    238         * Remove a file system listener.
    239         *
    240         * @param listener The file system listener
    241         */
    242        public void removeListener(final FileAlterationListener listener) {
    243            if (listener != null) {
    244                while (listeners.remove(listener)) {
    245                }
    246            }
    247        }
    248
    249        /**
    250         * Returns the set of registered file system listeners.
    251         *
    252         * @return The file system listeners
    253         */
    254        public Iterable<FileAlterationListener> getListeners() {
    255            return listeners;
    256        }
    257
    258        /**
    259         * Initialize the observer.
    260         *
    261         * @throws Exception if an error occurs
    262         */
    263        public void initialize() throws Exception {
    264            rootEntry.refresh(rootEntry.getFile());
    265            File[] files = listFiles(rootEntry.getFile());
    266            FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
    267            for (int i = 0; i < files.length; i++) {
    268                children[i] = createFileEntry(rootEntry, files[i]);
    269            }
    270            rootEntry.setChildren(children);
    271        }
    272
    273        /**
    274         * Final processing.
    275         *
    276         * @throws Exception if an error occurs
    277         */
    278        public void destroy() throws Exception {
    279        }
    280
    281        /**
    282         * Check whether the file and its chlidren have been created, modified or deleted.
    283         */
    284        public void checkAndNotify() {
    285
    286            /* fire onStart() */
    287            for (FileAlterationListener listener : listeners) {
    288                listener.onStart(this);
    289            }
    290
    291            /* fire directory/file events */
    292            File rootFile = rootEntry.getFile();
    293            if (rootFile.exists()) {
    294                checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
    295            } else if (rootEntry.isExists()) {
    296                checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
    297            } else {
    298                // Didn't exist and still doesn't
    299            }
    300
    301            /* fire onStop() */
    302            for (FileAlterationListener listener : listeners) {
    303                listener.onStop(this);
    304            }
    305        }
    306
    307        /**
    308         * Compare two file lists for files which have been created, modified or deleted.
    309         *
    310         * @param parent The parent entry
    311         * @param previous The original list of files
    312         * @param files  The current list of files
    313         */
    314        private void checkAndNotify(FileEntry parent, FileEntry[] previous, File[] files) {
    315            int c = 0;
    316            FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
    317            for (FileEntry entry : previous) {
    318                while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
    319                    current[c] = createFileEntry(parent, files[c]);
    320                    doCreate(current[c]);
    321                    c++;
    322                }
    323                if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
    324                    doMatch(entry, files[c]);
    325                    checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
    326                    current[c] = entry;
    327                    c++;
    328                } else {
    329                    checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
    330                    doDelete(entry);
    331                }
    332            }
    333            for (; c < files.length; c++) {
    334                current[c] = createFileEntry(parent, files[c]);
    335                doCreate(current[c]);
    336            }
    337            parent.setChildren(current);
    338        }
    339
    340        /**
    341         * Create a new file entry for the specified file.
    342         *
    343         * @param parent The parent file entry
    344         * @param file The file to create an entry for
    345         * @return A new file entry
    346         */
    347        private FileEntry createFileEntry(FileEntry parent, File file) {
    348            FileEntry entry = parent.newChildInstance(file);
    349            entry.refresh(file);
    350            File[] files = listFiles(file);
    351            FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
    352            for (int i = 0; i < files.length; i++) {
    353                children[i] = createFileEntry(entry, files[i]);
    354            }
    355            entry.setChildren(children);
    356            return entry;
    357        }
    358
    359        /**
    360         * Fire directory/file created events to the registered listeners.
    361         *
    362         * @param entry The file entry
    363         */
    364        private void doCreate(FileEntry entry) {
    365            for (FileAlterationListener listener : listeners) {
    366                if (entry.isDirectory()) {
    367                    listener.onDirectoryCreate(entry.getFile());
    368                } else {
    369                    listener.onFileCreate(entry.getFile());
    370                }
    371            }
    372            FileEntry[] children = entry.getChildren();
    373            for (FileEntry aChildren : children) {
    374                doCreate(aChildren);
    375            }
    376        }
    377
    378        /**
    379         * Fire directory/file change events to the registered listeners.
    380         *
    381         * @param entry The previous file system entry
    382         * @param file The current file
    383         */
    384        private void doMatch(FileEntry entry, File file) {
    385            if (entry.refresh(file)) {
    386                for (FileAlterationListener listener : listeners) {
    387                    if (entry.isDirectory()) {
    388                        listener.onDirectoryChange(file);
    389                    } else {
    390                        listener.onFileChange(file);
    391                    }
    392                }
    393            }
    394        }
    395
    396        /**
    397         * Fire directory/file delete events to the registered listeners.
    398         *
    399         * @param entry The file entry
    400         */
    401        private void doDelete(FileEntry entry) {
    402            for (FileAlterationListener listener : listeners) {
    403                if (entry.isDirectory()) {
    404                    listener.onDirectoryDelete(entry.getFile());
    405                } else {
    406                    listener.onFileDelete(entry.getFile());
    407                }
    408            }
    409        }
    410
    411        /**
    412         * List 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(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         * Provide a String representation of this observer.
    434         *
    435         * @return a String representation of this observer
    436         */
    437        @Override
    438        public String toString() {
    439            StringBuilder builder = new StringBuilder();
    440            builder.append(getClass().getSimpleName());
    441            builder.append("[file='");
    442            builder.append(getDirectory().getPath());
    443            builder.append('\'');
    444            if (fileFilter != null) {
    445                builder.append(", ");
    446                builder.append(fileFilter.toString());
    447            }
    448            builder.append(", listeners=");
    449            builder.append(listeners.size());
    450            builder.append("]");
    451            return builder.toString();
    452        }
    453
    454    }