/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.jooby.filewatcher;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
import java.io.IOException;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
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.WatchEvent.Modifier;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.inject.Inject;
import org.jooby.Env;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.inject.Injector;
class FileMonitor implements Runnable {
/** The logging system. */
private final Logger log = LoggerFactory.getLogger(getClass());
private final Injector injector;
private final WatchService watcher;
private final Map<WatchKey, Path> keys = new ConcurrentHashMap<>();
private final Map<Path, FileEventOptions> options = new ConcurrentHashMap<>();
private final Set<FileEventOptions> optionList;
@Inject
public FileMonitor(final Injector injector, final Env env,
final WatchService watcher, final Set<FileEventOptions> optionList) throws IOException {
this.injector = injector;
this.watcher = watcher;
this.optionList = optionList;
// start monitor:
ExecutorService monitor = Executors.newSingleThreadExecutor(task -> {
Thread thread = new Thread(task, "file-watcher");
thread.setDaemon(true);
return thread;
});
env.onStop(monitor::shutdown);
monitor.execute(this);
}
@Override
public void run() {
// register directory:
for (FileEventOptions options : optionList) {
log.debug("watching: {}", options);
try {
registerTree(options.path(), options);
} catch (IOException x) {
log.error("traversal of {} resulted in exception", options.path(), x);
}
}
boolean process = true;
while (process) {
try {
process = poll(watcher.take());
} catch (InterruptedException x) {
Thread.currentThread().interrupt();
process = false;
}
}
}
@SuppressWarnings("unchecked")
private boolean poll(final WatchKey key) {
Path source = keys.get(key);
if (source == null) {
log.debug("ignoring key {}", key);
return true;
}
for (WatchEvent<?> event : key.pollEvents()) {
Kind<?> kind = event.kind();
if (kind != OVERFLOW) {
// Context for directory entry event is the file name of entry
Path path = source.resolve((Path) event.context());
log.debug("found {}({})", path, kind);
// handler
FileEventOptions options = this.options.get(source);
PathMatcher filter = options.filter();
if (filter.matches(path)) {
FileEventHandler handler = options.handler(injector::getInstance);
try {
handler.handle((Kind<Path>) kind, path);
} catch (IOException x) {
log.debug("execution of {}({}) resulted in exception", path, kind, x);
}
} else {
log.debug("ignoring {}({})", kind, path);
}
// if directory is created, and watching recursively, then
// register it and its sub-directories
if (options.recursive() && kind == ENTRY_CREATE && Files.isDirectory(path)) {
try {
registerTree(path, options);
} catch (IOException x) {
log.debug("execution of {}({}) resulted in exception", path, kind, x);
}
}
} else {
log.trace("execution of {} resulted in {}", event.context(), kind);
}
}
// reset key and remove from set if directory no longer accessible
if (!key.reset()) {
keys.remove(key);
}
return !keys.isEmpty();
}
private void register(final Path path, final FileEventOptions options) throws IOException {
Kind<?>[] kinds = options.kinds();
Modifier modifier = options.modifier();
WatchKey key = path.register(watcher, kinds, modifier);
this.keys.put(key, path);
this.options.put(path, options);
}
private void registerTree(final Path path, final FileEventOptions options) throws IOException {
if (options.recursive()) {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs)
throws IOException {
register(dir, options);
return FileVisitResult.CONTINUE;
}
});
} else {
register(path, options);
}
}
}