blob: cc92ae658594c5a099e87226c36b849ac04ab340 [file] [log] [blame]
//
// ========================================================================
// Copyright (c) 1995-2014 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.util;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Scanner
*
* Utility for scanning a directory for added, removed and changed
* files and reporting these events via registered Listeners.
*
*/
public class Scanner extends AbstractLifeCycle
{
private static final Logger LOG = Log.getLogger(Scanner.class);
private static int __scannerId=0;
private int _scanInterval;
private int _scanCount = 0;
private final List<Listener> _listeners = new ArrayList<Listener>();
private final Map<String,TimeNSize> _prevScan = new HashMap<String,TimeNSize> ();
private final Map<String,TimeNSize> _currentScan = new HashMap<String,TimeNSize> ();
private FilenameFilter _filter;
private final List<File> _scanDirs = new ArrayList<File>();
private volatile boolean _running = false;
private boolean _reportExisting = true;
private boolean _reportDirs = true;
private Timer _timer;
private TimerTask _task;
private int _scanDepth=0;
public enum Notification { ADDED, CHANGED, REMOVED };
private final Map<String,Notification> _notifications = new HashMap<String,Notification>();
static class TimeNSize
{
final long _lastModified;
final long _size;
public TimeNSize(long lastModified, long size)
{
_lastModified = lastModified;
_size = size;
}
@Override
public int hashCode()
{
return (int)_lastModified^(int)_size;
}
@Override
public boolean equals(Object o)
{
if (o instanceof TimeNSize)
{
TimeNSize tns = (TimeNSize)o;
return tns._lastModified==_lastModified && tns._size==_size;
}
return false;
}
@Override
public String toString()
{
return "[lm="+_lastModified+",s="+_size+"]";
}
}
/**
* Listener
*
* Marker for notifications re file changes.
*/
public interface Listener
{
}
public interface ScanListener extends Listener
{
public void scan();
}
public interface DiscreteListener extends Listener
{
public void fileChanged (String filename) throws Exception;
public void fileAdded (String filename) throws Exception;
public void fileRemoved (String filename) throws Exception;
}
public interface BulkListener extends Listener
{
public void filesChanged (List<String> filenames) throws Exception;
}
/**
* Listener that notifies when a scan has started and when it has ended.
*/
public interface ScanCycleListener extends Listener
{
public void scanStarted(int cycle) throws Exception;
public void scanEnded(int cycle) throws Exception;
}
/**
*
*/
public Scanner ()
{
}
/**
* Get the scan interval
* @return interval between scans in seconds
*/
public int getScanInterval()
{
return _scanInterval;
}
/**
* Set the scan interval
* @param scanInterval pause between scans in seconds, or 0 for no scan after the initial scan.
*/
public synchronized void setScanInterval(int scanInterval)
{
_scanInterval = scanInterval;
schedule();
}
/**
* Set the location of the directory to scan.
* @param dir
* @deprecated use setScanDirs(List dirs) instead
*/
@Deprecated
public void setScanDir (File dir)
{
_scanDirs.clear();
_scanDirs.add(dir);
}
/**
* Get the location of the directory to scan
* @return the first directory (of {@link #getScanDirs()} being scanned)
* @deprecated use getScanDirs() instead
*/
@Deprecated
public File getScanDir ()
{
return (_scanDirs==null?null:(File)_scanDirs.get(0));
}
public void setScanDirs (List<File> dirs)
{
_scanDirs.clear();
_scanDirs.addAll(dirs);
}
public synchronized void addScanDir( File dir )
{
_scanDirs.add( dir );
}
public List<File> getScanDirs ()
{
return Collections.unmodifiableList(_scanDirs);
}
/* ------------------------------------------------------------ */
/**
* @param recursive True if scanning is recursive
* @see #setScanDepth(int)
*/
public void setRecursive (boolean recursive)
{
_scanDepth=recursive?-1:0;
}
/* ------------------------------------------------------------ */
/**
* @return True if scanning is fully recursive (scandepth==-1)
* @see #getScanDepth()
*/
public boolean getRecursive ()
{
return _scanDepth==-1;
}
/* ------------------------------------------------------------ */
/** Get the scanDepth.
* @return the scanDepth
*/
public int getScanDepth()
{
return _scanDepth;
}
/* ------------------------------------------------------------ */
/** Set the scanDepth.
* @param scanDepth the scanDepth to set
*/
public void setScanDepth(int scanDepth)
{
_scanDepth = scanDepth;
}
/**
* Apply a filter to files found in the scan directory.
* Only files matching the filter will be reported as added/changed/removed.
* @param filter
*/
public void setFilenameFilter (FilenameFilter filter)
{
_filter = filter;
}
/**
* Get any filter applied to files in the scan dir.
* @return the filename filter
*/
public FilenameFilter getFilenameFilter ()
{
return _filter;
}
/* ------------------------------------------------------------ */
/**
* Whether or not an initial scan will report all files as being
* added.
* @param reportExisting if true, all files found on initial scan will be
* reported as being added, otherwise not
*/
public void setReportExistingFilesOnStartup (boolean reportExisting)
{
_reportExisting = reportExisting;
}
/* ------------------------------------------------------------ */
public boolean getReportExistingFilesOnStartup()
{
return _reportExisting;
}
/* ------------------------------------------------------------ */
/** Set if found directories should be reported.
* @param dirs
*/
public void setReportDirs(boolean dirs)
{
_reportDirs=dirs;
}
/* ------------------------------------------------------------ */
public boolean getReportDirs()
{
return _reportDirs;
}
/* ------------------------------------------------------------ */
/**
* Add an added/removed/changed listener
* @param listener
*/
public synchronized void addListener (Listener listener)
{
if (listener == null)
return;
_listeners.add(listener);
}
/**
* Remove a registered listener
* @param listener the Listener to be removed
*/
public synchronized void removeListener (Listener listener)
{
if (listener == null)
return;
_listeners.remove(listener);
}
/**
* Start the scanning action.
*/
@Override
public synchronized void doStart()
{
if (_running)
return;
_running = true;
if (_reportExisting)
{
// if files exist at startup, report them
scan();
scan(); // scan twice so files reported as stable
}
else
{
//just register the list of existing files and only report changes
scanFiles();
_prevScan.putAll(_currentScan);
}
schedule();
}
public TimerTask newTimerTask ()
{
return new TimerTask()
{
@Override
public void run() { scan(); }
};
}
public Timer newTimer ()
{
return new Timer("Scanner-"+__scannerId++, true);
}
public void schedule ()
{
if (_running)
{
if (_timer!=null)
_timer.cancel();
if (_task!=null)
_task.cancel();
if (getScanInterval() > 0)
{
_timer = newTimer();
_task = newTimerTask();
_timer.schedule(_task, 1010L*getScanInterval(),1010L*getScanInterval());
}
}
}
/**
* Stop the scanning.
*/
@Override
public synchronized void doStop()
{
if (_running)
{
_running = false;
if (_timer!=null)
_timer.cancel();
if (_task!=null)
_task.cancel();
_task=null;
_timer=null;
}
}
/**
* Perform a pass of the scanner and report changes
*/
public synchronized void scan ()
{
reportScanStart(++_scanCount);
scanFiles();
reportDifferences(_currentScan, _prevScan);
_prevScan.clear();
_prevScan.putAll(_currentScan);
reportScanEnd(_scanCount);
for (Listener l : _listeners)
{
try
{
if (l instanceof ScanListener)
((ScanListener)l).scan();
}
catch (Exception e)
{
LOG.warn(e);
}
catch (Error e)
{
LOG.warn(e);
}
}
}
/**
* Recursively scan all files in the designated directories.
*/
public synchronized void scanFiles ()
{
if (_scanDirs==null)
return;
_currentScan.clear();
Iterator<File> itor = _scanDirs.iterator();
while (itor.hasNext())
{
File dir = itor.next();
if ((dir != null) && (dir.exists()))
try
{
scanFile(dir.getCanonicalFile(), _currentScan,0);
}
catch (IOException e)
{
LOG.warn("Error scanning files.", e);
}
}
}
/**
* Report the adds/changes/removes to the registered listeners
*
* @param currentScan the info from the most recent pass
* @param oldScan info from the previous pass
*/
public synchronized void reportDifferences (Map<String,TimeNSize> currentScan, Map<String,TimeNSize> oldScan)
{
// scan the differences and add what was found to the map of notifications:
Set<String> oldScanKeys = new HashSet<String>(oldScan.keySet());
// Look for new and changed files
for (Map.Entry<String, TimeNSize> entry: currentScan.entrySet())
{
String file = entry.getKey();
if (!oldScanKeys.contains(file))
{
Notification old=_notifications.put(file,Notification.ADDED);
if (old!=null)
{
switch(old)
{
case REMOVED:
case CHANGED:
_notifications.put(file,Notification.CHANGED);
}
}
}
else if (!oldScan.get(file).equals(currentScan.get(file)))
{
Notification old=_notifications.put(file,Notification.CHANGED);
if (old!=null)
{
switch(old)
{
case ADDED:
_notifications.put(file,Notification.ADDED);
}
}
}
}
// Look for deleted files
for (String file : oldScan.keySet())
{
if (!currentScan.containsKey(file))
{
Notification old=_notifications.put(file,Notification.REMOVED);
if (old!=null)
{
switch(old)
{
case ADDED:
_notifications.remove(file);
}
}
}
}
if (LOG.isDebugEnabled())
LOG.debug("scanned "+_scanDirs+": "+_notifications);
// Process notifications
// Only process notifications that are for stable files (ie same in old and current scan).
List<String> bulkChanges = new ArrayList<String>();
for (Iterator<Entry<String,Notification>> iter = _notifications.entrySet().iterator();iter.hasNext();)
{
Entry<String,Notification> entry=iter.next();
String file=entry.getKey();
// Is the file stable?
if (oldScan.containsKey(file))
{
if (!oldScan.get(file).equals(currentScan.get(file)))
continue;
}
else if (currentScan.containsKey(file))
continue;
// File is stable so notify
Notification notification=entry.getValue();
iter.remove();
bulkChanges.add(file);
switch(notification)
{
case ADDED:
reportAddition(file);
break;
case CHANGED:
reportChange(file);
break;
case REMOVED:
reportRemoval(file);
break;
}
}
if (!bulkChanges.isEmpty())
reportBulkChanges(bulkChanges);
}
/**
* Get last modified time on a single file or recurse if
* the file is a directory.
* @param f file or directory
* @param scanInfoMap map of filenames to last modified times
*/
private void scanFile (File f, Map<String,TimeNSize> scanInfoMap, int depth)
{
try
{
if (!f.exists())
return;
if (f.isFile() || depth>0&& _reportDirs && f.isDirectory())
{
if ((_filter == null) || ((_filter != null) && _filter.accept(f.getParentFile(), f.getName())))
{
String name = f.getCanonicalPath();
scanInfoMap.put(name, new TimeNSize(f.lastModified(),f.length()));
}
}
// If it is a directory, scan if it is a known directory or the depth is OK.
if (f.isDirectory() && (depth<_scanDepth || _scanDepth==-1 || _scanDirs.contains(f)))
{
File[] files = f.listFiles();
if (files != null)
{
for (int i=0;i<files.length;i++)
scanFile(files[i], scanInfoMap,depth+1);
}
else
LOG.warn("Error listing files in directory {}", f);
}
}
catch (IOException e)
{
LOG.warn("Error scanning watched files", e);
}
}
private void warn(Object listener,String filename,Throwable th)
{
LOG.warn(listener+" failed on '"+filename, th);
}
/**
* Report a file addition to the registered FileAddedListeners
* @param filename
*/
private void reportAddition (String filename)
{
Iterator<Listener> itor = _listeners.iterator();
while (itor.hasNext())
{
Listener l = itor.next();
try
{
if (l instanceof DiscreteListener)
((DiscreteListener)l).fileAdded(filename);
}
catch (Exception e)
{
warn(l,filename,e);
}
catch (Error e)
{
warn(l,filename,e);
}
}
}
/**
* Report a file removal to the FileRemovedListeners
* @param filename
*/
private void reportRemoval (String filename)
{
Iterator<Listener> itor = _listeners.iterator();
while (itor.hasNext())
{
Object l = itor.next();
try
{
if (l instanceof DiscreteListener)
((DiscreteListener)l).fileRemoved(filename);
}
catch (Exception e)
{
warn(l,filename,e);
}
catch (Error e)
{
warn(l,filename,e);
}
}
}
/**
* Report a file change to the FileChangedListeners
* @param filename
*/
private void reportChange (String filename)
{
Iterator<Listener> itor = _listeners.iterator();
while (itor.hasNext())
{
Listener l = itor.next();
try
{
if (l instanceof DiscreteListener)
((DiscreteListener)l).fileChanged(filename);
}
catch (Exception e)
{
warn(l,filename,e);
}
catch (Error e)
{
warn(l,filename,e);
}
}
}
private void reportBulkChanges (List<String> filenames)
{
Iterator<Listener> itor = _listeners.iterator();
while (itor.hasNext())
{
Listener l = itor.next();
try
{
if (l instanceof BulkListener)
((BulkListener)l).filesChanged(filenames);
}
catch (Exception e)
{
warn(l,filenames.toString(),e);
}
catch (Error e)
{
warn(l,filenames.toString(),e);
}
}
}
/**
* signal any scan cycle listeners that a scan has started
*/
private void reportScanStart(int cycle)
{
for (Listener listener : _listeners)
{
try
{
if (listener instanceof ScanCycleListener)
{
((ScanCycleListener)listener).scanStarted(cycle);
}
}
catch (Exception e)
{
LOG.warn(listener + " failed on scan start for cycle " + cycle, e);
}
}
}
/**
* sign
*/
private void reportScanEnd(int cycle)
{
for (Listener listener : _listeners)
{
try
{
if (listener instanceof ScanCycleListener)
{
((ScanCycleListener)listener).scanEnded(cycle);
}
}
catch (Exception e)
{
LOG.warn(listener + " failed on scan end for cycle " + cycle, e);
}
}
}
}