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}