| /* |
| * 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); |
| } |