|
|
|
@ -21,6 +21,7 @@ import java.io.FileFilter;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.Collection;
|
|
|
|
|
import java.util.Collections;
|
|
|
|
|
import java.util.HashMap;
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.LinkedHashSet;
|
|
|
|
|
import java.util.List;
|
|
|
|
@ -44,7 +45,7 @@ public class FileSystemWatcher {
|
|
|
|
|
|
|
|
|
|
private static final long DEFAULT_QUIET_PERIOD = 400;
|
|
|
|
|
|
|
|
|
|
private List<FileChangeListener> listeners = new ArrayList<FileChangeListener>();
|
|
|
|
|
private final List<FileChangeListener> listeners = new ArrayList<FileChangeListener>();
|
|
|
|
|
|
|
|
|
|
private final boolean daemon;
|
|
|
|
|
|
|
|
|
@ -52,14 +53,16 @@ public class FileSystemWatcher {
|
|
|
|
|
|
|
|
|
|
private final long quietPeriod;
|
|
|
|
|
|
|
|
|
|
private Thread watchThread;
|
|
|
|
|
private final AtomicInteger remainingScans = new AtomicInteger(-1);
|
|
|
|
|
|
|
|
|
|
private AtomicInteger remainingScans = new AtomicInteger(-1);
|
|
|
|
|
private final Map<File, FolderSnapshot> folders = new HashMap<File, FolderSnapshot>();
|
|
|
|
|
|
|
|
|
|
private Map<File, FolderSnapshot> folders = new LinkedHashMap<File, FolderSnapshot>();
|
|
|
|
|
private Thread watchThread;
|
|
|
|
|
|
|
|
|
|
private FileFilter triggerFilter;
|
|
|
|
|
|
|
|
|
|
private final Object monitor = new Object();
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a new {@link FileSystemWatcher} instance.
|
|
|
|
|
*/
|
|
|
|
@ -89,10 +92,12 @@ public class FileSystemWatcher {
|
|
|
|
|
* {@link #start() started}.
|
|
|
|
|
* @param fileChangeListener the listener to add
|
|
|
|
|
*/
|
|
|
|
|
public synchronized void addListener(FileChangeListener fileChangeListener) {
|
|
|
|
|
public void addListener(FileChangeListener fileChangeListener) {
|
|
|
|
|
Assert.notNull(fileChangeListener, "FileChangeListener must not be null");
|
|
|
|
|
checkNotStarted();
|
|
|
|
|
this.listeners.add(fileChangeListener);
|
|
|
|
|
synchronized (this.monitor) {
|
|
|
|
|
checkNotStarted();
|
|
|
|
|
this.listeners.add(fileChangeListener);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
@ -102,8 +107,10 @@ public class FileSystemWatcher {
|
|
|
|
|
*/
|
|
|
|
|
public void addSourceFolders(Iterable<File> folders) {
|
|
|
|
|
Assert.notNull(folders, "Folders must not be null");
|
|
|
|
|
for (File folder : folders) {
|
|
|
|
|
addSourceFolder(folder);
|
|
|
|
|
synchronized (this.monitor) {
|
|
|
|
|
for (File folder : folders) {
|
|
|
|
|
addSourceFolder(folder);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -112,54 +119,49 @@ public class FileSystemWatcher {
|
|
|
|
|
* {@link #start() started}.
|
|
|
|
|
* @param folder the folder to monitor
|
|
|
|
|
*/
|
|
|
|
|
public synchronized void addSourceFolder(File folder) {
|
|
|
|
|
public void addSourceFolder(File folder) {
|
|
|
|
|
Assert.notNull(folder, "Folder must not be null");
|
|
|
|
|
Assert.isTrue(folder.isDirectory(),
|
|
|
|
|
"Folder '" + folder + "' must exist and must" + " be a directory");
|
|
|
|
|
checkNotStarted();
|
|
|
|
|
this.folders.put(folder, null);
|
|
|
|
|
synchronized (this.monitor) {
|
|
|
|
|
checkNotStarted();
|
|
|
|
|
this.folders.put(folder, null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Set an optional {@link FileFilter} used to limit the files that trigger a change.
|
|
|
|
|
* @param triggerFilter a trigger filter or null
|
|
|
|
|
*/
|
|
|
|
|
public synchronized void setTriggerFilter(FileFilter triggerFilter) {
|
|
|
|
|
this.triggerFilter = triggerFilter;
|
|
|
|
|
public void setTriggerFilter(FileFilter triggerFilter) {
|
|
|
|
|
synchronized (this.monitor) {
|
|
|
|
|
this.triggerFilter = triggerFilter;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void checkNotStarted() {
|
|
|
|
|
Assert.state(this.watchThread == null, "FileSystemWatcher already started");
|
|
|
|
|
synchronized (this.monitor) {
|
|
|
|
|
Assert.state(this.watchThread == null, "FileSystemWatcher already started");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Start monitoring the source folder for changes.
|
|
|
|
|
*/
|
|
|
|
|
public synchronized void start() {
|
|
|
|
|
saveInitialSnapshots();
|
|
|
|
|
if (this.watchThread == null) {
|
|
|
|
|
this.watchThread = new Thread() {
|
|
|
|
|
@Override
|
|
|
|
|
public void run() {
|
|
|
|
|
int remaining = FileSystemWatcher.this.remainingScans.get();
|
|
|
|
|
while (remaining > 0 || remaining == -1) {
|
|
|
|
|
try {
|
|
|
|
|
if (remaining > 0) {
|
|
|
|
|
FileSystemWatcher.this.remainingScans.decrementAndGet();
|
|
|
|
|
}
|
|
|
|
|
scan();
|
|
|
|
|
}
|
|
|
|
|
catch (InterruptedException ex) {
|
|
|
|
|
// Ignore
|
|
|
|
|
}
|
|
|
|
|
remaining = FileSystemWatcher.this.remainingScans.get();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
this.watchThread.setName("File Watcher");
|
|
|
|
|
this.watchThread.setDaemon(this.daemon);
|
|
|
|
|
this.remainingScans = new AtomicInteger(-1);
|
|
|
|
|
this.watchThread.start();
|
|
|
|
|
public void start() {
|
|
|
|
|
synchronized (this.monitor) {
|
|
|
|
|
saveInitialSnapshots();
|
|
|
|
|
if (this.watchThread == null) {
|
|
|
|
|
Map<File, FolderSnapshot> localFolders = new HashMap<File, FolderSnapshot>();
|
|
|
|
|
localFolders.putAll(this.folders);
|
|
|
|
|
this.watchThread = new Thread(new Watcher(this.remainingScans,
|
|
|
|
|
new ArrayList<FileChangeListener>(this.listeners),
|
|
|
|
|
this.triggerFilter, this.pollInterval, this.quietPeriod,
|
|
|
|
|
localFolders));
|
|
|
|
|
this.watchThread.setName("File Watcher");
|
|
|
|
|
this.watchThread.setDaemon(this.daemon);
|
|
|
|
|
this.watchThread.start();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -169,72 +171,10 @@ public class FileSystemWatcher {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void scan() throws InterruptedException {
|
|
|
|
|
Thread.sleep(this.pollInterval - this.quietPeriod);
|
|
|
|
|
Map<File, FolderSnapshot> previous;
|
|
|
|
|
Map<File, FolderSnapshot> current = this.folders;
|
|
|
|
|
do {
|
|
|
|
|
previous = current;
|
|
|
|
|
current = getCurrentSnapshots();
|
|
|
|
|
Thread.sleep(this.quietPeriod);
|
|
|
|
|
}
|
|
|
|
|
while (isDifferent(previous, current));
|
|
|
|
|
if (isDifferent(this.folders, current)) {
|
|
|
|
|
updateSnapshots(current.values());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean isDifferent(Map<File, FolderSnapshot> previous,
|
|
|
|
|
Map<File, FolderSnapshot> current) {
|
|
|
|
|
if (!previous.keySet().equals(current.keySet())) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
for (Map.Entry<File, FolderSnapshot> entry : previous.entrySet()) {
|
|
|
|
|
FolderSnapshot previousFolder = entry.getValue();
|
|
|
|
|
FolderSnapshot currentFolder = current.get(entry.getKey());
|
|
|
|
|
if (!previousFolder.equals(currentFolder, this.triggerFilter)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Map<File, FolderSnapshot> getCurrentSnapshots() {
|
|
|
|
|
Map<File, FolderSnapshot> snapshots = new LinkedHashMap<File, FolderSnapshot>();
|
|
|
|
|
for (File folder : this.folders.keySet()) {
|
|
|
|
|
snapshots.put(folder, new FolderSnapshot(folder));
|
|
|
|
|
}
|
|
|
|
|
return snapshots;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void updateSnapshots(Collection<FolderSnapshot> snapshots) {
|
|
|
|
|
Map<File, FolderSnapshot> updated = new LinkedHashMap<File, FolderSnapshot>();
|
|
|
|
|
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
|
|
|
|
|
for (FolderSnapshot snapshot : snapshots) {
|
|
|
|
|
FolderSnapshot previous = this.folders.get(snapshot.getFolder());
|
|
|
|
|
updated.put(snapshot.getFolder(), snapshot);
|
|
|
|
|
ChangedFiles changedFiles = previous.getChangedFiles(snapshot,
|
|
|
|
|
this.triggerFilter);
|
|
|
|
|
if (!changedFiles.getFiles().isEmpty()) {
|
|
|
|
|
changeSet.add(changedFiles);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!changeSet.isEmpty()) {
|
|
|
|
|
fireListeners(Collections.unmodifiableSet(changeSet));
|
|
|
|
|
}
|
|
|
|
|
this.folders = updated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void fireListeners(Set<ChangedFiles> changeSet) {
|
|
|
|
|
for (FileChangeListener listener : this.listeners) {
|
|
|
|
|
listener.onChange(changeSet);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Stop monitoring the source folders.
|
|
|
|
|
*/
|
|
|
|
|
public synchronized void stop() {
|
|
|
|
|
public void stop() {
|
|
|
|
|
stopAfter(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@ -242,23 +182,131 @@ public class FileSystemWatcher {
|
|
|
|
|
* Stop monitoring the source folders.
|
|
|
|
|
* @param remainingScans the number of remaining scans
|
|
|
|
|
*/
|
|
|
|
|
synchronized void stopAfter(int remainingScans) {
|
|
|
|
|
Thread thread = this.watchThread;
|
|
|
|
|
if (thread != null) {
|
|
|
|
|
this.remainingScans.set(remainingScans);
|
|
|
|
|
if (remainingScans <= 0) {
|
|
|
|
|
thread.interrupt();
|
|
|
|
|
void stopAfter(int remainingScans) {
|
|
|
|
|
synchronized (this.monitor) {
|
|
|
|
|
Thread thread = this.watchThread;
|
|
|
|
|
if (thread != null) {
|
|
|
|
|
this.remainingScans.set(remainingScans);
|
|
|
|
|
if (remainingScans <= 0) {
|
|
|
|
|
thread.interrupt();
|
|
|
|
|
}
|
|
|
|
|
if (Thread.currentThread() != thread) {
|
|
|
|
|
try {
|
|
|
|
|
thread.join();
|
|
|
|
|
}
|
|
|
|
|
catch (InterruptedException ex) {
|
|
|
|
|
Thread.currentThread().interrupt();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.watchThread = null;
|
|
|
|
|
}
|
|
|
|
|
if (Thread.currentThread() != thread) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static final class Watcher implements Runnable {
|
|
|
|
|
|
|
|
|
|
private final AtomicInteger remainingScans;
|
|
|
|
|
|
|
|
|
|
private final List<FileChangeListener> listeners;
|
|
|
|
|
|
|
|
|
|
private final FileFilter triggerFilter;
|
|
|
|
|
|
|
|
|
|
private final long pollInterval;
|
|
|
|
|
|
|
|
|
|
private final long quietPeriod;
|
|
|
|
|
|
|
|
|
|
private Map<File, FolderSnapshot> folders;
|
|
|
|
|
|
|
|
|
|
private Watcher(AtomicInteger remainingScans, List<FileChangeListener> listeners,
|
|
|
|
|
FileFilter triggerFilter, long pollInterval, long quietPeriod,
|
|
|
|
|
Map<File, FolderSnapshot> folders) {
|
|
|
|
|
this.remainingScans = remainingScans;
|
|
|
|
|
this.listeners = listeners;
|
|
|
|
|
this.triggerFilter = triggerFilter;
|
|
|
|
|
this.pollInterval = pollInterval;
|
|
|
|
|
this.quietPeriod = quietPeriod;
|
|
|
|
|
this.folders = folders;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void run() {
|
|
|
|
|
int remainingScans = this.remainingScans.get();
|
|
|
|
|
while (remainingScans > 0 || remainingScans == -1) {
|
|
|
|
|
try {
|
|
|
|
|
thread.join();
|
|
|
|
|
if (remainingScans > 0) {
|
|
|
|
|
this.remainingScans.decrementAndGet();
|
|
|
|
|
}
|
|
|
|
|
scan();
|
|
|
|
|
}
|
|
|
|
|
catch (InterruptedException ex) {
|
|
|
|
|
Thread.currentThread().interrupt();
|
|
|
|
|
// Ignore
|
|
|
|
|
}
|
|
|
|
|
remainingScans = this.remainingScans.get();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private void scan() throws InterruptedException {
|
|
|
|
|
Thread.sleep(this.pollInterval - this.quietPeriod);
|
|
|
|
|
Map<File, FolderSnapshot> previous;
|
|
|
|
|
Map<File, FolderSnapshot> current = this.folders;
|
|
|
|
|
do {
|
|
|
|
|
previous = current;
|
|
|
|
|
current = getCurrentSnapshots();
|
|
|
|
|
Thread.sleep(this.quietPeriod);
|
|
|
|
|
}
|
|
|
|
|
while (isDifferent(previous, current));
|
|
|
|
|
if (isDifferent(this.folders, current)) {
|
|
|
|
|
updateSnapshots(current.values());
|
|
|
|
|
}
|
|
|
|
|
this.watchThread = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private boolean isDifferent(Map<File, FolderSnapshot> previous,
|
|
|
|
|
Map<File, FolderSnapshot> current) {
|
|
|
|
|
if (!previous.keySet().equals(current.keySet())) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
for (Map.Entry<File, FolderSnapshot> entry : previous.entrySet()) {
|
|
|
|
|
FolderSnapshot previousFolder = entry.getValue();
|
|
|
|
|
FolderSnapshot currentFolder = current.get(entry.getKey());
|
|
|
|
|
if (!previousFolder.equals(currentFolder, this.triggerFilter)) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Map<File, FolderSnapshot> getCurrentSnapshots() {
|
|
|
|
|
Map<File, FolderSnapshot> snapshots = new LinkedHashMap<File, FolderSnapshot>();
|
|
|
|
|
for (File folder : this.folders.keySet()) {
|
|
|
|
|
snapshots.put(folder, new FolderSnapshot(folder));
|
|
|
|
|
}
|
|
|
|
|
return snapshots;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void updateSnapshots(Collection<FolderSnapshot> snapshots) {
|
|
|
|
|
Map<File, FolderSnapshot> updated = new LinkedHashMap<File, FolderSnapshot>();
|
|
|
|
|
Set<ChangedFiles> changeSet = new LinkedHashSet<ChangedFiles>();
|
|
|
|
|
for (FolderSnapshot snapshot : snapshots) {
|
|
|
|
|
FolderSnapshot previous = this.folders.get(snapshot.getFolder());
|
|
|
|
|
updated.put(snapshot.getFolder(), snapshot);
|
|
|
|
|
ChangedFiles changedFiles = previous.getChangedFiles(snapshot,
|
|
|
|
|
this.triggerFilter);
|
|
|
|
|
if (!changedFiles.getFiles().isEmpty()) {
|
|
|
|
|
changeSet.add(changedFiles);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!changeSet.isEmpty()) {
|
|
|
|
|
fireListeners(Collections.unmodifiableSet(changeSet));
|
|
|
|
|
}
|
|
|
|
|
this.folders = updated;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void fireListeners(Set<ChangedFiles> changeSet) {
|
|
|
|
|
for (FileChangeListener listener : this.listeners) {
|
|
|
|
|
listener.onChange(changeSet);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|