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.time.Duration;
20  import java.util.HashMap;
21  import java.util.Map;
22  import java.util.Stack;
23  import java.util.concurrent.ThreadFactory;
24  import java.util.stream.Stream;
25  
26  import org.apache.commons.lang3.concurrent.BasicThreadFactory;
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.apache.commons.vfs2.FileListener;
30  import org.apache.commons.vfs2.FileMonitor;
31  import org.apache.commons.vfs2.FileName;
32  import org.apache.commons.vfs2.FileObject;
33  import org.apache.commons.vfs2.FileSystemException;
34  import org.apache.commons.vfs2.provider.AbstractFileSystem;
35  
36  /**
37   * A polling {@link FileMonitor} implementation.
38   * <p>
39   * The DefaultFileMonitor is a Thread based polling file system monitor with a 1 second delay.
40   * </p>
41   *
42   * <h2>Design:</h2>
43   * <p>
44   * There is a Map of monitors known as FileMonitorAgents. With the thread running, each FileMonitorAgent object is asked
45   * to "check" on the file it is responsible for. To do this check, the cache is cleared.
46   * </p>
47   * <ul>
48   * <li>If the file existed before the refresh and it no longer exists, a delete event is fired.</li>
49   * <li>If the file existed before the refresh and it still exists, check the last modified timestamp to see if that has
50   * 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 re-build its list of children, so that they can
55   * be accurately checked when there are new children.
56   * </p>
57   * <p>
58   * New files are detected during each "check" as each file does a check for new children. If new children are found,
59   * create events are fired recursively if recursive descent is enabled.
60   * </p>
61   * <p>
62   * For performance reasons, added a delay that increases as the number of files monitored increases. The default is a
63   * delay of 1 second for every 1000 files processed.
64   * </p>
65   * <h2>Example usage:</h2>
66   *
67   * <pre>
68   * FileSystemManager fsManager = VFS.getManager();
69   * FileObject listenDir = fsManager.resolveFile("/home/username/monitored/");
70   *
71   * DefaultFileMonitor fm = new DefaultFileMonitor(new CustomFileListener());
72   * fm.setRecursive(true);
73   * fm.addFile(listenDir);
74   * fm.start();
75   * </pre>
76   *
77   * <em>(where CustomFileListener is a class that implements the FileListener interface.)</em>
78   */
79  // TODO Add a Builder so we can construct and start.
80  public class DefaultFileMonitor implements Runnable, FileMonitor, AutoCloseable {
81  
82      /**
83       * File monitor agent.
84       */
85      private static final class FileMonitorAgent {
86  
87          private final FileObject fileObject;
88          private final DefaultFileMonitor defaultFileMonitor;
89          private boolean exists;
90          private long timestamp;
91          private Map<FileName, Object> children;
92  
93          private FileMonitorAgent(final DefaultFileMonitor defaultFileMonitor, final FileObject fileObject) {
94              this.defaultFileMonitor = defaultFileMonitor;
95              this.fileObject = fileObject;
96  
97              refresh();
98              resetChildrenList();
99  
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 }