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