View Javadoc

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