//
// ========================================================================
// Copyright (c) 1995-2019 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.
/**
* 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.
/**
* 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.
/**
* 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
Returns: interval between scans in seconds
/**
* Get the scan interval
*
* @return interval between scans in seconds
*/
public synchronized int getScanInterval()
{
return _scanInterval;
}
Set the scan interval
Params: - scanInterval – pause between scans in seconds, or 0 for no scan after the initial scan.
/**
* 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();
}
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);
}
Params: - recursive – True if scanning is recursive
See Also:
/**
* @param recursive True if scanning is recursive
* @see #setScanDepth(int)
*/
public void setRecursive(boolean recursive)
{
_scanDepth = recursive ? -1 : 0;
}
See Also: Returns: True if scanning is fully recursive (scandepth==-1)
/**
* @return True if scanning is fully recursive (scandepth==-1)
* @see #getScanDepth()
*/
public boolean getRecursive()
{
return _scanDepth == -1;
}
Get the scanDepth.
Returns: the scanDepth
/**
* Get the scanDepth.
*
* @return the scanDepth
*/
public int getScanDepth()
{
return _scanDepth;
}
Set the scanDepth.
Params: - scanDepth – the scanDepth to set
/**
* 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.
Params: - filter – the filename filter to use
/**
* Apply a filter to files found in the scan directory.
* Only files matching the filter will be reported as added/changed/removed.
*
* @param filter the filename filter to use
*/
public void setFilenameFilter(FilenameFilter filter)
{
_filter = filter;
}
Get any filter applied to files in the scan dir.
Returns: the filename 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.
Params: - reportExisting – if true, all files found on initial scan will be
reported as being added, otherwise not
/**
* 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.
Params: - dirs – true to report directory changes as well
/**
* Set if found directories should be reported.
*
* @param dirs true to report directory changes as well
*/
public void setReportDirs(boolean dirs)
{
_reportDirs = dirs;
}
public boolean getReportDirs()
{
return _reportDirs;
}
Add an added/removed/changed listener
Params: - listener – the listener to add
/**
* Add an added/removed/changed listener
*
* @param listener the listener to add
*/
public synchronized void addListener(Listener listener)
{
if (listener == null)
return;
_listeners.add(listener);
}
Remove a registered listener
Params: - listener – the Listener to be removed
/**
* 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.
/**
* 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.
/**
* 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;
}
}
Params: - path – tests if the path exists
Returns: true if the path exists in one of the scandirs
/**
* @param path tests if the path exists
* @return true if the path exists in one of the scandirs
*/
public boolean exists(String path)
{
for (File dir : _scanDirs)
{
if (new File(dir, path).exists())
return true;
}
return false;
}
Perform a pass of the scanner and report changes
/**
* 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.
/**
* 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
Params: - currentScan – the info from the most recent pass
- oldScan – info from the previous pass
/**
* 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);
break;
default:
break;
}
}
}
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);
break;
default:
break;
}
}
}
}
// 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);
break;
default:
break;
}
}
}
}
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;
default:
break;
}
}
if (!bulkChanges.isEmpty())
reportBulkChanges(bulkChanges);
}
Get last modified time on a single file or recurse if
the file is a directory.
Params: - f – file or directory
- scanInfoMap – map of filenames to last modified times
/**
* 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())))
{
if (LOG.isDebugEnabled())
LOG.debug("scan accepted {}", f);
String name = f.getCanonicalPath();
scanInfoMap.put(name, new TimeNSize(f.lastModified(), f.isDirectory() ? 0 : f.length()));
}
else
{
if (LOG.isDebugEnabled())
LOG.debug("scan rejected {}", f);
}
}
// 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
Params: - filename – the filename
/**
* Report a file addition to the registered FileAddedListeners
*
* @param filename the 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
Params: - filename – the filename
/**
* Report a file removal to the FileRemovedListeners
*
* @param filename the 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
Params: - filename – the filename
/**
* Report a file change to the FileChangedListeners
*
* @param filename the 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
/**
* 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
/**
* 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);
}
}
}
}