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.time.Duration; 020import java.util.HashMap; 021import java.util.Map; 022import java.util.Stack; 023import java.util.concurrent.ThreadFactory; 024import java.util.stream.Stream; 025 026import org.apache.commons.lang3.concurrent.BasicThreadFactory; 027import org.apache.commons.logging.Log; 028import org.apache.commons.logging.LogFactory; 029import org.apache.commons.vfs2.FileListener; 030import org.apache.commons.vfs2.FileMonitor; 031import org.apache.commons.vfs2.FileName; 032import org.apache.commons.vfs2.FileObject; 033import org.apache.commons.vfs2.FileSystemException; 034import org.apache.commons.vfs2.provider.AbstractFileSystem; 035 036/** 037 * A polling {@link FileMonitor} implementation. 038 * <p> 039 * The DefaultFileMonitor is a Thread based polling file system monitor with a 1 second delay. 040 * </p> 041 * 042 * <h2>Design:</h2> 043 * <p> 044 * There is a Map of monitors known as FileMonitorAgents. With the thread running, each FileMonitorAgent object is asked 045 * to "check" on the file it is responsible for. To do this check, the cache is cleared. 046 * </p> 047 * <ul> 048 * <li>If the file existed before the refresh and it no longer exists, a delete event is fired.</li> 049 * <li>If the file existed before the refresh and it still exists, check the last modified timestamp to see if that has 050 * changed.</li> 051 * <li>If it has, fire a change event.</li> 052 * </ul> 053 * <p> 054 * With each file delete, the FileMonitorAgent of the parent is asked to re-build its list of children, so that they can 055 * be accurately checked when there are new children. 056 * </p> 057 * <p> 058 * New files are detected during each "check" as each file does a check for new children. If new children are found, 059 * create events are fired recursively if recursive descent is enabled. 060 * </p> 061 * <p> 062 * For performance reasons, added a delay that increases as the number of files monitored increases. The default is a 063 * delay of 1 second for every 1000 files processed. 064 * </p> 065 * <h2>Example usage:</h2> 066 * 067 * <pre> 068 * FileSystemManager fsManager = VFS.getManager(); 069 * FileObject listenDir = fsManager.resolveFile("/home/username/monitored/"); 070 * 071 * DefaultFileMonitor fm = new DefaultFileMonitor(new CustomFileListener()); 072 * fm.setRecursive(true); 073 * fm.addFile(listenDir); 074 * fm.start(); 075 * </pre> 076 * 077 * <em>(where CustomFileListener is a class that implements the FileListener interface.)</em> 078 */ 079// TODO Add a Builder so we can construct and start. 080public class DefaultFileMonitor implements Runnable, FileMonitor, AutoCloseable { 081 082 /** 083 * File monitor agent. 084 */ 085 private static final class FileMonitorAgent { 086 087 private final FileObject fileObject; 088 private final DefaultFileMonitor defaultFileMonitor; 089 private boolean exists; 090 private long timestamp; 091 private Map<FileName, Object> children; 092 093 private FileMonitorAgent(final DefaultFileMonitor defaultFileMonitor, final FileObject fileObject) { 094 this.defaultFileMonitor = defaultFileMonitor; 095 this.fileObject = fileObject; 096 097 refresh(); 098 resetChildrenList(); 099 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}