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