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.IOException;
022import java.io.Serializable;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Comparator;
026import java.util.List;
027import java.util.Objects;
028import java.util.concurrent.CopyOnWriteArrayList;
029import java.util.stream.Stream;
030
031import org.apache.commons.io.FileUtils;
032import org.apache.commons.io.IOCase;
033import org.apache.commons.io.build.AbstractOrigin;
034import org.apache.commons.io.build.AbstractOriginSupplier;
035import org.apache.commons.io.comparator.NameFileComparator;
036import org.apache.commons.io.filefilter.TrueFileFilter;
037
038/**
039 * FileAlterationObserver represents the state of files below a root directory, checking the file system and notifying listeners of create, change or delete
040 * events.
041 * <p>
042 * To use this implementation:
043 * </p>
044 * <ul>
045 * <li>Create {@link FileAlterationListener} implementation(s) that process the file/directory create, change and delete events</li>
046 * <li>Register the listener(s) with a {@link FileAlterationObserver} for the appropriate directory.</li>
047 * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or run manually.</li>
048 * </ul>
049 * <h2>Basic Usage</h2> Create a {@link FileAlterationObserver} for the directory and register the listeners:
050 *
051 * <pre>
052 *      File directory = new File(FileUtils.current(), "src");
053 *      FileAlterationObserver observer = new FileAlterationObserver(directory);
054 *      observer.addListener(...);
055 *      observer.addListener(...);
056 * </pre>
057 * <p>
058 * To manually observe a directory, initialize the observer and invoked the {@link #checkAndNotify()} method as required:
059 * </p>
060 *
061 * <pre>
062 *      // initialize
063 *      observer.init();
064 *      ...
065 *      // invoke as required
066 *      observer.checkAndNotify();
067 *      ...
068 *      observer.checkAndNotify();
069 *      ...
070 *      // finished
071 *      observer.finish();
072 * </pre>
073 * <p>
074 * Alternatively, register the observer(s) with a {@link FileAlterationMonitor}, which creates a new thread, invoking the observer at the specified interval:
075 * </p>
076 *
077 * <pre>
078 *      long interval = ...
079 *      FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
080 *      monitor.addObserver(observer);
081 *      monitor.start();
082 *      ...
083 *      monitor.stop();
084 * </pre>
085 *
086 * <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
087 * that are of interest. This makes it more efficient and reduces the noise from <em>unwanted</em> file system events.
088 * <p>
089 * <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>
090 * implementations for this purpose.
091 * </p>
092 * <p>
093 * 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
094 * {@link FileAlterationObserver} in the following way:
095 * </p>
096 *
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 * <p>
115 * {@link FileEntry} represents the state of a file or directory, capturing {@link File} attributes at a point in time. Custom implementations of
116 * {@link FileEntry} can be used to capture additional properties that the basic implementation does not support. The {@link FileEntry#refresh(File)} method is
117 * used to determine if a file or directory has changed since the last check and stores the current state of the {@link File}'s properties.
118 * </p>
119 * <h2>Deprecating Serialization</h2>
120 * <p>
121 * <em>Serialization is deprecated and will be removed in 3.0.</em>
122 * </p>
123 *
124 * @see FileAlterationListener
125 * @see FileAlterationMonitor
126 * @since 2.0
127 */
128public class FileAlterationObserver implements Serializable {
129
130    /**
131     * Builds instances of {@link FileAlterationObserver}.
132     *
133     * @since 2.18.0
134     */
135    public static final class Builder extends AbstractOriginSupplier<FileAlterationObserver, Builder> {
136
137        private FileEntry rootEntry;
138        private FileFilter fileFilter;
139        private IOCase ioCase;
140
141        private Builder() {
142            // empty
143        }
144
145        /**
146         * Gets a new {@link FileAlterationObserver} instance.
147         *
148         * @throws IOException                   if an I/O error occurs converting to an {@link File} using {@link AbstractOrigin#getFile()}.
149         * @see #getUnchecked()
150         */
151        @Override
152        public FileAlterationObserver get() throws IOException {
153            return new FileAlterationObserver(rootEntry != null ? rootEntry : new FileEntry(checkOrigin().getFile()), fileFilter, toComparator(ioCase));
154        }
155
156        /**
157         * Sets the file filter or null if none.
158         *
159         * @param fileFilter file filter or null if none.
160         * @return This instance.
161         */
162        public Builder setFileFilter(final FileFilter fileFilter) {
163            this.fileFilter = fileFilter;
164            return asThis();
165        }
166
167        /**
168         * Sets what case sensitivity to use comparing file names, null means system sensitive.
169         *
170         * @param ioCase what case sensitivity to use comparing file names, null means system sensitive.
171         * @return This instance.
172         */
173        public Builder setIOCase(final IOCase ioCase) {
174            this.ioCase = ioCase;
175            return asThis();
176        }
177
178        /**
179         * Sets the root directory to observe.
180         *
181         * @param rootEntry the root directory to observe.
182         * @return This instance.
183         */
184        public Builder setRootEntry(final FileEntry rootEntry) {
185            this.rootEntry = rootEntry;
186            return asThis();
187        }
188
189    }
190
191    private static final long serialVersionUID = 1185122225658782848L;
192
193    /**
194     * Creates a new builder.
195     *
196     * @return a new builder.
197     * @since 2.18.0
198     */
199    public static Builder builder() {
200        return new Builder();
201    }
202
203    private static Comparator<File> toComparator(final IOCase ioCase) {
204        switch (IOCase.value(ioCase, IOCase.SYSTEM)) {
205        case SYSTEM:
206            return NameFileComparator.NAME_SYSTEM_COMPARATOR;
207        case INSENSITIVE:
208            return NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
209        default:
210            return NameFileComparator.NAME_COMPARATOR;
211        }
212    }
213
214    /**
215     * List of listeners.
216     */
217    private final transient List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>();
218
219    /**
220     * The root directory to observe.
221     */
222    private final FileEntry rootEntry;
223
224    /**
225     * The file filter or null if none.
226     */
227    private final transient FileFilter fileFilter;
228
229    /**
230     * Compares file names.
231     */
232    private final Comparator<File> comparator;
233
234    /**
235     * Constructs an observer for the specified directory.
236     *
237     * @param directory the directory to observe.
238     * @deprecated Use {@link #builder()}.
239     */
240    @Deprecated
241    public FileAlterationObserver(final File directory) {
242        this(directory, null);
243    }
244
245    /**
246     * Constructs an observer for the specified directory and file filter.
247     *
248     * @param directory  The directory to observe.
249     * @param fileFilter The file filter or null if none.
250     * @deprecated Use {@link #builder()}.
251     */
252    @Deprecated
253    public FileAlterationObserver(final File directory, final FileFilter fileFilter) {
254        this(directory, fileFilter, null);
255    }
256
257    /**
258     * Constructs an observer for the specified directory, file filter and file comparator.
259     *
260     * @param directory  The directory to observe.
261     * @param fileFilter The file filter or null if none.
262     * @param ioCase     What case sensitivity to use comparing file names, null means system sensitive.
263     * @deprecated Use {@link #builder()}.
264     */
265    @Deprecated
266    public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase ioCase) {
267        this(new FileEntry(directory), fileFilter, ioCase);
268    }
269
270    /**
271     * Constructs an observer for the specified directory, file filter and file comparator.
272     *
273     * @param rootEntry  The root directory to observe.
274     * @param fileFilter The file filter or null if none.
275     * @param comparator How to compare files.
276     */
277    private FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final Comparator<File> comparator) {
278        Objects.requireNonNull(rootEntry, "rootEntry");
279        Objects.requireNonNull(rootEntry.getFile(), "rootEntry.getFile()");
280        this.rootEntry = rootEntry;
281        this.fileFilter = fileFilter != null ? fileFilter : TrueFileFilter.INSTANCE;
282        this.comparator = Objects.requireNonNull(comparator, "comparator");
283    }
284
285    /**
286     * Constructs an observer for the specified directory, file filter and file comparator.
287     *
288     * @param rootEntry  The root directory to observe.
289     * @param fileFilter The file filter or null if none.
290     * @param ioCase     What case sensitivity to use comparing file names, null means system sensitive.
291     */
292    protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase ioCase) {
293        this(rootEntry, fileFilter, toComparator(ioCase));
294    }
295
296    /**
297     * Constructs an observer for the specified directory.
298     *
299     * @param directoryName the name of the directory to observe.
300     * @deprecated Use {@link #builder()}.
301     */
302    @Deprecated
303    public FileAlterationObserver(final String directoryName) {
304        this(new File(directoryName));
305    }
306
307    /**
308     * Constructs an observer for the specified directory and file filter.
309     *
310     * @param directoryName the name of the directory to observe.
311     * @param fileFilter    The file filter or null if none.
312     * @deprecated Use {@link #builder()}.
313     */
314    @Deprecated
315    public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) {
316        this(new File(directoryName), fileFilter);
317    }
318
319    /**
320     * Constructs an observer for the specified directory, file filter and file comparator.
321     *
322     * @param directoryName the name of the directory to observe.
323     * @param fileFilter    The file filter or null if none.
324     * @param ioCase        what case sensitivity to use comparing file names, null means system sensitive.
325     * @deprecated Use {@link #builder()}.
326     */
327    @Deprecated
328    public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase ioCase) {
329        this(new File(directoryName), fileFilter, ioCase);
330    }
331
332    /**
333     * Adds a file system listener.
334     *
335     * @param listener The file system listener.
336     */
337    public void addListener(final FileAlterationListener listener) {
338        if (listener != null) {
339            listeners.add(listener);
340        }
341    }
342
343    /**
344     * Compares two file lists for files which have been created, modified or deleted.
345     *
346     * @param parentEntry     The parent entry.
347     * @param previousEntries The original list of file entries.
348     * @param currentEntries  The current list of files entries.
349     */
350    private void checkAndFire(final FileEntry parentEntry, final FileEntry[] previousEntries, final File[] currentEntries) {
351        int c = 0;
352        final FileEntry[] actualEntries = currentEntries.length > 0 ? new FileEntry[currentEntries.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
353        for (final FileEntry previousEntry : previousEntries) {
354            while (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) > 0) {
355                actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
356                fireOnCreate(actualEntries[c]);
357                c++;
358            }
359            if (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) == 0) {
360                fireOnChange(previousEntry, currentEntries[c]);
361                checkAndFire(previousEntry, previousEntry.getChildren(), listFiles(currentEntries[c]));
362                actualEntries[c] = previousEntry;
363                c++;
364            } else {
365                checkAndFire(previousEntry, previousEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
366                fireOnDelete(previousEntry);
367            }
368        }
369        for (; c < currentEntries.length; c++) {
370            actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
371            fireOnCreate(actualEntries[c]);
372        }
373        parentEntry.setChildren(actualEntries);
374    }
375
376    /**
377     * Checks whether the file and its children have been created, modified or deleted.
378     */
379    public void checkAndNotify() {
380
381        // fire onStart()
382        listeners.forEach(listener -> listener.onStart(this));
383
384        // fire directory/file events
385        final File rootFile = rootEntry.getFile();
386        if (rootFile.exists()) {
387            checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
388        } else if (rootEntry.isExists()) {
389            checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
390        }
391        // Else: Didn't exist and still doesn't
392
393        // fire onStop()
394        listeners.forEach(listener -> listener.onStop(this));
395    }
396
397    /**
398     * Creates a new file entry for the specified file.
399     *
400     * @param parent The parent file entry.
401     * @param file   The file to wrap.
402     * @return A new file entry.
403     */
404    private FileEntry createFileEntry(final FileEntry parent, final File file) {
405        final FileEntry entry = parent.newChildInstance(file);
406        entry.refresh(file);
407        entry.setChildren(listFileEntries(file, entry));
408        return entry;
409    }
410
411    /**
412     * Final processing.
413     *
414     * @throws Exception if an error occurs.
415     */
416    @SuppressWarnings("unused") // Possibly thrown from subclasses.
417    public void destroy() throws Exception {
418        // noop
419    }
420
421    /**
422     * Fires directory/file change events to the registered listeners.
423     *
424     * @param entry The previous file system entry.
425     * @param file  The current file.
426     */
427    private void fireOnChange(final FileEntry entry, final File file) {
428        if (entry.refresh(file)) {
429            listeners.forEach(listener -> {
430                if (entry.isDirectory()) {
431                    listener.onDirectoryChange(file);
432                } else {
433                    listener.onFileChange(file);
434                }
435            });
436        }
437    }
438
439    /**
440     * Fires directory/file created events to the registered listeners.
441     *
442     * @param entry The file entry.
443     */
444    private void fireOnCreate(final FileEntry entry) {
445        listeners.forEach(listener -> {
446            if (entry.isDirectory()) {
447                listener.onDirectoryCreate(entry.getFile());
448            } else {
449                listener.onFileCreate(entry.getFile());
450            }
451        });
452        Stream.of(entry.getChildren()).forEach(this::fireOnCreate);
453    }
454
455    /**
456     * Fires directory/file delete events to the registered listeners.
457     *
458     * @param entry The file entry.
459     */
460    private void fireOnDelete(final FileEntry entry) {
461        listeners.forEach(listener -> {
462            if (entry.isDirectory()) {
463                listener.onDirectoryDelete(entry.getFile());
464            } else {
465                listener.onFileDelete(entry.getFile());
466            }
467        });
468    }
469
470    Comparator<File> getComparator() {
471        return comparator;
472    }
473
474    /**
475     * Returns the directory being observed.
476     *
477     * @return the directory being observed.
478     */
479    public File getDirectory() {
480        return rootEntry.getFile();
481    }
482
483    /**
484     * Returns the fileFilter.
485     *
486     * @return the fileFilter.
487     * @since 2.1
488     */
489    public FileFilter getFileFilter() {
490        return fileFilter;
491    }
492
493    /**
494     * Returns the set of registered file system listeners.
495     *
496     * @return The file system listeners
497     */
498    public Iterable<FileAlterationListener> getListeners() {
499        return new ArrayList<>(listeners);
500    }
501
502    /**
503     * Initializes the observer.
504     *
505     * @throws Exception if an error occurs.
506     */
507    @SuppressWarnings("unused") // Possibly thrown from subclasses.
508    public void initialize() throws Exception {
509        rootEntry.refresh(rootEntry.getFile());
510        rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry));
511    }
512
513    /**
514     * Lists the file entries in {@code file}.
515     *
516     * @param file  The directory to list.
517     * @param entry the parent entry.
518     * @return The child file entries.
519     */
520    private FileEntry[] listFileEntries(final File file, final FileEntry entry) {
521        return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new);
522    }
523
524    /**
525     * Lists the contents of a directory.
526     *
527     * @param directory The directory to list.
528     * @return the directory contents or a zero length array if the empty or the file is not a directory
529     */
530    private File[] listFiles(final File directory) {
531        return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY;
532    }
533
534    /**
535     * Removes a file system listener.
536     *
537     * @param listener The file system listener.
538     */
539    public void removeListener(final FileAlterationListener listener) {
540        if (listener != null) {
541            listeners.removeIf(listener::equals);
542        }
543    }
544
545    private File[] sort(final File[] files) {
546        if (files == null) {
547            return FileUtils.EMPTY_FILE_ARRAY;
548        }
549        if (files.length > 1) {
550            Arrays.sort(files, comparator);
551        }
552        return files;
553    }
554
555    /**
556     * Returns a String representation of this observer.
557     *
558     * @return a String representation of this observer.
559     */
560    @Override
561    public String toString() {
562        final StringBuilder builder = new StringBuilder();
563        builder.append(getClass().getSimpleName());
564        builder.append("[file='");
565        builder.append(getDirectory().getPath());
566        builder.append('\'');
567        builder.append(", ");
568        builder.append(fileFilter.toString());
569        builder.append(", listeners=");
570        builder.append(listeners.size());
571        builder.append("]");
572        return builder.toString();
573    }
574
575}