//
//  ========================================================================
//  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); } } } }