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 }