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 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 */
122public 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}