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