All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.eclipse.jetty.util.Scanner Maven / Gradle / Ivy

There is a newer version: 2024.11.18751.20241128T090041Z-241100
Show newest version
// 
// ========================================================================
// Copyright (c) 1995-2022 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.
 *
 * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
 */
@Deprecated(since = "2021-05-27")
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
     *
     * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
     */
    @Deprecated(since = "2021-05-27")
    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.
     *
     * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
     */
    @Deprecated(since = "2021-05-27")
    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.
     *
     * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
     */
    @Deprecated(since = "2021-05-27")
    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.
     *
     * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
     */
    @Deprecated(since = "2021-05-27")
    public interface Listener {
    }

    // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
    @Deprecated(since = "2021-05-27")
    public interface ScanListener extends Listener {

        void scan();
    }

    // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
    @Deprecated(since = "2021-05-27")
    public interface DiscreteListener extends Listener {

        void fileChanged(String filename) throws Exception;

        void fileAdded(String filename) throws Exception;

        void fileRemoved(String filename) throws Exception;
    }

    // @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
    @Deprecated(since = "2021-05-27")
    public interface BulkListener extends Listener {

        void filesChanged(List filenames) throws Exception;
    }

    /**
     *  Listener that notifies when a scan has started and when it has ended.
     *
     * @deprecated The Eclipse Jetty and Apache Felix Http Jetty packages are no longer supported.
     */
    @Deprecated(since = "2021-05-27")
    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 twice so files reported as stable
            scan();
        } 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);
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy