org.eclipse.jetty.util.Scanner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ehcache Show documentation
Show all versions of ehcache Show documentation
Ehcache is an open source, standards-based cache used to boost performance,
offload the database and simplify scalability. Ehcache is robust, proven and full-featured and
this has made it the most widely-used Java-based cache.
The newest version!
//
// ========================================================================
// Copyright (c) 1995-2021 Mort Bay Consulting Pty Ltd and others.
// ------------------------------------------------------------------------
// 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.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
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 java.util.function.Predicate;
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
{
/**
* When walking a directory, a depth of 1 ensures that
* the directory's descendants are visited, not just the
* directory itself (as a file).
*
* @see Visitor#preVisitDirectory
*/
public static final int DEFAULT_SCAN_DEPTH = 1;
public static final int MAX_SCAN_DEPTH = Integer.MAX_VALUE;
private static final Logger LOG = Log.getLogger(Scanner.class);
private static int __scannerId = 0;
private int _scanInterval;
private int _scanCount = 0;
private final List _listeners = new ArrayList<>();
private final Map _prevScan = new HashMap<>();
private final Map _currentScan = new HashMap<>();
private FilenameFilter _filter;
private final Map> _scannables = new HashMap<>();
private volatile boolean _running = false;
private boolean _reportExisting = true;
private boolean _reportDirs = true;
private Timer _timer;
private TimerTask _task;
private int _scanDepth = DEFAULT_SCAN_DEPTH;
public enum Notification
{
ADDED, CHANGED, REMOVED
}
private final Map _notifications = new HashMap<>();
/**
* PathMatcherSet
*
* A set of PathMatchers for testing Paths against path matching patterns via
* @see IncludeExcludeSet
*/
static class PathMatcherSet extends HashSet implements Predicate
{
@Override
public boolean test(Path p)
{
for (PathMatcher pm : this)
{
if (pm.matches(p))
return true;
}
return false;
}
}
/**
* TimeNSize
*
* Metadata about a file: Last modified time and file size.
*/
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 + "]";
}
}
/**
* Visitor
*
* A FileVisitor for walking a subtree of paths. The Scanner uses
* this to examine the dirs and files it has been asked to scan.
*/
class Visitor implements FileVisitor
{
Map scanInfoMap;
IncludeExcludeSet rootIncludesExcludes;
Path root;
public Visitor(Path root, IncludeExcludeSet rootIncludesExcludes, Map scanInfoMap)
{
this.root = root;
this.rootIncludesExcludes = rootIncludesExcludes;
this.scanInfoMap = scanInfoMap;
}
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
if (!Files.exists(dir))
return FileVisitResult.SKIP_SUBTREE;
File f = dir.toFile();
//if we want to report directories and we haven't already seen it
if (_reportDirs && !scanInfoMap.containsKey(f.getCanonicalPath()))
{
boolean accepted = false;
if (rootIncludesExcludes != null && !rootIncludesExcludes.isEmpty())
{
//accepted if not explicitly excluded and either is explicitly included or there are no explicit inclusions
boolean result = rootIncludesExcludes.test(dir);
if (result)
accepted = true;
}
else
{
if (_filter == null || _filter.accept(f.getParentFile(), f.getName()))
accepted = true;
}
if (accepted)
{
scanInfoMap.put(f.getCanonicalPath(), new TimeNSize(f.lastModified(), f.isDirectory() ? 0 : f.length()));
if (LOG.isDebugEnabled()) LOG.debug("scan accepted dir {} mod={}", f, f.lastModified());
}
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
if (!Files.exists(file))
return FileVisitResult.CONTINUE;
File f = file.toFile();
boolean accepted = false;
if (f.isFile() || (f.isDirectory() && _reportDirs && !scanInfoMap.containsKey(f.getCanonicalPath())))
{
if (rootIncludesExcludes != null && !rootIncludesExcludes.isEmpty())
{
//accepted if not explicitly excluded and either is explicitly included or there are no explicit inclusions
boolean result = rootIncludesExcludes.test(file);
if (result)
accepted = true;
}
else if (_filter == null || _filter.accept(f.getParentFile(), f.getName()))
accepted = true;
}
if (accepted)
{
scanInfoMap.put(f.getCanonicalPath(), new TimeNSize(f.lastModified(), f.isDirectory() ? 0 : f.length()));
if (LOG.isDebugEnabled()) LOG.debug("scan accepted {} mod={}", f, f.lastModified());
}
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException
{
LOG.warn(exc);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException
{
return FileVisitResult.CONTINUE;
}
}
/**
* Listener
*
* Marker for notifications re file changes.
*/
public interface Listener
{
}
public interface ScanListener extends Listener
{
void scan();
}
public interface DiscreteListener extends Listener
{
void fileChanged(String filename) throws Exception;
void fileAdded(String filename) throws Exception;
void fileRemoved(String filename) throws Exception;
}
public interface BulkListener extends Listener
{
void filesChanged(List filenames) throws Exception;
}
/**
* Listener that notifies when a scan has started and when it has ended.
*/
public interface ScanCycleListener extends Listener
{
void scanStarted(int cycle) throws Exception;
void scanEnded(int cycle) throws Exception;
}
/**
*
*/
public Scanner()
{
}
/**
* Get the scan interval
*
* @return interval between scans in seconds
*/
public synchronized 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();
}
public void setScanDirs(List dirs)
{
_scannables.clear();
if (dirs == null)
return;
for (File f:dirs)
{
addScanDir(f);
}
}
@Deprecated
public synchronized void addScanDir(File dir)
{
if (dir == null)
return;
try
{
if (dir.isDirectory())
addDirectory(dir.toPath());
else
addFile(dir.toPath());
}
catch (Exception e)
{
LOG.warn(e);
}
}
/**
* Add a file to be scanned. The file must not be null, and must exist.
*
* @param p the Path of the file to scan.
* @throws IOException
*/
public synchronized void addFile(Path p) throws IOException
{
if (p == null)
throw new IllegalStateException("Null path");
File f = p.toFile();
if (!f.exists() || f.isDirectory())
throw new IllegalStateException("Not file or doesn't exist: " + f.getCanonicalPath());
_scannables.put(p, null);
}
/**
* Add a directory to be scanned. The directory must not be null and must exist.
*
* @param p the directory to scan.
* @return an IncludeExcludeSet to which the caller can add PathMatcher patterns to match
* @throws IOException
*/
public synchronized IncludeExcludeSet addDirectory(Path p)
throws IOException
{
if (p == null)
throw new IllegalStateException("Null path");
File f = p.toFile();
if (!f.exists() || !f.isDirectory())
throw new IllegalStateException("Not directory or doesn't exist: " + f.getCanonicalPath());
IncludeExcludeSet includesExcludes = _scannables.get(p);
if (includesExcludes == null)
{
includesExcludes = new IncludeExcludeSet<>(PathMatcherSet.class);
_scannables.put(p.toRealPath(), includesExcludes);
}
return includesExcludes;
}
@Deprecated
public List getScanDirs()
{
ArrayList files = new ArrayList<>();
for (Path p : _scannables.keySet())
files.add(p.toFile());
return Collections.unmodifiableList(files);
}
public Set getScannables()
{
return _scannables.keySet();
}
/**
* @param recursive True if scanning is recursive
* @see #setScanDepth(int)
*/
@Deprecated
public void setRecursive(boolean recursive)
{
_scanDepth = recursive ? Integer.MAX_VALUE : 1;
}
/**
* @return True if scanning is recursive
* @see #getScanDepth()
*/
@Deprecated
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 the filename filter to use
*/
@Deprecated
public void setFilenameFilter(FilenameFilter filter)
{
_filter = filter;
}
/**
* Get any filter applied to files in the scan dir.
*
* @return the filename filter
*/
@Deprecated
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 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
*
* @param listener the listener to add
*/
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 (LOG.isDebugEnabled())
LOG.debug("Scanner start: rprtExists={}, depth={}, rprtDirs={}, interval={}, filter={}, scannables={}",
_reportExisting, _scanDepth, _reportDirs, _scanInterval, _filter, _scannables);
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;
}
}
/**
* Clear the list of scannables. The scanner must first
* be in the stopped state.
*/
public void reset()
{
if (!isStopped())
throw new IllegalStateException("Not stopped");
//clear the scannables
_scannables.clear();
//clear the previous scans
_currentScan.clear();
_prevScan.clear();
}
/**
* @param path tests if the path exists
* @return true if the path exists in one of the scandirs
*/
public boolean exists(String path)
{
for (Path p : _scannables.keySet())
{
if (p.resolve(path).toFile().exists())
return true;
}
return false;
}
/**
* 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 (Throwable e)
{
LOG.warn(e);
}
}
}
/**
* Scan all of the given paths.
*/
public synchronized void scanFiles()
{
_currentScan.clear();
for (Entry> entry : _scannables.entrySet())
{
Path p = entry.getKey();
try
{
Files.walkFileTree(p, EnumSet.allOf(FileVisitOption.class), _scanDepth, new Visitor(p, entry.getValue(), _currentScan));
}
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
*/
private synchronized void reportDifferences(Map currentScan, Map oldScan)
{
// scan the differences and add what was found to the map of notifications:
Set oldScanKeys = new HashSet<>(oldScan.keySet());
// Look for new and changed files
for (Map.Entry 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 == Notification.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 == Notification.ADDED)
_notifications.remove(file);
}
}
if (LOG.isDebugEnabled())
LOG.debug("scanned " + _scannables.keySet() + ": " + _notifications);
// Process notifications
// Only process notifications that are for stable files (ie same in old and current scan).
List bulkChanges = new ArrayList<>();
for (Iterator> iter = _notifications.entrySet().iterator(); iter.hasNext(); )
{
Entry 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);
}
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 the filename
*/
private void reportAddition(String filename)
{
for (Listener l : _listeners)
{
try
{
if (l instanceof DiscreteListener)
((DiscreteListener)l).fileAdded(filename);
}
catch (Throwable e)
{
warn(l, filename, e);
}
}
}
/**
* Report a file removal to the FileRemovedListeners
*
* @param filename the filename
*/
private void reportRemoval(String filename)
{
for (Object l : _listeners)
{
try
{
if (l instanceof DiscreteListener)
((DiscreteListener)l).fileRemoved(filename);
}
catch (Throwable e)
{
warn(l, filename, e);
}
}
}
/**
* Report a file change to the FileChangedListeners
*
* @param filename the filename
*/
private void reportChange(String filename)
{
for (Listener l : _listeners)
{
try
{
if (l instanceof DiscreteListener)
((DiscreteListener)l).fileChanged(filename);
}
catch (Throwable e)
{
warn(l, filename, e);
}
}
}
/**
* Report the list of filenames for which changes were detected.
*
* @param filenames names of all files added/changed/removed
*/
private void reportBulkChanges(List filenames)
{
for (Listener l : _listeners)
{
try
{
if (l instanceof BulkListener)
((BulkListener)l).filesChanged(filenames);
}
catch (Throwable e)
{
warn(l, filenames.toString(), e);
}
}
}
/**
* Call ScanCycleListeners with start of scan
*/
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);
}
}
}
/**
* Call ScanCycleListeners with end of scan.
*/
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);
}
}
}
}