From f50c4fd4b417addd7c092a68cbedd492dc82b7ff Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Fri, 4 Jun 2021 17:07:05 +1000 Subject: [PATCH 1/4] Issue #6327 Remove an invalid RequestTest Signed-off-by: Jan Bartel --- .../org/eclipse/jetty/server/RequestTest.java | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 7fdcd8097aa..8d62a0fcf85 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -1013,75 +1013,6 @@ public class RequestTest assertThat(response, containsString(" 200 OK")); } - @Test - @Disabled("See issue #1175") - public void testMultiPartFormDataReadInputThenParams() throws Exception - { - final File tmpdir = MavenTestingUtils.getTargetTestingDir("multipart"); - FS.ensureEmpty(tmpdir); - - Handler handler = new AbstractHandler() - { - @Override - public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException - { - if (baseRequest.getDispatcherType() != DispatcherType.REQUEST) - return; - - // Fake a @MultiPartConfig'd servlet endpoint - MultipartConfigElement multipartConfig = new MultipartConfigElement(tmpdir.getAbsolutePath()); - request.setAttribute(Request.__MULTIPART_CONFIG_ELEMENT, multipartConfig); - - // Normal processing - baseRequest.setHandled(true); - - // Fake the commons-fileupload behavior - int length = request.getContentLength(); - InputStream in = request.getInputStream(); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - IO.copy(in, out, length); // KEY STEP (Don't Change!) commons-fileupload does not read to EOF - - // Record what happened as servlet response headers - response.setIntHeader("x-request-content-length", request.getContentLength()); - response.setIntHeader("x-request-content-read", out.size()); - String foo = request.getParameter("foo"); // uri query parameter - String bar = request.getParameter("bar"); // form-data content parameter - response.setHeader("x-foo", foo == null ? "null" : foo); - response.setHeader("x-bar", bar == null ? "null" : bar); - } - }; - - _server.stop(); - _server.setHandler(handler); - _server.start(); - - String multipart = "--AaBbCc\r\n" + - "content-disposition: form-data; name=\"bar\"\r\n" + - "\r\n" + - "BarContent\r\n" + - "--AaBbCc\r\n" + - "content-disposition: form-data; name=\"stuff\"\r\n" + - "Content-Type: text/plain;charset=ISO-8859-1\r\n" + - "\r\n" + - "000000000000000000000000000000000000000000000000000\r\n" + - "--AaBbCc--\r\n"; - - String request = "POST /?foo=FooUri HTTP/1.1\r\n" + - "Host: whatever\r\n" + - "Content-Type: multipart/form-data; boundary=\"AaBbCc\"\r\n" + - "Content-Length: " + multipart.getBytes().length + "\r\n" + - "Connection: close\r\n" + - "\r\n" + - multipart; - - HttpTester.Response response = HttpTester.parseResponse(_connector.getResponse(request)); - - // It should always be possible to read query string - assertThat("response.x-foo", response.get("x-foo"), is("FooUri")); - // Not possible to read request content parameters? - assertThat("response.x-bar", response.get("x-bar"), is("null")); // TODO: should this work? - } - @Test public void testPartialRead() throws Exception { From 9cc7517d65b4dce64e0c115fbb722fac80ded033 Mon Sep 17 00:00:00 2001 From: Lachlan Roberts Date: Fri, 28 May 2021 11:33:15 +1000 Subject: [PATCH 2/4] Issue #6330 - Improve javadoc for CustomRequestLog %H Signed-off-by: Lachlan Roberts --- .../main/java/org/eclipse/jetty/server/CustomRequestLog.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java b/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java index 9cfd28e557c..c4cb3b61960 100644 --- a/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java +++ b/jetty-server/src/main/java/org/eclipse/jetty/server/CustomRequestLog.java @@ -152,7 +152,9 @@ import static java.lang.invoke.MethodType.methodType; * * * %H - * The request protocol. + * Returns the name and version of the protocol the request uses in the form + * protocol/majorVersion.minorVersion, for example, HTTP/1.1. For HTTP servlets, + * the value returned is the same as the value of the CGI variable SERVER_PROTOCOL. * * * From de37267ae5cac405d044e49679e340d7913e3bd5 Mon Sep 17 00:00:00 2001 From: Jan Bartel Date: Fri, 4 Jun 2021 23:08:53 +1000 Subject: [PATCH 3/4] Issue #6327 Fix cookie leak test (#6344) * Issue #6327 Fix cookie leak test Signed-off-by: Jan Bartel --- .../org/eclipse/jetty/server/RequestTest.java | 114 ++++++++++++------ 1 file changed, 74 insertions(+), 40 deletions(-) diff --git a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java index 8d62a0fcf85..ef22b43f6c2 100644 --- a/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java +++ b/jetty-server/src/test/java/org/eclipse/jetty/server/RequestTest.java @@ -1427,69 +1427,80 @@ public class RequestTest assertEquals("value", cookies.get(0).getValue()); } - @Disabled("No longer relevant") @Test public void testCookieLeak() throws Exception { - final String[] cookie = new String[10]; + CookieRequestTester tester = new CookieRequestTester(); + _handler._checker = tester; - _handler._checker = (request, response) -> - { - Arrays.fill(cookie, null); - - Cookie[] cookies = request.getCookies(); - for (int i = 0; cookies != null && i < cookies.length; i++) - { - cookie[i] = cookies[i].getValue(); - } - return true; - }; - - String request = "POST / HTTP/1.1\r\n" + + String[] cookies = new String[10]; + tester.setCookieArray(cookies); + LocalEndPoint endp = _connector.connect(); + endp.addInput("POST / HTTP/1.1\r\n" + "Host: whatever\r\n" + "Cookie: other=cookie\r\n" + - "\r\n" + - "POST / HTTP/1.1\r\n" + + "\r\n"); + endp.getResponse(); + assertEquals("cookie", cookies[0]); + assertNull(cookies[1]); + + cookies = new String[10]; + tester.setCookieArray(cookies); + endp.addInput("POST / HTTP/1.1\r\n" + "Host: whatever\r\n" + "Cookie: name=value\r\n" + "Connection: close\r\n" + - "\r\n"; + "\r\n"); + endp.getResponse(); + assertEquals("value", cookies[0]); + assertNull(cookies[1]); - _connector.getResponse(request); - - assertEquals("value", cookie[0]); - assertNull(cookie[1]); - - request = "POST / HTTP/1.1\r\n" + + endp = _connector.connect(); + cookies = new String[10]; + tester.setCookieArray(cookies); + endp.addInput("POST / HTTP/1.1\r\n" + "Host: whatever\r\n" + "Cookie: name=value\r\n" + - "\r\n" + - "POST / HTTP/1.1\r\n" + + "\r\n"); + endp.getResponse(); + assertEquals("value", cookies[0]); + assertNull(cookies[1]); + + cookies = new String[10]; + tester.setCookieArray(cookies); + endp.addInput("POST / HTTP/1.1\r\n" + "Host: whatever\r\n" + "Cookie: \r\n" + "Connection: close\r\n" + - "\r\n"; + "\r\n"); + endp.getResponse(); + assertNull(cookies[0]); + assertNull(cookies[1]); - _connector.getResponse(request); - assertNull(cookie[0]); - assertNull(cookie[1]); - - request = "POST / HTTP/1.1\r\n" + + endp = _connector.connect(); + cookies = new String[10]; + tester.setCookieArray(cookies); + endp.addInput("POST / HTTP/1.1\r\n" + "Host: whatever\r\n" + "Cookie: name=value\r\n" + "Cookie: other=cookie\r\n" + - "\r\n" + - "POST / HTTP/1.1\r\n" + + "\r\n"); + endp.getResponse(); + assertEquals("value", cookies[0]); + assertEquals("cookie", cookies[1]); + assertNull(cookies[2]); + + cookies = new String[10]; + tester.setCookieArray(cookies); + endp.addInput("POST / HTTP/1.1\r\n" + "Host: whatever\r\n" + "Cookie: name=value\r\n" + "Cookie:\r\n" + "Connection: close\r\n" + - "\r\n"; - - _connector.getResponse(request); - - assertEquals("value", cookie[0]); - assertNull(cookie[1]); + "\r\n"); + endp.getResponse(); + assertEquals("value", cookies[0]); + assertNull(cookies[1]); } @Test @@ -1878,6 +1889,29 @@ public class RequestTest boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException; } + private static class CookieRequestTester implements RequestTester + { + private String[] _cookieValues; + + public void setCookieArray(String[] cookieValues) + { + _cookieValues = cookieValues; + } + + @Override + public boolean check(HttpServletRequest request, HttpServletResponse response) throws IOException + { + Arrays.fill(_cookieValues, null); + + Cookie[] cookies = request.getCookies(); + for (int i = 0; cookies != null && i < cookies.length; i++) + { + _cookieValues[i] = cookies[i].getValue(); + } + return true; + } + } + private static class TestRequest extends Request { public static final String TEST_SESSION_ID = "abc123"; From e3faf81860073f5adabb73ed46c74715e0b1ee8e Mon Sep 17 00:00:00 2001 From: Greg Wilkins Date: Fri, 4 Jun 2021 23:10:54 +1000 Subject: [PATCH 4/4] Fix #6114 Deploy symlink webapps (#6317) * Fix #6114 Deploy symlink webapps Use Path.toRealPath rather than getCanonicalPath in the Scanner Make following symlinks configurable Signed-off-by: Greg Wilkins --- .../deploy/providers/ScanningAppProvider.java | 19 +- .../deploy/providers/WebAppProvider.java | 4 +- .../deploy/providers/WebAppProviderTest.java | 16 +- .../src/test/resources/jetty-deploy-wars.xml | 1 + .../java/org/eclipse/jetty/util/Scanner.java | 188 +++++++++++++----- 5 files changed, 162 insertions(+), 66 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 eb66704f59d..a4263442c1b 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 @@ -48,6 +48,7 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements private final List _monitored = new CopyOnWriteArrayList<>(); private int _scanInterval = 10; private Scanner _scanner; + private boolean _useRealPaths; private final Scanner.DiscreteListener _scannerListener = new Scanner.DiscreteListener() { @@ -81,6 +82,22 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements addBean(_appMap); } + /** + * @return True if the real path of the scanned files should be used for deployment. + */ + public boolean isUseRealPaths() + { + return _useRealPaths; + } + + /** + * @param useRealPaths True if the real path of the scanned files should be used for deployment. + */ + public void setUseRealPaths(boolean useRealPaths) + { + _useRealPaths = useRealPaths; + } + protected void setFilenameFilter(FilenameFilter filter) { if (isRunning()) @@ -128,7 +145,7 @@ public abstract class ScanningAppProvider extends ContainerLifeCycle implements LOG.warn("Does not exist: {}", resource); } - _scanner = new Scanner(); + _scanner = new Scanner(null, _useRealPaths); _scanner.setScanDirs(files); _scanner.setScanInterval(_scanInterval); _scanner.setFilenameFilter(_filenameFilter); 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 daf54df9475..24c6958c667 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 @@ -375,7 +375,7 @@ public class WebAppProvider extends ScanningAppProvider { //if a .xml file exists for it, then redeploy that instead File xml = new File(parent, xmlname); - super.fileChanged(xml.getCanonicalPath()); + super.fileChanged(xml.getPath()); return; } @@ -384,7 +384,7 @@ public class WebAppProvider extends ScanningAppProvider { //if a .XML file exists for it, then redeploy that instead File xml = new File(parent, xmlname); - super.fileChanged(xml.getCanonicalPath()); + super.fileChanged(xml.getPath()); return; } diff --git a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/WebAppProviderTest.java b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/WebAppProviderTest.java index eb6684de9f2..90a687b9663 100644 --- a/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/WebAppProviderTest.java +++ b/jetty-deploy/src/test/java/org/eclipse/jetty/deploy/providers/WebAppProviderTest.java @@ -14,7 +14,6 @@ package org.eclipse.jetty.deploy.providers; import java.io.File; -import java.net.URL; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.Path; @@ -35,7 +34,6 @@ import org.eclipse.jetty.util.resource.Resource; import org.eclipse.jetty.webapp.WebAppContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledOnOs; import org.junit.jupiter.api.extension.ExtendWith; @@ -45,7 +43,6 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assumptions.assumeTrue; import static org.junit.jupiter.api.condition.OS.LINUX; -import static org.junit.jupiter.api.condition.OS.MAC; @ExtendWith(WorkDirExtension.class) public class WebAppProviderTest @@ -68,10 +65,13 @@ public class WebAppProviderTest // Make symlink Path pathWar3 = MavenTestingUtils.getTestResourcePathFile("webapps/foo-webapp-3.war"); + Path pathFoo = jetty.getJettyDir("webapps/foo.war").toPath(); Path pathBar = jetty.getJettyDir("webapps/bar.war").toPath(); + Path pathBob = jetty.getJettyDir("webapps/bob.war").toPath(); try { Files.createSymbolicLink(pathBar, pathWar3); + Files.createSymbolicLink(pathBob, pathFoo); symlinkSupported = true; } catch (UnsupportedOperationException | FileSystemException e) @@ -95,12 +95,11 @@ public class WebAppProviderTest jetty.stop(); } - @Disabled("See issue #1200") @Test public void testStartupContext() { // Check Server for Handlers - jetty.assertWebAppContextsExists("/bar", "/foo"); + jetty.assertWebAppContextsExists("/bar", "/foo", "/bob"); File workDir = jetty.getJettyDir("workish"); @@ -109,10 +108,9 @@ public class WebAppProviderTest assertDirNotExists("root of work directory", workDir, "jsp"); // Test for correct behaviour - assertTrue(hasJettyGeneratedPath(workDir, "foo.war"), "Should have generated directory in work directory: " + workDir); + assertTrue(hasJettyGeneratedPath(workDir, "foo_war"), "Should have generated directory in work directory: " + workDir); } - @Disabled("See issue #1200") @Test public void testStartupSymlinkContext() { @@ -124,11 +122,11 @@ public class WebAppProviderTest assertTrue(barLink.isFile(), "bar.war link isFile: " + barLink.toString()); // Check Server for expected Handlers - jetty.assertWebAppContextsExists("/bar", "/foo"); + jetty.assertWebAppContextsExists("/bar", "/foo", "/bob"); // Test for expected work/temp directory behaviour File workDir = jetty.getJettyDir("workish"); - assertTrue(hasJettyGeneratedPath(workDir, "bar.war"), "Should have generated directory in work directory: " + workDir); + assertTrue(hasJettyGeneratedPath(workDir, "bar_war"), "Should have generated directory in work directory: " + workDir); } @Test diff --git a/jetty-deploy/src/test/resources/jetty-deploy-wars.xml b/jetty-deploy/src/test/resources/jetty-deploy-wars.xml index 50c192bdb07..8730443711b 100644 --- a/jetty-deploy/src/test/resources/jetty-deploy-wars.xml +++ b/jetty-deploy/src/test/resources/jetty-deploy-wars.xml @@ -18,6 +18,7 @@ /webapps 1 /workish + false 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 4a00309ee9b..38df54c9abe 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 @@ -20,6 +20,7 @@ import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; +import java.nio.file.LinkOption; import java.nio.file.Path; import java.nio.file.PathMatcher; import java.nio.file.attribute.BasicFileAttributes; @@ -35,6 +36,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; +import java.util.stream.Collectors; import org.eclipse.jetty.util.component.ContainerLifeCycle; import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler; @@ -47,6 +49,8 @@ import org.slf4j.LoggerFactory; * * Utility for scanning a directory for added, removed and changed * files and reporting these events via registered Listeners. + * The scanner operates on the {@link Path#toRealPath(LinkOption...)} of the files scanned and + * can be configured to follow symlinks. */ public class Scanner extends ContainerLifeCycle { @@ -63,7 +67,7 @@ public class Scanner extends ContainerLifeCycle private int _scanInterval; private final AtomicInteger _scanCount = new AtomicInteger(0); private final List _listeners = new CopyOnWriteArrayList<>(); - private Map _prevScan; + private Map _prevScan; private FilenameFilter _filter; private final Map> _scannables = new ConcurrentHashMap<>(); private boolean _reportExisting = true; @@ -71,6 +75,7 @@ public class Scanner extends ContainerLifeCycle private Scheduler.Task _task; private final Scheduler _scheduler; private int _scanDepth = DEFAULT_SCAN_DEPTH; + private final LinkOption[] _linkOptions; private enum Status { @@ -150,11 +155,11 @@ public class Scanner extends ContainerLifeCycle */ private class Visitor implements FileVisitor { - Map scanInfoMap; + Map scanInfoMap; IncludeExcludeSet rootIncludesExcludes; Path root; - public Visitor(Path root, IncludeExcludeSet rootIncludesExcludes, Map scanInfoMap) + private Visitor(Path root, IncludeExcludeSet rootIncludesExcludes, Map scanInfoMap) { this.root = root; this.rootIncludesExcludes = rootIncludesExcludes; @@ -167,10 +172,11 @@ public class Scanner extends ContainerLifeCycle if (!Files.exists(dir)) return FileVisitResult.SKIP_SUBTREE; + dir = dir.toRealPath(_linkOptions); File f = dir.toFile(); //if we want to report directories and we haven't already seen it - if (_reportDirs && !scanInfoMap.containsKey(f.getCanonicalPath())) + if (_reportDirs && !scanInfoMap.containsKey(dir)) { boolean accepted = false; if (rootIncludesExcludes != null && !rootIncludesExcludes.isEmpty()) @@ -186,7 +192,7 @@ public class Scanner extends ContainerLifeCycle if (accepted) { - scanInfoMap.put(f.getCanonicalPath(), new MetaData(f.lastModified(), f.isDirectory() ? 0 : f.length())); + scanInfoMap.put(dir, new MetaData(f.lastModified(), f.isDirectory() ? 0 : f.length())); if (LOG.isDebugEnabled()) LOG.debug("scan accepted dir {} mod={}", f, f.lastModified()); } } @@ -195,20 +201,22 @@ public class Scanner extends ContainerLifeCycle } @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException + public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) throws IOException { - if (!Files.exists(file)) + path = path.toRealPath(_linkOptions); + + if (!Files.exists(path)) return FileVisitResult.CONTINUE; - File f = file.toFile(); + File f = path.toFile(); boolean accepted = false; - if (f.isFile() || (f.isDirectory() && _reportDirs && !scanInfoMap.containsKey(f.getCanonicalPath()))) + if (f.isFile() || (f.isDirectory() && _reportDirs && !scanInfoMap.containsKey(path))) { if (rootIncludesExcludes != null && !rootIncludesExcludes.isEmpty()) { //accepted if not explicitly excluded and either is explicitly included or there are no explicit inclusions - accepted = rootIncludesExcludes.test(file); + accepted = rootIncludesExcludes.test(path); } else if (_filter == null || _filter.accept(f.getParentFile(), f.getName())) accepted = true; @@ -216,7 +224,7 @@ public class Scanner extends ContainerLifeCycle if (accepted) { - scanInfoMap.put(f.getCanonicalPath(), new MetaData(f.lastModified(), f.isDirectory() ? 0 : f.length())); + scanInfoMap.put(path, new MetaData(f.lastModified(), f.isDirectory() ? 0 : f.length())); if (LOG.isDebugEnabled()) LOG.debug("scan accepted {} mod={}", f, f.lastModified()); } @@ -251,10 +259,62 @@ public class Scanner extends ContainerLifeCycle */ public interface DiscreteListener extends Listener { + /** + * Called when a file is changed. + * Default implementation calls {@link #fileChanged(String)}. + * @param path the {@link Path#toRealPath(LinkOption...)} of the changed file + * @throws Exception May be thrown for handling errors + */ + default void pathChanged(Path path) throws Exception + { + path.toString(); + fileChanged(path.toString()); + } + + /** + * Called when a file is added. + * Default implementation calls {@link #fileAdded(String)}. + * @param path the {@link Path#toRealPath(LinkOption...)} of the added file + * @throws Exception May be thrown for handling errors + */ + default void pathAdded(Path path) throws Exception + { + fileAdded(path.toString()); + } + + /** + * Called when a file is removed. + * Default implementation calls {@link #fileRemoved(String)}. + * @param path the {@link Path#toRealPath(LinkOption...)} of the removed file + * @throws Exception May be thrown for handling errors + */ + default void pathRemoved(Path path) throws Exception + { + fileRemoved(path.toString()); + } + + /** + * Called when a file is changed. + * May not be called if {@link #pathChanged(Path)} is overridden. + * @param filename the {@link Path#toRealPath(LinkOption...)} as a string of the changed file + * @throws Exception May be thrown for handling errors + */ void fileChanged(String filename) throws Exception; + /** + * Called when a file is added. + * May not be called if {@link #pathAdded(Path)} is overridden. + * @param filename the {@link Path#toRealPath(LinkOption...)} as a string of the added file + * @throws Exception May be thrown for handling errors + */ void fileAdded(String filename) throws Exception; + /** + * Called when a file is removed. + * May not be called if {@link #pathRemoved(Path)} is overridden. + * @param filename the {@link Path#toRealPath(LinkOption...)} as a string of the removed file + * @throws Exception May be thrown for handling errors + */ void fileRemoved(String filename) throws Exception; } @@ -263,7 +323,12 @@ public class Scanner extends ContainerLifeCycle */ public interface BulkListener extends Listener { - public void filesChanged(Set filenames) throws Exception; + default void pathsChanged(Set paths) throws Exception + { + filesChanged(paths.stream().map(Path::toString).collect(Collectors.toSet())); + } + + void filesChanged(Set filenames) throws Exception; } /** @@ -282,14 +347,24 @@ public class Scanner extends ContainerLifeCycle public Scanner() { - this(new ScheduledExecutorScheduler("Scanner-" + SCANNER_IDS.getAndIncrement(), true, 1)); + this(null); } public Scanner(Scheduler scheduler) + { + this(scheduler, true); + } + + /** + * @param scheduler The scheduler to use for scanning. + * @param reportRealPaths If true, the {@link Listener}s are called with the real path of scanned files. + */ + public Scanner(Scheduler scheduler, boolean reportRealPaths) { //Create the scheduler and start it - _scheduler = scheduler; + _scheduler = scheduler == null ? new ScheduledExecutorScheduler("Scanner-" + SCANNER_IDS.getAndIncrement(), true, 1) : scheduler; addBean(_scheduler); + _linkOptions = reportRealPaths ? new LinkOption[0] : new LinkOption[] {LinkOption.NOFOLLOW_LINKS}; } /** @@ -335,21 +410,24 @@ public class Scanner extends ContainerLifeCycle /** * Add a file to be scanned. The file must not be null, and must exist. * - * @param p the Path of the file to scan. + * @param path the Path of the file to scan. */ - public void addFile(Path p) + public void addFile(Path path) { if (isRunning()) throw new IllegalStateException("Scanner started"); - if (p == null) + if (path == null) throw new IllegalStateException("Null path"); - if (!Files.exists(p) || Files.isDirectory(p)) - throw new IllegalStateException("Not file or doesn't exist: " + p); try { - _scannables.putIfAbsent(p.toRealPath(), new IncludeExcludeSet<>(PathMatcherSet.class)); + // Always follow links when check ultimate type of the path + Path real = path.toRealPath(); + if (!Files.exists(real) || Files.isDirectory(real)) + throw new IllegalStateException("Not file or doesn't exist: " + path); + + _scannables.putIfAbsent(real, new IncludeExcludeSet<>(PathMatcherSet.class)); } catch (IOException e) { @@ -371,13 +449,15 @@ public class Scanner extends ContainerLifeCycle if (p == null) throw new IllegalStateException("Null path"); - if (!Files.exists(p) || !Files.isDirectory(p)) - throw new IllegalStateException("Not directory or doesn't exist: " + p); - try { + // Check status of the real path + Path real = p.toRealPath(); + if (!Files.exists(real) || !Files.isDirectory(real)) + throw new IllegalStateException("Not directory or doesn't exist: " + p); + IncludeExcludeSet includesExcludes = new IncludeExcludeSet<>(PathMatcherSet.class); - IncludeExcludeSet prev = _scannables.putIfAbsent(p.toRealPath(), includesExcludes); + IncludeExcludeSet prev = _scannables.putIfAbsent(real, includesExcludes); if (prev != null) includesExcludes = prev; return includesExcludes; @@ -625,7 +705,7 @@ public class Scanner extends ContainerLifeCycle { int cycle = _scanCount.incrementAndGet(); reportScanStart(cycle); - Map currentScan = scanFiles(); + Map currentScan = scanFiles(); reportDifferences(currentScan, _prevScan == null ? Collections.emptyMap() : Collections.unmodifiableMap(_prevScan)); _prevScan = currentScan; reportScanEnd(cycle); @@ -634,9 +714,9 @@ public class Scanner extends ContainerLifeCycle /** * Scan all of the given paths. */ - private Map scanFiles() + private Map scanFiles() { - Map currentScan = new HashMap<>(); + Map currentScan = new HashMap<>(); for (Map.Entry> entry : _scannables.entrySet()) { try @@ -660,20 +740,20 @@ public class Scanner extends ContainerLifeCycle * @param currentScan the info from the most recent pass * @param oldScan info from the previous pass */ - private void reportDifferences(Map currentScan, Map oldScan) + private void reportDifferences(Map currentScan, Map oldScan) { - Map changes = new HashMap<>(); + Map changes = new HashMap<>(); //Handle deleted files - Set oldScanKeys = new HashSet<>(oldScan.keySet()); + Set oldScanKeys = new HashSet<>(oldScan.keySet()); oldScanKeys.removeAll(currentScan.keySet()); - for (String file : oldScanKeys) + for (Path path : oldScanKeys) { - changes.put(file, Notification.REMOVED); + changes.put(path, Notification.REMOVED); } // Handle new and changed files - for (Map.Entry entry : currentScan.entrySet()) + for (Map.Entry entry : currentScan.entrySet()) { MetaData current = entry.getValue(); MetaData previous = oldScan.get(entry.getKey()); @@ -714,7 +794,7 @@ public class Scanner extends ContainerLifeCycle LOG.debug("scanned {}", _scannables.keySet()); //Call the DiscreteListeners - for (Map.Entry entry : changes.entrySet()) + for (Map.Entry entry : changes.entrySet()) { switch (entry.getValue()) { @@ -736,28 +816,28 @@ public class Scanner extends ContainerLifeCycle reportBulkChanges(changes.keySet()); } - private void warn(Object listener, String filename, Throwable th) + private void warn(Object listener, Path path, Throwable th) { - LOG.warn("{} failed on '{}'", listener, filename, th); + LOG.warn("{} failed on '{}'", listener, path, th); } /** * Report a file addition to the registered FileAddedListeners * - * @param filename the filename + * @param path the path */ - private void reportAddition(String filename) + private void reportAddition(Path path) { for (Listener l : _listeners) { try { if (l instanceof DiscreteListener) - ((DiscreteListener)l).fileAdded(filename); + ((DiscreteListener)l).pathAdded(path); } catch (Throwable e) { - warn(l, filename, e); + warn(l, path, e); } } } @@ -765,20 +845,20 @@ public class Scanner extends ContainerLifeCycle /** * Report a file removal to the FileRemovedListeners * - * @param filename the filename + * @param path the path of the removed filename */ - private void reportRemoval(String filename) + private void reportRemoval(Path path) { for (Object l : _listeners) { try { if (l instanceof DiscreteListener) - ((DiscreteListener)l).fileRemoved(filename); + ((DiscreteListener)l).pathRemoved(path); } catch (Throwable e) { - warn(l, filename, e); + warn(l, path, e); } } } @@ -786,11 +866,11 @@ public class Scanner extends ContainerLifeCycle /** * Report a file change to the FileChangedListeners * - * @param filename the filename + * @param path the path of the changed file */ - private void reportChange(String filename) + private void reportChange(Path path) { - if (filename == null) + if (path == null) return; for (Listener l : _listeners) @@ -798,11 +878,11 @@ public class Scanner extends ContainerLifeCycle try { if (l instanceof DiscreteListener) - ((DiscreteListener)l).fileChanged(filename); + ((DiscreteListener)l).pathChanged(path); } catch (Throwable e) { - warn(l, filename, e); + warn(l, path, e); } } } @@ -810,11 +890,11 @@ public class Scanner extends ContainerLifeCycle /** * Report the list of filenames for which changes were detected. * - * @param filenames names of all files added/changed/removed + * @param paths The paths of all files added/changed/removed */ - private void reportBulkChanges(Set filenames) + private void reportBulkChanges(Set paths) { - if (filenames == null || filenames.isEmpty()) + if (paths == null || paths.isEmpty()) return; for (Listener l : _listeners) @@ -822,11 +902,11 @@ public class Scanner extends ContainerLifeCycle try { if (l instanceof BulkListener) - ((BulkListener)l).filesChanged(filenames); + ((BulkListener)l).pathsChanged(paths); } catch (Throwable e) { - warn(l, filenames.toString(), e); + LOG.warn("{} failed on '{}'", l, paths, e); } } }