1 // 2 // ======================================================================== 3 // Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd. 4 // ------------------------------------------------------------------------ 5 // All rights reserved. This program and the accompanying materials 6 // are made available under the terms of the Eclipse Public License v1.0 7 // and Apache License v2.0 which accompanies this distribution. 8 // 9 // The Eclipse Public License is available at 10 // http://www.eclipse.org/legal/epl-v10.html 11 // 12 // The Apache License v2.0 is available at 13 // http://www.opensource.org/licenses/apache2.0.php 14 // 15 // You may elect to redistribute this code under either of these licenses. 16 // ======================================================================== 17 // 18 19 20 package org.eclipse.jetty.util; 21 22 import java.io.File; 23 import java.io.FilenameFilter; 24 import java.io.IOException; 25 import java.util.ArrayList; 26 import java.util.Collections; 27 import java.util.HashMap; 28 import java.util.HashSet; 29 import java.util.Iterator; 30 import java.util.List; 31 import java.util.Map; 32 import java.util.Map.Entry; 33 import java.util.Set; 34 import java.util.Timer; 35 import java.util.TimerTask; 36 37 import org.eclipse.jetty.util.component.AbstractLifeCycle; 38 import org.eclipse.jetty.util.log.Log; 39 import org.eclipse.jetty.util.log.Logger; 40 41 42 /** 43 * Scanner 44 * 45 * Utility for scanning a directory for added, removed and changed 46 * files and reporting these events via registered Listeners. 47 * 48 */ 49 public class Scanner extends AbstractLifeCycle 50 { 51 private static final Logger LOG = Log.getLogger(Scanner.class); 52 private static int __scannerId=0; 53 private int _scanInterval; 54 private int _scanCount = 0; 55 private final List<Listener> _listeners = new ArrayList<Listener>(); 56 private final Map<String,TimeNSize> _prevScan = new HashMap<String,TimeNSize> (); 57 private final Map<String,TimeNSize> _currentScan = new HashMap<String,TimeNSize> (); 58 private FilenameFilter _filter; 59 private final List<File> _scanDirs = new ArrayList<File>(); 60 private volatile boolean _running = false; 61 private boolean _reportExisting = true; 62 private boolean _reportDirs = true; 63 private Timer _timer; 64 private TimerTask _task; 65 private int _scanDepth=0; 66 67 public enum Notification { ADDED, CHANGED, REMOVED }; 68 private final Map<String,Notification> _notifications = new HashMap<String,Notification>(); 69 70 static class TimeNSize 71 { 72 final long _lastModified; 73 final long _size; 74 TimeNSize(long lastModified, long size)75 public TimeNSize(long lastModified, long size) 76 { 77 _lastModified = lastModified; 78 _size = size; 79 } 80 81 @Override hashCode()82 public int hashCode() 83 { 84 return (int)_lastModified^(int)_size; 85 } 86 87 @Override equals(Object o)88 public boolean equals(Object o) 89 { 90 if (o instanceof TimeNSize) 91 { 92 TimeNSize tns = (TimeNSize)o; 93 return tns._lastModified==_lastModified && tns._size==_size; 94 } 95 return false; 96 } 97 98 @Override toString()99 public String toString() 100 { 101 return "[lm="+_lastModified+",s="+_size+"]"; 102 } 103 } 104 105 /** 106 * Listener 107 * 108 * Marker for notifications re file changes. 109 */ 110 public interface Listener 111 { 112 } 113 114 public interface ScanListener extends Listener 115 { scan()116 public void scan(); 117 } 118 119 public interface DiscreteListener extends Listener 120 { fileChanged(String filename)121 public void fileChanged (String filename) throws Exception; fileAdded(String filename)122 public void fileAdded (String filename) throws Exception; fileRemoved(String filename)123 public void fileRemoved (String filename) throws Exception; 124 } 125 126 127 public interface BulkListener extends Listener 128 { filesChanged(List<String> filenames)129 public void filesChanged (List<String> filenames) throws Exception; 130 } 131 132 /** 133 * Listener that notifies when a scan has started and when it has ended. 134 */ 135 public interface ScanCycleListener extends Listener 136 { scanStarted(int cycle)137 public void scanStarted(int cycle) throws Exception; scanEnded(int cycle)138 public void scanEnded(int cycle) throws Exception; 139 } 140 141 /** 142 * 143 */ Scanner()144 public Scanner () 145 { 146 } 147 148 /** 149 * Get the scan interval 150 * @return interval between scans in seconds 151 */ getScanInterval()152 public int getScanInterval() 153 { 154 return _scanInterval; 155 } 156 157 /** 158 * Set the scan interval 159 * @param scanInterval pause between scans in seconds, or 0 for no scan after the initial scan. 160 */ setScanInterval(int scanInterval)161 public synchronized void setScanInterval(int scanInterval) 162 { 163 _scanInterval = scanInterval; 164 schedule(); 165 } 166 167 /** 168 * Set the location of the directory to scan. 169 * @param dir 170 * @deprecated use setScanDirs(List dirs) instead 171 */ 172 @Deprecated setScanDir(File dir)173 public void setScanDir (File dir) 174 { 175 _scanDirs.clear(); 176 _scanDirs.add(dir); 177 } 178 179 /** 180 * Get the location of the directory to scan 181 * @return the first directory (of {@link #getScanDirs()} being scanned) 182 * @deprecated use getScanDirs() instead 183 */ 184 @Deprecated getScanDir()185 public File getScanDir () 186 { 187 return (_scanDirs==null?null:(File)_scanDirs.get(0)); 188 } 189 setScanDirs(List<File> dirs)190 public void setScanDirs (List<File> dirs) 191 { 192 _scanDirs.clear(); 193 _scanDirs.addAll(dirs); 194 } 195 addScanDir( File dir )196 public synchronized void addScanDir( File dir ) 197 { 198 _scanDirs.add( dir ); 199 } 200 getScanDirs()201 public List<File> getScanDirs () 202 { 203 return Collections.unmodifiableList(_scanDirs); 204 } 205 206 /* ------------------------------------------------------------ */ 207 /** 208 * @param recursive True if scanning is recursive 209 * @see #setScanDepth(int) 210 */ setRecursive(boolean recursive)211 public void setRecursive (boolean recursive) 212 { 213 _scanDepth=recursive?-1:0; 214 } 215 216 /* ------------------------------------------------------------ */ 217 /** 218 * @return True if scanning is fully recursive (scandepth==-1) 219 * @see #getScanDepth() 220 */ getRecursive()221 public boolean getRecursive () 222 { 223 return _scanDepth==-1; 224 } 225 226 /* ------------------------------------------------------------ */ 227 /** Get the scanDepth. 228 * @return the scanDepth 229 */ getScanDepth()230 public int getScanDepth() 231 { 232 return _scanDepth; 233 } 234 235 /* ------------------------------------------------------------ */ 236 /** Set the scanDepth. 237 * @param scanDepth the scanDepth to set 238 */ setScanDepth(int scanDepth)239 public void setScanDepth(int scanDepth) 240 { 241 _scanDepth = scanDepth; 242 } 243 244 /** 245 * Apply a filter to files found in the scan directory. 246 * Only files matching the filter will be reported as added/changed/removed. 247 * @param filter 248 */ setFilenameFilter(FilenameFilter filter)249 public void setFilenameFilter (FilenameFilter filter) 250 { 251 _filter = filter; 252 } 253 254 /** 255 * Get any filter applied to files in the scan dir. 256 * @return the filename filter 257 */ getFilenameFilter()258 public FilenameFilter getFilenameFilter () 259 { 260 return _filter; 261 } 262 263 /* ------------------------------------------------------------ */ 264 /** 265 * Whether or not an initial scan will report all files as being 266 * added. 267 * @param reportExisting if true, all files found on initial scan will be 268 * reported as being added, otherwise not 269 */ setReportExistingFilesOnStartup(boolean reportExisting)270 public void setReportExistingFilesOnStartup (boolean reportExisting) 271 { 272 _reportExisting = reportExisting; 273 } 274 275 /* ------------------------------------------------------------ */ getReportExistingFilesOnStartup()276 public boolean getReportExistingFilesOnStartup() 277 { 278 return _reportExisting; 279 } 280 281 /* ------------------------------------------------------------ */ 282 /** Set if found directories should be reported. 283 * @param dirs 284 */ setReportDirs(boolean dirs)285 public void setReportDirs(boolean dirs) 286 { 287 _reportDirs=dirs; 288 } 289 290 /* ------------------------------------------------------------ */ getReportDirs()291 public boolean getReportDirs() 292 { 293 return _reportDirs; 294 } 295 296 /* ------------------------------------------------------------ */ 297 /** 298 * Add an added/removed/changed listener 299 * @param listener 300 */ addListener(Listener listener)301 public synchronized void addListener (Listener listener) 302 { 303 if (listener == null) 304 return; 305 _listeners.add(listener); 306 } 307 308 309 310 /** 311 * Remove a registered listener 312 * @param listener the Listener to be removed 313 */ removeListener(Listener listener)314 public synchronized void removeListener (Listener listener) 315 { 316 if (listener == null) 317 return; 318 _listeners.remove(listener); 319 } 320 321 322 /** 323 * Start the scanning action. 324 */ 325 @Override doStart()326 public synchronized void doStart() 327 { 328 if (_running) 329 return; 330 331 _running = true; 332 333 if (_reportExisting) 334 { 335 // if files exist at startup, report them 336 scan(); 337 scan(); // scan twice so files reported as stable 338 } 339 else 340 { 341 //just register the list of existing files and only report changes 342 scanFiles(); 343 _prevScan.putAll(_currentScan); 344 } 345 schedule(); 346 } 347 newTimerTask()348 public TimerTask newTimerTask () 349 { 350 return new TimerTask() 351 { 352 @Override 353 public void run() { scan(); } 354 }; 355 } 356 357 public Timer newTimer () 358 { 359 return new Timer("Scanner-"+__scannerId++, true); 360 } 361 362 public void schedule () 363 { 364 if (_running) 365 { 366 if (_timer!=null) 367 _timer.cancel(); 368 if (_task!=null) 369 _task.cancel(); 370 if (getScanInterval() > 0) 371 { 372 _timer = newTimer(); 373 _task = newTimerTask(); 374 _timer.schedule(_task, 1010L*getScanInterval(),1010L*getScanInterval()); 375 } 376 } 377 } 378 /** 379 * Stop the scanning. 380 */ 381 @Override 382 public synchronized void doStop() 383 { 384 if (_running) 385 { 386 _running = false; 387 if (_timer!=null) 388 _timer.cancel(); 389 if (_task!=null) 390 _task.cancel(); 391 _task=null; 392 _timer=null; 393 } 394 } 395 396 /** 397 * Perform a pass of the scanner and report changes 398 */ 399 public synchronized void scan () 400 { 401 reportScanStart(++_scanCount); 402 scanFiles(); 403 reportDifferences(_currentScan, _prevScan); 404 _prevScan.clear(); 405 _prevScan.putAll(_currentScan); 406 reportScanEnd(_scanCount); 407 408 for (Listener l : _listeners) 409 { 410 try 411 { 412 if (l instanceof ScanListener) 413 ((ScanListener)l).scan(); 414 } 415 catch (Exception e) 416 { 417 LOG.warn(e); 418 } 419 catch (Error e) 420 { 421 LOG.warn(e); 422 } 423 } 424 } 425 426 /** 427 * Recursively scan all files in the designated directories. 428 */ 429 public synchronized void scanFiles () 430 { 431 if (_scanDirs==null) 432 return; 433 434 _currentScan.clear(); 435 Iterator<File> itor = _scanDirs.iterator(); 436 while (itor.hasNext()) 437 { 438 File dir = itor.next(); 439 440 if ((dir != null) && (dir.exists())) 441 try 442 { 443 scanFile(dir.getCanonicalFile(), _currentScan,0); 444 } 445 catch (IOException e) 446 { 447 LOG.warn("Error scanning files.", e); 448 } 449 } 450 } 451 452 453 /** 454 * Report the adds/changes/removes to the registered listeners 455 * 456 * @param currentScan the info from the most recent pass 457 * @param oldScan info from the previous pass 458 */ 459 public synchronized void reportDifferences (Map<String,TimeNSize> currentScan, Map<String,TimeNSize> oldScan) 460 { 461 // scan the differences and add what was found to the map of notifications: 462 463 Set<String> oldScanKeys = new HashSet<String>(oldScan.keySet()); 464 465 // Look for new and changed files 466 for (Map.Entry<String, TimeNSize> entry: currentScan.entrySet()) 467 { 468 String file = entry.getKey(); 469 if (!oldScanKeys.contains(file)) 470 { 471 Notification old=_notifications.put(file,Notification.ADDED); 472 if (old!=null) 473 { 474 switch(old) 475 { 476 case REMOVED: 477 case CHANGED: 478 _notifications.put(file,Notification.CHANGED); 479 } 480 } 481 } 482 else if (!oldScan.get(file).equals(currentScan.get(file))) 483 { 484 Notification old=_notifications.put(file,Notification.CHANGED); 485 if (old!=null) 486 { 487 switch(old) 488 { 489 case ADDED: 490 _notifications.put(file,Notification.ADDED); 491 } 492 } 493 } 494 } 495 496 // Look for deleted files 497 for (String file : oldScan.keySet()) 498 { 499 if (!currentScan.containsKey(file)) 500 { 501 Notification old=_notifications.put(file,Notification.REMOVED); 502 if (old!=null) 503 { 504 switch(old) 505 { 506 case ADDED: 507 _notifications.remove(file); 508 } 509 } 510 } 511 } 512 513 if (LOG.isDebugEnabled()) 514 LOG.debug("scanned "+_scanDirs+": "+_notifications); 515 516 // Process notifications 517 // Only process notifications that are for stable files (ie same in old and current scan). 518 List<String> bulkChanges = new ArrayList<String>(); 519 for (Iterator<Entry<String,Notification>> iter = _notifications.entrySet().iterator();iter.hasNext();) 520 { 521 Entry<String,Notification> entry=iter.next(); 522 String file=entry.getKey(); 523 524 // Is the file stable? 525 if (oldScan.containsKey(file)) 526 { 527 if (!oldScan.get(file).equals(currentScan.get(file))) 528 continue; 529 } 530 else if (currentScan.containsKey(file)) 531 continue; 532 533 // File is stable so notify 534 Notification notification=entry.getValue(); 535 iter.remove(); 536 bulkChanges.add(file); 537 switch(notification) 538 { 539 case ADDED: 540 reportAddition(file); 541 break; 542 case CHANGED: 543 reportChange(file); 544 break; 545 case REMOVED: 546 reportRemoval(file); 547 break; 548 } 549 } 550 if (!bulkChanges.isEmpty()) 551 reportBulkChanges(bulkChanges); 552 } 553 554 555 /** 556 * Get last modified time on a single file or recurse if 557 * the file is a directory. 558 * @param f file or directory 559 * @param scanInfoMap map of filenames to last modified times 560 */ 561 private void scanFile (File f, Map<String,TimeNSize> scanInfoMap, int depth) 562 { 563 try 564 { 565 if (!f.exists()) 566 return; 567 568 if (f.isFile() || depth>0&& _reportDirs && f.isDirectory()) 569 { 570 if ((_filter == null) || ((_filter != null) && _filter.accept(f.getParentFile(), f.getName()))) 571 { 572 String name = f.getCanonicalPath(); 573 scanInfoMap.put(name, new TimeNSize(f.lastModified(),f.length())); 574 } 575 } 576 577 // If it is a directory, scan if it is a known directory or the depth is OK. 578 if (f.isDirectory() && (depth<_scanDepth || _scanDepth==-1 || _scanDirs.contains(f))) 579 { 580 File[] files = f.listFiles(); 581 if (files != null) 582 { 583 for (int i=0;i<files.length;i++) 584 scanFile(files[i], scanInfoMap,depth+1); 585 } 586 else 587 LOG.warn("Error listing files in directory {}", f); 588 589 } 590 } 591 catch (IOException e) 592 { 593 LOG.warn("Error scanning watched files", e); 594 } 595 } 596 597 private void warn(Object listener,String filename,Throwable th) 598 { 599 LOG.warn(listener+" failed on '"+filename, th); 600 } 601 602 /** 603 * Report a file addition to the registered FileAddedListeners 604 * @param filename 605 */ 606 private void reportAddition (String filename) 607 { 608 Iterator<Listener> itor = _listeners.iterator(); 609 while (itor.hasNext()) 610 { 611 Listener l = itor.next(); 612 try 613 { 614 if (l instanceof DiscreteListener) 615 ((DiscreteListener)l).fileAdded(filename); 616 } 617 catch (Exception e) 618 { 619 warn(l,filename,e); 620 } 621 catch (Error e) 622 { 623 warn(l,filename,e); 624 } 625 } 626 } 627 628 629 /** 630 * Report a file removal to the FileRemovedListeners 631 * @param filename 632 */ 633 private void reportRemoval (String filename) 634 { 635 Iterator<Listener> itor = _listeners.iterator(); 636 while (itor.hasNext()) 637 { 638 Object l = itor.next(); 639 try 640 { 641 if (l instanceof DiscreteListener) 642 ((DiscreteListener)l).fileRemoved(filename); 643 } 644 catch (Exception e) 645 { 646 warn(l,filename,e); 647 } 648 catch (Error e) 649 { 650 warn(l,filename,e); 651 } 652 } 653 } 654 655 656 /** 657 * Report a file change to the FileChangedListeners 658 * @param filename 659 */ 660 private void reportChange (String filename) 661 { 662 Iterator<Listener> itor = _listeners.iterator(); 663 while (itor.hasNext()) 664 { 665 Listener l = itor.next(); 666 try 667 { 668 if (l instanceof DiscreteListener) 669 ((DiscreteListener)l).fileChanged(filename); 670 } 671 catch (Exception e) 672 { 673 warn(l,filename,e); 674 } 675 catch (Error e) 676 { 677 warn(l,filename,e); 678 } 679 } 680 } 681 682 private void reportBulkChanges (List<String> filenames) 683 { 684 Iterator<Listener> itor = _listeners.iterator(); 685 while (itor.hasNext()) 686 { 687 Listener l = itor.next(); 688 try 689 { 690 if (l instanceof BulkListener) 691 ((BulkListener)l).filesChanged(filenames); 692 } 693 catch (Exception e) 694 { 695 warn(l,filenames.toString(),e); 696 } 697 catch (Error e) 698 { 699 warn(l,filenames.toString(),e); 700 } 701 } 702 } 703 704 /** 705 * signal any scan cycle listeners that a scan has started 706 */ 707 private void reportScanStart(int cycle) 708 { 709 for (Listener listener : _listeners) 710 { 711 try 712 { 713 if (listener instanceof ScanCycleListener) 714 { 715 ((ScanCycleListener)listener).scanStarted(cycle); 716 } 717 } 718 catch (Exception e) 719 { 720 LOG.warn(listener + " failed on scan start for cycle " + cycle, e); 721 } 722 } 723 } 724 725 /** 726 * sign 727 */ 728 private void reportScanEnd(int cycle) 729 { 730 for (Listener listener : _listeners) 731 { 732 try 733 { 734 if (listener instanceof ScanCycleListener) 735 { 736 ((ScanCycleListener)listener).scanEnded(cycle); 737 } 738 } 739 catch (Exception e) 740 { 741 LOG.warn(listener + " failed on scan end for cycle " + cycle, e); 742 } 743 } 744 } 745 746 } 747