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 <janb@webtide.com>
This commit is contained in:
Jan Bartel 2019-11-06 15:55:20 +11:00 committed by GitHub
parent 37aa5a17e0
commit aefbdfca58
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 562 additions and 276 deletions

View File

@ -141,6 +141,7 @@ public abstract class ScanningAppProvider extends AbstractLifeCycle implements A
_scanner.setRecursive(_recursive); _scanner.setRecursive(_recursive);
_scanner.setFilenameFilter(_filenameFilter); _scanner.setFilenameFilter(_filenameFilter);
_scanner.setReportDirs(true); _scanner.setReportDirs(true);
_scanner.setScanDepth(1); //consider direct dir children of monitored dir
_scanner.addListener(_scannerListener); _scanner.addListener(_scannerListener);
_scanner.start(); _scanner.start();
} }

View File

@ -81,6 +81,11 @@ public class WebAppProvider extends ScanningAppProvider
String lowername = name.toLowerCase(Locale.ENGLISH); String lowername = name.toLowerCase(Locale.ENGLISH);
File file = new File(dir, name); File file = new File(dir, name);
Resource r = Resource.newResource(file);
if (getMonitoredResources().contains(r) && r.isDirectory())
{
return false;
}
// ignore hidden files // ignore hidden files
if (lowername.startsWith(".")) if (lowername.startsWith("."))

View File

@ -50,6 +50,7 @@ import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.server.handler.ContextHandlerCollection; import org.eclipse.jetty.server.handler.ContextHandlerCollection;
import org.eclipse.jetty.server.handler.HandlerCollection; import org.eclipse.jetty.server.handler.HandlerCollection;
import org.eclipse.jetty.util.PathWatcher; import org.eclipse.jetty.util.PathWatcher;
import org.eclipse.jetty.util.Scanner;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.xml.XmlConfiguration; 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 * A scanner to check for changes to the webapp
*/ */
protected PathWatcher scanner; protected Scanner scanner;
/** /**
* A scanner to check ENTER hits on the console * 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 // start the scanner thread (if necessary) on the main webapp
if (isScanningEnabled()) 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<String> 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(); configureScanner();
startScanner(); startScanner();
} }
@ -523,7 +542,6 @@ public abstract class AbstractJettyMojo extends AbstractMojo
XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(path.toFile())); XmlConfiguration xmlConfiguration = new XmlConfiguration(Resource.toURL(path.toFile()));
getLog().info("Applying context xml file " + contextXml); getLog().info("Applying context xml file " + contextXml);
xmlConfiguration.configure(webApp);
} }
//If no contextPath was specified, go with default of project artifactid //If no contextPath was specified, go with default of project artifactid
@ -562,8 +580,6 @@ public abstract class AbstractJettyMojo extends AbstractMojo
if (!isScanningEnabled()) if (!isScanningEnabled())
return; return;
scanner.setNotifyExistingOnStart(false);
scanner.start(); scanner.start();
} }

View File

@ -22,6 +22,7 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; 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.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject; import org.apache.maven.project.MavenProject;
import org.eclipse.jetty.maven.plugin.utils.MavenProjectHelper; import org.eclipse.jetty.maven.plugin.utils.MavenProjectHelper;
import org.eclipse.jetty.util.PathWatcher; import org.eclipse.jetty.util.IncludeExcludeSet;
import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
import org.eclipse.jetty.util.StringUtil; import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection; import org.eclipse.jetty.util.resource.ResourceCollection;
@ -145,9 +145,6 @@ public class JettyRunMojo extends AbstractJettyMojo
protected Resource originalBaseResource; protected Resource originalBaseResource;
/**
* @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#execute()
*/
@Override @Override
public void execute() throws MojoExecutionException, MojoFailureException public void execute() throws MojoExecutionException, MojoFailureException
{ {
@ -157,8 +154,6 @@ public class JettyRunMojo extends AbstractJettyMojo
/** /**
* Verify the configuration given in the pom. * Verify the configuration given in the pom.
*
* @see AbstractJettyMojo#checkPomConfiguration()
*/ */
@Override @Override
public boolean checkPomConfiguration() throws MojoExecutionException public boolean checkPomConfiguration() throws MojoExecutionException
@ -229,9 +224,6 @@ public class JettyRunMojo extends AbstractJettyMojo
super.finishConfigurationBeforeStart(); super.finishConfigurationBeforeStart();
} }
/**
* @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureWebApplication()
*/
@Override @Override
public void configureWebApplication() throws Exception public void configureWebApplication() throws Exception
{ {
@ -317,9 +309,6 @@ public class JettyRunMojo extends AbstractJettyMojo
getLog().info("Webapp directory = " + webAppSourceDirectory.getCanonicalPath()); getLog().info("Webapp directory = " + webAppSourceDirectory.getCanonicalPath());
} }
/**
* @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureScanner()
*/
@Override @Override
public void configureScanner() public void configureScanner()
throws MojoExecutionException throws MojoExecutionException
@ -332,36 +321,6 @@ public class JettyRunMojo extends AbstractJettyMojo
{ {
throw new MojoExecutionException("Error forming scan list", e); throw new MojoExecutionException("Error forming scan list", e);
} }
scanner.addListener(new PathWatcher.EventListListener()
{
@Override
public void onPathWatchEvents(List<PathWatchEvent> 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 public void gatherScannables() throws Exception
@ -369,33 +328,37 @@ public class JettyRunMojo extends AbstractJettyMojo
if (webApp.getDescriptor() != null) if (webApp.getDescriptor() != null)
{ {
Resource r = Resource.newResource(webApp.getDescriptor()); Resource r = Resource.newResource(webApp.getDescriptor());
scanner.watch(r.getFile().toPath()); scanner.addFile(r.getFile().toPath());
} }
if (webApp.getJettyEnvXml() != null) if (webApp.getJettyEnvXml() != null)
scanner.watch(new File(webApp.getJettyEnvXml()).toPath()); scanner.addFile(new File(webApp.getJettyEnvXml()).toPath());
if (webApp.getDefaultsDescriptor() != null) if (webApp.getDefaultsDescriptor() != null)
{ {
if (!WebAppContext.WEB_DEFAULTS_XML.equals(webApp.getDefaultsDescriptor())) 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) 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")); File jettyWebXmlFile = findJettyWebXmlFile(new File(webAppSourceDirectory, "WEB-INF"));
if (jettyWebXmlFile != null) if (jettyWebXmlFile != null)
{ {
scanner.watch(jettyWebXmlFile.toPath()); scanner.addFile(jettyWebXmlFile.toPath());
} }
//make sure each of the war artifacts is added to the scanner //make sure each of the war artifacts is added to the scanner
for (Artifact a : getWarArtifacts()) 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 //handle the explicit extra scan targets
@ -405,87 +368,81 @@ public class JettyRunMojo extends AbstractJettyMojo
{ {
if (f.isDirectory()) if (f.isDirectory())
{ {
PathWatcher.Config config = new PathWatcher.Config(f.toPath()); scanner.addDirectory(f.toPath());
config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH);
scanner.watch(config);
} }
else else
scanner.watch(f.toPath()); scanner.addFile(f.toPath());
} }
} }
scanner.addFile(project.getFile().toPath());
//handle the extra scan patterns //handle the extra scan patterns
if (scanTargetPatterns != null) if (scanTargetPatterns != null)
{ {
for (ScanTargetPattern p : scanTargetPatterns) for (ScanTargetPattern p : scanTargetPatterns)
{ {
PathWatcher.Config config = new PathWatcher.Config(p.getDirectory().toPath()); IncludeExcludeSet<PathMatcher, Path> includesExcludes = scanner.addDirectory(p.getDirectory().toPath());
config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); p.configureIncludesExcludeSet(includesExcludes);
for (String pattern : p.getExcludes())
{
config.addExcludeGlobRelative(pattern);
}
for (String pattern : p.getIncludes())
{
config.addIncludeGlobRelative(pattern);
}
scanner.watch(config);
} }
} }
scanner.watch(project.getFile().toPath());
if (webApp.getTestClasses() != null && webApp.getTestClasses().exists()) if (webApp.getTestClasses() != null && webApp.getTestClasses().exists())
{ {
PathWatcher.Config config = new PathWatcher.Config(webApp.getTestClasses().toPath()); Path p = webApp.getTestClasses().toPath();
config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); IncludeExcludeSet<PathMatcher, Path> includeExcludeSet = scanner.addDirectory(p);
if (scanTestClassesPattern != null) 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()) if (webApp.getClasses() != null && webApp.getClasses().exists())
{ {
PathWatcher.Config config = new PathWatcher.Config(webApp.getClasses().toPath()); Path p = webApp.getClasses().toPath();
config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); IncludeExcludeSet<PathMatcher, Path> includeExcludes = scanner.addDirectory(p);
if (scanClassesPattern != null) 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) if (webApp.getWebInfLib() != null)
{ {
for (File f : webApp.getWebInfLib()) for (File f : webApp.getWebInfLib())
{ {
PathWatcher.Config config = new PathWatcher.Config(f.toPath()); if (f.isDirectory())
config.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); scanner.addDirectory(f.toPath());
scanner.watch(config); else
scanner.addFile(f.toPath());
} }
} }
} }
/**
* @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#restartWebApp(boolean)
*/
@Override @Override
public void restartWebApp(boolean reconfigureScanner) throws Exception public void restartWebApp(boolean reconfigureScanner) throws Exception
{ {
@ -661,9 +618,6 @@ public class JettyRunMojo extends AbstractJettyMojo
return Resource.newResource(dir.getCanonicalPath()); return Resource.newResource(dir.getCanonicalPath());
} }
/**
*
*/
private List<Artifact> getWarArtifacts() private List<Artifact> getWarArtifacts()
{ {
if (warArtifacts != null) if (warArtifacts != null)
@ -704,9 +658,6 @@ public class JettyRunMojo extends AbstractJettyMojo
return null; return null;
} }
/**
*
*/
protected String getJavaBin() protected String getJavaBin()
{ {
String[] javaexes = new String[] String[] javaexes = new String[]

View File

@ -19,7 +19,7 @@
package org.eclipse.jetty.maven.plugin; package org.eclipse.jetty.maven.plugin;
import java.io.File; import java.io.File;
import java.util.List; import java.io.IOException;
import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException; 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.Mojo;
import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.plugins.annotations.ResolutionScope;
import org.eclipse.jetty.util.PathWatcher;
import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
/** /**
* <p> * <p>
@ -55,9 +53,6 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo
@Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}", required = true) @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}", required = true)
private File war; private File war;
/**
* @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#execute()
*/
@Override @Override
public void execute() throws MojoExecutionException, MojoFailureException public void execute() throws MojoExecutionException, MojoFailureException
{ {
@ -71,70 +66,55 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo
super.finishConfigurationBeforeStart(); super.finishConfigurationBeforeStart();
} }
/**
* @see AbstractJettyMojo#configureScanner()
*/
@Override @Override
public void configureScanner() throws MojoExecutionException public void configureScanner() throws MojoExecutionException
{ {
scanner.watch(project.getFile().toPath()); try
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())
{ {
PathWatcher.Config classesConfig = new PathWatcher.Config(classes.toPath()); scanner.addFile(project.getFile().toPath());
classesConfig.setRecurseDepth(PathWatcher.Config.UNLIMITED_DEPTH); File webInfDir = new File(war, "WEB-INF");
scanner.watch(classesConfig); 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"); File classes = new File(webInfDir, "classes");
if (lib.exists()) if (classes.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<PathWatchEvent> events)
{ {
try try
{ {
boolean reconfigure = false; scanner.addDirectory(webApp.getClasses().toPath());
for (PathWatchEvent e : events)
{
if (e.getPath().equals(project.getFile().toPath()))
{
reconfigure = true;
break;
}
}
restartWebApp(reconfigure);
} }
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 @Override
public void restartWebApp(boolean reconfigureScanner) throws Exception public void restartWebApp(boolean reconfigureScanner) throws Exception
{ {
@ -161,9 +141,6 @@ public class JettyRunWarExplodedMojo extends AbstractJettyMojo
getLog().info("Restart completed."); getLog().info("Restart completed.");
} }
/**
* @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#configureWebApplication()
*/
@Override @Override
public void configureWebApplication() throws Exception public void configureWebApplication() throws Exception
{ {

View File

@ -19,7 +19,7 @@
package org.eclipse.jetty.maven.plugin; package org.eclipse.jetty.maven.plugin;
import java.io.File; import java.io.File;
import java.util.List; import java.io.IOException;
import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException; 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.Mojo;
import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope; import org.apache.maven.plugins.annotations.ResolutionScope;
import org.eclipse.jetty.util.PathWatcher;
import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
/** /**
* <p> * <p>
@ -56,9 +54,6 @@ public class JettyRunWarMojo extends AbstractJettyMojo
@Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}.war", required = true) @Parameter(defaultValue = "${project.build.directory}/${project.build.finalName}.war", required = true)
private File war; private File war;
/**
* @see org.apache.maven.plugin.Mojo#execute()
*/
@Override @Override
public void execute() throws MojoExecutionException, MojoFailureException public void execute() throws MojoExecutionException, MojoFailureException
{ {
@ -80,45 +75,20 @@ public class JettyRunWarMojo extends AbstractJettyMojo
webApp.setWar(war.getCanonicalPath()); webApp.setWar(war.getCanonicalPath());
} }
/**
* @see AbstractJettyMojo#configureScanner()
*/
@Override @Override
public void configureScanner() throws MojoExecutionException public void configureScanner() throws MojoExecutionException
{ {
scanner.watch(project.getFile().toPath()); try
scanner.watch(war.toPath());
scanner.addListener(new PathWatcher.EventListListener()
{ {
scanner.addFile(project.getFile().toPath());
@Override scanner.addFile(war.toPath());
public void onPathWatchEvents(List<PathWatchEvent> events) }
{ catch (IOException e)
try {
{ throw new MojoExecutionException("Error configuring scanner", e);
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);
}
}
});
} }
/**
* @see org.eclipse.jetty.maven.plugin.AbstractJettyMojo#restartWebApp(boolean)
*/
@Override @Override
public void restartWebApp(boolean reconfigureScanner) throws Exception public void restartWebApp(boolean reconfigureScanner) throws Exception
{ {

View File

@ -24,7 +24,7 @@ import java.util.List;
/** /**
* ScanPattern * ScanPattern
* *
* A pattern of includes and excludes. * Ant-style pattern of includes and excludes.
*/ */
public class ScanPattern public class ScanPattern
{ {

View File

@ -19,9 +19,13 @@
package org.eclipse.jetty.maven.plugin; package org.eclipse.jetty.maven.plugin;
import java.io.File; import java.io.File;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.eclipse.jetty.util.IncludeExcludeSet;
/** /**
* ScanTargetPattern * ScanTargetPattern
* *
@ -87,4 +91,21 @@ public class ScanTargetPattern
{ {
return (_pattern == null ? Collections.emptyList() : _pattern.getExcludes()); return (_pattern == null ? Collections.emptyList() : _pattern.getExcludes());
} }
public void configureIncludesExcludeSet(IncludeExcludeSet<PathMatcher, Path> 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));
}
}
} }

View File

@ -21,8 +21,16 @@ package org.eclipse.jetty.util;
import java.io.File; import java.io.File;
import java.io.FilenameFilter; import java.io.FilenameFilter;
import java.io.IOException; 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.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
@ -32,6 +40,7 @@ import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.function.Predicate;
import org.eclipse.jetty.util.component.AbstractLifeCycle; import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log; import org.eclipse.jetty.util.log.Log;
@ -45,6 +54,16 @@ import org.eclipse.jetty.util.log.Logger;
*/ */
public class Scanner extends AbstractLifeCycle 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 final Logger LOG = Log.getLogger(Scanner.class);
private static int __scannerId = 0; private static int __scannerId = 0;
private int _scanInterval; private int _scanInterval;
@ -53,13 +72,13 @@ public class Scanner extends AbstractLifeCycle
private final Map<String, TimeNSize> _prevScan = new HashMap<>(); private final Map<String, TimeNSize> _prevScan = new HashMap<>();
private final Map<String, TimeNSize> _currentScan = new HashMap<>(); private final Map<String, TimeNSize> _currentScan = new HashMap<>();
private FilenameFilter _filter; private FilenameFilter _filter;
private final List<File> _scanDirs = new ArrayList<>(); private final Map<Path, IncludeExcludeSet<PathMatcher, Path>> _scannables = new HashMap<>();
private volatile boolean _running = false; private volatile boolean _running = false;
private boolean _reportExisting = true; private boolean _reportExisting = true;
private boolean _reportDirs = true; private boolean _reportDirs = true;
private Timer _timer; private Timer _timer;
private TimerTask _task; private TimerTask _task;
private int _scanDepth = 0; private int _scanDepth = DEFAULT_SCAN_DEPTH;
public enum Notification public enum Notification
{ {
@ -68,6 +87,31 @@ public class Scanner extends AbstractLifeCycle
private final Map<String, Notification> _notifications = new HashMap<>(); private final Map<String, Notification> _notifications = new HashMap<>();
/**
* PathMatcherSet
*
* A set of PathMatchers for testing Paths against path matching patterns via
* @see IncludeExcludeSet
*/
static class PathMatcherSet extends HashSet<PathMatcher> implements Predicate<Path>
{
@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 static class TimeNSize
{ {
final long _lastModified; 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<Path>
{
Map<String, TimeNSize> scanInfoMap;
IncludeExcludeSet<PathMatcher,Path> rootIncludesExcludes;
Path root;
public Visitor(Path root, IncludeExcludeSet<PathMatcher,Path> rootIncludesExcludes, Map<String, TimeNSize> 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 * Listener
* *
@ -171,36 +314,110 @@ public class Scanner extends AbstractLifeCycle
public void setScanDirs(List<File> dirs) public void setScanDirs(List<File> dirs)
{ {
_scanDirs.clear(); _scannables.clear();
_scanDirs.addAll(dirs); if (dirs == null)
return;
for (File f:dirs)
{
addScanDir(f);
}
} }
@Deprecated
public synchronized void addScanDir(File dir) 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<PathMatcher, Path> 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<PathMatcher, Path> includesExcludes = _scannables.get(p);
if (includesExcludes == null)
{
includesExcludes = new IncludeExcludeSet<>(PathMatcherSet.class);
_scannables.put(p.toRealPath(), includesExcludes);
}
return includesExcludes;
}
@Deprecated
public List<File> getScanDirs() public List<File> getScanDirs()
{ {
return Collections.unmodifiableList(_scanDirs); ArrayList<File> files = new ArrayList<>();
for (Path p : _scannables.keySet())
files.add(p.toFile());
return Collections.unmodifiableList(files);
}
public Set<Path> getScannables()
{
return _scannables.keySet();
} }
/** /**
* @param recursive True if scanning is recursive * @param recursive True if scanning is recursive
* @see #setScanDepth(int) * @see #setScanDepth(int)
*/ */
@Deprecated
public void setRecursive(boolean recursive) 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() * @see #getScanDepth()
*/ */
@Deprecated
public boolean getRecursive() 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 * @param filter the filename filter to use
*/ */
@Deprecated
public void setFilenameFilter(FilenameFilter filter) public void setFilenameFilter(FilenameFilter filter)
{ {
_filter = filter; _filter = filter;
@ -239,6 +457,7 @@ public class Scanner extends AbstractLifeCycle
* *
* @return the filename filter * @return the filename filter
*/ */
@Deprecated
public FilenameFilter getFilenameFilter() public FilenameFilter getFilenameFilter()
{ {
return _filter; return _filter;
@ -310,6 +529,9 @@ public class Scanner extends AbstractLifeCycle
return; return;
_running = true; _running = true;
if (LOG.isDebugEnabled())
LOG.debug("Scanner start: rprtExists={}, depth={}, rprtDirs={}, interval={}, filter={}, scannables={}",
_reportExisting, _scanDepth, _reportDirs, _scanInterval, _filter, _scannables);
if (_reportExisting) if (_reportExisting)
{ {
@ -378,15 +600,32 @@ public class Scanner extends AbstractLifeCycle
} }
} }
/**
* 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 * @param path tests if the path exists
* @return true if the path exists in one of the scandirs * @return true if the path exists in one of the scandirs
*/ */
public boolean exists(String path) 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 true;
} }
return false; 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() public synchronized void scanFiles()
{ {
_currentScan.clear(); _currentScan.clear();
for (File dir : _scanDirs) for (Path p : _scannables.keySet())
{ {
if ((dir != null) && (dir.exists())) try
{ {
try Files.walkFileTree(p, EnumSet.allOf(FileVisitOption.class),_scanDepth, new Visitor(p, _scannables.get(p), _currentScan));
{ }
scanFile(dir.getCanonicalFile(), _currentScan, 0); catch (IOException e)
} {
catch (IOException e) LOG.warn("Error scanning files.", e);
{
LOG.warn("Error scanning files.", e);
}
} }
} }
} }
@ -449,7 +685,6 @@ public class Scanner extends AbstractLifeCycle
private synchronized void reportDifferences(Map<String, TimeNSize> currentScan, Map<String, TimeNSize> oldScan) private synchronized void reportDifferences(Map<String, TimeNSize> currentScan, Map<String, TimeNSize> oldScan)
{ {
// scan the differences and add what was found to the map of notifications: // scan the differences and add what was found to the map of notifications:
Set<String> oldScanKeys = new HashSet<>(oldScan.keySet()); Set<String> oldScanKeys = new HashSet<>(oldScan.keySet());
// Look for new and changed files // Look for new and changed files
@ -489,16 +724,16 @@ public class Scanner extends AbstractLifeCycle
} }
if (LOG.isDebugEnabled()) if (LOG.isDebugEnabled())
LOG.debug("scanned " + _scanDirs + ": " + _notifications); LOG.debug("scanned " + _scannables.keySet() + ": " + _notifications);
// Process notifications // Process notifications
// Only process notifications that are for stable files (ie same in old and current scan). // Only process notifications that are for stable files (ie same in old and current scan).
List<String> bulkChanges = new ArrayList<>(); List<String> bulkChanges = new ArrayList<>();
for (Iterator<Entry<String, Notification>> iter = _notifications.entrySet().iterator(); iter.hasNext(); ) for (Iterator<Entry<String, Notification>> iter = _notifications.entrySet().iterator(); iter.hasNext(); )
{ {
Entry<String, Notification> entry = iter.next(); Entry<String, Notification> entry = iter.next();
String file = entry.getKey(); String file = entry.getKey();
// Is the file stable? // Is the file stable?
if (oldScan.containsKey(file)) if (oldScan.containsKey(file))
{ {
@ -529,57 +764,6 @@ public class Scanner extends AbstractLifeCycle
reportBulkChanges(bulkChanges); 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<String, TimeNSize> 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) private void warn(Object listener, String filename, Throwable th)
{ {
LOG.warn(listener + " failed on '" + filename, 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<String> filenames) private void reportBulkChanges(List<String> filenames)
{ {
for (Listener l : _listeners) 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) 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) private void reportScanEnd(int cycle)
{ {

View File

@ -22,6 +22,8 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.util.List; import java.util.List;
import java.util.concurrent.BlockingQueue; import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue; 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.FS;
import org.eclipse.jetty.toolchain.test.MavenTestingUtils; import org.eclipse.jetty.toolchain.test.MavenTestingUtils;
import org.eclipse.jetty.util.Scanner.Notification; import org.eclipse.jetty.util.Scanner.Notification;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIfSystemProperty; import org.junit.jupiter.api.condition.DisabledIfSystemProperty;
import org.junit.jupiter.api.condition.DisabledOnOs; 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.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
@ -61,6 +65,8 @@ public class ScannerTest
_scanner = new Scanner(); _scanner = new Scanner();
_scanner.addScanDir(_directory); _scanner.addScanDir(_directory);
_scanner.setScanInterval(0); _scanner.setScanInterval(0);
_scanner.setReportDirs(false);
_scanner.setReportExistingFilesOnStartup(false);
_scanner.addListener(new Scanner.DiscreteListener() _scanner.addListener(new Scanner.DiscreteListener()
{ {
@Override @Override
@ -89,8 +95,8 @@ public class ScannerTest
_bulk.add(filenames); _bulk.add(filenames);
} }
}); });
_scanner.start();
_scanner.start();
_scanner.scan(); _scanner.scan();
assertTrue(_queue.isEmpty()); assertTrue(_queue.isEmpty());
@ -116,6 +122,155 @@ public class ScannerTest
} }
} }
@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<Event> queue = new LinkedBlockingQueue<Event>();
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<Event> queue = new LinkedBlockingQueue<Event>();
//only scan the *.txt files for changes
Scanner scanner = new Scanner();
IncludeExcludeSet<PathMatcher, Path> 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 @Test
@DisabledOnOs(WINDOWS) // TODO: needs review @DisabledOnOs(WINDOWS) // TODO: needs review
@DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review @DisabledIfSystemProperty(named = "env", matches = "ci") // TODO: SLOW, needs review
@ -126,6 +281,7 @@ public class ScannerTest
// takes 2 scans to notice a0 and check that it is stable // takes 2 scans to notice a0 and check that it is stable
_scanner.scan(); _scanner.scan();
_scanner.scan(); _scanner.scan();
Event event = _queue.poll(); Event event = _queue.poll();
assertNotNull(event, "Event should not be null"); assertNotNull(event, "Event should not be null");
assertEquals(_directory + "/a0", event._filename); assertEquals(_directory + "/a0", event._filename);