465854 - Provide java.nio.file.WatchService alternative for jetty-util Scanner

+ Adding Proof of Concept for replacment, along with demo main, and test
  cases to show how it works.
This commit is contained in:
Joakim Erdfelt 2015-04-29 12:32:24 -07:00
parent df63400dbe
commit fee2255a5f
3 changed files with 1398 additions and 0 deletions

View File

@ -0,0 +1,837 @@
//
// ========================================================================
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.util;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.IOException;
import java.nio.file.ClosedWatchServiceException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.util.component.AbstractLifeCycle;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
/**
* Watch a Path (and sub directories) for Path changes.
* <p>
* Suitable replacement for the old {@link Scanner} implementation.
* <p>
* Allows for configured Excludes and Includes using {@link FileSystem#getPathMatcher(String)} syntax.
* <p>
* Reports activity via registered {@link Listener}s
*/
public class PathWatcher extends AbstractLifeCycle implements Runnable
{
public static class Config
{
protected final Path dir;
protected int recurseDepth = 0; // 0 means no sub-directories are scanned
protected List<PathMatcher> includes;
protected List<PathMatcher> excludes;
protected boolean excludeHidden = false;
public Config(Path path)
{
this.dir = path;
includes = new ArrayList<>();
excludes = new ArrayList<>();
}
/**
* Add an exclude PathMatcher
*
* @param matcher
* the path matcher for this exclude
*/
public void addExclude(PathMatcher matcher)
{
this.excludes.add(matcher);
}
/**
* Add an exclude PathMatcher
*
* @param syntaxAndPattern
* the PathMatcher syntax and pattern to use
* @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
*/
public void addExclude(String syntaxAndPattern)
{
if (LOG.isDebugEnabled())
{
LOG.debug("Adding exclude: [{}]",syntaxAndPattern);
}
addExclude(dir.getFileSystem().getPathMatcher(syntaxAndPattern));
}
/**
* Exclude hidden files and hidden directories
*/
public void addExcludeHidden()
{
if (!excludeHidden)
{
if (LOG.isDebugEnabled())
{
LOG.debug("Adding hidden files and directories to exclusions");
}
excludeHidden = true;
addExclude("regex:^.*/\\..*$"); // ignore hidden files
addExclude("regex:^.*/\\..*/.*$"); // ignore files in hidden directories
}
}
/**
* Add multiple exclude PathMatchers
*
* @param syntaxAndPatterns
* the list of PathMatcher syntax and patterns to use
* @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
*/
public void addExcludes(List<String> syntaxAndPatterns)
{
for (String syntaxAndPattern : syntaxAndPatterns)
{
addExclude(syntaxAndPattern);
}
}
/**
* Add an include PathMatcher
*
* @param matcher
* the path matcher for this include
*/
public void addInclude(PathMatcher matcher)
{
this.includes.add(matcher);
}
/**
* Add an include PathMatcher
*
* @param syntaxAndPattern
* the PathMatcher syntax and pattern to use
* @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
*/
public void addInclude(String syntaxAndPattern)
{
if (LOG.isDebugEnabled())
{
LOG.debug("Adding include: [{}]",syntaxAndPattern);
}
addInclude(dir.getFileSystem().getPathMatcher(syntaxAndPattern));
}
/**
* Add multiple include PathMatchers
*
* @param syntaxAndPatterns
* the list of PathMatcher syntax and patterns to use
* @see FileSystem#getPathMatcher(String) for detail on syntax and pattern
*/
public void addIncludes(List<String> syntaxAndPatterns)
{
for (String syntaxAndPattern : syntaxAndPatterns)
{
addInclude(syntaxAndPattern);
}
}
/**
* Build a new config from a this configuration.
* <p>
* Useful for working with sub-directories that also need to be watched.
*
* @param dir
* the directory to build new Config from (using this config as source of includes/excludes)
* @return the new Config
*/
public Config asSubConfig(Path dir)
{
Config subconfig = new Config(dir);
subconfig.includes = this.includes;
subconfig.excludes = this.excludes;
subconfig.recurseDepth = this.recurseDepth - 1;
return subconfig;
}
public int getRecurseDepth()
{
return recurseDepth;
}
private boolean hasMatch(Path path, List<PathMatcher> matchers)
{
for (PathMatcher matcher : matchers)
{
if (matcher.matches(path))
{
return true;
}
}
return false;
}
public boolean isExcluded(Path dir) throws IOException
{
if (excludeHidden)
{
if (Files.isHidden(dir))
{
return true;
}
}
if (excludes.isEmpty())
{
// no excludes == everything allowed
return false;
}
return hasMatch(dir,excludes);
}
public boolean isIncluded(Path dir)
{
if (includes.isEmpty())
{
// no includes == everything allowed
return true;
}
return hasMatch(dir,includes);
}
public boolean matches(Path path)
{
try
{
return !isExcluded(path) && isIncluded(path);
}
catch (IOException e)
{
LOG.warn("Unable to match path: " + path,e);
return false;
}
}
/**
* Set the recurse depth for the directory scanning.
* <p>
* 0 indicates no recursion, 1 is only one directory deep, and so on.
*
* @param depth
* the number of directories deep to recurse
*/
public void setRecurseDepth(int depth)
{
this.recurseDepth = depth;
}
/**
* Determine if the provided child directory should be recursed into
* based on the configured {@link #setRecurseDepth(int)}
*
* @param child
* the child directory to test against
* @return true if recurse should occur, false otherwise
*/
public boolean shouldRecurseDirectory(Path child)
{
if (!child.startsWith(child))
{
// not part of parent? don't recurse
return false;
}
int childDepth = dir.relativize(child).getNameCount();
return (childDepth <= recurseDepth);
}
@Override
public String toString()
{
StringBuilder s = new StringBuilder();
s.append(dir);
if (recurseDepth > 0)
{
s.append(" [depth=").append(recurseDepth).append("]");
}
return s.toString();
}
}
/**
* Listener for path change events
*/
public static interface Listener
{
void onPathWatchEvent(PathWatchEvent event);
}
public static class PathWatchEvent
{
private final Path path;
private final PathWatchEventType type;
private int count;
private long timestamp;
public PathWatchEvent(Path path, PathWatchEventType type)
{
this.path = path;
this.count = 0;
this.type = type;
this.timestamp = System.currentTimeMillis();
}
public PathWatchEvent(Path path, WatchEvent<Path> event)
{
this.path = path;
this.count = event.count();
if (event.kind() == ENTRY_CREATE)
{
this.type = PathWatchEventType.ADDED;
}
else if (event.kind() == ENTRY_DELETE)
{
this.type = PathWatchEventType.DELETED;
}
else if (event.kind() == ENTRY_MODIFY)
{
this.type = PathWatchEventType.MODIFIED;
}
else
{
this.type = PathWatchEventType.UNKNOWN;
}
this.timestamp = System.currentTimeMillis();
}
@Override
public boolean equals(Object obj)
{
if (this == obj)
{
return true;
}
if (obj == null)
{
return false;
}
if (getClass() != obj.getClass())
{
return false;
}
PathWatchEvent other = (PathWatchEvent)obj;
if (path == null)
{
if (other.path != null)
{
return false;
}
}
else if (!path.equals(other.path))
{
return false;
}
if (type != other.type)
{
return false;
}
return true;
}
/**
* Check the timestamp to see if it is expired.
* <p>
* Updates timestamp to 'now' on use of this method.
*
* @param expiredDuration
* the expired duration past the timestamp to be considered expired
* @param expiredUnit
* the unit of time for the expired check
* @return true if expired, false if not
*/
public boolean expiredCheck(long expiredDuration, TimeUnit expiredUnit)
{
long now = System.currentTimeMillis();
long pastdue = this.timestamp + expiredUnit.toMillis(expiredDuration);
if (now >= pastdue)
{
return true;
}
this.timestamp = now;
return false;
}
public int getCount()
{
return count;
}
public Path getPath()
{
return path;
}
public long getTimestamp()
{
return timestamp;
}
public PathWatchEventType getType()
{
return type;
}
@Override
public int hashCode()
{
final int prime = 31;
int result = 1;
result = (prime * result) + ((path == null) ? 0 : path.hashCode());
result = (prime * result) + ((type == null) ? 0 : type.hashCode());
return result;
}
public void incrementCount(int num)
{
this.count += num;
}
@Override
public String toString()
{
return String.format("PathWatchEvent[%s|%s,count=%d]",type,path,count);
}
}
public static enum PathWatchEventType
{
ADDED,
DELETED,
MODIFIED,
UNKNOWN;
}
private static final Logger LOG = Log.getLogger(PathWatcher.class);
@SuppressWarnings("unchecked")
protected static <T> WatchEvent<T> cast(WatchEvent<?> event)
{
return (WatchEvent<T>)event;
}
private WatchService watcher;
private Map<WatchKey, Config> keys = new HashMap<>();
private List<Listener> listeners = new ArrayList<>();
private List<PathWatchEvent> pendingAddEvents = new ArrayList<>();
private long updateQuietTimeDuration = 500;
private TimeUnit updateQuietTimeUnit = TimeUnit.MILLISECONDS;
private Thread thread;
public PathWatcher() throws IOException
{
this.watcher = FileSystems.getDefault().newWatchService();
}
/**
* Add a directory watch configuration to the the PathWatcher.
*
* @param baseDir
* the base directory configuration to watch
* @throws IOException
* if unable to setup the directory watch
*/
public void addDirectoryWatch(final Config baseDir) throws IOException
{
if (LOG.isDebugEnabled())
{
LOG.debug("Watching directory {}",baseDir);
}
Files.walkFileTree(baseDir.dir,new SimpleFileVisitor<Path>()
{
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException
{
FileVisitResult result = FileVisitResult.SKIP_SUBTREE;
if (LOG.isDebugEnabled())
{
LOG.debug("preVisitDirectory: {}",dir);
}
// Is directory not specifically excluded?
if (!baseDir.isExcluded(dir))
{
if (baseDir.isIncluded(dir))
{
// Directory is specifically included in PathMatcher, then
// it should be notified as such to interested listeners
PathWatchEvent event = new PathWatchEvent(dir,PathWatchEventType.ADDED);
if (LOG.isDebugEnabled())
{
LOG.debug("Pending {}",event);
}
pendingAddEvents.add(event);
}
// Recurse Directory, based on configured depth
if (baseDir.shouldRecurseDirectory(dir))
{
register(dir,baseDir);
result = FileVisitResult.CONTINUE;
}
}
if (LOG.isDebugEnabled())
{
LOG.debug("preVisitDirectory: result {}",result);
}
return result;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException
{
if (baseDir.matches(file))
{
PathWatchEvent event = new PathWatchEvent(file,PathWatchEventType.ADDED);
if (LOG.isDebugEnabled())
{
LOG.debug("Pending {}",event);
}
pendingAddEvents.add(event);
}
return FileVisitResult.CONTINUE;
}
});
}
public void addFileWatch(final Path file) throws IOException
{
if (LOG.isDebugEnabled())
{
LOG.debug("Watching file {}",file);
}
Path abs = file;
if (!abs.isAbsolute())
{
abs = file.toAbsolutePath();
}
Config config = new Config(abs.getParent());
// the include for the directory itself
config.addInclude("glob:" + abs.getParent().toString());
// the include for the file
config.addInclude("glob:" + abs.toString());
addDirectoryWatch(config);
}
public void addListener(Listener listener)
{
listeners.add(listener);
}
private void appendConfigId(StringBuilder s)
{
List<Path> dirs = new ArrayList<>();
for (Config config : keys.values())
{
dirs.add(config.dir);
}
Collections.sort(dirs);
s.append("[");
if (dirs.size() > 0)
{
s.append(dirs.get(0));
if (dirs.size() > 1)
{
s.append(" (+").append(dirs.size() - 1).append(")");
}
}
else
{
s.append("<null>");
}
s.append("]");
}
@Override
protected void doStart() throws Exception
{
// Start Thread for watcher take/pollKeys loop
StringBuilder threadId = new StringBuilder();
threadId.append("PathWatcher-Thread");
appendConfigId(threadId);
thread = new Thread(this,threadId.toString());
thread.start();
super.doStart();
}
@Override
protected void doStop() throws Exception
{
watcher.close();
super.doStop();
}
public Iterator<Listener> getListeners()
{
return listeners.iterator();
}
public long getUpdateQuietTimeMillis()
{
return TimeUnit.MILLISECONDS.convert(updateQuietTimeDuration,updateQuietTimeUnit);
}
protected void notifyOnPathWatchEvent(PathWatchEvent event)
{
for (Listener listener : listeners)
{
try
{
listener.onPathWatchEvent(event);
}
catch (Throwable t)
{
LOG.warn(t);
}
}
}
protected void register(Path dir, Config root) throws IOException
{
LOG.debug("Registering watch on {}",dir);
WatchKey key = dir.register(watcher,ENTRY_CREATE,ENTRY_DELETE,ENTRY_MODIFY);
keys.put(key,root.asSubConfig(dir));
}
public boolean removeListener(Listener listener)
{
return listeners.remove(listener);
}
@Override
public void run()
{
Map<Path, PathWatchEvent> pendingUpdateEvents = new HashMap<>();
// Start the java.nio watching
if (LOG.isDebugEnabled())
{
LOG.debug("Starting java.nio file watching with {}",watcher);
}
while (true)
{
WatchKey key = null;
try
{
// Process old events (from addDirectoryWatch())
if (!pendingAddEvents.isEmpty())
{
for (PathWatchEvent event : pendingAddEvents)
{
notifyOnPathWatchEvent(event);
}
pendingAddEvents.clear();
}
// Process new events
if (pendingUpdateEvents.isEmpty())
{
if (LOG.isDebugEnabled())
{
LOG.debug("Waiting for take()");
}
// wait for any event
key = watcher.take();
}
else
{
if (LOG.isDebugEnabled())
{
LOG.debug("Waiting for poll({}, {})",updateQuietTimeDuration,updateQuietTimeUnit);
}
key = watcher.poll(updateQuietTimeDuration,updateQuietTimeUnit);
if (key == null)
{
// no new event encountered.
for (Path path : new HashSet<Path>(pendingUpdateEvents.keySet()))
{
PathWatchEvent pending = pendingUpdateEvents.get(path);
if (pending.expiredCheck(updateQuietTimeDuration,updateQuietTimeUnit))
{
// it is expired
// notify that update is complete
notifyOnPathWatchEvent(pending);
// remove from pending list
pendingUpdateEvents.remove(path);
}
}
continue; // loop again
}
}
}
catch (ClosedWatchServiceException e)
{
// Normal shutdown of watcher
return;
}
catch (InterruptedException e)
{
if (isRunning())
{
LOG.warn(e);
}
else
{
LOG.ignore(e);
}
return;
}
Config config = keys.get(key);
if (config == null)
{
if (LOG.isDebugEnabled())
{
LOG.debug("WatchKey not recognized: {}",key);
}
continue;
}
for (WatchEvent<?> event : key.pollEvents())
{
@SuppressWarnings("unchecked")
WatchEvent.Kind<Path> kind = (Kind<Path>)event.kind();
WatchEvent<Path> ev = cast(event);
Path name = ev.context();
Path child = config.dir.resolve(name);
if (kind == ENTRY_CREATE)
{
// handle special case for registering new directories
// recursively
if (Files.isDirectory(child,LinkOption.NOFOLLOW_LINKS))
{
try
{
addDirectoryWatch(config.asSubConfig(child));
}
catch (IOException e)
{
LOG.warn(e);
}
}
else if (config.matches(child))
{
notifyOnPathWatchEvent(new PathWatchEvent(child,ev));
}
}
else if (config.matches(child))
{
if (kind == ENTRY_MODIFY)
{
// handle modify events with a quiet time before they
// are notified to the listeners
PathWatchEvent pending = pendingUpdateEvents.get(child);
if (pending == null)
{
// new pending update
pendingUpdateEvents.put(child,new PathWatchEvent(child,ev));
}
else
{
// see if pending is expired
if (pending.expiredCheck(updateQuietTimeDuration,updateQuietTimeUnit))
{
// it is expired, notify that update is complete
notifyOnPathWatchEvent(pending);
// remove from pending list
pendingUpdateEvents.remove(child);
}
else
{
// update the count (useful for debugging)
pending.incrementCount(ev.count());
}
}
}
else
{
notifyOnPathWatchEvent(new PathWatchEvent(child,ev));
}
}
}
if (!key.reset())
{
keys.remove(key);
if (keys.isEmpty())
{
return; // all done, no longer monitoring anything
}
}
}
}
public void setUpdateQuietTime(long duration, TimeUnit unit)
{
this.updateQuietTimeDuration = duration;
this.updateQuietTimeUnit = unit;
}
@Override
public String toString()
{
StringBuilder s = new StringBuilder(this.getClass().getName());
appendConfigId(s);
return s.toString();
}
}

View File

@ -0,0 +1,91 @@
//
// ========================================================================
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.util;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
public class PathWatcherDemo implements PathWatcher.Listener
{
private static final Logger LOG = Log.getLogger(PathWatcherDemo.class);
public static void main(String[] args)
{
List<Path> paths = new ArrayList<>();
for (String arg : args)
{
paths.add(new File(arg).toPath());
}
if (paths.isEmpty())
{
LOG.warn("No paths specified on command line");
System.exit(-1);
}
PathWatcherDemo demo = new PathWatcherDemo();
try
{
demo.run(paths);
}
catch (Throwable t)
{
LOG.warn(t);
}
}
public void run(List<Path> paths) throws Exception
{
PathWatcher watcher = new PathWatcher();
watcher.addListener(new PathWatcherDemo());
List<String> excludes = new ArrayList<>();
excludes.add("glob:*.bak"); // ignore backup files
excludes.add("regex:^.*/\\~[^/]*$"); // ignore scratch files
for (Path path : paths)
{
if (Files.isDirectory(path))
{
PathWatcher.Config config = new PathWatcher.Config(path);
config.addExcludeHidden();
config.addExcludes(excludes);
watcher.addDirectoryWatch(config);
}
else
{
watcher.addFileWatch(path);
}
}
watcher.start();
}
@Override
public void onPathWatchEvent(PathWatchEvent event)
{
LOG.info("onPathWatchEvent: {}",event);
}
}

View File

@ -0,0 +1,470 @@
//
// ========================================================================
// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.util;
import static java.nio.file.StandardOpenOption.*;
import static org.eclipse.jetty.util.PathWatcher.PathWatchEventType.*;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.eclipse.jetty.toolchain.test.FS;
import org.eclipse.jetty.toolchain.test.TestingDir;
import org.eclipse.jetty.util.PathWatcher.PathWatchEvent;
import org.eclipse.jetty.util.PathWatcher.PathWatchEventType;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.junit.Rule;
import org.junit.Test;
public class PathWatcherTest
{
public static class PathWatchEventCapture implements PathWatcher.Listener
{
private static final Logger LOG = Log.getLogger(PathWatcherTest.PathWatchEventCapture.class);
private final Path baseDir;
/**
* Map of relative paths seen, to their events seen (in order seen)
*/
public Map<String, List<PathWatchEventType>> events = new HashMap<>();
public PathWatchEventCapture(Path baseDir)
{
this.baseDir = baseDir;
}
@Override
public void onPathWatchEvent(PathWatchEvent event)
{
synchronized (events)
{
Path relativePath = this.baseDir.relativize(event.getPath());
String key = relativePath.toString().replace(File.separatorChar,'/');
List<PathWatchEventType> types = this.events.get(key);
if (types == null)
{
types = new ArrayList<>();
this.events.put(key,types);
}
types.add(event.getType());
this.events.put(key,types);
LOG.debug("Captured Event: {} | {}",event.getType(),key);
}
}
/**
* Validate the events seen match expectations.
* <p>
* Note: order of events is only important when looking at a specific file or directory. Events for multiple
* files can overlap in ways that this assertion doesn't care about.
*
* @param expectedEvents
* the events expected
*/
public void assertEvents(Map<String, PathWatchEventType[]> expectedEvents)
{
assertThat("Event match (file|diretory) count",this.events.size(),is(expectedEvents.size()));
for (Map.Entry<String, PathWatchEventType[]> entry : expectedEvents.entrySet())
{
String relativePath = entry.getKey();
PathWatchEventType[] expectedTypes = entry.getValue();
assertEvents(relativePath,expectedTypes);
}
}
/**
* Validate the events seen match expectations.
* <p>
* Note: order of events is only important when looking at a specific file or directory. Events for multiple
* files can overlap in ways that this assertion doesn't care about.
*
* @param relativePath
* the test relative path to look for
*
* @param expectedEvents
* the events expected
*/
public void assertEvents(String relativePath, PathWatchEventType... expectedEvents)
{
synchronized (events)
{
List<PathWatchEventType> actualEvents = this.events.get(relativePath);
assertThat("Events for path [" + relativePath + "]",actualEvents,contains(expectedEvents));
}
}
}
private static void updateFile(Path path, String newContents) throws IOException
{
try (BufferedWriter writer = Files.newBufferedWriter(path,StandardCharsets.UTF_8,CREATE,TRUNCATE_EXISTING,WRITE))
{
writer.append(newContents);
writer.flush();
}
}
/**
* Update (optionally create) a file over time.
* <p>
* The file will be created in a slowed down fashion, over the time specified.
*
* @param path
* the file to update / create
* @param fileSize
* the ultimate file size to create
* @param timeDuration
* the time duration to take to create the file (approximate, not 100% accurate)
* @param timeUnit
* the time unit to take to create the file
* @throws IOException
* if unable to write file
* @throws InterruptedException
* if sleep between writes was interrupted
*/
private void updateFileOverTime(Path path, int fileSize, int timeDuration, TimeUnit timeUnit) throws IOException, InterruptedException
{
// how long to sleep between writes
int sleepMs = 100;
// how many millis to spend writing entire file size
long totalMs = timeUnit.toMillis(timeDuration);
// how many write chunks to write
int writeCount = (int)((int)totalMs / (int)sleepMs);
// average chunk buffer
int chunkBufLen = fileSize / writeCount;
byte chunkBuf[] = new byte[chunkBufLen];
Arrays.fill(chunkBuf,(byte)'x');
try (OutputStream out = Files.newOutputStream(path,CREATE,TRUNCATE_EXISTING,WRITE))
{
int left = fileSize;
while (left > 0)
{
int len = Math.min(left,chunkBufLen);
out.write(chunkBuf,0,len);
left -= chunkBufLen;
TimeUnit.MILLISECONDS.sleep(sleepMs);
out.flush();
}
}
}
private static final int KB = 1024;
private static final int MB = KB * KB;
@Rule
public TestingDir testdir = new TestingDir();
@Test
public void testConfig_ShouldRecurse_0() throws IOException
{
Path dir = testdir.getEmptyDir().toPath();
// Create a few directories
Files.createDirectories(dir.resolve("a/b/c/d"));
PathWatcher.Config config = new PathWatcher.Config(dir);
config.setRecurseDepth(0);
assertThat("Config.recurse[0].shouldRecurse[./a/b]",config.shouldRecurseDirectory(dir.resolve("a/b")),is(false));
assertThat("Config.recurse[0].shouldRecurse[./a]",config.shouldRecurseDirectory(dir.resolve("a")),is(false));
assertThat("Config.recurse[0].shouldRecurse[./]",config.shouldRecurseDirectory(dir),is(false));
}
@Test
public void testConfig_ShouldRecurse_1() throws IOException
{
Path dir = testdir.getEmptyDir().toPath();
// Create a few directories
Files.createDirectories(dir.resolve("a/b/c/d"));
PathWatcher.Config config = new PathWatcher.Config(dir);
config.setRecurseDepth(1);
assertThat("Config.recurse[1].shouldRecurse[./a/b]",config.shouldRecurseDirectory(dir.resolve("a/b")),is(false));
assertThat("Config.recurse[1].shouldRecurse[./a]",config.shouldRecurseDirectory(dir.resolve("a")),is(true));
assertThat("Config.recurse[1].shouldRecurse[./]",config.shouldRecurseDirectory(dir),is(true));
}
@Test
public void testConfig_ShouldRecurse_2() throws IOException
{
Path dir = testdir.getEmptyDir().toPath();
// Create a few directories
Files.createDirectories(dir.resolve("a/b/c/d"));
PathWatcher.Config config = new PathWatcher.Config(dir);
config.setRecurseDepth(2);
assertThat("Config.recurse[1].shouldRecurse[./a/b/c]",config.shouldRecurseDirectory(dir.resolve("a/b/c")),is(false));
assertThat("Config.recurse[1].shouldRecurse[./a/b]",config.shouldRecurseDirectory(dir.resolve("a/b")),is(true));
assertThat("Config.recurse[1].shouldRecurse[./a]",config.shouldRecurseDirectory(dir.resolve("a")),is(true));
assertThat("Config.recurse[1].shouldRecurse[./]",config.shouldRecurseDirectory(dir),is(true));
}
/**
* When starting up the PathWatcher, the events should occur
* indicating files that are of interest that already exist
* on the filesystem.
*
* @throws Exception
* on test failure
*/
@Test
public void testStartupFindFiles() throws Exception
{
Path dir = testdir.getEmptyDir().toPath();
// Files we are interested in
Files.createFile(dir.resolve("foo.war"));
Files.createDirectories(dir.resolve("bar/WEB-INF"));
Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
// Files we don't care about
Files.createFile(dir.resolve("foo.war.backup"));
Files.createFile(dir.resolve(".hidden.war"));
Files.createDirectories(dir.resolve(".wat/WEB-INF"));
Files.createFile(dir.resolve(".wat/huh.war"));
Files.createFile(dir.resolve(".wat/WEB-INF/web.xml"));
PathWatcher pathWatcher = new PathWatcher();
pathWatcher.setUpdateQuietTime(300,TimeUnit.MILLISECONDS);
// Add listener
PathWatchEventCapture capture = new PathWatchEventCapture(dir);
pathWatcher.addListener(capture);
// Add test dir configuration
PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
baseDirConfig.setRecurseDepth(2);
baseDirConfig.addExcludeHidden();
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*.war");
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*/WEB-INF/web.xml");
pathWatcher.addDirectoryWatch(baseDirConfig);
try
{
pathWatcher.start();
// Let quiet time do its thing
TimeUnit.MILLISECONDS.sleep(500);
Map<String, PathWatchEventType[]> expected = new HashMap<>();
expected.put("bar/WEB-INF/web.xml",new PathWatchEventType[] { ADDED });
expected.put("foo.war",new PathWatchEventType[] { ADDED });
capture.assertEvents(expected);
}
finally
{
pathWatcher.stop();
}
}
@Test
public void testDeployFiles_Update_Delete() throws Exception
{
Path dir = testdir.getEmptyDir().toPath();
// Files we are interested in
Files.createFile(dir.resolve("foo.war"));
Files.createDirectories(dir.resolve("bar/WEB-INF"));
Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
PathWatcher pathWatcher = new PathWatcher();
pathWatcher.setUpdateQuietTime(300,TimeUnit.MILLISECONDS);
// Add listener
PathWatchEventCapture capture = new PathWatchEventCapture(dir);
pathWatcher.addListener(capture);
// Add test dir configuration
PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
baseDirConfig.setRecurseDepth(2);
baseDirConfig.addExcludeHidden();
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*.war");
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*/WEB-INF/web.xml");
pathWatcher.addDirectoryWatch(baseDirConfig);
try
{
pathWatcher.start();
// Pretend that startup occurred
TimeUnit.MILLISECONDS.sleep(500);
// Update web.xml
updateFile(dir.resolve("bar/WEB-INF/web.xml"),"Hello Update");
FS.touch(dir.resolve("bar/WEB-INF/web.xml").toFile());
// Delete war
Files.delete(dir.resolve("foo.war"));
// Let quiet time elapse
TimeUnit.MILLISECONDS.sleep(500);
Map<String, PathWatchEventType[]> expected = new HashMap<>();
expected.put("bar/WEB-INF/web.xml",new PathWatchEventType[] { ADDED, MODIFIED });
expected.put("foo.war",new PathWatchEventType[] { ADDED, DELETED });
capture.assertEvents(expected);
}
finally
{
pathWatcher.stop();
}
}
@Test
public void testDeployFiles_NewWar() throws Exception
{
Path dir = testdir.getEmptyDir().toPath();
// Files we are interested in
Files.createFile(dir.resolve("foo.war"));
Files.createDirectories(dir.resolve("bar/WEB-INF"));
Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
PathWatcher pathWatcher = new PathWatcher();
pathWatcher.setUpdateQuietTime(300,TimeUnit.MILLISECONDS);
// Add listener
PathWatchEventCapture capture = new PathWatchEventCapture(dir);
pathWatcher.addListener(capture);
// Add test dir configuration
PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
baseDirConfig.setRecurseDepth(2);
baseDirConfig.addExcludeHidden();
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*.war");
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*/WEB-INF/web.xml");
pathWatcher.addDirectoryWatch(baseDirConfig);
try
{
pathWatcher.start();
// Pretend that startup occurred
TimeUnit.MILLISECONDS.sleep(500);
// New war added
updateFile(dir.resolve("hello.war"),"Hello Update");
// Let quiet time elapse
TimeUnit.MILLISECONDS.sleep(500);
Map<String, PathWatchEventType[]> expected = new HashMap<>();
expected.put("bar/WEB-INF/web.xml",new PathWatchEventType[] { ADDED });
expected.put("foo.war",new PathWatchEventType[] { ADDED });
expected.put("hello.war",new PathWatchEventType[] { ADDED, MODIFIED });
capture.assertEvents(expected);
}
finally
{
pathWatcher.stop();
}
}
/**
* Pretend to add a new war file that is large, and being copied into place
* using some sort of technique that is slow enough that it takes a while for
* the entire war file to exist in place.
* <p>
* This is to test the quiet time logic to ensure that only a single MODIFIED event occurs on this new war file
*
* @throws Exception
* on test failure
*/
@Test
public void testDeployFiles_NewWar_LargeSlowCopy() throws Exception
{
Path dir = testdir.getEmptyDir().toPath();
// Files we are interested in
Files.createFile(dir.resolve("foo.war"));
Files.createDirectories(dir.resolve("bar/WEB-INF"));
Files.createFile(dir.resolve("bar/WEB-INF/web.xml"));
PathWatcher pathWatcher = new PathWatcher();
pathWatcher.setUpdateQuietTime(300,TimeUnit.MILLISECONDS);
// Add listener
PathWatchEventCapture capture = new PathWatchEventCapture(dir);
pathWatcher.addListener(capture);
// Add test dir configuration
PathWatcher.Config baseDirConfig = new PathWatcher.Config(dir);
baseDirConfig.setRecurseDepth(2);
baseDirConfig.addExcludeHidden();
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*.war");
baseDirConfig.addInclude("glob:" + dir.toAbsolutePath().toString() + "/*/WEB-INF/web.xml");
pathWatcher.addDirectoryWatch(baseDirConfig);
try
{
pathWatcher.start();
// Pretend that startup occurred
TimeUnit.MILLISECONDS.sleep(500);
// New war added (slowly)
updateFileOverTime(dir.resolve("hello.war"),50 * MB,3,TimeUnit.SECONDS);
// Let quiet time elapse
TimeUnit.MILLISECONDS.sleep(500);
Map<String, PathWatchEventType[]> expected = new HashMap<>();
expected.put("bar/WEB-INF/web.xml",new PathWatchEventType[] { ADDED });
expected.put("foo.war",new PathWatchEventType[] { ADDED });
expected.put("hello.war",new PathWatchEventType[] { ADDED, MODIFIED });
capture.assertEvents(expected);
}
finally
{
pathWatcher.stop();
}
}
}