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.util.HashMap; 020import java.util.Map; 021import java.util.Stack; 022 023import org.apache.commons.logging.Log; 024import org.apache.commons.logging.LogFactory; 025import org.apache.commons.vfs2.FileListener; 026import org.apache.commons.vfs2.FileMonitor; 027import org.apache.commons.vfs2.FileName; 028import org.apache.commons.vfs2.FileObject; 029import org.apache.commons.vfs2.FileSystemException; 030import org.apache.commons.vfs2.provider.AbstractFileSystem; 031 032/** 033 * A polling {@link FileMonitor} implementation. 034 * <p> 035 * The DefaultFileMonitor is a Thread based polling file system monitor with a 1 second delay. 036 * 037 * <h2>Design:</h2> 038 * 039 * There is a Map of monitors known as FileMonitorAgents. With the thread running, each FileMonitorAgent object is asked 040 * to "check" on the file it is responsible for. To do this check, the cache is cleared. 041 * <ul> 042 * <li>If the file existed before the refresh and it no longer exists, a delete event is fired.</li> 043 * <li>If the file existed before the refresh and it still exists, check the last modified timestamp to see if that has 044 * changed.</li> 045 * <li>If it has, fire a change event.</li> 046 * </ul> 047 * With each file delete, the FileMonitorAgent of the parent is asked to re-build its list of children, so that they can 048 * be accurately checked when there are new children. 049 * <p> 050 * New files are detected during each "check" as each file does a check for new children. If new children are found, 051 * create events are fired recursively if recursive descent is enabled. 052 * <p> 053 * For performance reasons, added a delay that increases as the number of files monitored increases. The default is a 054 * delay of 1 second for every 1000 files processed. 055 * 056 * <h2>Example usage:</h2> 057 * 058 * <pre> 059 * FileSystemManager fsManager = VFS.getManager(); 060 * FileObject listendir = fsManager.resolveFile("/home/username/monitored/"); 061 * 062 * DefaultFileMonitor fm = new DefaultFileMonitor(new CustomFileListener()); 063 * fm.setRecursive(true); 064 * fm.addFile(listendir); 065 * fm.start(); 066 * </pre> 067 * 068 * <i>(where CustomFileListener is a class that implements the FileListener interface.)</i> 069 */ 070public class DefaultFileMonitor implements Runnable, FileMonitor { 071 private static final Log LOG = LogFactory.getLog(DefaultFileMonitor.class); 072 073 private static final long DEFAULT_DELAY = 1000; 074 075 private static final int DEFAULT_MAX_FILES = 1000; 076 077 /** 078 * Map from FileName to FileObject being monitored. 079 */ 080 private final Map<FileName, FileMonitorAgent> monitorMap = new HashMap<>(); 081 082 /** 083 * The low priority thread used for checking the files being monitored. 084 */ 085 private Thread monitorThread; 086 087 /** 088 * File objects to be removed from the monitor map. 089 */ 090 private final Stack<FileObject> deleteStack = new Stack<>(); 091 092 /** 093 * File objects to be added to the monitor map. 094 */ 095 private final Stack<FileObject> addStack = new Stack<>(); 096 097 /** 098 * A flag used to determine if the monitor thread should be running. 099 */ 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 public DefaultFileMonitor(final FileListener listener) { 123 this.listener = listener; 124 } 125 126 /** 127 * Access method to get the recursive setting when adding files for monitoring. 128 * 129 * @return true if monitoring is enabled for children. 130 */ 131 public boolean isRecursive() { 132 return this.recursive; 133 } 134 135 /** 136 * Access method to set the recursive setting when adding files for monitoring. 137 * 138 * @param newRecursive true if monitoring should be enabled for children. 139 */ 140 public void setRecursive(final boolean newRecursive) { 141 this.recursive = newRecursive; 142 } 143 144 /** 145 * Access method to get the current FileListener object notified when there are changes with the files added. 146 * 147 * @return The FileListener. 148 */ 149 FileListener getFileListener() { 150 return this.listener; 151 } 152 153 /** 154 * Adds a file to be monitored. 155 * 156 * @param file The FileObject to monitor. 157 */ 158 @Override 159 public void addFile(final FileObject file) { 160 doAddFile(file); 161 try { 162 // add all direct children too 163 if (file.getType().hasChildren()) { 164 // Traverse the children 165 final FileObject[] children = file.getChildren(); 166 for (final FileObject element : children) { 167 doAddFile(element); 168 } 169 } 170 } catch (final FileSystemException fse) { 171 LOG.error(fse.getLocalizedMessage(), fse); 172 } 173 } 174 175 /** 176 * Adds a file to be monitored. 177 * 178 * @param file The FileObject to add. 179 */ 180 private void doAddFile(final FileObject file) { 181 synchronized (this.monitorMap) { 182 if (this.monitorMap.get(file.getName()) == null) { 183 this.monitorMap.put(file.getName(), new FileMonitorAgent(this, file)); 184 185 try { 186 if (this.listener != null) { 187 file.getFileSystem().addListener(file, this.listener); 188 } 189 190 if (file.getType().hasChildren() && this.recursive) { 191 // Traverse the children 192 final FileObject[] children = file.getChildren(); 193 for (final FileObject element : children) { 194 this.addFile(element); // Add depth first 195 } 196 } 197 198 } catch (final FileSystemException fse) { 199 LOG.error(fse.getLocalizedMessage(), fse); 200 } 201 202 } 203 } 204 } 205 206 /** 207 * Removes a file from being monitored. 208 * 209 * @param file The FileObject to remove from monitoring. 210 */ 211 @Override 212 public void removeFile(final FileObject file) { 213 synchronized (this.monitorMap) { 214 final FileName fn = file.getName(); 215 if (this.monitorMap.get(fn) != null) { 216 FileObject parent; 217 try { 218 parent = file.getParent(); 219 } catch (final FileSystemException fse) { 220 parent = null; 221 } 222 223 this.monitorMap.remove(fn); 224 225 if (parent != null) { // Not the root 226 final FileMonitorAgent parentAgent = this.monitorMap.get(parent.getName()); 227 if (parentAgent != null) { 228 parentAgent.resetChildrenList(); 229 } 230 } 231 } 232 } 233 } 234 235 /** 236 * Queues a file for removal from being monitored. 237 * 238 * @param file The FileObject to be removed from being monitored. 239 */ 240 protected void queueRemoveFile(final FileObject file) { 241 this.deleteStack.push(file); 242 } 243 244 /** 245 * Get the delay between runs. 246 * 247 * @return The delay period. 248 */ 249 public long getDelay() { 250 return delay; 251 } 252 253 /** 254 * Set the delay between runs. 255 * 256 * @param delay The delay period. 257 */ 258 public void setDelay(final long delay) { 259 if (delay > 0) { 260 this.delay = delay; 261 } else { 262 this.delay = DEFAULT_DELAY; 263 } 264 } 265 266 /** 267 * get the number of files to check per run. 268 * 269 * @return The number of files to check per iteration. 270 */ 271 public int getChecksPerRun() { 272 return checksPerRun; 273 } 274 275 /** 276 * set the number of files to check per run. a additional delay will be added if there are more files to check 277 * 278 * @param checksPerRun a value less than 1 will disable this feature 279 */ 280 public void setChecksPerRun(final int checksPerRun) { 281 this.checksPerRun = checksPerRun; 282 } 283 284 /** 285 * Queues a file for addition to be monitored. 286 * 287 * @param file The FileObject to add. 288 */ 289 protected void queueAddFile(final FileObject file) { 290 this.addStack.push(file); 291 } 292 293 /** 294 * Starts monitoring the files that have been added. 295 */ 296 public void start() { 297 if (this.monitorThread == null) { 298 this.monitorThread = new Thread(this); 299 this.monitorThread.setDaemon(true); 300 this.monitorThread.setPriority(Thread.MIN_PRIORITY); 301 } 302 this.monitorThread.start(); 303 } 304 305 /** 306 * Stops monitoring the files that have been added. 307 */ 308 public void stop() { 309 this.shouldRun = false; 310 } 311 312 /** 313 * Asks the agent for each file being monitored to check its file for changes. 314 */ 315 @Override 316 public void run() { 317 mainloop: while (!monitorThread.isInterrupted() && this.shouldRun) { 318 // For each entry in the map 319 Object[] fileNames; 320 synchronized (this.monitorMap) { 321 fileNames = this.monitorMap.keySet().toArray(); 322 } 323 for (int iterFileNames = 0; iterFileNames < fileNames.length; iterFileNames++) { 324 final FileName fileName = (FileName) fileNames[iterFileNames]; 325 FileMonitorAgent agent; 326 synchronized (this.monitorMap) { 327 agent = this.monitorMap.get(fileName); 328 } 329 if (agent != null) { 330 agent.check(); 331 } 332 333 if (getChecksPerRun() > 0 && (iterFileNames + 1) % getChecksPerRun() == 0) { 334 try { 335 Thread.sleep(getDelay()); 336 } catch (final InterruptedException e) { 337 // Woke up. 338 } 339 } 340 341 if (monitorThread.isInterrupted() || !this.shouldRun) { 342 continue mainloop; 343 } 344 } 345 346 while (!this.addStack.empty()) { 347 this.addFile(this.addStack.pop()); 348 } 349 350 while (!this.deleteStack.empty()) { 351 this.removeFile(this.deleteStack.pop()); 352 } 353 354 try { 355 Thread.sleep(getDelay()); 356 } catch (final InterruptedException e) { 357 continue; 358 } 359 } 360 361 this.shouldRun = true; 362 } 363 364 /** 365 * File monitor agent. 366 */ 367 private static final class FileMonitorAgent { 368 private final FileObject file; 369 private final DefaultFileMonitor fm; 370 371 private boolean exists; 372 private long timestamp; 373 private Map<FileName, Object> children; 374 375 private FileMonitorAgent(final DefaultFileMonitor fm, final FileObject file) { 376 this.fm = fm; 377 this.file = file; 378 379 this.refresh(); 380 this.resetChildrenList(); 381 382 try { 383 this.exists = this.file.exists(); 384 } catch (final FileSystemException fse) { 385 this.exists = false; 386 this.timestamp = -1; 387 } 388 389 if (this.exists) { 390 try { 391 this.timestamp = this.file.getContent().getLastModifiedTime(); 392 } catch (final FileSystemException fse) { 393 this.timestamp = -1; 394 } 395 } 396 } 397 398 private void resetChildrenList() { 399 try { 400 if (this.file.getType().hasChildren()) { 401 this.children = new HashMap<>(); 402 final FileObject[] childrenList = this.file.getChildren(); 403 for (final FileObject element : childrenList) { 404 this.children.put(element.getName(), new Object()); // null? 405 } 406 } 407 } catch (final FileSystemException fse) { 408 this.children = null; 409 } 410 } 411 412 /** 413 * Clear the cache and re-request the file object 414 */ 415 private void refresh() { 416 try { 417 this.file.refresh(); 418 } catch (final FileSystemException fse) { 419 LOG.error(fse.getLocalizedMessage(), fse); 420 } 421 } 422 423 /** 424 * Recursively fires create events for all children if recursive descent is enabled. Otherwise the create event 425 * is only fired for the initial FileObject. 426 * 427 * @param child The child to add. 428 */ 429 private void fireAllCreate(final FileObject child) { 430 // Add listener so that it can be triggered 431 if (this.fm.getFileListener() != null) { 432 child.getFileSystem().addListener(child, this.fm.getFileListener()); 433 } 434 435 ((AbstractFileSystem) child.getFileSystem()).fireFileCreated(child); 436 437 // Remove it because a listener is added in the queueAddFile 438 if (this.fm.getFileListener() != null) { 439 child.getFileSystem().removeListener(child, this.fm.getFileListener()); 440 } 441 442 this.fm.queueAddFile(child); // Add 443 444 try { 445 if (this.fm.isRecursive() && child.getType().hasChildren()) { 446 final FileObject[] newChildren = child.getChildren(); 447 for (final FileObject element : newChildren) { 448 fireAllCreate(element); 449 } 450 } 451 } catch (final FileSystemException fse) { 452 LOG.error(fse.getLocalizedMessage(), fse); 453 } 454 } 455 456 /** 457 * Only checks for new children. If children are removed, they'll eventually be checked. 458 */ 459 private void checkForNewChildren() { 460 try { 461 if (this.file.getType().hasChildren()) { 462 final FileObject[] newChildren = this.file.getChildren(); 463 if (this.children != null) { 464 // See which new children are not listed in the current children map. 465 final Map<FileName, Object> newChildrenMap = new HashMap<>(); 466 final Stack<FileObject> missingChildren = new Stack<>(); 467 468 for (int i = 0; i < newChildren.length; i++) { 469 newChildrenMap.put(newChildren[i].getName(), new Object()); // null ? 470 // If the child's not there 471 if (!this.children.containsKey(newChildren[i].getName())) { 472 missingChildren.push(newChildren[i]); 473 } 474 } 475 476 this.children = newChildrenMap; 477 478 // If there were missing children 479 if (!missingChildren.empty()) { 480 481 while (!missingChildren.empty()) { 482 final FileObject child = missingChildren.pop(); 483 this.fireAllCreate(child); 484 } 485 } 486 487 } else { 488 // First set of children - Break out the cigars 489 if (newChildren.length > 0) { 490 this.children = new HashMap<>(); 491 } 492 for (final FileObject element : newChildren) { 493 this.children.put(element.getName(), new Object()); // null? 494 this.fireAllCreate(element); 495 } 496 } 497 } 498 } catch (final FileSystemException fse) { 499 LOG.error(fse.getLocalizedMessage(), fse); 500 } 501 } 502 503 private void check() { 504 this.refresh(); 505 506 try { 507 // If the file existed and now doesn't 508 if (this.exists && !this.file.exists()) { 509 this.exists = this.file.exists(); 510 this.timestamp = -1; 511 512 // Fire delete event 513 514 ((AbstractFileSystem) this.file.getFileSystem()).fireFileDeleted(this.file); 515 516 // Remove listener in case file is re-created. Don't want to fire twice. 517 if (this.fm.getFileListener() != null) { 518 this.file.getFileSystem().removeListener(this.file, this.fm.getFileListener()); 519 } 520 521 // Remove from map 522 this.fm.queueRemoveFile(this.file); 523 } else if (this.exists && this.file.exists()) { 524 525 // Check the timestamp to see if it has been modified 526 if (this.timestamp != this.file.getContent().getLastModifiedTime()) { 527 this.timestamp = this.file.getContent().getLastModifiedTime(); 528 // Fire change event 529 530 // Don't fire if it's a folder because new file children 531 // and deleted files in a folder have their own event triggered. 532 if (!this.file.getType().hasChildren()) { 533 ((AbstractFileSystem) this.file.getFileSystem()).fireFileChanged(this.file); 534 } 535 } 536 537 } else if (!this.exists && this.file.exists()) { 538 this.exists = this.file.exists(); 539 this.timestamp = this.file.getContent().getLastModifiedTime(); 540 // Don't fire if it's a folder because new file children 541 // and deleted files in a folder have their own event triggered. 542 if (!this.file.getType().hasChildren()) { 543 ((AbstractFileSystem) this.file.getFileSystem()).fireFileCreated(this.file); 544 } 545 } 546 547 this.checkForNewChildren(); 548 549 } catch (final FileSystemException fse) { 550 LOG.error(fse.getLocalizedMessage(), fse); 551 } 552 } 553 554 } 555}