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 1304052 2012-03-22 20:55:29Z ggregory $
120     * @since 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         * Return the fileFilter.
228         *
229         * @return the fileFilter
230         * @since 2.1
231         */
232        public FileFilter getFileFilter() {
233            return fileFilter;
234        }
235    
236        /**
237         * Add a file system listener.
238         *
239         * @param listener The file system listener
240         */
241        public void addListener(final FileAlterationListener listener) {
242            if (listener != null) {
243                listeners.add(listener);
244            }
245        }
246    
247        /**
248         * Remove a file system listener.
249         *
250         * @param listener The file system listener
251         */
252        public void removeListener(final FileAlterationListener listener) {
253            if (listener != null) {
254                while (listeners.remove(listener)) {
255                }
256            }
257        }
258    
259        /**
260         * Returns the set of registered file system listeners.
261         *
262         * @return The file system listeners
263         */
264        public Iterable<FileAlterationListener> getListeners() {
265            return listeners;
266        }
267    
268        /**
269         * Initialize the observer.
270         *
271         * @throws Exception if an error occurs
272         */
273        public void initialize() throws Exception {
274            rootEntry.refresh(rootEntry.getFile());
275            File[] files = listFiles(rootEntry.getFile());
276            FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
277            for (int i = 0; i < files.length; i++) {
278                children[i] = createFileEntry(rootEntry, files[i]);
279            }
280            rootEntry.setChildren(children);
281        }
282    
283        /**
284         * Final processing.
285         *
286         * @throws Exception if an error occurs
287         */
288        public void destroy() throws Exception {
289        }
290    
291        /**
292         * Check whether the file and its chlidren have been created, modified or deleted.
293         */
294        public void checkAndNotify() {
295    
296            /* fire onStart() */
297            for (FileAlterationListener listener : listeners) {
298                listener.onStart(this);
299            }
300    
301            /* fire directory/file events */
302            File rootFile = rootEntry.getFile();
303            if (rootFile.exists()) {
304                checkAndNotify(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
305            } else if (rootEntry.isExists()) {
306                checkAndNotify(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
307            } else {
308                // Didn't exist and still doesn't
309            }
310    
311            /* fire onStop() */
312            for (FileAlterationListener listener : listeners) {
313                listener.onStop(this);
314            }
315        }
316    
317        /**
318         * Compare two file lists for files which have been created, modified or deleted.
319         *
320         * @param parent The parent entry
321         * @param previous The original list of files
322         * @param files  The current list of files
323         */
324        private void checkAndNotify(FileEntry parent, FileEntry[] previous, File[] files) {
325            int c = 0;
326            FileEntry[] current = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
327            for (FileEntry entry : previous) {
328                while (c < files.length && comparator.compare(entry.getFile(), files[c]) > 0) {
329                    current[c] = createFileEntry(parent, files[c]);
330                    doCreate(current[c]);
331                    c++;
332                }
333                if (c < files.length && comparator.compare(entry.getFile(), files[c]) == 0) {
334                    doMatch(entry, files[c]);
335                    checkAndNotify(entry, entry.getChildren(), listFiles(files[c]));
336                    current[c] = entry;
337                    c++;
338                } else {
339                    checkAndNotify(entry, entry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
340                    doDelete(entry);
341                }
342            }
343            for (; c < files.length; c++) {
344                current[c] = createFileEntry(parent, files[c]);
345                doCreate(current[c]);
346            }
347            parent.setChildren(current);
348        }
349    
350        /**
351         * Create a new file entry for the specified file.
352         *
353         * @param parent The parent file entry
354         * @param file The file to create an entry for
355         * @return A new file entry
356         */
357        private FileEntry createFileEntry(FileEntry parent, File file) {
358            FileEntry entry = parent.newChildInstance(file);
359            entry.refresh(file);
360            File[] files = listFiles(file);
361            FileEntry[] children = files.length > 0 ? new FileEntry[files.length] : FileEntry.EMPTY_ENTRIES;
362            for (int i = 0; i < files.length; i++) {
363                children[i] = createFileEntry(entry, files[i]);
364            }
365            entry.setChildren(children);
366            return entry;
367        }
368    
369        /**
370         * Fire directory/file created events to the registered listeners.
371         *
372         * @param entry The file entry
373         */
374        private void doCreate(FileEntry entry) {
375            for (FileAlterationListener listener : listeners) {
376                if (entry.isDirectory()) {
377                    listener.onDirectoryCreate(entry.getFile());
378                } else {
379                    listener.onFileCreate(entry.getFile());
380                }
381            }
382            FileEntry[] children = entry.getChildren();
383            for (FileEntry aChildren : children) {
384                doCreate(aChildren);
385            }
386        }
387    
388        /**
389         * Fire directory/file change events to the registered listeners.
390         *
391         * @param entry The previous file system entry
392         * @param file The current file
393         */
394        private void doMatch(FileEntry entry, File file) {
395            if (entry.refresh(file)) {
396                for (FileAlterationListener listener : listeners) {
397                    if (entry.isDirectory()) {
398                        listener.onDirectoryChange(file);
399                    } else {
400                        listener.onFileChange(file);
401                    }
402                }
403            }
404        }
405    
406        /**
407         * Fire directory/file delete events to the registered listeners.
408         *
409         * @param entry The file entry
410         */
411        private void doDelete(FileEntry entry) {
412            for (FileAlterationListener listener : listeners) {
413                if (entry.isDirectory()) {
414                    listener.onDirectoryDelete(entry.getFile());
415                } else {
416                    listener.onFileDelete(entry.getFile());
417                }
418            }
419        }
420    
421        /**
422         * List the contents of a directory
423         *
424         * @param file The file to list the contents of
425         * @return the directory contents or a zero length array if
426         * the empty or the file is not a directory
427         */
428        private File[] listFiles(File file) {
429            File[] children = null;
430            if (file.isDirectory()) {
431                children = fileFilter == null ? file.listFiles() : file.listFiles(fileFilter);
432            }
433            if (children == null) {
434                children = FileUtils.EMPTY_FILE_ARRAY;
435            }
436            if (comparator != null && children.length > 1) {
437                Arrays.sort(children, comparator);
438            }
439            return children;
440        }
441    
442        /**
443         * Provide a String representation of this observer.
444         *
445         * @return a String representation of this observer
446         */
447        @Override
448        public String toString() {
449            StringBuilder builder = new StringBuilder();
450            builder.append(getClass().getSimpleName());
451            builder.append("[file='");
452            builder.append(getDirectory().getPath());
453            builder.append('\'');
454            if (fileFilter != null) {
455                builder.append(", ");
456                builder.append(fileFilter.toString());
457            }
458            builder.append(", listeners=");
459            builder.append(listeners.size());
460            builder.append("]");
461            return builder.toString();
462        }
463    
464    }