Merge remote-tracking branch 'origin/jetty-9.4.x' into jetty-10.0.x

This commit is contained in:
Jan Bartel 2019-11-06 16:09:02 +11:00
commit f55fbdb7eb
13 changed files with 616 additions and 287 deletions

View File

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

View File

@ -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("."))

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.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.PathResource;
import org.eclipse.jetty.util.resource.Resource;
@ -223,7 +224,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
@ -459,7 +460,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<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();
startScanner();
}
@ -524,7 +543,6 @@ public abstract class AbstractJettyMojo extends AbstractMojo
XmlConfiguration xmlConfiguration = new XmlConfiguration(new PathResource(path));
getLog().info("Applying context xml file " + contextXml);
xmlConfiguration.configure(webApp);
}
//If no contextPath was specified, go with default of project artifactid
@ -563,8 +581,6 @@ public abstract class AbstractJettyMojo extends AbstractMojo
if (!isScanningEnabled())
return;
scanner.setNotifyExistingOnStart(false);
scanner.start();
}

View File

@ -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<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
@ -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<PathMatcher, Path> 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<PathMatcher, Path> 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<PathMatcher, Path> 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
{
@ -665,9 +622,6 @@ public class JettyRunMojo extends AbstractJettyMojo
return Resource.newResource(dir.getCanonicalPath());
}
/**
*
*/
private List<Artifact> getWarArtifacts()
{
if (warArtifacts != null)
@ -708,9 +662,6 @@ public class JettyRunMojo extends AbstractJettyMojo
return null;
}
/**
*
*/
protected String getJavaBin()
{
String[] javaexes = {"java", "java.exe"};

View File

@ -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;
/**
* <p>
@ -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<PathWatchEvent> 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
{

View File

@ -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;
/**
* <p>
@ -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<PathWatchEvent> 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
{

View File

@ -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
{

View File

@ -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<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

@ -79,6 +79,12 @@
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-util-ajax</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -26,7 +26,12 @@ import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
@ -442,23 +447,31 @@ public class ErrorHandler extends AbstractHandler
private void writeErrorJson(HttpServletRequest request, PrintWriter writer, int code, String message)
{
writer
.append("{\n")
.append(" url: \"").append(request.getRequestURI()).append("\",\n")
.append(" status: \"").append(Integer.toString(code)).append("\",\n")
.append(" message: ").append(QuotedStringTokenizer.quote(message)).append(",\n");
Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME);
if (servlet != null)
writer.append("servlet: \"").append(servlet.toString()).append("\",\n");
Throwable cause = (Throwable)request.getAttribute(Dispatcher.ERROR_EXCEPTION);
Object servlet = request.getAttribute(Dispatcher.ERROR_SERVLET_NAME);
Map<String,String> json = new HashMap<>();
json.put("url", request.getRequestURI());
json.put("status", Integer.toString(code));
json.put("message", message);
if (servlet != null)
{
json.put("servlet", servlet.toString());
}
int c = 0;
while (cause != null)
{
writer.append(" cause").append(Integer.toString(c++)).append(": ")
.append(QuotedStringTokenizer.quote(cause.toString())).append(",\n");
json.put("cause" + c++, cause.toString());
cause = cause.getCause();
}
writer.append("}");
writer.append(json.entrySet().stream()
.map(e -> QuotedStringTokenizer.quote(e.getKey()) +
":" +
QuotedStringTokenizer.quote((e.getValue())))
.collect(Collectors.joining(",\n", "{\n", "\n}")));
}
protected void writeErrorPageStacks(HttpServletRequest request, Writer writer)

View File

@ -19,6 +19,8 @@
package org.eclipse.jetty.server;
import java.io.IOException;
import java.util.Map;
import javax.servlet.DispatcherType;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
@ -30,6 +32,7 @@ import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.tools.HttpTester;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.util.ajax.JSON;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
@ -302,4 +305,25 @@ public class ErrorHandlerTest
assertThat("Response status code", response.getStatus(), is(444));
}
@Test
public void testJsonResponse() throws Exception
{
String rawResponse = connector.getResponse(
"GET /badmessage/444 HTTP/1.1\r\n" +
"Host: Localhost\r\n" +
"Accept: text/json\r\n" +
"\r\n");
HttpTester.Response response = HttpTester.parseResponse(rawResponse);
assertThat("Response status code", response.getStatus(), is(444));
System.out.println("response:" + response.getContent());
Map<Object,Object> jo = (Map) JSON.parse(response.getContent());
assertThat("url field null", jo.get("url"), is(notNullValue()));
assertThat("status field null", jo.get("status"), is(notNullValue()));
assertThat("message field null", jo.get("message"), is(notNullValue()));
}
}

View File

@ -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<String, TimeNSize> _prevScan = new HashMap<>();
private final Map<String, TimeNSize> _currentScan = new HashMap<>();
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 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<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
{
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
*
@ -171,36 +314,110 @@ public class Scanner extends AbstractLifeCycle
public void setScanDirs(List<File> 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<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()
{
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
* @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<String, TimeNSize> currentScan, Map<String, TimeNSize> oldScan)
{
// scan the differences and add what was found to the map of notifications:
Set<String> oldScanKeys = new HashSet<>(oldScan.keySet());
// Look for new and changed files
@ -492,16 +727,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<String> bulkChanges = new ArrayList<>();
for (Iterator<Entry<String, Notification>> iter = _notifications.entrySet().iterator(); iter.hasNext(); )
{
Entry<String, Notification> entry = iter.next();
String file = entry.getKey();
// Is the file stable?
if (oldScan.containsKey(file))
{
@ -534,57 +769,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<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)
{
LOG.warn(listener + " failed on '" + filename, th);
@ -653,6 +837,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)
{
for (Listener l : _listeners)
@ -670,7 +859,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)
{
@ -691,7 +880,7 @@ public class Scanner extends AbstractLifeCycle
}
/**
* sign
* Call ScanCycleListeners with end of scan.
*/
private void reportScanEnd(int cycle)
{

View File

@ -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<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
@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);