View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2013 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 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     /**
168      * Set the location of the directory to scan.
169      * @param dir
170      * @deprecated use setScanDirs(List dirs) instead
171      */
172     @Deprecated
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
185     public File getScanDir ()
186     {
187         return (_scanDirs==null?null:(File)_scanDirs.get(0));
188     }
189 
190     public void setScanDirs (List<File> dirs)
191     {
192         _scanDirs.clear(); 
193         _scanDirs.addAll(dirs);
194     }
195     
196     public synchronized void addScanDir( File dir )
197     {
198         _scanDirs.add( dir );
199     }
200     
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      */
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      */
221     public boolean getRecursive ()
222     {
223         return _scanDepth==-1;
224     }
225     
226     /* ------------------------------------------------------------ */
227     /** Get the scanDepth.
228      * @return the scanDepth
229      */
230     public int getScanDepth()
231     {
232         return _scanDepth;
233     }
234 
235     /* ------------------------------------------------------------ */
236     /** Set the scanDepth.
237      * @param scanDepth the scanDepth to set
238      */
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      */
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      */
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      */
270     public void setReportExistingFilesOnStartup (boolean reportExisting)
271     {
272         _reportExisting = reportExisting;
273     }
274 
275     /* ------------------------------------------------------------ */
276     public boolean getReportExistingFilesOnStartup()
277     {
278         return _reportExisting;
279     }
280     
281     /* ------------------------------------------------------------ */
282     /** Set if found directories should be reported.
283      * @param dirs
284      */
285     public void setReportDirs(boolean dirs)
286     {
287         _reportDirs=dirs;
288     }
289     
290     /* ------------------------------------------------------------ */
291     public boolean getReportDirs()
292     {
293         return _reportDirs;
294     }
295     
296     /* ------------------------------------------------------------ */
297     /**
298      * Add an added/removed/changed listener
299      * @param listener
300      */
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      */
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
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 
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 }