blob: b6de977be6f5f4c20614b8ca04e8c7524babead0 [file] [log] [blame]
/*
* 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.apache.felix.fileinstall.internal;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.FileVisitor;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.PathMatcher;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static java.nio.file.LinkOption.NOFOLLOW_LINKS;
import static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE;
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;
/**
* A File watching service
*/
public abstract class Watcher implements Closeable {
private Path root;
private boolean watch = true;
private WatchService watcher;
private PathMatcher dirMatcher;
private PathMatcher fileMatcher;
private final Map<WatchKey, Path> keys = new ConcurrentHashMap<WatchKey, Path>();
private volatile long lastModified;
private final Map<Path, Boolean> processedMap = new ConcurrentHashMap<Path, Boolean>();
public void init() throws IOException {
if (root == null) {
Iterable<Path> rootDirectories = getFileSystem().getRootDirectories();
for (Path rootDirectory : rootDirectories) {
if (rootDirectory != null) {
root = rootDirectory;
break;
}
}
}
if (!Files.exists(root)) {
fail("Root path does not exist: " + root);
} else if (!Files.isDirectory(root)) {
fail("Root path is not a directory: " + root);
}
if (watcher == null) {
watcher = watch ? getFileSystem().newWatchService() : null;
}
}
public void close() throws IOException {
if (watcher != null) {
watcher.close();
}
}
public long getLastModified() {
return lastModified;
}
// Properties
//-------------------------------------------------------------------------
public void setRootPath(String rootPath) {
Path path = new File(rootPath).getAbsoluteFile().toPath();
setRoot(path);
}
public void setRootDirectory(File directory) {
setRoot(directory.toPath());
}
public Path getRoot() {
return root;
}
public void setRoot(Path root) {
this.root = root;
}
public boolean isWatch() {
return watch;
}
public void setWatch(boolean watch) {
this.watch = watch;
}
public WatchService getWatcher() {
return watcher;
}
public void setWatcher(WatchService watcher) {
this.watcher = watcher;
}
public PathMatcher getDirMatcher() {
return dirMatcher;
}
public void setDirMatcher(PathMatcher dirMatcher) {
this.dirMatcher = dirMatcher;
}
public PathMatcher getFileMatcher() {
return fileMatcher;
}
public void setFileMatcher(PathMatcher fileMatcher) {
this.fileMatcher = fileMatcher;
}
// Implementation methods
//-------------------------------------------------------------------------
public void rescan() throws IOException {
for (WatchKey key : keys.keySet()) {
key.cancel();
}
keys.clear();
Files.walkFileTree(root,
EnumSet.of(FileVisitOption.FOLLOW_LINKS),
Integer.MAX_VALUE,
new FilteringFileVisitor());
}
public void processEvents() {
while (true) {
WatchKey key = watcher.poll();
if (key == null) {
break;
}
Path dir = keys.get(key);
if (dir == null) {
warn("Could not find key for " + key);
continue;
}
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind kind = event.kind();
WatchEvent<Path> ev = (WatchEvent<Path>)event;
// Context for directory entry event is the file name of entry
Path name = ev.context();
Path child = dir.resolve(name);
debug("Processing event {} on path {}", kind, child);
if (kind == OVERFLOW) {
// rescan();
continue;
}
try {
if (kind == ENTRY_CREATE) {
if (Files.isDirectory(child)) {
// if directory is created, and watching recursively, then
// register it and its sub-directories
Files.walkFileTree(child, new FilteringFileVisitor());
} else if (Files.isRegularFile(child)) {
scan(child);
}
} else if (kind == ENTRY_MODIFY) {
if (Files.isRegularFile(child)) {
scan(child);
}
} else if (kind == ENTRY_DELETE) {
unscan(child);
}
} catch (IOException x) {
// ignore to keep sample readbale
x.printStackTrace();
}
}
// reset key and remove from set if directory no longer accessible
boolean valid = key.reset();
if (!valid) {
debug("Removing key " + key + " and dir " + dir + " from keys");
keys.remove(key);
// all directories are inaccessible
if (keys.isEmpty()) {
break;
}
}
}
}
private void scan(final Path file) throws IOException {
if (isMatchesFile(file)) {
process(file);
processedMap.put(file, Boolean.TRUE);
}
}
protected boolean isMatchesFile(Path file) {
boolean matches = true;
if (fileMatcher != null) {
Path rel = root.relativize(file);
matches = fileMatcher.matches(rel);
}
return matches;
}
private void unscan(final Path file) throws IOException {
if (isMatchesFile(file)) {
onRemove(file);
lastModified = System.currentTimeMillis();
} else {
// lets find all the files that now no longer exist
List<Path> files = new ArrayList<Path>(processedMap.keySet());
for (Path path : files) {
if (!Files.exists(path)) {
debug("File has been deleted: " + path);
processedMap.remove(path);
if (isMatchesFile(path)) {
onRemove(file);
lastModified = System.currentTimeMillis();
}
}
}
}
}
private void watch(final Path path) throws IOException {
if (watcher != null) {
WatchKey key = path.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
keys.put(key, path);
debug("Watched path " + path + " key " + key);
} else {
warn("No watcher yet for path " + path);
}
}
protected FileSystem getFileSystem() {
return FileSystems.getDefault();
}
public class FilteringFileVisitor implements FileVisitor<Path> {
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
if (Thread.interrupted()) {
throw new InterruptedIOException();
}
if (dirMatcher != null) {
Path rel = root.relativize(dir);
if (!"".equals(rel.toString()) && !dirMatcher.matches(rel)) {
return FileVisitResult.SKIP_SUBTREE;
}
}
watch(dir);
return FileVisitResult.CONTINUE;
}
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (Thread.interrupted()) {
throw new InterruptedIOException();
}
scan(file);
return FileVisitResult.CONTINUE;
}
public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
return FileVisitResult.CONTINUE;
}
}
/**
* Throws an invalid argument exception after logging a warning
* just in case the stack trace gets gobbled up by application containers
* like spring or blueprint, at least the error message will be clearly shown in the log
*
*/
public void fail(String message) {
warn(message);
throw new IllegalArgumentException(message);
}
protected abstract void debug(String message, Object... args);
protected abstract void warn(String message, Object... args);
protected abstract void process(Path path);
protected abstract void onRemove(Path path);
}