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