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