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