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:
parent
df63400dbe
commit
fee2255a5f
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue