/** * Copyright 2014 Eediom 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 org.araqne.log.api.nio; import java.io.File; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.FileSystems; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributes; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * @author xeraph */ public class FileEventWatcher { private final Logger slog = LoggerFactory.getLogger(FileEventWatcher.class); private static final Kind<?>[] EVENTS = new Kind[] { StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE }; private final Pattern fileNamePattern; private final boolean recursive; private Matcher fileNameMatcher; private String basePath; private WatchService ws; private FileSystem fs; private Path root; private Map<String, WatchItem> watchPaths = new HashMap<String, WatchItem>(); private Set<FileEventListener> listeners = new HashSet<FileEventListener>(); public FileEventWatcher(String basePath, Pattern fileNamePattern, boolean recursive) throws IOException { this.basePath = basePath; this.fileNamePattern = fileNamePattern; this.recursive = recursive; this.fs = FileSystems.getDefault(); this.root = fs.getPath(basePath); this.ws = fs.newWatchService(); if (fileNamePattern != null) fileNameMatcher = fileNamePattern.matcher(""); Files.walkFileTree(root, new DirectoryRegister()); } public void poll(int millis) throws IOException { // base directory is removed and can be regenerated if (!watchPaths.containsKey(root.toFile().getAbsolutePath())) { try { ws.close(); } catch (IOException e) { } this.ws = fs.newWatchService(); Files.walkFileTree(root, new DirectoryRegister()); } WatchKey wk = null; try { while ((wk = ws.poll(millis, TimeUnit.MILLISECONDS)) != null) { for (WatchEvent<?> evt : wk.pollEvents()) { if (slog.isDebugEnabled()) slog.debug("araqne-logapi-nio: watchable [{}] context [{}] kind [{}] valid [{}]", new Object[] { wk.watchable(), evt.context(), evt.kind(), wk.isValid() }); Path p = fs.getPath(wk.watchable().toString(), evt.context().toString()); File f = p.toFile(); if (evt.kind().equals(StandardWatchEventKinds.ENTRY_CREATE)) { if (Files.isDirectory(p)) { try { WatchKey newKey = p.register(ws, EVENTS); watchPaths.put(f.getAbsolutePath(), new WatchItem(newKey)); slog.debug("araqne-logapi-nio: adding watch path [{}]", f.getAbsolutePath()); Files.walkFileTree(p, new DirectoryRegister()); } catch (IOException e) { slog.error("araqne-logapi-nio: failed to watching directory [{}]", f.getAbsolutePath()); } } else { if (slog.isDebugEnabled()) slog.debug("araqne-logapi-nio: path [{}] is not directory", f.getAbsolutePath()); } if (isTargetFile(f)) { WatchItem item = watchPaths.get(f.getParent()); item.files.add(f.getName()); for (FileEventListener listener : listeners) { try { listener.onCreate(f); } catch (Throwable t) { slog.warn("araqne-logapi-nio: file event listener should not throw any exception", t); } } } } else if (evt.kind().equals(StandardWatchEventKinds.ENTRY_MODIFY)) { if (slog.isDebugEnabled()) slog.debug("araqne-logapi-nio: modified path [{}]", f.getAbsolutePath()); if (isTargetFile(f)) { for (FileEventListener listener : listeners) { try { listener.onModify(f); } catch (Throwable t) { slog.warn("araqne-logapi-nio: file event listener should not throw any exception", t); } } } } else if (evt.kind().equals(StandardWatchEventKinds.ENTRY_DELETE)) { slog.debug("araqne-logapi-nio: checking remove target path [{}]", f.getAbsolutePath()); if (watchPaths.containsKey(f.getAbsolutePath())) { invokeUnregisterRecursively(f.getAbsolutePath()); } else if (isTargetFile(f, true)) { slog.debug("araqne-logapi-nio: checking remove target path [{}]", f.getAbsolutePath()); WatchItem item = watchPaths.get(f.getParent()); if (item != null) { item.files.remove(f.getName()); } else { slog.debug("araqne-logapi-nio: item not found for [{}]", f.getAbsolutePath()); } for (FileEventListener listener : listeners) { try { listener.onDelete(f); } catch (Throwable t) { slog.warn("araqne-logapi-nio: file event listener should not throw any exception", t); } } } } } wk.reset(); } } catch (InterruptedException e) { } } private void invokeUnregisterRecursively(String path) { Set<File> files = new HashSet<File>(); for (String dir : new ArrayList<String>(watchPaths.keySet())) { if (dir.startsWith(path)) { WatchItem item = watchPaths.remove(dir); item.key.cancel(); slog.debug("araqne-logapi-nio: cancel watch path [{}]", dir); for (String file : item.files) { files.add(new File(dir, file)); } } } for (File f : files) { for (FileEventListener listener : listeners) { try { listener.onDelete(f); } catch (Throwable t) { slog.warn("araqne-logapi-nio: file event listener should not throw any exception", t); } } } } private boolean isTargetFile(File f) { return isTargetFile(f, false); } private boolean isTargetFile(File f, boolean noFileCheck) { if (fileNameMatcher == null) return noFileCheck || f.isFile(); fileNameMatcher.reset(f.getName()); return (noFileCheck || f.isFile()) && fileNameMatcher.matches(); } public void addListener(FileEventListener listener) { listeners.add(listener); } public void removeListener(FileEventListener listener) { listeners.remove(listener); } public void close() { try { ws.close(); } catch (Throwable t) { } try { fs.close(); } catch (Throwable t) { } } private class DirectoryRegister implements FileVisitor<Path> { @Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { if (!recursive && !dir.equals(root)) return FileVisitResult.SKIP_SUBTREE; WatchKey wk = dir.register(ws, EVENTS); watchPaths.put(dir.toFile().getAbsolutePath(), new WatchItem(wk)); return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { File f = file.toFile(); if (isTargetFile(f)) { WatchItem item = watchPaths.get(f.getParent()); item.files.add(f.getName()); for (FileEventListener listener : listeners) { try { listener.onCreate(f); } catch (Throwable t) { slog.warn("araqne-logapi-nio: file event listener should not throw any exception", t); } } } return FileVisitResult.CONTINUE; } @Override public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { return FileVisitResult.CONTINUE; } } private class WatchItem { private WatchKey key; private Set<String> files = new HashSet<String>(); public WatchItem(WatchKey key) { this.key = key; } } @Override public String toString() { return "file watcher: base path [" + basePath + "], recursive [" + recursive + "], file name pattern [" + fileNamePattern + "]"; } }