View Javadoc

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