package com.barbarysoftware.watchservice;
import com.barbarysoftware.jna.*;
import com.sun.jna.NativeLong;
import com.sun.jna.Pointer;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* This class contains the bulk of my implementation of the Watch Service API. It hooks into Carbon's
* File System Events API.
*
* @author Steve McLeod
*/
class MacOSXListeningWatchService extends AbstractWatchService {
// need to keep reference to callbacks to prevent garbage collection
@SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
private final List<CarbonAPI.FSEventStreamCallback> callbackList = new ArrayList<CarbonAPI.FSEventStreamCallback>();
private final List<CFRunLoopThread> threadList = new ArrayList<CFRunLoopThread>();
@Override
WatchKey register(WatchableFile watchableFile, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifers) throws IOException {
final File file = watchableFile.getFile();
final Map<File, Long> lastModifiedMap = createLastModifiedMap(file);
final String s = file.getAbsolutePath();
final Pointer[] values = {CFStringRef.toCFString(s).getPointer()};
final CFArrayRef pathsToWatch = CarbonAPI.INSTANCE.CFArrayCreate(null, values, CFIndex.valueOf(1), null);
final MacOSXWatchKey watchKey = new MacOSXWatchKey(this, events);
final double latency = 1.0; /* Latency in seconds */
final long kFSEventStreamEventIdSinceNow = -1; // this is 0xFFFFFFFFFFFFFFFF
final int kFSEventStreamCreateFlagNoDefer = 0x00000002;
final CarbonAPI.FSEventStreamCallback callback = new MacOSXListeningCallback(watchKey, lastModifiedMap);
callbackList.add(callback);
final FSEventStreamRef stream = CarbonAPI.INSTANCE.FSEventStreamCreate(
Pointer.NULL,
callback,
Pointer.NULL,
pathsToWatch,
kFSEventStreamEventIdSinceNow,
latency,
kFSEventStreamCreateFlagNoDefer);
final CFRunLoopThread thread = new CFRunLoopThread(stream, file);
thread.setDaemon(true);
thread.start();
threadList.add(thread);
return watchKey;
}
public static class CFRunLoopThread extends Thread {
private final FSEventStreamRef streamRef;
private CFRunLoopRef runLoop;
public CFRunLoopThread(FSEventStreamRef streamRef, File file) {
super("WatchService for " + file);
this.streamRef = streamRef;
}
@Override
public void run() {
runLoop = CarbonAPI.INSTANCE.CFRunLoopGetCurrent();
final CFStringRef runLoopMode = CFStringRef.toCFString("kCFRunLoopDefaultMode");
CarbonAPI.INSTANCE.FSEventStreamScheduleWithRunLoop(streamRef, runLoop, runLoopMode);
CarbonAPI.INSTANCE.FSEventStreamStart(streamRef);
CarbonAPI.INSTANCE.CFRunLoopRun();
}
public CFRunLoopRef getRunLoop() {
return runLoop;
}
public FSEventStreamRef getStreamRef() {
return streamRef;
}
}
private Map<File, Long> createLastModifiedMap(File file) {
Map<File, Long> lastModifiedMap = new ConcurrentHashMap<File, Long>();
for (File child : recursiveListFiles(file)) {
lastModifiedMap.put(child, child.lastModified());
}
return lastModifiedMap;
}
private static Set<File> recursiveListFiles(File file) {
Set<File> files = new HashSet<File>();
files.add(file.getAbsoluteFile());
if (file.isDirectory()) {
for (File child : file.listFiles()) {
files.addAll(recursiveListFiles(child));
}
}
return files;
}
@Override
void implClose() throws IOException {
for (CFRunLoopThread thread : threadList) {
CarbonAPI.INSTANCE.CFRunLoopStop(thread.getRunLoop());
CarbonAPI.INSTANCE.FSEventStreamStop(thread.getStreamRef());
}
threadList.clear();
callbackList.clear();
}
private static class MacOSXListeningCallback implements CarbonAPI.FSEventStreamCallback {
private final MacOSXWatchKey watchKey;
private final Map<File, Long> lastModifiedMap;
private MacOSXListeningCallback(MacOSXWatchKey watchKey, Map<File, Long> lastModifiedMap) {
this.watchKey = watchKey;
this.lastModifiedMap = lastModifiedMap;
}
public void invoke(FSEventStreamRef streamRef, Pointer clientCallBackInfo, NativeLong numEvents, Pointer eventPaths, Pointer /* array of unsigned int */ eventFlags, /* array of unsigned long */ Pointer eventIds) {
final int length = numEvents.intValue();
for (String folderName : eventPaths.getStringArray(0, length)) {
final Set<File> filesOnDisk = recursiveListFiles(new File(folderName));
final List<File> createdFiles = findCreatedFiles(filesOnDisk);
final List<File> modifiedFiles = findModifiedFiles(filesOnDisk);
final List<File> deletedFiles = findDeletedFiles(folderName, filesOnDisk);
for (File file : createdFiles) {
if (watchKey.isReportCreateEvents()) {
watchKey.signalEvent(StandardWatchEventKind.ENTRY_CREATE, file);
}
lastModifiedMap.put(file, file.lastModified());
}
for (File file : modifiedFiles) {
if (watchKey.isReportModifyEvents()) {
watchKey.signalEvent(StandardWatchEventKind.ENTRY_MODIFY, file);
}
lastModifiedMap.put(file, file.lastModified());
}
for (File file : deletedFiles) {
if (watchKey.isReportDeleteEvents()) {
watchKey.signalEvent(StandardWatchEventKind.ENTRY_DELETE, file);
}
lastModifiedMap.remove(file);
}
}
}
private List<File> findModifiedFiles(Set<File> filesOnDisk) {
List<File> modifiedFileList = new ArrayList<File>();
for (File file : filesOnDisk) {
final Long lastModified = lastModifiedMap.get(file);
if (lastModified != null && lastModified != file.lastModified()) {
modifiedFileList.add(file);
}
}
return modifiedFileList;
}
private List<File> findCreatedFiles(Set<File> filesOnDisk) {
List<File> createdFileList = new ArrayList<File>();
for (File file : filesOnDisk) {
if (!lastModifiedMap.containsKey(file)) {
createdFileList.add(file);
}
}
return createdFileList;
}
private List<File> findDeletedFiles(String folderName, Set<File> filesOnDisk) {
List<File> deletedFileList = new ArrayList<File>();
for (File file : lastModifiedMap.keySet()) {
if (file.getAbsolutePath().startsWith(folderName) && !filesOnDisk.contains(file)) {
deletedFileList.add(file);
}
}
return deletedFileList;
}
}
}