From aefbdfca587277bc3d27a7e0b8faff3eb0ba4d05 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Wed, 6 Nov 2019 15:55:20 +1100 Subject: [PATCH] Issue #2266 Rework Scanner and use it for Jetty Maven Plugin (#4239) * Issue #2266 Rework Scanner and use it for Jetty Maven Plugin Signed-off-by: Jan Bartel --- .../deploy/providers/ScanningAppProvider.java | 1 + .../deploy/providers/WebAppProvider.java | 5 + .../jetty/maven/plugin/AbstractJettyMojo.java | 26 +- .../jetty/maven/plugin/JettyRunMojo.java | 137 +++---- .../maven/plugin/JettyRunWarExplodedMojo.java | 95 ++--- .../jetty/maven/plugin/JettyRunWarMojo.java | 48 +-- .../jetty/maven/plugin/ScanPattern.java | 2 +- .../jetty/maven/plugin/ScanTargetPattern.java | 21 ++ .../java/org/eclipse/jetty/util/Scanner.java | 345 ++++++++++++++---- .../org/eclipse/jetty/util/ScannerTest.java | 158 +++++++- 10 files changed, 562 insertions(+), 276 deletions(-) diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java index 0507816c76b..9a27d2bf93a 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/ScanningAppProvider.java @@ -141,6 +141,7 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A _scanner.setRecursive(_recursive); _scanner.setFilenameFilter(_filenameFilter); _scanner.setReportDirs(true); + _scanner.setScanDepth(1); //consider direct dir children of monitored dir _scanner.addListener(_scannerListener); _scanner.start(); } diff --git a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java index 9cc87b055c3..663d9963b72 100644 --- a/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java +++ b/jetty-deploy/src/main/java/org/eclipse/jetty/deploy/providers/WebAppProvider.java @@ -81,6 +81,11 @@ public class WebAppProvider extends ScanningAppProvider String lowername = name.toLowerCase(Locale.ENGLISH); File file = new File(dir, name); + Resource r = Resource.newResource(file); + if (getMonitoredResources().contains(r) && r.isDirectory()) + { + return false; + } // ignore hidden files if (lowername.startsWith(".")) diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java index ad7c6b4b667..1977a35a590 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/AbstractJettyMojo.java @@ -50,6 +50,7 @@ import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.util.PathWatcher; +import org.eclipse.jetty.util.Scanner; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.xml.XmlConfiguration; @@ -222,7 +223,7 @@ public abstract class AbstractJettyMojo extends AbstractMojo /** * A scanner to check for changes to the webapp */ - protected PathWatcher scanner; + protected Scanner scanner; /** * A scanner to check ENTER hits on the console @@ -458,7 +459,25 @@ public abstract class AbstractJettyMojo extends AbstractMojo // start the scanner thread (if necessary) on the main webapp if (isScanningEnabled()) { - scanner = new PathWatcher(); + scanner = new Scanner(); + scanner.setScanInterval(scanIntervalSeconds); + scanner.setScanDepth(Scanner.MAX_SCAN_DEPTH); //always fully walk directory hierarchies + scanner.setReportExistingFilesOnStartup(false); + scanner.addListener(new Scanner.BulkListener() + { + public void filesChanged(List changes) + { + try + { + boolean reconfigure = changes.contains(project.getFile().getCanonicalPath()); + restartWebApp(reconfigure); + } + catch (Exception e) + { + getLog().error("Error reconfiguring/restarting webapp after change in watched files",e); + } + } + }); configureScanner(); startScanner(); } @@ -523,7 +542,6 @@ public abstract class AbstractJettyMojo extends AbstractMojo XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(path.toFile())); getLog().info("Applying context xml file " + contextXml); - xmlConfiguration.configure(webApp); } //If no contextPath was specified, go with default of project artifactid @@ -562,8 +580,6 @@ public abstract class AbstractJettyMojo extends AbstractMojo if (!isScanningEnabled()) return; - scanner.setNotifyExistingOnStart(false); - scanner.start(); } diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java index 8e6d5277266..c939be694bd 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunMojo.java @@ -22,6 +22,7 @@ import java.io.File; import java.io.IOException; import java.net.URL; import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -41,8 +42,7 @@ import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.project.MavenProject; import org.eclipse.jetty.maven.plugin.utils.MavenProjectHelper; -import org.eclipse.jetty.util.PathWatcher; -import org.eclipse.jetty.util.PathWatcher.PathWatchEvent; +import org.eclipse.jetty.util.IncludeExcludeSet; import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.ResourceCollection; @@ -145,9 +145,6 @@ public class JettyRunMojo extends AbstractJettyMojo protected Resource originalBaseResource; - /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#execute() - */ @Override public void execute() throws MojoExecutionException, MojoFailureException { @@ -157,8 +154,6 @@ public class JettyRunMojo extends AbstractJettyMojo /** * Verify the configuration given in the pom. - * - * @see AbstractJettyMojo#checkPomConfiguration() */ @Override public boolean checkPomConfiguration() throws MojoExecutionException @@ -229,9 +224,6 @@ public class JettyRunMojo extends AbstractJettyMojo super.finishConfigurationBeforeStart(); } - /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureWebApplication() - */ @Override public void configureWebApplication() throws Exception { @@ -317,9 +309,6 @@ public class JettyRunMojo extends AbstractJettyMojo getLog().info("Webapp directory = " + webAppSourceDirectory.getCanonicalPath()); } - /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureScanner() - */ @Override public void configureScanner() throws MojoExecutionException @@ -332,36 +321,6 @@ public class JettyRunMojo extends AbstractJettyMojo { throw new MojoExecutionException("Error forming scan list", e); } - - scanner.addListener(new PathWatcher.EventListListener() - { - - @Override - public void onPathWatchEvents(List events) - { - try - { - boolean reconfigure = false; - if (events != null) - { - for (PathWatchEvent e : events) - { - if (e.getPath().equals(project.getFile().toPath())) - { - reconfigure = true; - break; - } - } - } - - restartWebApp(reconfigure); - } - catch (Exception e) - { - getLog().error("Error reconfiguring/restarting webapp after change in watched files", e); - } - } - }); } public void gatherScannables() throws Exception @@ -369,33 +328,37 @@ public class JettyRunMojo extends AbstractJettyMojo if (webApp.getDescriptor() != null) { Resource r = Resource.newResource(webApp.getDescriptor()); - scanner.watch(r.getFile().toPath()); + scanner.addFile(r.getFile().toPath()); } if (webApp.getJettyEnvXml() != null) - scanner.watch(new File(webApp.getJettyEnvXml()).toPath()); + scanner.addFile(new File(webApp.getJettyEnvXml()).toPath()); if (webApp.getDefaultsDescriptor() != null) { if (!WebAppContext.WEB_DEFAULTS_XML.equals(webApp.getDefaultsDescriptor())) - scanner.watch(new File(webApp.getDefaultsDescriptor()).toPath()); + scanner.addFile(new File(webApp.getDefaultsDescriptor()).toPath()); } if (webApp.getOverrideDescriptor() != null) { - scanner.watch(new File(webApp.getOverrideDescriptor()).toPath()); + scanner.addFile(new File(webApp.getOverrideDescriptor()).toPath()); } File jettyWebXmlFile = findJettyWebXmlFile(new File(webAppSourceDirectory, "WEB-INF")); if (jettyWebXmlFile != null) { - scanner.watch(jettyWebXmlFile.toPath()); + scanner.addFile(jettyWebXmlFile.toPath()); } //make sure each of the war artifacts is added to the scanner for (Artifact a : getWarArtifacts()) { - scanner.watch(a.getFile().toPath()); + File f = a.getFile(); + if (a.getFile().isDirectory()) + scanner.addDirectory(f.toPath()); + else + scanner.addFile(f.toPath()); } //handle the explicit extra scan targets @@ -405,87 +368,81 @@ public class JettyRunMojo extends AbstractJettyMojo { if (f.isDirectory()) { - PathWatcher.Config config = new PathWatcher.Config(f.toPath()); - config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); - scanner.watch(config); + scanner.addDirectory(f.toPath()); } else - scanner.watch(f.toPath()); + scanner.addFile(f.toPath()); } } - + + scanner.addFile(project.getFile().toPath()); + //handle the extra scan patterns if (scanTargetPatterns != null) { for (ScanTargetPattern p : scanTargetPatterns) { - PathWatcher.Config config = new PathWatcher.Config(p.getDirectory().toPath()); - config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); - for (String pattern : p.getExcludes()) - { - config.addExcludeGlobRelative(pattern); - } - for (String pattern : p.getIncludes()) - { - config.addIncludeGlobRelative(pattern); - } - scanner.watch(config); + IncludeExcludeSet includesExcludes = scanner.addDirectory(p.getDirectory().toPath()); + p.configureIncludesExcludeSet(includesExcludes); } } - scanner.watch(project.getFile().toPath()); - if (webApp.getTestClasses() != null && webApp.getTestClasses().exists()) { - PathWatcher.Config config = new PathWatcher.Config(webApp.getTestClasses().toPath()); - config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); + Path p = webApp.getTestClasses().toPath(); + IncludeExcludeSet includeExcludeSet = scanner.addDirectory(p); + if (scanTestClassesPattern != null) { - for (String p : scanTestClassesPattern.getExcludes()) + for (String s : scanTestClassesPattern.getExcludes()) { - config.addExcludeGlobRelative(p); + if (!s.startsWith("glob:")) + s = "glob:" + s; + includeExcludeSet.exclude(p.getFileSystem().getPathMatcher(s)); } - for (String p : scanTestClassesPattern.getIncludes()) + for (String s : scanTestClassesPattern.getIncludes()) { - config.addIncludeGlobRelative(p); + if (!s.startsWith("glob:")) + s = "glob:" + s; + includeExcludeSet.include(p.getFileSystem().getPathMatcher(s)); } } - scanner.watch(config); } if (webApp.getClasses() != null && webApp.getClasses().exists()) { - PathWatcher.Config config = new PathWatcher.Config(webApp.getClasses().toPath()); - config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); + Path p = webApp.getClasses().toPath(); + IncludeExcludeSet includeExcludes = scanner.addDirectory(p); if (scanClassesPattern != null) { - for (String p : scanClassesPattern.getExcludes()) + for (String s : scanClassesPattern.getExcludes()) { - config.addExcludeGlobRelative(p); + if (!s.startsWith("glob:")) + s = "glob:" + s; + includeExcludes.exclude(p.getFileSystem().getPathMatcher(s)); } - for (String p : scanClassesPattern.getIncludes()) + for (String s : scanClassesPattern.getIncludes()) { - config.addIncludeGlobRelative(p); + if (!s.startsWith("glob:")) + s = "glob:" + s; + includeExcludes.include(p.getFileSystem().getPathMatcher(s)); } } - scanner.watch(config); } if (webApp.getWebInfLib() != null) { for (File f : webApp.getWebInfLib()) { - PathWatcher.Config config = new PathWatcher.Config(f.toPath()); - config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); - scanner.watch(config); + if (f.isDirectory()) + scanner.addDirectory(f.toPath()); + else + scanner.addFile(f.toPath()); } } } - /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#restartWebApp(boolean) - */ @Override public void restartWebApp(boolean reconfigureScanner) throws Exception { @@ -661,9 +618,6 @@ public class JettyRunMojo extends AbstractJettyMojo return Resource.newResource(dir.getCanonicalPath()); } - /** - * - */ private List getWarArtifacts() { if (warArtifacts != null) @@ -704,9 +658,6 @@ public class JettyRunMojo extends AbstractJettyMojo return null; } - /** - * - */ protected String getJavaBin() { String[] javaexes = new String[] diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarExplodedMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarExplodedMojo.java index e41aa04f7ee..65de9d95f5b 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarExplodedMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarExplodedMojo.java @@ -19,7 +19,7 @@ package org.eclipse.jetty.maven.plugin; import java.io.File; -import java.util.List; +import java.io.IOException; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -28,8 +28,6 @@ import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.eclipse.jetty.util.PathWatcher; -import org.eclipse.jetty.util.PathWatcher.PathWatchEvent; /** *

@@ -55,9 +53,6 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}", required = true) private File war; - /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#execute() - */ @Override public void execute() throws MojoExecutionException, MojoFailureException { @@ -71,70 +66,55 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo super.finishConfigurationBeforeStart(); } - /** - * @see AbstractJettyMojo#configureScanner() - */ @Override public void configureScanner() throws MojoExecutionException { - scanner.watch(project.getFile().toPath()); - File webInfDir = new File(war, "WEB-INF"); - File webXml = new File(webInfDir, "web.xml"); - if (webXml.exists()) - scanner.watch(webXml.toPath()); - File jettyWebXmlFile = findJettyWebXmlFile(webInfDir); - if (jettyWebXmlFile != null) - scanner.watch(jettyWebXmlFile.toPath()); - File jettyEnvXmlFile = new File(webInfDir, "jetty-env.xml"); - if (jettyEnvXmlFile.exists()) - scanner.watch(jettyEnvXmlFile.toPath()); - - File classes = new File(webInfDir, "classes"); - if (classes.exists()) + try { - PathWatcher.Config classesConfig = new PathWatcher.Config(classes.toPath()); - classesConfig.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); - scanner.watch(classesConfig); - } + scanner.addFile(project.getFile().toPath()); + File webInfDir = new File(war, "WEB-INF"); + File webXml = new File(webInfDir, "web.xml"); + if (webXml.exists()) + scanner.addFile(webXml.toPath()); + File jettyWebXmlFile = findJettyWebXmlFile(webInfDir); + if (jettyWebXmlFile != null) + scanner.addFile(jettyWebXmlFile.toPath()); + File jettyEnvXmlFile = new File(webInfDir, "jetty-env.xml"); + if (jettyEnvXmlFile.exists()) + scanner.addFile(jettyEnvXmlFile.toPath()); - File lib = new File(webInfDir, "lib"); - if (lib.exists()) - { - PathWatcher.Config libConfig = new PathWatcher.Config(lib.toPath()); - libConfig.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); - scanner.watch(libConfig); - } - - scanner.addListener(new PathWatcher.EventListListener() - { - - @Override - public void onPathWatchEvents(List events) + File classes = new File(webInfDir, "classes"); + if (classes.exists()) { try { - boolean reconfigure = false; - for (PathWatchEvent e : events) - { - if (e.getPath().equals(project.getFile().toPath())) - { - reconfigure = true; - break; - } - } - restartWebApp(reconfigure); + scanner.addDirectory(webApp.getClasses().toPath()); } - catch (Exception e) + catch (IOException e) { - getLog().error("Error reconfiguring/restarting webapp after change in watched files", e); + throw new MojoExecutionException("Error scanning classes", e); } } - }); + + File lib = new File(webInfDir, "lib"); + if (lib.exists()) + { + try + { + scanner.addDirectory(lib.toPath()); + } + catch (IOException e) + { + throw new MojoExecutionException("Error scanning lib", e); + } + } + } + catch (IOException e) + { + throw new MojoExecutionException("Error configuring scanner", e); + } } - /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#restartWebApp(boolean) - */ @Override public void restartWebApp(boolean reconfigureScanner) throws Exception { @@ -161,9 +141,6 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo getLog().info("Restart completed."); } - /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureWebApplication() - */ @Override public void configureWebApplication() throws Exception { diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarMojo.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarMojo.java index df69738a26a..71e14d84554 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarMojo.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/JettyRunWarMojo.java @@ -19,7 +19,7 @@ package org.eclipse.jetty.maven.plugin; import java.io.File; -import java.util.List; +import java.io.IOException; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; @@ -28,8 +28,6 @@ import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.ResolutionScope; -import org.eclipse.jetty.util.PathWatcher; -import org.eclipse.jetty.util.PathWatcher.PathWatchEvent; /** *

@@ -56,9 +54,6 @@ public class JettyRunWarMojo extends AbstractJettyMojo @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}.war", required = true) private File war; - /** - * @see org.apache.maven.plugin.Mojo#execute() - */ @Override public void execute() throws MojoExecutionException, MojoFailureException { @@ -80,45 +75,20 @@ public class JettyRunWarMojo extends AbstractJettyMojo webApp.setWar(war.getCanonicalPath()); } - /** - * @see AbstractJettyMojo#configureScanner() - */ @Override public void configureScanner() throws MojoExecutionException { - scanner.watch(project.getFile().toPath()); - scanner.watch(war.toPath()); - - scanner.addListener(new PathWatcher.EventListListener() + try { - - @Override - public void onPathWatchEvents(List events) - { - try - { - boolean reconfigure = false; - for (PathWatchEvent e : events) - { - if (e.getPath().equals(project.getFile().toPath())) - { - reconfigure = true; - break; - } - } - restartWebApp(reconfigure); - } - catch (Exception e) - { - getLog().error("Error reconfiguring/restarting webapp after change in watched files", e); - } - } - }); + scanner.addFile(project.getFile().toPath()); + scanner.addFile(war.toPath()); + } + catch (IOException e) + { + throw new MojoExecutionException("Error configuring scanner", e); + } } - /** - * @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#restartWebApp(boolean) - */ @Override public void restartWebApp(boolean reconfigureScanner) throws Exception { diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ScanPattern.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ScanPattern.java index 61fb6c166ac..a4fb84e6790 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ScanPattern.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ScanPattern.java @@ -24,7 +24,7 @@ import java.util.List; /** * ScanPattern * - * A pattern of includes and excludes. + * Ant-style pattern of includes and excludes. */ public class ScanPattern { diff --git a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ScanTargetPattern.java b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ScanTargetPattern.java index 567a66113ba..b0a6c1f2c29 100644 --- a/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ScanTargetPattern.java +++ b/jetty-maven-plugin/src/main/java/org/eclipse/jetty/maven/plugin/ScanTargetPattern.java @@ -19,9 +19,13 @@ package org.eclipse.jetty.maven.plugin; import java.io.File; +import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.util.Collections; import java.util.List; +import org.eclipse.jetty.util.IncludeExcludeSet; + /** * ScanTargetPattern * @@ -87,4 +91,21 @@ public class ScanTargetPattern { return (_pattern == null ? Collections.emptyList() : _pattern.getExcludes()); } + + public void configureIncludesExcludeSet(IncludeExcludeSet includesExcludes) + { + for (String include:getIncludes()) + { + if (!include.startsWith("glob:")) + include = "glob:" + include; + includesExcludes.include(_directory.toPath().getFileSystem().getPathMatcher(include)); + } + + for (String exclude:getExcludes()) + { + if (!exclude.startsWith("glob:")) + exclude = "glob:" + exclude; + includesExcludes.exclude(_directory.toPath().getFileSystem().getPathMatcher(exclude)); + } + } } diff --git a/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java b/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java index 226209d9634..28aba9587c9 100644 --- a/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java +++ b/jetty-util/src/main/java/org/eclipse/jetty/util/Scanner.java @@ -21,8 +21,16 @@ 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; @@ -32,6 +40,7 @@ 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; @@ -45,6 +54,16 @@ import org.eclipse.jetty.util.log.Logger; */ 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; @@ -53,13 +72,13 @@ public class Scanner extends AbstractLifeCycle private final Map _prevScan = new HashMap<>(); private final Map _currentScan = new HashMap<>(); private FilenameFilter _filter; - private final List _scanDirs = new ArrayList<>(); + 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 = 0; + private int _scanDepth = DEFAULT_SCAN_DEPTH; public enum Notification { @@ -67,7 +86,32 @@ public class Scanner extends AbstractLifeCycle } 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; @@ -103,6 +147,105 @@ public class Scanner extends AbstractLifeCycle } } + /** + * 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. + */ + public 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 (Boolean.TRUE == 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 (Boolean.TRUE == 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 * @@ -171,36 +314,110 @@ public class Scanner extends AbstractLifeCycle public void setScanDirs(List dirs) { - _scanDirs.clear(); - _scanDirs.addAll(dirs); + _scannables.clear(); + if (dirs == null) + return; + + for (File f:dirs) + { + addScanDir(f); + } } + @Deprecated public synchronized void addScanDir(File dir) { - _scanDirs.add(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() { - return Collections.unmodifiableList(_scanDirs); + 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 ? -1 : 0; + _scanDepth = recursive ? Integer.MAX_VALUE : 1; } /** - * @return True if scanning is fully recursive (scandepth==-1) + * @return True if scanning is recursive * @see #getScanDepth() */ + @Deprecated public boolean getRecursive() { - return _scanDepth == -1; + return _scanDepth > 1; } /** @@ -229,6 +446,7 @@ public class Scanner extends AbstractLifeCycle * * @param filter the filename filter to use */ + @Deprecated public void setFilenameFilter(FilenameFilter filter) { _filter = filter; @@ -239,6 +457,7 @@ public class Scanner extends AbstractLifeCycle * * @return the filename filter */ + @Deprecated public FilenameFilter getFilenameFilter() { return _filter; @@ -310,6 +529,9 @@ public class Scanner extends AbstractLifeCycle return; _running = true; + if (LOG.isDebugEnabled()) + LOG.debug("Scanner start: rprtExists={}, depth={}, rprtDirs={}, interval={}, filter={}, scannables={}", + _reportExisting, _scanDepth, _reportDirs, _scanInterval, _filter, _scannables); if (_reportExisting) { @@ -377,6 +599,23 @@ public class Scanner extends AbstractLifeCycle _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 @@ -384,9 +623,9 @@ public class Scanner extends AbstractLifeCycle */ public boolean exists(String path) { - for (File dir : _scanDirs) + for (Path p : _scannables.keySet()) { - if (new File(dir, path).exists()) + if (p.resolve(path).toFile().exists()) return true; } return false; @@ -419,23 +658,20 @@ public class Scanner extends AbstractLifeCycle } /** - * Recursively scan all files in the designated directories. + * Scan all of the given paths. */ public synchronized void scanFiles() { _currentScan.clear(); - for (File dir : _scanDirs) + for (Path p : _scannables.keySet()) { - if ((dir != null) && (dir.exists())) + try { - try - { - scanFile(dir.getCanonicalFile(), _currentScan, 0); - } - catch (IOException e) - { - LOG.warn("Error scanning files.", e); - } + Files.walkFileTree(p, EnumSet.allOf(FileVisitOption.class),_scanDepth, new Visitor(p, _scannables.get(p), _currentScan)); + } + catch (IOException e) + { + LOG.warn("Error scanning files.", e); } } } @@ -449,7 +685,6 @@ public class Scanner extends AbstractLifeCycle 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 @@ -489,16 +724,16 @@ public class Scanner extends AbstractLifeCycle } if (LOG.isDebugEnabled()) - LOG.debug("scanned " + _scanDirs + ": " + _notifications); + 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)) { @@ -529,57 +764,6 @@ public class Scanner extends AbstractLifeCycle 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 scanInfoMap, int depth) - { - try - { - if (!f.exists()) - return; - - if (f.isFile() || depth > 0 && _reportDirs && f.isDirectory()) - { - if (_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 (File file : files) - { - scanFile(file, 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); @@ -648,6 +832,11 @@ public class Scanner extends AbstractLifeCycle } } + /** + * 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) @@ -665,7 +854,7 @@ public class Scanner extends AbstractLifeCycle } /** - * signal any scan cycle listeners that a scan has started + * Call ScanCycleListeners with start of scan */ private void reportScanStart(int cycle) { @@ -686,7 +875,7 @@ public class Scanner extends AbstractLifeCycle } /** - * sign + * Call ScanCycleListeners with end of scan. */ private void reportScanEnd(int cycle) { diff --git a/jetty-util/src/test/java/org/eclipse/jetty/util/ScannerTest.java b/jetty-util/src/test/java/org/eclipse/jetty/util/ScannerTest.java index b8dc6072fbd..1a80174af7c 100644 --- a/jetty-util/src/test/java/org/eclipse/jetty/util/ScannerTest.java +++ b/jetty-util/src/test/java/org/eclipse/jetty/util/ScannerTest.java @@ -22,6 +22,8 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; @@ -30,12 +32,14 @@ import java.util.concurrent.TimeUnit; import org.eclipse.jetty.toolchain.test.FS; import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.util.Scanner.Notification; +import org.hamcrest.Matchers; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.junit.jupiter.api.condition.DisabledOnOs; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -61,6 +65,8 @@ public class ScannerTest _scanner = new Scanner(); _scanner.addScanDir(_directory); _scanner.setScanInterval(0); + _scanner.setReportDirs(false); + _scanner.setReportExistingFilesOnStartup(false); _scanner.addListener(new Scanner.DiscreteListener() { @Override @@ -89,8 +95,8 @@ public class ScannerTest _bulk.add(filenames); } }); + _scanner.start(); - _scanner.scan(); assertTrue(_queue.isEmpty()); @@ -115,6 +121,155 @@ public class ScannerTest _notification = notification; } } + + @Test + public void testDepth() throws Exception + { + File root = new File (_directory, "root"); + FS.ensureDirExists(root); + FS.touch(new File(root, "foo.foo")); + FS.touch(new File(root, "foo2.foo")); + File dir = new File(root, "xxx"); + FS.ensureDirExists(dir); + File x1 = new File(dir, "xxx.foo"); + FS.touch(x1); + File x2 = new File(dir, "xxx2.foo"); + FS.touch(x2); + File dir2 = new File(dir, "yyy"); + FS.ensureDirExists(dir2); + File y1 = new File(dir2, "yyy.foo"); + FS.touch(y1); + File y2 = new File(dir2, "yyy2.foo"); + FS.touch(y2); + + BlockingQueue queue = new LinkedBlockingQueue(); + Scanner scanner = new Scanner(); + scanner.setScanInterval(0); + scanner.setScanDepth(0); + scanner.setReportDirs(true); + scanner.setReportExistingFilesOnStartup(true); + scanner.addDirectory(root.toPath()); + scanner.addListener(new Scanner.DiscreteListener() + { + @Override + public void fileRemoved(String filename) throws Exception + { + queue.add(new Event(filename, Notification.REMOVED)); + } + + @Override + public void fileChanged(String filename) throws Exception + { + queue.add(new Event(filename, Notification.CHANGED)); + } + + @Override + public void fileAdded(String filename) throws Exception + { + queue.add(new Event(filename, Notification.ADDED)); + } + }); + + scanner.start(); + Event e = queue.take(); + assertNotNull(e); + assertEquals(Notification.ADDED, e._notification); + assertTrue(e._filename.endsWith(root.getName())); + queue.clear(); + scanner.stop(); + scanner.reset(); + + //Depth one should report the dir itself and its file and dir direct children + scanner.setScanDepth(1); + scanner.addDirectory(root.toPath()); + scanner.start(); + assertEquals(4, queue.size()); + queue.clear(); + scanner.stop(); + scanner.reset(); + + //Depth 2 should report the dir itself, all file children, xxx and xxx's children + scanner.setScanDepth(2); + scanner.addDirectory(root.toPath()); + scanner.start(); + + assertEquals(7, queue.size()); + scanner.stop(); + } + + @Test + public void testPatterns() throws Exception + { + //test include and exclude patterns + File root = new File(_directory, "proot"); + FS.ensureDirExists(root); + + File ttt = new File(root, "ttt.txt"); + FS.touch(ttt); + FS.touch(new File(root, "ttt.foo")); + File dir = new File(root, "xxx"); + FS.ensureDirExists(dir); + + File x1 = new File(dir, "ttt.xxx"); + FS.touch(x1); + File x2 = new File(dir, "xxx.txt"); + FS.touch(x2); + + File dir2 = new File(dir, "yyy"); + FS.ensureDirExists(dir2); + File y1 = new File(dir2, "ttt.yyy"); + FS.touch(y1); + File y2 = new File(dir2, "yyy.txt"); + FS.touch(y2); + + BlockingQueue queue = new LinkedBlockingQueue(); + //only scan the *.txt files for changes + Scanner scanner = new Scanner(); + IncludeExcludeSet pattern = scanner.addDirectory(root.toPath()); + pattern.exclude(root.toPath().getFileSystem().getPathMatcher("glob:**/*.foo")); + pattern.exclude(root.toPath().getFileSystem().getPathMatcher("glob:**/ttt.xxx")); + scanner.setScanInterval(0); + scanner.setScanDepth(2); //should never see any files from subdir yyy + scanner.setReportDirs(false); + scanner.setReportExistingFilesOnStartup(false); + scanner.addListener(new Scanner.DiscreteListener() + { + @Override + public void fileRemoved(String filename) throws Exception + { + queue.add(new Event(filename, Notification.REMOVED)); + } + + @Override + public void fileChanged(String filename) throws Exception + { + queue.add(new Event(filename, Notification.CHANGED)); + } + + @Override + public void fileAdded(String filename) throws Exception + { + queue.add(new Event(filename, Notification.ADDED)); + } + }); + + scanner.start(); + assertTrue(queue.isEmpty()); + + Thread.sleep(1100); // make sure time in seconds changes + FS.touch(ttt); + FS.touch(x2); + FS.touch(x1); + FS.touch(y2); + scanner.scan(); + scanner.scan(); //2 scans for file to be considered settled + + assertThat(queue.size(), Matchers.equalTo(2)); + for (Event e : queue) + { + assertTrue(e._filename.endsWith("ttt.txt") || e._filename.endsWith("xxx.txt")); + } + } @Test @DisabledOnOs(WINDOWS) // TODO: needs review @@ -126,6 +281,7 @@ public class ScannerTest // takes 2 scans to notice a0 and check that it is stable _scanner.scan(); _scanner.scan(); + Event event = _queue.poll(); assertNotNull(event, "Event should not be null"); assertEquals(_directory + "/a0", event._filename);