/**
* Copyright 2015 Netflix, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is
* distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See
* the License for the specific language governing permissions and limitations under the License.
*/
package rx.fileutils;
import com.barbarysoftware.watchservice.MacOSXWatchServiceFactory;
import com.barbarysoftware.watchservice.WatchableFile;
import com.sun.nio.file.SensitivityWatchEventModifier;
import rx.Observable;
import rx.Scheduler;
import rx.Subscriber;
import rx.schedulers.Schedulers;
import java.io.IOException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.*;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
public final class FileSystemWatcher {
static final boolean IS_MAC;
static {
String os = System.getProperty("os.name").toLowerCase();
IS_MAC = os.contains("mac");
}
private FileSystemWatcher() {}
private static class FileSystemEventOnSubscribe implements Observable.OnSubscribe<FileSystemEvent> {
private WatchService watcher;
private Scheduler scheduler;
private volatile boolean close = false;
private final Set<Path> watchedPaths;
public FileSystemEventOnSubscribe(
Map<Path, FileSystemEventKind[]> paths,
Scheduler scheduler,
boolean scanCurrentFs
) {
watchedPaths = new HashSet<Path>();
if (scanCurrentFs) {
paths.forEach((path, kind) -> {
if (Arrays.asList(kind).contains(FileSystemEventKind.ENTRY_CREATE)) {
watchedPaths.add(path);
}
});
}
try {
if (IS_MAC) {
this.watcher = MacOSXWatchServiceFactory.newWatchService();
for (Path path : paths.keySet()) {
final WatchableFile watchableFile = new WatchableFile(path);
FileSystemEventKind[] kinds = paths.get(path);
watchableFile.register(watcher, FileSystemEventKind.toWatchEventKinds(kinds));
}
}
else {
this.watcher = FileSystems.getDefault().newWatchService();
for (Path path : paths.keySet()) {
FileSystemEventKind[] kinds = paths.get(path);
path.register(watcher, FileSystemEventKind.toWatchEventKinds(kinds), SensitivityWatchEventModifier.HIGH);
}
}
this.scheduler = scheduler;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public FileSystemEventOnSubscribe(Map<Path, FileSystemEventKind[]> paths, Scheduler scheduler) {
this(paths, scheduler, false);
}
@Override
public void call(Subscriber<? super FileSystemEvent> subscriber) {
// scan the watchedPaths and trigger fake ENTRY_CREATE events
watchedPaths.forEach(path -> {
getEventsForCurrentFiles(path).forEach(event -> {
FileSystemEvent fileSystemEvent = new FileSystemEvent(event);
subscriber.onNext(fileSystemEvent);
});
});
Scheduler.Worker worker = scheduler.createWorker();
subscriber.add(worker);
worker.schedule(() -> {
do {
try {
WatchKey key = watcher.take();
if (key == null) {
continue;
}
for (WatchEvent<?> event : key.pollEvents()) {
FileSystemEvent fileSystemEvent = new FileSystemEvent(event);
subscriber.onNext(fileSystemEvent);
}
if (!key.reset()) {
close();
}
} catch (Throwable t) {
subscriber.onError(t);
}
} while (!close);
subscriber.onCompleted();
});
}
public void close() {
this.close = true;
try {
watcher.close();
} catch (Exception e) {}
}
/**
* Return fake ENTRY_CREATE events for the current files.
* This simplify the code by treating current files the same way as new files.
*/
private List<WatchEvent<Path>> getEventsForCurrentFiles(Path directory) {
final List<WatchEvent<Path>> events = new ArrayList<>();
try {
Files.walkFileTree(directory, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
events.add(pathToWatchEvent(path));
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult preVisitDirectory(Path path, BasicFileAttributes attrs) {
events.add(pathToWatchEvent(path));
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
e.printStackTrace();
}
return events;
}
private WatchEvent<Path> pathToWatchEvent(Path path) {
return new WatchEvent<Path>() {
@Override
public Kind<Path> kind() {
return ENTRY_CREATE;
}
@Override
public int count() {
return 1;
}
@Override
public Path context() {
return path;
}
};
}
}
public static class Builder {
private Map<Path, FileSystemEventKind[]> paths = new HashMap<>();
private Scheduler scheduler = Schedulers.newThread();
private boolean scanCurrentFS = false;
Builder() {}
public Builder addPath(Path path, FileSystemEventKind... kinds) {
paths.put(path, kinds);
return this;
}
public Builder addPaths(Map<Path, FileSystemEventKind[]> paths) {
this.paths.putAll(paths);
return this;
}
public Builder withScheduler(Scheduler scheduler) {
this.scheduler = scheduler;
return this;
}
public Builder withCurrentFsScanning(boolean enable) {
this.scanCurrentFS = enable;
return this;
}
public Observable<FileSystemEvent> build() {
try {
FileSystemEventOnSubscribe fileSystemEventOnSubscribe
= new FileSystemEventOnSubscribe(paths, scheduler, scanCurrentFS);
Observable<FileSystemEvent> fileSystemEventObservable
= Observable.create(fileSystemEventOnSubscribe);
fileSystemEventObservable
.doOnUnsubscribe(fileSystemEventOnSubscribe::close);
return fileSystemEventObservable;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public static Builder newBuilder() {
return new Builder();
}
}