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.Queue;
29  import java.util.Set;
30  import java.util.Timer;
31  import java.util.TimerTask;
32  import java.util.concurrent.ConcurrentLinkedQueue;
33  
34  import org.eclipse.jetty.util.log.Log;
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   * TODO AbstractLifeCycle
44   */
45  public class Scanner
46  {
47      private static int __scannerId=0;
48      private int _scanInterval;
49      private final List<Listener> _listeners = new ArrayList<Listener>();
50      private final Map<String,Long> _prevScan = new HashMap<String,Long> ();
51      private final Map<String,Long> _currentScan = new HashMap<String,Long> ();
52      private FilenameFilter _filter;
53      private final List<File> _scanDirs = new ArrayList<File>();
54      private volatile boolean _running = false;
55      private boolean _reportExisting = true;
56      private boolean _reportDirs = true;
57      private Timer _timer;
58      private TimerTask _task;
59      private int _scanDepth=0;
60  
61  
62      /**
63       * Listener
64       * 
65       * Marker for notifications re file changes.
66       */
67      public interface Listener
68      {
69      }
70  
71      
72      public interface DiscreteListener extends Listener
73      {
74          public void fileChanged (String filename) throws Exception;
75          public void fileAdded (String filename) throws Exception;
76          public void fileRemoved (String filename) throws Exception;
77      }
78      
79      
80      public interface BulkListener extends Listener
81      {
82          public void filesChanged (List<String> filenames) throws Exception;
83      }
84  
85  
86      /**
87       * 
88       */
89      public Scanner ()
90      {       
91      }
92  
93      /**
94       * Get the scan interval
95       * @return interval between scans in seconds
96       */
97      public int getScanInterval()
98      {
99          return _scanInterval;
100     }
101 
102     /**
103      * Set the scan interval
104      * @param scanInterval pause between scans in seconds, or 0 for no scan after the initial scan.
105      */
106     public synchronized void setScanInterval(int scanInterval)
107     {
108         _scanInterval = scanInterval;
109         schedule();
110     }
111 
112     /**
113      * Set the location of the directory to scan.
114      * @param dir
115      * @deprecated use setScanDirs(List dirs) instead
116      */
117     @Deprecated
118     public void setScanDir (File dir)
119     {
120         _scanDirs.clear(); 
121         _scanDirs.add(dir);
122     }
123 
124     /**
125      * Get the location of the directory to scan
126      * @return the first directory (of {@link #getScanDirs()} being scanned)
127      * @deprecated use getScanDirs() instead
128      */
129     @Deprecated
130     public File getScanDir ()
131     {
132         return (_scanDirs==null?null:(File)_scanDirs.get(0));
133     }
134 
135     public void setScanDirs (List<File> dirs)
136     {
137         _scanDirs.clear(); 
138         _scanDirs.addAll(dirs);
139     }
140     
141     public List<File> getScanDirs ()
142     {
143         return Collections.unmodifiableList(_scanDirs);
144     }
145     
146     /* ------------------------------------------------------------ */
147     /**
148      * @param recursive True if scanning is recursive
149      * @see  #setScanDepth(int)
150      */
151     public void setRecursive (boolean recursive)
152     {
153         _scanDepth=recursive?-1:0;
154     }
155     
156     /* ------------------------------------------------------------ */
157     /**
158      * @return True if scanning is fully recursive (scandepth==-1)
159      * @see #getScanDepth()
160      */
161     public boolean getRecursive ()
162     {
163         return _scanDepth==-1;
164     }
165     
166     /* ------------------------------------------------------------ */
167     /** Get the scanDepth.
168      * @return the scanDepth
169      */
170     public int getScanDepth()
171     {
172         return _scanDepth;
173     }
174 
175     /* ------------------------------------------------------------ */
176     /** Set the scanDepth.
177      * @param scanDepth the scanDepth to set
178      */
179     public void setScanDepth(int scanDepth)
180     {
181         _scanDepth = scanDepth;
182     }
183 
184     /**
185      * Apply a filter to files found in the scan directory.
186      * Only files matching the filter will be reported as added/changed/removed.
187      * @param filter
188      */
189     public void setFilenameFilter (FilenameFilter filter)
190     {
191         _filter = filter;
192     }
193 
194     /**
195      * Get any filter applied to files in the scan dir.
196      * @return the filename filter
197      */
198     public FilenameFilter getFilenameFilter ()
199     {
200         return _filter;
201     }
202 
203     /* ------------------------------------------------------------ */
204     /**
205      * Whether or not an initial scan will report all files as being
206      * added.
207      * @param reportExisting if true, all files found on initial scan will be 
208      * reported as being added, otherwise not
209      */
210     public void setReportExistingFilesOnStartup (boolean reportExisting)
211     {
212         _reportExisting = reportExisting;
213     }
214 
215     /* ------------------------------------------------------------ */
216     public boolean getReportExistingFilesOnStartup()
217     {
218         return _reportExisting;
219     }
220     
221     /* ------------------------------------------------------------ */
222     /** Set if found directories should be reported.
223      * @param dirs
224      */
225     public void setReportDirs(boolean dirs)
226     {
227         _reportDirs=dirs;
228     }
229     
230     /* ------------------------------------------------------------ */
231     public boolean getReportDirs()
232     {
233         return _reportDirs;
234     }
235     
236     /* ------------------------------------------------------------ */
237     /**
238      * Add an added/removed/changed listener
239      * @param listener
240      */
241     public synchronized void addListener (Listener listener)
242     {
243         if (listener == null)
244             return;
245         _listeners.add(listener);   
246     }
247 
248 
249 
250     /**
251      * Remove a registered listener
252      * @param listener the Listener to be removed
253      */
254     public synchronized void removeListener (Listener listener)
255     {
256         if (listener == null)
257             return;
258         _listeners.remove(listener);    
259     }
260 
261 
262     /**
263      * Start the scanning action.
264      */
265     public synchronized void start ()
266     {
267         if (_running)
268             return;
269 
270         _running = true;
271 
272         if (_reportExisting)
273         {
274             // if files exist at startup, report them
275             scan();
276         }
277         else
278         {
279             //just register the list of existing files and only report changes
280             scanFiles();
281             _prevScan.putAll(_currentScan);
282         }
283         schedule();
284     }
285 
286     public TimerTask newTimerTask ()
287     {
288         return new TimerTask()
289         {
290             @Override
291             public void run() { scan(); }
292         };
293     }
294 
295     public Timer newTimer ()
296     {
297         return new Timer("Scanner-"+__scannerId++, true);
298     }
299     
300     public void schedule ()
301     {  
302         if (_running)
303         {
304             if (_timer!=null)
305                 _timer.cancel();
306             if (_task!=null)
307                 _task.cancel();
308             if (getScanInterval() > 0)
309             {
310                 _timer = newTimer();
311                 _task = newTimerTask();
312                 _timer.schedule(_task, 1000L*getScanInterval(),1000L*getScanInterval());
313             }
314         }
315     }
316     /**
317      * Stop the scanning.
318      */
319     public synchronized void stop ()
320     {
321         if (_running)
322         {
323             _running = false; 
324             if (_timer!=null)
325                 _timer.cancel();
326             if (_task!=null)
327                 _task.cancel();
328             _task=null;
329             _timer=null;
330         }
331     }
332 
333     /**
334      * Perform a pass of the scanner and report changes
335      */
336     public synchronized void scan ()
337     {
338         scanFiles();
339         reportDifferences(_currentScan, _prevScan);
340         _prevScan.clear();
341         _prevScan.putAll(_currentScan);
342     }
343 
344     /**
345      * Recursively scan all files in the designated directories.
346      */
347     public synchronized void scanFiles ()
348     {
349         if (_scanDirs==null)
350             return;
351         
352         _currentScan.clear();
353         Iterator<File> itor = _scanDirs.iterator();
354         while (itor.hasNext())
355         {
356             File dir = itor.next();
357             
358             if ((dir != null) && (dir.exists()))
359                 try
360                 {
361                     scanFile(dir.getCanonicalFile(), _currentScan,0);
362                 }
363                 catch (IOException e)
364                 {
365                     Log.warn("Error scanning files.", e);
366                 }
367         }
368     }
369 
370 
371     /**
372      * Report the adds/changes/removes to the registered listeners
373      * 
374      * @param currentScan the info from the most recent pass
375      * @param oldScan info from the previous pass
376      */
377     public void reportDifferences (Map<String,Long> currentScan, Map<String,Long> oldScan) 
378     {
379         List<String> bulkChanges = new ArrayList<String>();
380         
381         Set<String> oldScanKeys = new HashSet<String>(oldScan.keySet());
382         Iterator<Entry<String, Long>> itor = currentScan.entrySet().iterator();
383         while (itor.hasNext())
384         {
385             Map.Entry<String, Long> entry = itor.next();
386             if (!oldScanKeys.contains(entry.getKey()))
387             {
388                 Log.debug("File added: "+entry.getKey());
389                 reportAddition ((String)entry.getKey());
390                 bulkChanges.add(entry.getKey());
391             }
392             else if (!oldScan.get(entry.getKey()).equals(entry.getValue()))
393             {
394                 Log.debug("File changed: "+entry.getKey());
395                 reportChange((String)entry.getKey());
396                 oldScanKeys.remove(entry.getKey());
397                 bulkChanges.add(entry.getKey());
398             }
399             else
400                 oldScanKeys.remove(entry.getKey());
401         }
402 
403         if (!oldScanKeys.isEmpty())
404         {
405 
406             Iterator<String> keyItor = oldScanKeys.iterator();
407             while (keyItor.hasNext())
408             {
409                 String filename = (String)keyItor.next();
410                 Log.debug("File removed: "+filename);
411                 reportRemoval(filename);
412                 bulkChanges.add(filename);
413             }
414         }
415         
416         if (!bulkChanges.isEmpty())
417             reportBulkChanges(bulkChanges);
418     }
419 
420 
421     /**
422      * Get last modified time on a single file or recurse if
423      * the file is a directory. 
424      * @param f file or directory
425      * @param scanInfoMap map of filenames to last modified times
426      */
427     private void scanFile (File f, Map<String,Long> scanInfoMap, int depth)
428     {
429         try
430         {
431             if (!f.exists())
432                 return;
433 
434             if (f.isFile() || depth>0&& _reportDirs && f.isDirectory())
435             {
436                 if ((_filter == null) || ((_filter != null) && _filter.accept(f.getParentFile(), f.getName())))
437                 {
438                     String name = f.getCanonicalPath();
439                     long lastModified = f.lastModified();
440                     scanInfoMap.put(name, new Long(lastModified));
441                 }
442             }
443             
444             // If it is a directory, scan if it is a known directory or the depth is OK.
445             if (f.isDirectory() && (depth<_scanDepth || _scanDepth==-1 || _scanDirs.contains(f)))
446             {
447                 File[] files = f.listFiles();
448                 for (int i=0;i<files.length;i++)
449                     scanFile(files[i], scanInfoMap,depth+1);
450             }
451         }
452         catch (IOException e)
453         {
454             Log.warn("Error scanning watched files", e);
455         }
456     }
457 
458     private void warn(Object listener,String filename,Throwable th)
459     {
460         Log.warn(th);
461         Log.warn(listener+" failed on '"+filename);
462     }
463 
464     /**
465      * Report a file addition to the registered FileAddedListeners
466      * @param filename
467      */
468     private void reportAddition (String filename)
469     {
470         Iterator<Listener> itor = _listeners.iterator();
471         while (itor.hasNext())
472         {
473             Listener l = itor.next();
474             try
475             {
476                 if (l instanceof DiscreteListener)
477                     ((DiscreteListener)l).fileAdded(filename);
478             }
479             catch (Exception e)
480             {
481                 warn(l,filename,e);
482             }
483             catch (Error e)
484             {
485                 warn(l,filename,e);
486             }
487         }
488     }
489 
490 
491     /**
492      * Report a file removal to the FileRemovedListeners
493      * @param filename
494      */
495     private void reportRemoval (String filename)
496     {
497         Iterator<Listener> itor = _listeners.iterator();
498         while (itor.hasNext())
499         {
500             Object l = itor.next();
501             try
502             {
503                 if (l instanceof DiscreteListener)
504                     ((DiscreteListener)l).fileRemoved(filename);
505             }
506             catch (Exception e)
507             {
508                 warn(l,filename,e);
509             }
510             catch (Error e)
511             {
512                 warn(l,filename,e);
513             }
514         }
515     }
516 
517 
518     /**
519      * Report a file change to the FileChangedListeners
520      * @param filename
521      */
522     private void reportChange (String filename)
523     {
524         Iterator<Listener> itor = _listeners.iterator();
525         while (itor.hasNext())
526         {
527             Listener l = itor.next();
528             try
529             {
530                 if (l instanceof DiscreteListener)
531                     ((DiscreteListener)l).fileChanged(filename);
532             }
533             catch (Exception e)
534             {
535                 warn(l,filename,e);
536             }
537             catch (Error e)
538             {
539                 warn(l,filename,e);
540             }
541         }
542     }
543     
544     private void reportBulkChanges (List<String> filenames)
545     {
546         Iterator<Listener> itor = _listeners.iterator();
547         while (itor.hasNext())
548         {
549             Listener l = itor.next();
550             try
551             {
552                 if (l instanceof BulkListener)
553                     ((BulkListener)l).filesChanged(filenames);
554             }
555             catch (Exception e)
556             {
557                 warn(l,filenames.toString(),e);
558             }
559             catch (Error e)
560             {
561                 warn(l,filenames.toString(),e);
562             }
563         }
564     }
565 
566 }