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