package org.smartly.commons.io; import org.smartly.commons.logging.Level; import org.smartly.commons.logging.Logger; import org.smartly.commons.logging.util.LoggingUtils; import org.smartly.commons.util.FormatUtils; import org.smartly.commons.util.PathUtils; import java.io.IOException; import java.lang.ref.WeakReference; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; import java.util.*; import static java.nio.file.LinkOption.NOFOLLOW_LINKS; import static java.nio.file.StandardWatchEventKinds.*; /** * Simple file observer. * <p/> * This FileObserver works like Android FileObserver * <p/> * Monitors files to fire an event after files are accessed or changed * by any process on the device (including this one). * FileObserver is an abstract class; subclasses must implement the event handler onEvent(int, String). * Each FileObserver instance monitors a single file or directory. * If a directory is monitored as 'recursive', events will be triggered for all files and subdirectories (recursively) * inside the monitored directory. * An event mask is used to specify which changes or actions to report. * Event type constants are used to describe the possible changes in the event mask as well as what actually * happened in event callbacks. */ public class FileObserver { public static final int EVENT_MODIFY = 0x00000002; /* File was modified */ public static final int EVENT_CREATE = 0x00000100; /* Subfile was created */ public static final int EVENT_DELETE = 0x00000200; /* Subfile was deleted */ public static final int ALL_EVENTS = EVENT_MODIFY | EVENT_DELETE | EVENT_CREATE; // instance private final String _path; private final boolean _isDirectory; private final Path _observedPath; private final boolean _recursive; private final boolean _verbose; private final int _mask; private final Set<WatchKey> _keys; private final IFileObserverListener _listener; private boolean _paused; public FileObserver(final String path, final IFileObserverListener listener) { this(path, false, false, ALL_EVENTS, listener); } public FileObserver(final String path, final boolean recursive, final boolean verbose, final IFileObserverListener listener) { this(path, recursive, verbose, ALL_EVENTS, listener); } public FileObserver(final String path, final boolean recursive, final boolean verbose, final int mask, final IFileObserverListener listener) { _paused = false; _recursive = recursive; _verbose = verbose; _path = PathUtils.toUnixPath(path); if (PathUtils.isDirectory(_path)) { _observedPath = Paths.get(_path); _isDirectory = true; } else { _observedPath = Paths.get(PathUtils.getParent(_path)); _isDirectory = false; } _mask = mask; _keys = new HashSet<WatchKey>(); _listener = listener; } protected void finalize() throws Throwable { try { stopWatching(); } finally { super.finalize(); } } @Override public String toString() { final StringBuilder result = new StringBuilder(); result.append(this.getClass().getName()).append("{"); result.append("path: ").append(_path); result.append(", "); result.append("observed_path: ").append(_observedPath); result.append(", "); result.append("recursive: ").append(_recursive); result.append(", "); result.append("verbose: ").append(_verbose); // events result.append("events: ["); if ((_mask & EVENT_CREATE) == EVENT_CREATE) { result.append(eventToString(EVENT_CREATE)); } if ((_mask & EVENT_DELETE) == EVENT_DELETE) { result.append(eventToString(EVENT_DELETE)); } if ((_mask & EVENT_MODIFY) == EVENT_MODIFY) { result.append(eventToString(EVENT_MODIFY)); } result.append("]"); result.append("}"); return result.toString(); } public final boolean isDirectory() { return _isDirectory; } public final boolean isRecursive() { return _recursive; } public final boolean isVerbose() { return _verbose; } public final int getMask() { return _mask; } public final String getPath() { return _path; } public final Path getObservedPath() { return _observedPath; } public final boolean isPaused() { return _paused; } public final void pause() { _paused = true; } public final void resume() { _paused = false; } public final String startWatching() throws IOException { return getObserverThread().startWatching(this); } public final void stopWatching() { try { getObserverThread().stopWatching(this); } catch (Throwable ignored) { } } /** * Join main thread until interrupt is called */ public final void join() { try { joinObserverThread(); } catch (Throwable ignored) { } } public final void interrupt() { interruptObserverThread(); } // -------------------------------------------------------------------- // p r i v a t e // -------------------------------------------------------------------- private void onEvent(int event, final String path) { if (null != _listener) { _listener.onEvent(event, path); } } private void addKey(final WatchKey key) { _keys.add(key); } private void addKeys(final Collection<WatchKey> keys) { _keys.addAll(keys); } // -------------------------------------------------------------------- // S T A T I C // -------------------------------------------------------------------- public static final String eventToString(final int event) { if (EVENT_CREATE == event) { return "CREATE"; } else if (EVENT_DELETE == event) { return "DELETE"; } else if (EVENT_MODIFY == event) { return "MODIFY"; } return "UNKNOWN"; } @SuppressWarnings("unchecked") static <T> WatchEvent<T> cast(WatchEvent<?> event) { return (WatchEvent<T>) event; } // late initialized observer thread private static ObserverThread __observerThread; private static ObserverThread getObserverThread() throws IOException { if (null == __observerThread) { __observerThread = new ObserverThread(); __observerThread.start(); } return __observerThread; } private static void joinObserverThread() throws InterruptedException { if (null != __observerThread) { __observerThread.join(); } } private static void interruptObserverThread() { if (null != __observerThread) { __observerThread.interrupt(); __observerThread = null; } } // -------------------------------------------------------------------- // E M B E D D E D // -------------------------------------------------------------------- private static class ObserverThread extends Thread { private final WatchService _watcher; private final Map<WatchKey, Path> _keys; private final Map<String, WeakReference<FileObserver>> _observers; public ObserverThread() throws IOException { super("FileObserver"); super.setPriority(Thread.NORM_PRIORITY); super.setDaemon(true); _observers = Collections.synchronizedMap(new HashMap<String, WeakReference<FileObserver>>()); _watcher = FileSystems.getDefault().newWatchService(); _keys = new HashMap<WatchKey, Path>(); } public void run() { this.observe(); } public String startWatching(final FileObserver observer) throws IOException { final Path path = observer.getObservedPath(); final String key = this.getKey(observer); synchronized (_observers) { if (!_observers.containsKey(key)) { //-- register watch --// final WatchKey wkey = this.registerWatch(path, observer.isVerbose(), observer.getMask()); if (null != wkey) { observer.addKey(wkey); } //-- add observer for main loop --// final WeakReference<FileObserver> ref = new WeakReference<FileObserver>(observer); _observers.put(key, ref); } } return key; } public void stopWatching(final FileObserver observer) { final String key = this.getKey(observer); synchronized (_observers) { if (_observers.containsKey(key)) { //-- remove observer from main loop --// final WeakReference<FileObserver> ref = _observers.remove(key); //-- remove watch --// if (null != ref && null != ref.get()) { this.removeWatch(ref.get()); } } } } // -------------------------------------------------------------------- // p r i v a t e // -------------------------------------------------------------------- private Logger getLogger() { return LoggingUtils.getLogger(this); } /** * Main loop to keep thread alive */ private void observe() { try { while (!super.isInterrupted()) { Thread.sleep(200); final Collection<WeakReference<FileObserver>> references = _observers.values(); for (final WeakReference reference : references) { final FileObserver observer = (FileObserver) reference.get(); if (null != observer) { this.watch(observer); } } } } catch (Throwable ignored) { } } private void log(final FileObserver observer, final String msg) { if (null != observer && observer.isVerbose()) { this.getLogger().log(Level.INFO, msg); } } private void log(final String msg) { this.getLogger().log(Level.INFO, msg); } private void error(final FileObserver observer, final String msg, final Throwable t) { if (null != observer && observer.isVerbose()) { this.getLogger().log(Level.SEVERE, msg, t); } } private String getKey(final FileObserver observer) { final Path path = observer.getObservedPath(); return path.toString(); } private void watch(final FileObserver observer) { // wait for key to be signalled WatchKey key; try { key = _watcher.take(); } catch (InterruptedException x) { return; } final Path dir = _keys.get(key); if (dir == null) { this.log(observer, "WatchKey not recognized!!"); return; } for (final WatchEvent<?> event : key.pollEvents()) { final WatchEvent.Kind kind = event.kind(); // OVERFLOW event does nothing if (kind == OVERFLOW) { continue; } // Context for directory entry event is the file name of entry final WatchEvent<Path> ev = cast(event); final Path name = ev.context(); final Path child = dir.resolve(name); // print out event this.log(observer, FormatUtils.format("{0}: {1}", event.kind().name(), child)); // if directory is created, and watching recursively, then // register it and its sub-directories if (observer.isRecursive() && (kind == ENTRY_CREATE)) { try { if (Files.isDirectory(child, NOFOLLOW_LINKS)) { final Set<WatchKey> keys = this.registerAll(child, observer.isVerbose(), observer.getMask()); if (!keys.isEmpty()) { observer.addKeys(keys); } } } catch (IOException x) { // ignore to keep sample readbale } } //-- call onEvent --// if (!observer.isPaused()) { try { final String childPath = PathUtils.toUnixPath(child.toString()); if (observer.isDirectory()) { observer.onEvent(this.convert(event), childPath); } else if (observer.getPath().equalsIgnoreCase(childPath)) { observer.onEvent(this.convert(event), childPath); } } catch (Throwable throwable) { this.error(observer, "Unhandled throwable " + throwable.toString() + " (returned by observer " + observer + ")", throwable); } } } // reset key and remove from set if directory no longer accessible boolean valid = key.reset(); if (!valid) { _keys.remove(key); // all directories are inaccessible if (_keys.isEmpty()) { return; } } } private Set<WatchKey> registerAll(final Path start, final boolean verbose, final int mask) throws IOException { final Set<WatchKey> keys = new HashSet<WatchKey>(); // register directory and sub-directories Files.walkFileTree(start, new SimpleFileVisitor<Path>() { @Override public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { keys.add(registerWatch(dir, verbose, mask)); return FileVisitResult.CONTINUE; } }); return keys; } /** * Register the given directory with the WatchService */ private WatchKey registerWatch(final Path dir, final boolean verbose, final int mask) throws IOException { // prepare flags final Set<WatchEvent.Kind<Path>> flags = new HashSet<WatchEvent.Kind<Path>>(); if ((mask & EVENT_CREATE) == EVENT_CREATE) { flags.add(ENTRY_CREATE); } if ((mask & EVENT_DELETE) == EVENT_DELETE) { flags.add(ENTRY_DELETE); } if ((mask & EVENT_MODIFY) == EVENT_MODIFY) { flags.add(ENTRY_MODIFY); } final WatchEvent.Kind<Path>[] array = new WatchEvent.Kind[flags.size()]; final WatchKey key = dir.register(_watcher, flags.toArray(array)); if (verbose) { final Path prev = _keys.get(key); if (prev == null) { this.log(FormatUtils.format("register: {0}", dir)); } else { if (!dir.equals(prev)) { this.log(FormatUtils.format("update: {0} -> {1}", prev, dir)); } } } _keys.put(key, dir); return key; } private void removeWatch(final FileObserver observer) { if (null != observer) { final Set<WatchKey> keys = observer._keys; for (final WatchKey key : keys) { key.cancel(); } } } private int convert(final WatchEvent event) { return convert(event.kind()); } private int convert(final WatchEvent.Kind kind) { if (null != kind) { if (kind.equals(ENTRY_CREATE)) { return EVENT_CREATE; } else if (kind.equals(ENTRY_DELETE)) { return EVENT_DELETE; } else if (kind.equals(ENTRY_MODIFY)) { return EVENT_MODIFY; } } return 0; } } }