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.vfs2.impl;
018
019import java.time.Duration;
020import java.util.HashMap;
021import java.util.Map;
022import java.util.Stack;
023import java.util.concurrent.ThreadFactory;
024import java.util.stream.Stream;
025
026import org.apache.commons.lang3.concurrent.BasicThreadFactory;
027import org.apache.commons.logging.Log;
028import org.apache.commons.logging.LogFactory;
029import org.apache.commons.vfs2.FileListener;
030import org.apache.commons.vfs2.FileMonitor;
031import org.apache.commons.vfs2.FileName;
032import org.apache.commons.vfs2.FileObject;
033import org.apache.commons.vfs2.FileSystemException;
034import org.apache.commons.vfs2.provider.AbstractFileSystem;
035
036/**
037 * A polling {@link FileMonitor} implementation.
038 * <p>
039 * The DefaultFileMonitor is a Thread based polling file system monitor with a 1 second delay.
040 * </p>
041 *
042 * <h2>Design:</h2>
043 * <p>
044 * There is a Map of monitors known as FileMonitorAgents. With the thread running, each FileMonitorAgent object is asked
045 * to "check" on the file it is responsible for. To do this check, the cache is cleared.
046 * </p>
047 * <ul>
048 * <li>If the file existed before the refresh and it no longer exists, a delete event is fired.</li>
049 * <li>If the file existed before the refresh and it still exists, check the last modified timestamp to see if that has
050 * changed.</li>
051 * <li>If it has, fire a change event.</li>
052 * </ul>
053 * <p>
054 * With each file delete, the FileMonitorAgent of the parent is asked to re-build its list of children, so that they can
055 * be accurately checked when there are new children.
056 * </p>
057 * <p>
058 * New files are detected during each "check" as each file does a check for new children. If new children are found,
059 * create events are fired recursively if recursive descent is enabled.
060 * </p>
061 * <p>
062 * For performance reasons, added a delay that increases as the number of files monitored increases. The default is a
063 * delay of 1 second for every 1000 files processed.
064 * </p>
065 * <h2>Example usage:</h2>
066 *
067 * <pre>
068 * FileSystemManager fsManager = VFS.getManager();
069 * FileObject listenDir = fsManager.resolveFile("/home/username/monitored/");
070 *
071 * DefaultFileMonitor fm = new DefaultFileMonitor(new CustomFileListener());
072 * fm.setRecursive(true);
073 * fm.addFile(listenDir);
074 * fm.start();
075 * </pre>
076 *
077 * <em>(where CustomFileListener is a class that implements the FileListener interface.)</em>
078 */
079// TODO Add a Builder so we can construct and start.
080public class DefaultFileMonitor implements Runnable, FileMonitor, AutoCloseable {
081
082    /**
083     * File monitor agent.
084     */
085    private static final class FileMonitorAgent {
086
087        private final FileObject fileObject;
088        private final DefaultFileMonitor defaultFileMonitor;
089        private boolean exists;
090        private long timestamp;
091        private Map<FileName, Object> children;
092
093        private FileMonitorAgent(final DefaultFileMonitor defaultFileMonitor, final FileObject fileObject) {
094            this.defaultFileMonitor = defaultFileMonitor;
095            this.fileObject = fileObject;
096
097            refresh();
098            resetChildrenList();
099
100            try {
101                exists = fileObject.exists();
102            } catch (final FileSystemException fse) {
103                exists = false;
104                timestamp = -1;
105            }
106
107            if (exists) {
108                try {
109                    timestamp = fileObject.getContent().getLastModifiedTime();
110                } catch (final FileSystemException fse) {
111                    timestamp = -1;
112                }
113            }
114        }
115
116        private void check() {
117            refresh();
118
119            try {
120                // If the file existed and now doesn't
121                if (exists && !fileObject.exists()) {
122                    exists = fileObject.exists();
123                    timestamp = -1;
124
125                    // Fire delete event
126
127                    ((AbstractFileSystem) fileObject.getFileSystem()).fireFileDeleted(fileObject);
128
129                    // Remove listener in case file is re-created. Don't want to fire twice.
130                    if (defaultFileMonitor.getFileListener() != null) {
131                        fileObject.getFileSystem().removeListener(fileObject, defaultFileMonitor.getFileListener());
132                    }
133
134                    // Remove from map
135                    defaultFileMonitor.queueRemoveFile(fileObject);
136                } else if (exists && fileObject.exists()) {
137
138                    // Check the timestamp to see if it has been modified
139                    if (timestamp != fileObject.getContent().getLastModifiedTime()) {
140                        timestamp = fileObject.getContent().getLastModifiedTime();
141                        // Fire change event
142
143                        // Don't fire if it's a folder because new file children
144                        // and deleted files in a folder have their own event triggered.
145                        if (!fileObject.getType().hasChildren()) {
146                            ((AbstractFileSystem) fileObject.getFileSystem()).fireFileChanged(fileObject);
147                        }
148                    }
149
150                } else if (!exists && fileObject.exists()) {
151                    exists = fileObject.exists();
152                    timestamp = fileObject.getContent().getLastModifiedTime();
153                    // Don't fire if it's a folder because new file children
154                    // and deleted files in a folder have their own event triggered.
155                    if (!fileObject.getType().hasChildren()) {
156                        ((AbstractFileSystem) fileObject.getFileSystem()).fireFileCreated(fileObject);
157                    }
158                }
159
160                checkForNewChildren();
161
162            } catch (final FileSystemException fse) {
163                LOG.error(fse.getLocalizedMessage(), fse);
164            }
165        }
166
167        /**
168         * Only checks for new children. If children are removed, they'll eventually be checked.
169         */
170        private void checkForNewChildren() {
171            try {
172                if (fileObject.getType().hasChildren()) {
173                    final FileObject[] newChildren = fileObject.getChildren();
174                    if (children != null) {
175                        // See which new children are not listed in the current children map.
176                        final Map<FileName, Object> newChildrenMap = new HashMap<>();
177                        final Stack<FileObject> missingChildren = new Stack<>();
178
179                        for (final FileObject element : newChildren) {
180                            newChildrenMap.put(element.getName(), new Object()); // null ?
181                            // If the child's not there
182                            if (!children.containsKey(element.getName())) {
183                                missingChildren.push(element);
184                            }
185                        }
186
187                        children = newChildrenMap;
188
189                        // If there were missing children
190                        if (!missingChildren.empty()) {
191
192                            while (!missingChildren.empty()) {
193                                fireAllCreate(missingChildren.pop());
194                            }
195                        }
196
197                    } else if (newChildren.length > 0) {
198                        // First set of children - Break out the cigars
199                        children = new HashMap<>();
200                        for (final FileObject element : newChildren) {
201                            children.put(element.getName(), new Object()); // null?
202                            fireAllCreate(element);
203                        }
204                    }
205                }
206            } catch (final FileSystemException fse) {
207                LOG.error(fse.getLocalizedMessage(), fse);
208            }
209        }
210
211        /**
212         * Recursively fires create events for all children if recursive descent is enabled. Otherwise the create event is only
213         * fired for the initial FileObject.
214         *
215         * @param child The child to add.
216         */
217        private void fireAllCreate(final FileObject child) {
218            // Add listener so that it can be triggered
219            if (defaultFileMonitor.getFileListener() != null) {
220                child.getFileSystem().addListener(child, defaultFileMonitor.getFileListener());
221            }
222
223            ((AbstractFileSystem) child.getFileSystem()).fireFileCreated(child);
224
225            // Remove it because a listener is added in the queueAddFile
226            if (defaultFileMonitor.getFileListener() != null) {
227                child.getFileSystem().removeListener(child, defaultFileMonitor.getFileListener());
228            }
229
230            defaultFileMonitor.queueAddFile(child); // Add
231
232            try {
233                if (defaultFileMonitor.isRecursive() && child.getType().hasChildren()) {
234                    Stream.of(child.getChildren()).forEach(this::fireAllCreate);
235                }
236            } catch (final FileSystemException fse) {
237                LOG.error(fse.getLocalizedMessage(), fse);
238            }
239        }
240
241        /**
242         * Clear the cache and re-request the file object.
243         */
244        private void refresh() {
245            try {
246                fileObject.refresh();
247            } catch (final FileSystemException fse) {
248                LOG.error(fse.getLocalizedMessage(), fse);
249            }
250        }
251
252        private void resetChildrenList() {
253            try {
254                if (fileObject.getType().hasChildren()) {
255                    children = new HashMap<>();
256                    for (final FileObject element : fileObject.getChildren()) {
257                        children.put(element.getName(), new Object()); // null?
258                    }
259                }
260            } catch (final FileSystemException fse) {
261                children = null;
262            }
263        }
264
265    }
266
267    private static final ThreadFactory THREAD_FACTORY = new BasicThreadFactory.Builder().daemon(true).priority(Thread.MIN_PRIORITY).build();
268
269    private static final Log LOG = LogFactory.getLog(DefaultFileMonitor.class);
270
271    private static final Duration DEFAULT_DELAY = Duration.ofSeconds(1);
272
273    private static final int DEFAULT_MAX_FILES = 1000;
274
275    /**
276     * Map from FileName to FileObject being monitored.
277     */
278    private final Map<FileName, FileMonitorAgent> monitorMap = new HashMap<>();
279
280    /**
281     * The low priority thread used for checking the files being monitored.
282     */
283    private Thread monitorThread;
284
285    /**
286     * File objects to be removed from the monitor map.
287     */
288    private final Stack<FileObject> deleteStack = new Stack<>();
289
290    /**
291     * File objects to be added to the monitor map.
292     */
293    private final Stack<FileObject> addStack = new Stack<>();
294
295    /**
296     * A flag used to determine if the monitor thread should be running.
297     */
298    private volatile boolean runFlag = true; // used for inter-thread communication
299
300    /**
301     * A flag used to determine if adding files to be monitored should be recursive.
302     */
303    private boolean recursive;
304
305    /**
306     * Sets the delay between checks
307     */
308    private Duration delay = DEFAULT_DELAY;
309
310    /**
311     * Sets the number of files to check until a delay will be inserted
312     */
313    private int checksPerRun = DEFAULT_MAX_FILES;
314
315    /**
316     * A listener object that if set, is notified on file creation and deletion.
317     */
318    private final FileListener listener;
319
320    /**
321     * Constructs a new instance with the given listener.
322     *
323     * @param listener the listener.
324     */
325    public DefaultFileMonitor(final FileListener listener) {
326        this.listener = listener;
327    }
328
329    /**
330     * Adds a file to be monitored.
331     *
332     * @param file The FileObject to monitor.
333     */
334    @Override
335    public void addFile(final FileObject file) {
336        synchronized (monitorMap) {
337            if (monitorMap.get(file.getName()) == null) {
338                monitorMap.put(file.getName(), new FileMonitorAgent(this, file));
339
340                try {
341                    if (listener != null) {
342                        file.getFileSystem().addListener(file, listener);
343                    }
344
345                    if (file.getType().hasChildren() && recursive) {
346                        // Traverse the children
347                        // Add depth first
348                        Stream.of(file.getChildren()).forEach(this::addFile);
349                    }
350
351                } catch (final FileSystemException fse) {
352                    LOG.error(fse.getLocalizedMessage(), fse);
353                }
354
355            }
356        }
357    }
358
359    @Override
360    public void close() {
361        runFlag = false;
362        if (monitorThread != null) {
363            monitorThread.interrupt();
364            try {
365                monitorThread.join();
366            } catch (final InterruptedException e) {
367                // ignore
368            }
369            monitorThread = null;
370        }
371    }
372
373    /**
374     * Gets the number of files to check per run.
375     *
376     * @return The number of files to check per iteration.
377     */
378    public int getChecksPerRun() {
379        return checksPerRun;
380    }
381
382    /**
383     * Gets the delay between runs.
384     *
385     * @return The delay period in milliseconds.
386     * @deprecated Use {@link #getDelayDuration()}.
387     */
388    @Deprecated
389    public long getDelay() {
390        return delay.toMillis();
391    }
392
393    /**
394     * Gets the delay between runs.
395     *
396     * @return The delay period.
397     */
398    public Duration getDelayDuration() {
399        return delay;
400    }
401
402    /**
403     * Gets the current FileListener object notified when there are changes with the files added.
404     *
405     * @return The FileListener.
406     */
407    FileListener getFileListener() {
408        return listener;
409    }
410
411    /**
412     * Tests the recursive setting when adding files for monitoring.
413     *
414     * @return true if monitoring is enabled for children.
415     */
416    public boolean isRecursive() {
417        return recursive;
418    }
419
420    /**
421     * Queues a file for addition to be monitored.
422     *
423     * @param file The FileObject to add.
424     */
425    protected void queueAddFile(final FileObject file) {
426        addStack.push(file);
427    }
428
429    /**
430     * Queues a file for removal from being monitored.
431     *
432     * @param file The FileObject to be removed from being monitored.
433     */
434    protected void queueRemoveFile(final FileObject file) {
435        deleteStack.push(file);
436    }
437
438    /**
439     * Removes a file from being monitored.
440     *
441     * @param file The FileObject to remove from monitoring.
442     */
443    @Override
444    public void removeFile(final FileObject file) {
445        synchronized (monitorMap) {
446            final FileName fn = file.getName();
447            if (monitorMap.get(fn) != null) {
448                FileObject parent;
449                try {
450                    parent = file.getParent();
451                } catch (final FileSystemException fse) {
452                    parent = null;
453                }
454
455                monitorMap.remove(fn);
456
457                if (parent != null) { // Not the root
458                    final FileMonitorAgent parentAgent = monitorMap.get(parent.getName());
459                    if (parentAgent != null) {
460                        parentAgent.resetChildrenList();
461                    }
462                }
463            }
464        }
465    }
466
467    /**
468     * Asks the agent for each file being monitored to check its file for changes.
469     */
470    @Override
471    public void run() {
472        mainloop: while (!monitorThread.isInterrupted() && runFlag) {
473            // For each entry in the map
474            final Object[] fileNames;
475            synchronized (monitorMap) {
476                fileNames = monitorMap.keySet().toArray();
477            }
478            for (int iterFileNames = 0; iterFileNames < fileNames.length; iterFileNames++) {
479                final FileName fileName = (FileName) fileNames[iterFileNames];
480                final FileMonitorAgent agent;
481                synchronized (monitorMap) {
482                    agent = monitorMap.get(fileName);
483                }
484                if (agent != null) {
485                    agent.check();
486                }
487
488                if (getChecksPerRun() > 0 && (iterFileNames + 1) % getChecksPerRun() == 0) {
489                    try {
490                        Thread.sleep(getDelayDuration().toMillis());
491                    } catch (final InterruptedException e) {
492                        // Woke up.
493                    }
494                }
495
496                if (monitorThread.isInterrupted() || !runFlag) {
497                    continue mainloop;
498                }
499            }
500
501            while (!addStack.empty()) {
502                addFile(addStack.pop());
503            }
504
505            while (!deleteStack.empty()) {
506                removeFile(deleteStack.pop());
507            }
508
509            try {
510                Thread.sleep(getDelayDuration().toMillis());
511            } catch (final InterruptedException e) {
512                continue;
513            }
514        }
515
516        runFlag = true;
517    }
518
519    /**
520     * Sets the number of files to check per run. An additional delay will be added if there are more files to check.
521     *
522     * @param checksPerRun a value less than 1 will disable this feature
523     */
524    public void setChecksPerRun(final int checksPerRun) {
525        this.checksPerRun = checksPerRun;
526    }
527
528    /**
529     * Sets the delay between runs.
530     *
531     * @param delay The delay period.
532     * @since 2.10.0
533     */
534    public void setDelay(final Duration delay) {
535        this.delay = delay == null || delay.isNegative() ? DEFAULT_DELAY : delay;
536    }
537
538    /**
539     * Sets the delay between runs.
540     *
541     * @param delay The delay period in milliseconds.
542     * @deprecated Use {@link #setDelay(Duration)}.
543     */
544    @Deprecated
545    public void setDelay(final long delay) {
546        setDelay(delay > 0 ? Duration.ofMillis(delay) : DEFAULT_DELAY);
547    }
548
549    /**
550     * Sets the recursive setting when adding files for monitoring.
551     *
552     * @param newRecursive true if monitoring should be enabled for children.
553     */
554    public void setRecursive(final boolean newRecursive) {
555        recursive = newRecursive;
556    }
557
558    /**
559     * Starts monitoring the files that have been added.
560     */
561    public synchronized void start() {
562        if (monitorThread == null) {
563            monitorThread = THREAD_FACTORY.newThread(this);
564        }
565        monitorThread.start();
566    }
567
568    /**
569     * Stops monitoring the files that have been added.
570     */
571    public synchronized void stop() {
572        close();
573    }
574}