1   //========================================================================
2   //$Id: Scanner.java 3021 2008-06-18 04:03:54Z janb $
3   //Copyright 2006 Mort Bay Consulting Pty. Ltd.
4   //------------------------------------------------------------------------
5   //Licensed under the Apache License, Version 2.0 (the "License");
6   //you may not use this file except in compliance with the License.
7   //You may obtain a copy of the License at 
8   //http://www.apache.org/licenses/LICENSE-2.0
9   //Unless required by applicable law or agreed to in writing, software
10  //distributed under the License is distributed on an "AS IS" BASIS,
11  //WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  //See the License for the specific language governing permissions and
13  //limitations under the License.
14  //========================================================================
15  
16  
17  package org.mortbay.util;
18  
19  import java.io.File;
20  import java.io.FilenameFilter;
21  import java.io.IOException;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  import java.util.Timer;
31  import java.util.TimerTask;
32  
33  import org.mortbay.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   * TODO AbstractLifeCycle
43   */
44  public class Scanner
45  {
46      private static int __scannerId=0;
47      private int _scanInterval;
48      private List _listeners = Collections.synchronizedList(new ArrayList());
49      private Map _prevScan = new HashMap();
50      private Map _currentScan = new HashMap();
51      private FilenameFilter _filter;
52      private List _scanDirs;
53      private volatile boolean _running = false;
54      private boolean _reportExisting = true;
55      private Timer _timer;
56      private TimerTask _task;
57      private boolean _recursive=true;
58  
59  
60      /**
61       * Listener
62       * 
63       * Marker for notifications re file changes.
64       */
65      public interface Listener
66      {
67      }
68  
69      
70      public interface DiscreteListener extends Listener
71      {
72          public void fileChanged (String filename) throws Exception;
73          public void fileAdded (String filename) throws Exception;
74          public void fileRemoved (String filename) throws Exception;
75      }
76      
77      
78      public interface BulkListener extends Listener
79      {
80          public void filesChanged (List filenames) throws Exception;
81      }
82  
83  
84      /**
85       * 
86       */
87      public Scanner ()
88      {       
89      }
90  
91      /**
92       * Get the scan interval
93       * @return interval between scans in seconds
94       */
95      public int getScanInterval()
96      {
97          return _scanInterval;
98      }
99  
100     /**
101      * Set the scan interval
102      * @param scanInterval pause between scans in seconds
103      */
104     public synchronized void setScanInterval(int scanInterval)
105     {
106         this._scanInterval = scanInterval;
107         schedule();
108     }
109 
110     /**
111      * Set the location of the directory to scan.
112      * @param dir
113      * @deprecated use setScanDirs(List dirs) instead
114      */
115     public void setScanDir (File dir)
116     {
117         _scanDirs = new ArrayList();
118         _scanDirs.add(dir);
119     }
120 
121     /**
122      * Get the location of the directory to scan
123      * @return
124      * @deprecated use getScanDirs() instead
125      */
126     public File getScanDir ()
127     {
128         return (_scanDirs==null?null:(File)_scanDirs.get(0));
129     }
130 
131     public void setScanDirs (List dirs)
132     {
133         _scanDirs = dirs;
134     }
135     
136     public List getScanDirs ()
137     {
138         return _scanDirs;
139     }
140     
141     public void setRecursive (boolean recursive)
142     {
143         _recursive=recursive;
144     }
145     
146     public boolean getRecursive ()
147     {
148         return _recursive;
149     }
150     /**
151      * Apply a filter to files found in the scan directory.
152      * Only files matching the filter will be reported as added/changed/removed.
153      * @param filter
154      */
155     public void setFilenameFilter (FilenameFilter filter)
156     {
157         this._filter = filter;
158     }
159 
160     /**
161      * Get any filter applied to files in the scan dir.
162      * @return
163      */
164     public FilenameFilter getFilenameFilter ()
165     {
166         return _filter;
167     }
168 
169     /**
170      * Whether or not an initial scan will report all files as being
171      * added.
172      * @param reportExisting if true, all files found on initial scan will be 
173      * reported as being added, otherwise not
174      */
175     public void setReportExistingFilesOnStartup (boolean reportExisting)
176     {
177         this._reportExisting = reportExisting;
178     }
179 
180     /**
181      * Add an added/removed/changed listener
182      * @param listener
183      */
184     public synchronized void addListener (Listener listener)
185     {
186         if (listener == null)
187             return;
188         _listeners.add(listener);   
189     }
190 
191 
192 
193     /**
194      * Remove a registered listener
195      * @param listener the Listener to be removed
196      */
197     public synchronized void removeListener (Listener listener)
198     {
199         if (listener == null)
200             return;
201         _listeners.remove(listener);    
202     }
203 
204 
205     /**
206      * Start the scanning action.
207      */
208     public synchronized void start ()
209     {
210         if (_running)
211             return;
212 
213         _running = true;
214 
215         if (_reportExisting)
216         {
217             // if files exist at startup, report them
218             scan();
219         }
220         else
221         {
222             //just register the list of existing files and only report changes
223             scanFiles();
224             _prevScan.putAll(_currentScan);
225         }
226         schedule();
227     }
228 
229     public TimerTask newTimerTask ()
230     {
231         return new TimerTask()
232         {
233             public void run() { scan(); }
234         };
235     }
236 
237     public Timer newTimer ()
238     {
239         return new Timer("Scanner-"+__scannerId++, true);
240     }
241     
242     public void schedule ()
243     {  
244         if (_running)
245         {
246             if (_timer!=null)
247                 _timer.cancel();
248             if (_task!=null)
249                 _task.cancel();
250             if (getScanInterval() > 0)
251             {
252                 _timer = newTimer();
253                 _task = newTimerTask();
254                 _timer.schedule(_task, 1000L*getScanInterval(),1000L*getScanInterval());
255             }
256         }
257     }
258     /**
259      * Stop the scanning.
260      */
261     public synchronized void stop ()
262     {
263         if (_running)
264         {
265             _running = false; 
266             if (_timer!=null)
267                 _timer.cancel();
268             if (_task!=null)
269                 _task.cancel();
270             _task=null;
271             _timer=null;
272         }
273     }
274 
275     /**
276      * Perform a pass of the scanner and report changes
277      */
278     public void scan ()
279     {
280         scanFiles();
281         reportDifferences(_currentScan, _prevScan);
282         _prevScan.clear();
283         _prevScan.putAll(_currentScan);
284     }
285 
286     /**
287      * Recursively scan all files in the designated directories.
288      * @return Map of name of file to last modified time
289      */
290     public void scanFiles ()
291     {
292         if (_scanDirs==null)
293             return;
294         
295         _currentScan.clear();
296         Iterator itor = _scanDirs.iterator();
297         while (itor.hasNext())
298         {
299             File dir = (File)itor.next();
300             
301             if ((dir != null) && (dir.exists()))
302                 scanFile(dir, _currentScan);
303         }
304     }
305 
306 
307     /**
308      * Report the adds/changes/removes to the registered listeners
309      * 
310      * @param currentScan the info from the most recent pass
311      * @param oldScan info from the previous pass
312      */
313     public void reportDifferences (Map currentScan, Map oldScan) 
314     {
315         List bulkChanges = new ArrayList();
316         
317         Set oldScanKeys = new HashSet(oldScan.keySet());
318         Iterator itor = currentScan.entrySet().iterator();
319         while (itor.hasNext())
320         {
321             Map.Entry entry = (Map.Entry)itor.next();
322             if (!oldScanKeys.contains(entry.getKey()))
323             {
324                 Log.debug("File added: "+entry.getKey());
325                 reportAddition ((String)entry.getKey());
326                 bulkChanges.add(entry.getKey());
327             }
328             else if (!oldScan.get(entry.getKey()).equals(entry.getValue()))
329             {
330                 Log.debug("File changed: "+entry.getKey());
331                 reportChange((String)entry.getKey());
332                 oldScanKeys.remove(entry.getKey());
333                 bulkChanges.add(entry.getKey());
334             }
335             else
336                 oldScanKeys.remove(entry.getKey());
337         }
338 
339         if (!oldScanKeys.isEmpty())
340         {
341 
342             Iterator keyItor = oldScanKeys.iterator();
343             while (keyItor.hasNext())
344             {
345                 String filename = (String)keyItor.next();
346                 Log.debug("File removed: "+filename);
347                 reportRemoval(filename);
348                 bulkChanges.add(filename);
349             }
350         }
351         
352         if (!bulkChanges.isEmpty())
353             reportBulkChanges(bulkChanges);
354     }
355 
356 
357     /**
358      * Get last modified time on a single file or recurse if
359      * the file is a directory. 
360      * @param f file or directory
361      * @param scanInfoMap map of filenames to last modified times
362      */
363     private void scanFile (File f, Map scanInfoMap)
364     {
365         try
366         {
367             if (!f.exists())
368                 return;
369 
370             if (f.isFile())
371             {
372                 if ((_filter == null) || ((_filter != null) && _filter.accept(f.getParentFile(), f.getName())))
373                 {
374                     String name = f.getCanonicalPath();
375                     long lastModified = f.lastModified();
376                     scanInfoMap.put(name, new Long(lastModified));
377                 }
378             }
379             else if (f.isDirectory() && (_recursive || _scanDirs.contains(f)))
380             {
381                 File[] files = f.listFiles();
382                 for (int i=0;i<files.length;i++)
383                     scanFile(files[i], scanInfoMap);
384             }
385         }
386         catch (IOException e)
387         {
388             Log.warn("Error scanning watched files", e);
389         }
390     }
391 
392     private void warn(Object listener,String filename,Throwable th)
393     {
394         Log.warn(th);
395         Log.warn(listener+" failed on '"+filename);
396     }
397 
398     /**
399      * Report a file addition to the registered FileAddedListeners
400      * @param filename
401      */
402     private void reportAddition (String filename)
403     {
404         Iterator itor = _listeners.iterator();
405         while (itor.hasNext())
406         {
407             Object l = itor.next();
408             try
409             {
410                 if (l instanceof DiscreteListener)
411                     ((DiscreteListener)l).fileAdded(filename);
412             }
413             catch (Exception e)
414             {
415                 warn(l,filename,e);
416             }
417             catch (Error e)
418             {
419                 warn(l,filename,e);
420             }
421         }
422     }
423 
424 
425     /**
426      * Report a file removal to the FileRemovedListeners
427      * @param filename
428      */
429     private void reportRemoval (String filename)
430     {
431         Iterator itor = _listeners.iterator();
432         while (itor.hasNext())
433         {
434             Object l = itor.next();
435             try
436             {
437                 if (l instanceof DiscreteListener)
438                     ((DiscreteListener)l).fileRemoved(filename);
439             }
440             catch (Exception e)
441             {
442                 warn(l,filename,e);
443             }
444             catch (Error e)
445             {
446                 warn(l,filename,e);
447             }
448         }
449     }
450 
451 
452     /**
453      * Report a file change to the FileChangedListeners
454      * @param filename
455      */
456     private void reportChange (String filename)
457     {
458         Iterator itor = _listeners.iterator();
459         while (itor.hasNext())
460         {
461             Object l = itor.next();
462             try
463             {
464                 if (l instanceof DiscreteListener)
465                     ((DiscreteListener)l).fileChanged(filename);
466             }
467             catch (Exception e)
468             {
469                 warn(l,filename,e);
470             }
471             catch (Error e)
472             {
473                 warn(l,filename,e);
474             }
475         }
476     }
477     
478     private void reportBulkChanges (List filenames)
479     {
480         Iterator itor = _listeners.iterator();
481         while (itor.hasNext())
482         {
483             Object l = itor.next();
484             try
485             {
486                 if (l instanceof BulkListener)
487                     ((BulkListener)l).filesChanged(filenames);
488             }
489             catch (Exception e)
490             {
491                 warn(l,filenames.toString(),e);
492             }
493             catch (Error e)
494             {
495                 warn(l,filenames.toString(),e);
496             }
497         }
498     }
499 
500 }