View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.impl;
18  
19  import java.util.HashMap;
20  import java.util.Map;
21  import java.util.Stack;
22  
23  import org.apache.commons.logging.Log;
24  import org.apache.commons.logging.LogFactory;
25  import org.apache.commons.vfs2.FileListener;
26  import org.apache.commons.vfs2.FileMonitor;
27  import org.apache.commons.vfs2.FileName;
28  import org.apache.commons.vfs2.FileObject;
29  import org.apache.commons.vfs2.FileSystemException;
30  import org.apache.commons.vfs2.provider.AbstractFileSystem;
31  
32  /**
33   * A polling {@link FileMonitor} implementation.
34   * <p>
35   * The DefaultFileMonitor is a Thread based polling file system monitor with a 1 second delay.
36   *
37   * <h2>Design:</h2>
38   *
39   * There is a Map of monitors known as FileMonitorAgents. With the thread running, each FileMonitorAgent object is asked
40   * to "check" on the file it is responsible for. To do this check, the cache is cleared.
41   * <ul>
42   * <li>If the file existed before the refresh and it no longer exists, a delete event is fired.</li>
43   * <li>If the file existed before the refresh and it still exists, check the last modified timestamp to see if that has
44   * changed.</li>
45   * <li>If it has, fire a change event.</li>
46   * </ul>
47   * With each file delete, the FileMonitorAgent of the parent is asked to re-build its list of children, so that they can
48   * be accurately checked when there are new children.
49   * <p>
50   * New files are detected during each "check" as each file does a check for new children. If new children are found,
51   * create events are fired recursively if recursive descent is enabled.
52   * <p>
53   * For performance reasons, added a delay that increases as the number of files monitored increases. The default is a
54   * delay of 1 second for every 1000 files processed.
55   *
56   * <h2>Example usage:</h2>
57   *
58   * <pre>
59   * FileSystemManager fsManager = VFS.getManager();
60   * FileObject listendir = fsManager.resolveFile("/home/username/monitored/");
61   *
62   * DefaultFileMonitor fm = new DefaultFileMonitor(new CustomFileListener());
63   * fm.setRecursive(true);
64   * fm.addFile(listendir);
65   * fm.start();
66   * </pre>
67   *
68   * <i>(where CustomFileListener is a class that implements the FileListener interface.)</i>
69   */
70  public class DefaultFileMonitor implements Runnable, FileMonitor {
71      private static final Log LOG = LogFactory.getLog(DefaultFileMonitor.class);
72  
73      private static final long DEFAULT_DELAY = 1000;
74  
75      private static final int DEFAULT_MAX_FILES = 1000;
76  
77      /**
78       * Map from FileName to FileObject being monitored.
79       */
80      private final Map<FileName, FileMonitorAgent> monitorMap = new HashMap<>();
81  
82      /**
83       * The low priority thread used for checking the files being monitored.
84       */
85      private Thread monitorThread;
86  
87      /**
88       * File objects to be removed from the monitor map.
89       */
90      private final Stack<FileObject> deleteStack = new Stack<>();
91  
92      /**
93       * File objects to be added to the monitor map.
94       */
95      private final Stack<FileObject> addStack = new Stack<>();
96  
97      /**
98       * A flag used to determine if the monitor thread should be running.
99       */
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     /**
123      * Creates a new instance with the given listener.
124      *
125      * @param listener the listener.
126      */
127     public DefaultFileMonitor(final FileListener listener) {
128         this.listener = listener;
129     }
130 
131     /**
132      * Access method to get the recursive setting when adding files for monitoring.
133      *
134      * @return true if monitoring is enabled for children.
135      */
136     public boolean isRecursive() {
137         return this.recursive;
138     }
139 
140     /**
141      * Access method to set the recursive setting when adding files for monitoring.
142      *
143      * @param newRecursive true if monitoring should be enabled for children.
144      */
145     public void setRecursive(final boolean newRecursive) {
146         this.recursive = newRecursive;
147     }
148 
149     /**
150      * Access method to get the current FileListener object notified when there are changes with the files added.
151      *
152      * @return The FileListener.
153      */
154     FileListener getFileListener() {
155         return this.listener;
156     }
157 
158     /**
159      * Adds a file to be monitored.
160      *
161      * @param file The FileObject to monitor.
162      */
163     @Override
164     public void addFile(final FileObject file) {
165         synchronized (this.monitorMap) {
166             if (this.monitorMap.get(file.getName()) == null) {
167                 this.monitorMap.put(file.getName(), new FileMonitorAgent(this, file));
168 
169                 try {
170                     if (this.listener != null) {
171                         file.getFileSystem().addListener(file, this.listener);
172                     }
173 
174                     if (file.getType().hasChildren() && this.recursive) {
175                         // Traverse the children
176                         final FileObject[] children = file.getChildren();
177                         for (final FileObject element : children) {
178                             this.addFile(element); // Add depth first
179                         }
180                     }
181 
182                 } catch (final FileSystemException fse) {
183                     LOG.error(fse.getLocalizedMessage(), fse);
184                 }
185 
186             }
187         }
188     }
189 
190     /**
191      * Removes a file from being monitored.
192      *
193      * @param file The FileObject to remove from monitoring.
194      */
195     @Override
196     public void removeFile(final FileObject file) {
197         synchronized (this.monitorMap) {
198             final FileName fn = file.getName();
199             if (this.monitorMap.get(fn) != null) {
200                 FileObject parent;
201                 try {
202                     parent = file.getParent();
203                 } catch (final FileSystemException fse) {
204                     parent = null;
205                 }
206 
207                 this.monitorMap.remove(fn);
208 
209                 if (parent != null) { // Not the root
210                     final FileMonitorAgent parentAgent = this.monitorMap.get(parent.getName());
211                     if (parentAgent != null) {
212                         parentAgent.resetChildrenList();
213                     }
214                 }
215             }
216         }
217     }
218 
219     /**
220      * Queues a file for removal from being monitored.
221      *
222      * @param file The FileObject to be removed from being monitored.
223      */
224     protected void queueRemoveFile(final FileObject file) {
225         this.deleteStack.push(file);
226     }
227 
228     /**
229      * Get the delay between runs.
230      *
231      * @return The delay period.
232      */
233     public long getDelay() {
234         return delay;
235     }
236 
237     /**
238      * Set the delay between runs.
239      *
240      * @param delay The delay period.
241      */
242     public void setDelay(final long delay) {
243         this.delay = delay > 0 ? delay : DEFAULT_DELAY;
244     }
245 
246     /**
247      * get the number of files to check per run.
248      *
249      * @return The number of files to check per iteration.
250      */
251     public int getChecksPerRun() {
252         return checksPerRun;
253     }
254 
255     /**
256      * set the number of files to check per run. a additional delay will be added if there are more files to check
257      *
258      * @param checksPerRun a value less than 1 will disable this feature
259      */
260     public void setChecksPerRun(final int checksPerRun) {
261         this.checksPerRun = checksPerRun;
262     }
263 
264     /**
265      * Queues a file for addition to be monitored.
266      *
267      * @param file The FileObject to add.
268      */
269     protected void queueAddFile(final FileObject file) {
270         this.addStack.push(file);
271     }
272 
273     /**
274      * Starts monitoring the files that have been added.
275      */
276     public void start() {
277         if (this.monitorThread == null) {
278             this.monitorThread = new Thread(this);
279             this.monitorThread.setDaemon(true);
280             this.monitorThread.setPriority(Thread.MIN_PRIORITY);
281         }
282         this.monitorThread.start();
283     }
284 
285     /**
286      * Stops monitoring the files that have been added.
287      */
288     public void stop() {
289         this.shouldRun = false;
290         if (this.monitorThread != null) {
291             this.monitorThread.interrupt();
292             try {
293                 this.monitorThread.join();
294             } catch (final InterruptedException e) {
295                 // ignore
296             }
297             this.monitorThread = null;
298         }
299     }
300 
301     /**
302      * Asks the agent for each file being monitored to check its file for changes.
303      */
304     @Override
305     public void run() {
306         mainloop: while (!monitorThread.isInterrupted() && this.shouldRun) {
307             // For each entry in the map
308             final Object[] fileNames;
309             synchronized (this.monitorMap) {
310                 fileNames = this.monitorMap.keySet().toArray();
311             }
312             for (int iterFileNames = 0; iterFileNames < fileNames.length; iterFileNames++) {
313                 final FileName./../../../org/apache/commons/vfs2/FileName.html#FileName">FileName fileName = (FileName) fileNames[iterFileNames];
314                 final FileMonitorAgent agent;
315                 synchronized (this.monitorMap) {
316                     agent = this.monitorMap.get(fileName);
317                 }
318                 if (agent != null) {
319                     agent.check();
320                 }
321 
322                 if (getChecksPerRun() > 0 && (iterFileNames + 1) % getChecksPerRun() == 0) {
323                     try {
324                         Thread.sleep(getDelay());
325                     } catch (final InterruptedException e) {
326                         // Woke up.
327                     }
328                 }
329 
330                 if (monitorThread.isInterrupted() || !this.shouldRun) {
331                     continue mainloop;
332                 }
333             }
334 
335             while (!this.addStack.empty()) {
336                 this.addFile(this.addStack.pop());
337             }
338 
339             while (!this.deleteStack.empty()) {
340                 this.removeFile(this.deleteStack.pop());
341             }
342 
343             try {
344                 Thread.sleep(getDelay());
345             } catch (final InterruptedException e) {
346                 continue;
347             }
348         }
349 
350         this.shouldRun = true;
351     }
352 
353     /**
354      * File monitor agent.
355      */
356     private static final class FileMonitorAgent {
357         private final FileObject fileObject;
358         private final DefaultFileMonitor defaultFileMonitor;
359 
360         private boolean exists;
361         private long timestamp;
362         private Map<FileName, Object> children;
363 
364         private FileMonitorAgent(final DefaultFileMonitor fm, final FileObject file) {
365             this.defaultFileMonitor = fm;
366             this.fileObject = file;
367 
368             this.refresh();
369             this.resetChildrenList();
370 
371             try {
372                 this.exists = this.fileObject.exists();
373             } catch (final FileSystemException fse) {
374                 this.exists = false;
375                 this.timestamp = -1;
376             }
377 
378             if (this.exists) {
379                 try {
380                     this.timestamp = this.fileObject.getContent().getLastModifiedTime();
381                 } catch (final FileSystemException fse) {
382                     this.timestamp = -1;
383                 }
384             }
385         }
386 
387         private void resetChildrenList() {
388             try {
389                 if (this.fileObject.getType().hasChildren()) {
390                     this.children = new HashMap<>();
391                     final FileObject[] childrenList = this.fileObject.getChildren();
392                     for (final FileObject element : childrenList) {
393                         this.children.put(element.getName(), new Object()); // null?
394                     }
395                 }
396             } catch (final FileSystemException fse) {
397                 this.children = null;
398             }
399         }
400 
401         /**
402          * Clear the cache and re-request the file object
403          */
404         private void refresh() {
405             try {
406                 this.fileObject.refresh();
407             } catch (final FileSystemException fse) {
408                 LOG.error(fse.getLocalizedMessage(), fse);
409             }
410         }
411 
412         /**
413          * Recursively fires create events for all children if recursive descent is enabled. Otherwise the create event
414          * is only fired for the initial FileObject.
415          *
416          * @param child The child to add.
417          */
418         private void fireAllCreate(final FileObject child) {
419             // Add listener so that it can be triggered
420             if (this.defaultFileMonitor.getFileListener() != null) {
421                 child.getFileSystem().addListener(child, this.defaultFileMonitor.getFileListener());
422             }
423 
424             ((AbstractFileSystem) child.getFileSystem()).fireFileCreated(child);
425 
426             // Remove it because a listener is added in the queueAddFile
427             if (this.defaultFileMonitor.getFileListener() != null) {
428                 child.getFileSystem().removeListener(child, this.defaultFileMonitor.getFileListener());
429             }
430 
431             this.defaultFileMonitor.queueAddFile(child); // Add
432 
433             try {
434                 if (this.defaultFileMonitor.isRecursive() && child.getType().hasChildren()) {
435                     final FileObject[] newChildren = child.getChildren();
436                     for (final FileObject element : newChildren) {
437                         fireAllCreate(element);
438                     }
439                 }
440             } catch (final FileSystemException fse) {
441                 LOG.error(fse.getLocalizedMessage(), fse);
442             }
443         }
444 
445         /**
446          * Only checks for new children. If children are removed, they'll eventually be checked.
447          */
448         private void checkForNewChildren() {
449             try {
450                 if (this.fileObject.getType().hasChildren()) {
451                     final FileObject[] newChildren = this.fileObject.getChildren();
452                     if (this.children != null) {
453                         // See which new children are not listed in the current children map.
454                         final Map<FileName, Object> newChildrenMap = new HashMap<>();
455                         final Stack<FileObject> missingChildren = new Stack<>();
456 
457                         for (final FileObject element : newChildren) {
458                             newChildrenMap.put(element.getName(), new Object()); // null ?
459                             // If the child's not there
460                             if (!this.children.containsKey(element.getName())) {
461                                 missingChildren.push(element);
462                             }
463                         }
464 
465                         this.children = newChildrenMap;
466 
467                         // If there were missing children
468                         if (!missingChildren.empty()) {
469 
470                             while (!missingChildren.empty()) {
471                                 final FileObject child = missingChildren.pop();
472                                 this.fireAllCreate(child);
473                             }
474                         }
475 
476                     } else if (newChildren.length > 0) {
477                         // First set of children - Break out the cigars
478                         this.children = new HashMap<>();
479                         for (final FileObject element : newChildren) {
480                             this.children.put(element.getName(), new Object()); // null?
481                             this.fireAllCreate(element);
482                         }
483                     }
484                 }
485             } catch (final FileSystemException fse) {
486                 LOG.error(fse.getLocalizedMessage(), fse);
487             }
488         }
489 
490         private void check() {
491             this.refresh();
492 
493             try {
494                 // If the file existed and now doesn't
495                 if (this.exists && !this.fileObject.exists()) {
496                     this.exists = this.fileObject.exists();
497                     this.timestamp = -1;
498 
499                     // Fire delete event
500 
501                     ((AbstractFileSystem) this.fileObject.getFileSystem()).fireFileDeleted(this.fileObject);
502 
503                     // Remove listener in case file is re-created. Don't want to fire twice.
504                     if (this.defaultFileMonitor.getFileListener() != null) {
505                         this.fileObject.getFileSystem().removeListener(this.fileObject, this.defaultFileMonitor.getFileListener());
506                     }
507 
508                     // Remove from map
509                     this.defaultFileMonitor.queueRemoveFile(this.fileObject);
510                 } else if (this.exists && this.fileObject.exists()) {
511 
512                     // Check the timestamp to see if it has been modified
513                     if (this.timestamp != this.fileObject.getContent().getLastModifiedTime()) {
514                         this.timestamp = this.fileObject.getContent().getLastModifiedTime();
515                         // Fire change event
516 
517                         // Don't fire if it's a folder because new file children
518                         // and deleted files in a folder have their own event triggered.
519                         if (!this.fileObject.getType().hasChildren()) {
520                             ((AbstractFileSystem) this.fileObject.getFileSystem()).fireFileChanged(this.fileObject);
521                         }
522                     }
523 
524                 } else if (!this.exists && this.fileObject.exists()) {
525                     this.exists = this.fileObject.exists();
526                     this.timestamp = this.fileObject.getContent().getLastModifiedTime();
527                     // Don't fire if it's a folder because new file children
528                     // and deleted files in a folder have their own event triggered.
529                     if (!this.fileObject.getType().hasChildren()) {
530                         ((AbstractFileSystem) this.fileObject.getFileSystem()).fireFileCreated(this.fileObject);
531                     }
532                 }
533 
534                 this.checkForNewChildren();
535 
536             } catch (final FileSystemException fse) {
537                 LOG.error(fse.getLocalizedMessage(), fse);
538             }
539         }
540 
541     }
542 }