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.vfs.impl;
18  
19  import org.apache.commons.logging.Log;
20  import org.apache.commons.logging.LogFactory;
21  import org.apache.commons.vfs.FileListener;
22  import org.apache.commons.vfs.FileMonitor;
23  import org.apache.commons.vfs.FileName;
24  import org.apache.commons.vfs.FileObject;
25  import org.apache.commons.vfs.FileSystemException;
26  import org.apache.commons.vfs.FileType;
27  import org.apache.commons.vfs.provider.AbstractFileSystem;
28  
29  import java.util.HashMap;
30  import java.util.Map;
31  import java.util.Stack;
32  
33  /***
34   * A polling {@link FileMonitor} implementation.<br />
35   * <br />
36   * The DefaultFileMonitor is a Thread based polling file system monitor with a 1
37   * second delay.<br />
38   * <br />
39   * <b>Design:</b>
40   * <p/>
41   * There is a Map of monitors known as FileMonitorAgents. With the thread running,
42   * each FileMonitorAgent object is asked to "check" on the file it is
43   * responsible for.
44   * To do this check, the cache is cleared.
45   * </p>
46   * <ul>
47   * <li>If the file existed before the refresh and it no longer exists, a delete
48   * event is fired.</li>
49   * <li>If the file existed before the refresh and it still exists, check the
50   * last modified timestamp to see if that has changed.</li>
51   * <li>If it has, fire a change event.</li>
52   * </ul>
53   * <p/>
54   * With each file delete, the FileMonitorAgent of the parent is asked to
55   * re-build its
56   * list of children, so that they can be accurately checked when there are new
57   * children.<br/>
58   * New files are detected during each "check" as each file does a check for new
59   * children.
60   * If new children are found, create events are fired recursively if recursive
61   * descent is
62   * enabled.
63   * </p>
64   * <p/>
65   * For performance reasons, added a delay that increases as the number of files
66   * monitored
67   * increases. The default is a delay of 1 second for every 1000 files processed.
68   * </p>
69   * <p/>
70   * <br /><b>Example usage:</b><pre>
71   * FileSystemManager fsManager = VFS.getManager();
72   * FileObject listendir = fsManager.resolveFile("/home/username/monitored/");
73   * <p/>
74   * DefaultFileMonitor fm = new DefaultFileMonitor(new CustomFileListener());
75   * fm.setRecursive(true);
76   * fm.addFile(listendir);
77   * fm.start();
78   * </pre>
79   * <i>(where CustomFileListener is a class that implements the FileListener
80   * interface.)</i>
81   *
82   * @author <a href="mailto:xknight@users.sourceforge.net">Christopher Ottley</a>
83   * @version $Revision: 484943 $ $Date: 2006-12-09 08:42:06 +0100 (Sa, 09 Dez 2006) $
84   */
85  public class DefaultFileMonitor implements Runnable, FileMonitor
86  {
87      private final static Log log = LogFactory.getLog(DefaultFileMonitor.class);
88  
89      /***
90       * Map from FileName to FileObject being monitored.
91       */
92      private final Map monitorMap = new HashMap();
93  
94      /***
95       * The low priority thread used for checking the files being monitored.
96       */
97      private Thread monitorThread;
98  
99      /***
100      * File objects to be removed from the monitor map.
101      */
102     private Stack deleteStack = new Stack();
103 
104     /***
105      * File objects to be added to the monitor map.
106      */
107     private Stack addStack = new Stack();
108 
109     /***
110      * A flag used to determine if the monitor thread should be running.
111      */
112     private boolean shouldRun = true;
113 
114     /***
115      * A flag used to determine if adding files to be monitored should be recursive.
116      */
117     private boolean recursive = false;
118 
119     /***
120      * Set the delay between checks
121      */
122     private long delay = 1000;
123 
124     /***
125      * Set the number of files to check until a delay will be inserted
126      */
127     private int checksPerRun = 1000;
128 
129     /***
130      * A listener object that if set, is notified on file creation and deletion.
131      */
132     private final FileListener listener;
133 
134     public DefaultFileMonitor(final FileListener listener)
135     {
136         this.listener = listener;
137     }
138 
139     /***
140      * Access method to get the recursive setting when adding files for monitoring.
141      */
142     public boolean isRecursive()
143     {
144         return this.recursive;
145     }
146 
147     /***
148      * Access method to set the recursive setting when adding files for monitoring.
149      */
150     public void setRecursive(final boolean newRecursive)
151     {
152         this.recursive = newRecursive;
153     }
154 
155     /***
156      * Access method to get the current FileListener object notified when there
157      * are changes with the files added.
158      */
159     FileListener getFileListener()
160     {
161         return this.listener;
162     }
163 
164     /***
165      * Adds a file to be monitored.
166      */
167     public void addFile(final FileObject file)
168     {
169         _addFile(file);
170         try
171         {
172             // add all direct children too
173             if (file.getType().hasChildren())
174             {
175                 // Traverse the children
176                 final FileObject[] children = file.getChildren();
177                 for (int i = 0; i < children.length; i++)
178                 {
179                     _addFile(children[i]);
180                 }
181             }
182         }
183         catch (FileSystemException fse)
184         {
185             log.error(fse.getLocalizedMessage(), fse);
186         }
187     }
188 
189     /***
190      * Adds a file to be monitored.
191      */
192     private void _addFile(final FileObject file)
193     {
194         synchronized (this.monitorMap)
195         {
196             if (this.monitorMap.get(file.getName()) == null)
197             {
198                 this.monitorMap.put(file.getName(), new FileMonitorAgent(this,
199                     file));
200 
201                 try
202                 {
203                     if (this.listener != null)
204                     {
205                         file.getFileSystem().addListener(file, this.listener);
206                     }
207 
208                     if (file.getType().hasChildren() && this.recursive)
209                     {
210                         // Traverse the children
211                         final FileObject[] children = file.getChildren();
212                         for (int i = 0; i < children.length; i++)
213                         {
214                             this.addFile(children[i]); // Add depth first
215                         }
216                     }
217 
218                 }
219                 catch (FileSystemException fse)
220                 {
221                     log.error(fse.getLocalizedMessage(), fse);
222                 }
223 
224             }
225         }
226     }
227 
228     /***
229      * Removes a file from being monitored.
230      */
231     public void removeFile(final FileObject file)
232     {
233         synchronized (this.monitorMap)
234         {
235             FileName fn = file.getName();
236             if (this.monitorMap.get(fn) != null)
237             {
238                 FileObject parent;
239                 try
240                 {
241                     parent = file.getParent();
242                 }
243                 catch (FileSystemException fse)
244                 {
245                     parent = null;
246                 }
247 
248                 this.monitorMap.remove(fn);
249 
250                 if (parent != null)
251                 { // Not the root
252                     FileMonitorAgent parentAgent =
253                         (FileMonitorAgent) this.monitorMap.get(parent.getName());
254                     if (parentAgent != null)
255                     {
256                         parentAgent.resetChildrenList();
257                     }
258                 }
259             }
260         }
261     }
262 
263     /***
264      * Queues a file for removal from being monitored.
265      */
266     protected void queueRemoveFile(final FileObject file)
267     {
268         this.deleteStack.push(file);
269     }
270 
271     /***
272      * Get the delay between runs
273      */
274     public long getDelay()
275     {
276         return delay;
277     }
278 
279     /***
280      * Set the delay between runs
281      */
282     public void setDelay(long delay)
283     {
284         if (delay > 0)
285         {
286             this.delay = delay;
287         }
288         else
289         {
290             this.delay = 1000;
291         }
292     }
293 
294     /***
295      * get the number of files to check per run
296      */
297     public int getChecksPerRun()
298     {
299         return checksPerRun;
300     }
301 
302     /***
303      * set the number of files to check per run.
304      * a additional delay will be added if there are more files to check
305      *
306      * @param checksPerRun a value less than 1 will disable this feature
307      */
308     public void setChecksPerRun(int checksPerRun)
309     {
310         this.checksPerRun = checksPerRun;
311     }
312 
313     /***
314      * Queues a file for addition to be monitored.
315      */
316     protected void queueAddFile(final FileObject file)
317     {
318         this.addStack.push(file);
319     }
320 
321     /***
322      * Starts monitoring the files that have been added.
323      */
324     public void start()
325     {
326         if (this.monitorThread == null)
327         {
328             this.monitorThread = new Thread(this);
329             this.monitorThread.setDaemon(true);
330             this.monitorThread.setPriority(Thread.MIN_PRIORITY);
331         }
332         this.monitorThread.start();
333     }
334 
335     /***
336      * Stops monitoring the files that have been added.
337      */
338     public void stop()
339     {
340         this.shouldRun = false;
341     }
342 
343     /***
344      * Asks the agent for each file being monitored to check its file for changes.
345      */
346     public void run()
347     {
348         mainloop:
349         while (!Thread.currentThread().isInterrupted() && this.shouldRun)
350         {
351             while (!this.deleteStack.empty())
352             {
353                 this.removeFile((FileObject) this.deleteStack.pop());
354             }
355 
356             // For each entry in the map
357             Object fileNames[];
358             synchronized (this.monitorMap)
359             {
360                 fileNames = this.monitorMap.keySet().toArray();
361             }
362             for (int iterFileNames = 0; iterFileNames < fileNames.length;
363                  iterFileNames++)
364             {
365                 FileName fileName = (FileName) fileNames[iterFileNames];
366                 FileMonitorAgent agent;
367                 synchronized (this.monitorMap)
368                 {
369                     agent = (FileMonitorAgent) this.monitorMap.get(fileName);
370                 }
371                 if (agent != null)
372                 {
373                     agent.check();
374                 }
375 
376                 if (getChecksPerRun() > 0)
377                 {
378                     if ((iterFileNames % getChecksPerRun()) == 0)
379                     {
380                         try
381                         {
382                             Thread.sleep(getDelay());
383                         }
384                         catch (InterruptedException e)
385                         {
386 
387                         }
388                     }
389                 }
390 
391                 if (Thread.currentThread().isInterrupted() || !this.shouldRun)
392                 {
393                     continue mainloop;
394                 }
395             }
396 
397             while (!this.addStack.empty())
398             {
399                 this.addFile((FileObject) this.addStack.pop());
400             }
401 
402             try
403             {
404                 Thread.sleep(getDelay());
405             }
406             catch (InterruptedException e)
407             {
408                 continue;
409             }
410         }
411 
412         this.shouldRun = true;
413     }
414 
415     /***
416      * File monitor agent.
417      */
418     private static class FileMonitorAgent
419     {
420         private final FileObject file;
421         private final DefaultFileMonitor fm;
422 
423         private boolean exists;
424         private long timestamp;
425         private Map children = null;
426 
427         private FileMonitorAgent(DefaultFileMonitor fm, FileObject file)
428         {
429             this.fm = fm;
430             this.file = file;
431 
432             this.refresh();
433             this.resetChildrenList();
434 
435             try
436             {
437                 this.exists = this.file.exists();
438             }
439             catch (FileSystemException fse)
440             {
441                 this.exists = false;
442             }
443 
444             try
445             {
446                 this.timestamp = this.file.getContent().getLastModifiedTime();
447             }
448             catch (FileSystemException fse)
449             {
450                 this.timestamp = -1;
451             }
452 
453         }
454 
455         private void resetChildrenList()
456         {
457             try
458             {
459                 if (this.file.getType().hasChildren())
460                 {
461                     this.children = new HashMap();
462                     FileObject[] childrenList = this.file.getChildren();
463                     for (int i = 0; i < childrenList.length; i++)
464                     {
465                         this.children.put(childrenList[i].getName(), new
466                             Object()); // null?
467                     }
468                 }
469             }
470             catch (FileSystemException fse)
471             {
472                 this.children = null;
473             }
474         }
475 
476 
477         /***
478          * Clear the cache and re-request the file object
479          */
480         private void refresh()
481         {
482             try
483             {
484 				this.file.refresh();
485 			}
486             catch (FileSystemException fse)
487             {
488                 log.error(fse.getLocalizedMessage(), fse);
489             }
490         }
491 
492 
493         /***
494          * Recursively fires create events for all children if recursive descent is
495          * enabled. Otherwise the create event is only fired for the initial
496          * FileObject.
497          */
498         private void fireAllCreate(FileObject child)
499         {
500             // Add listener so that it can be triggered
501             if (this.fm.getFileListener() != null)
502             {
503                 child.getFileSystem().addListener(child, this.fm.getFileListener());
504             }
505 
506             ((AbstractFileSystem) child.getFileSystem()).fireFileCreated(child);
507 
508             // Remove it because a listener is added in the queueAddFile
509             if (this.fm.getFileListener() != null)
510             {
511                 child.getFileSystem().removeListener(child,
512                     this.fm.getFileListener());
513             }
514 
515             this.fm.queueAddFile(child); // Add
516 
517             try
518             {
519 
520                 if (this.fm.isRecursive())
521                 {
522                     if (child.getType().hasChildren())
523                     {
524                         FileObject[] newChildren = child.getChildren();
525                         for (int i = 0; i < newChildren.length; i++)
526                         {
527                             fireAllCreate(newChildren[i]);
528                         }
529                     }
530                 }
531 
532             }
533             catch (FileSystemException fse)
534             {
535                 log.error(fse.getLocalizedMessage(), fse);
536             }
537         }
538 
539         /***
540          * Only checks for new children. If children are removed, they'll
541          * eventually be checked.
542          */
543         private void checkForNewChildren()
544         {
545             try
546             {
547                 if (this.file.getType().hasChildren())
548                 {
549                     FileObject[] newChildren = this.file.getChildren();
550                     if (this.children != null)
551                     {
552                         // See which new children are not listed in the current children map.
553                         Map newChildrenMap = new HashMap();
554                         Stack missingChildren = new Stack();
555 
556                         for (int i = 0; i < newChildren.length; i++)
557                         {
558                             newChildrenMap.put(newChildren[i].getName(), new
559                                 Object()); // null ?
560                             // If the child's not there
561                             if
562                                 (!this.children.containsKey(newChildren[i].getName()))
563                             {
564                                 missingChildren.push(newChildren[i]);
565                             }
566                         }
567 
568                         this.children = newChildrenMap;
569 
570                         // If there were missing children
571                         if (!missingChildren.empty())
572                         {
573 
574                             while (!missingChildren.empty())
575                             {
576                                 FileObject child = (FileObject)
577                                     missingChildren.pop();
578                                 this.fireAllCreate(child);
579                             }
580                         }
581 
582                     }
583                     else
584                     {
585                         // First set of children - Break out the cigars
586                         if (newChildren.length > 0)
587                         {
588                             this.children = new HashMap();
589                         }
590                         for (int i = 0; i < newChildren.length; i++)
591                         {
592                             this.children.put(newChildren[i].getName(), new
593                                 Object()); // null?
594                             this.fireAllCreate(newChildren[i]);
595                         }
596                     }
597                 }
598             }
599             catch (FileSystemException fse)
600             {
601                 log.error(fse.getLocalizedMessage(), fse);
602             }
603         }
604 
605         private void check()
606         {
607             this.refresh();
608 
609             try
610             {
611                 // If the file existed and now doesn't
612                 if (this.exists && !this.file.exists())
613                 {
614                     this.exists = this.file.exists();
615                     this.timestamp = -1;
616 
617                     // Fire delete event
618 
619                     ((AbstractFileSystem)
620                         this.file.getFileSystem()).fireFileDeleted(this.file);
621 
622                     // Remove listener in case file is re-created. Don't want to fire twice.
623                     if (this.fm.getFileListener() != null)
624                     {
625                         this.file.getFileSystem().removeListener(this.file,
626                             this.fm.getFileListener());
627                     }
628 
629                     // Remove from map
630                     this.fm.queueRemoveFile(this.file);
631                 }
632                 else if (this.exists && this.file.exists())
633                 {
634 
635                     // Check the timestamp to see if it has been modified
636                     if (this.timestamp !=
637                         this.file.getContent().getLastModifiedTime())
638                     {
639                         this.timestamp =
640                             this.file.getContent().getLastModifiedTime();
641                         // Fire change event
642 
643                         // Don't fire if it's a folder because new file children
644                         // and deleted files in a folder have their own event triggered.
645                         if (!this.file.getType().hasChildren())
646                         {
647                             ((AbstractFileSystem)
648                                 this.file.getFileSystem()).fireFileChanged(this.file);
649                         }
650                     }
651 
652                 }
653 
654                 this.checkForNewChildren();
655 
656             }
657             catch (FileSystemException fse)
658             {
659                 log.error(fse.getLocalizedMessage(), fse);
660             }
661         }
662 
663     }
664 
665 }