/*
* Copyright 2016 the original author or authors.
*
* This file is part of HotswapAgent.
*
* HotswapAgent is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 2 of the License, or (at your
* option) any later version.
*
* HotswapAgent is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with HotswapAgent. If not, see http://www.gnu.org/licenses/.
*/
package org.hotswap.agent.watch.nio;
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;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import org.hotswap.agent.logging.AgentLogger;
import org.hotswap.agent.logging.AgentLogger.Level;
import org.hotswap.agent.watch.WatchEventListener;
import org.hotswap.agent.watch.Watcher;
/**
* NIO2 watcher implementation for systems which support
* ExtendedWatchEventModifier.FILE_TREE
* <p/>
* Java 7 (NIO2) watch a directory (or tree) for changes to files.
* <p/>
* By http://docs.oracle.com/javase/tutorial/essential/io/examples/WatchDir.java
*
* @author Jiri Bubnik
* @author alpapad@gmail.com
*/
public abstract class AbstractNIO2Watcher implements Watcher {
protected AgentLogger LOGGER = AgentLogger.getLogger(this.getClass());
protected final static WatchEvent.Kind<?>[] KINDS = new WatchEvent.Kind<?>[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY };
protected WatchService watcher;
protected final Map<WatchKey, PathPair> keys;
private final Map<Path, List<WatchEventListener>> listeners = new ConcurrentHashMap<Path, List<WatchEventListener>>();
// keep track about which classloader requested which event
protected Map<WatchEventListener, ClassLoader> classLoaderListeners = new ConcurrentHashMap<WatchEventListener, ClassLoader>();
private Thread runner;
private volatile boolean stopped;
protected final EventDispatcher dispatcher;
public AbstractNIO2Watcher() throws IOException {
this.watcher = FileSystems.getDefault().newWatchService();
this.keys = new ConcurrentHashMap<WatchKey, PathPair>();
dispatcher = new EventDispatcher(listeners);
}
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
}
@Override
public synchronized void addEventListener(ClassLoader classLoader, URI pathPrefix, WatchEventListener listener) {
File path;
try {
// check that it is regular file
// toString() is weird and solves HiarchicalUriException for URI
// like "file:./src/resources/file.txt".
path = new File(pathPrefix);
} catch (IllegalArgumentException e) {
if (!LOGGER.isLevelEnabled(Level.TRACE)) {
LOGGER.warning("Unable to watch for path {}, not a local regular file or directory.", pathPrefix);
} else {
LOGGER.trace("Unable to watch for path {} exception", e, pathPrefix);
}
return;
}
try {
addDirectory(path.toPath());
} catch (IOException e) {
if (!LOGGER.isLevelEnabled(Level.TRACE)) {
LOGGER.warning("Unable to watch for path {}, not a local regular file or directory.", pathPrefix);
} else {
LOGGER.trace("Unable to watch path with prefix '{}' for changes.", e, pathPrefix);
}
return;
}
List<WatchEventListener> list = listeners.get(Paths.get(pathPrefix));
if (list == null) {
list = new ArrayList<WatchEventListener>();
listeners.put(Paths.get(pathPrefix), list);
}
list.add(listener);
if (classLoader != null) {
classLoaderListeners.put(listener, classLoader);
}
}
@Override
public void addEventListener(ClassLoader classLoader, URL pathPrefix, WatchEventListener listener) {
if (pathPrefix == null) {
return;
}
try {
addEventListener(classLoader, pathPrefix.toURI(), listener);
} catch (URISyntaxException e) {
throw new RuntimeException("Unable to convert URL to URI " + pathPrefix, e);
}
}
/**
* Remove all transformers registered with a classloader
*
* @param classLoader
*/
@Override
public void closeClassLoader(ClassLoader classLoader) {
for (Iterator<Entry<WatchEventListener, ClassLoader>> entryIterator = classLoaderListeners.entrySet().iterator(); entryIterator.hasNext();) {
Entry<WatchEventListener, ClassLoader> entry = entryIterator.next();
if (entry.getValue().equals(classLoader)) {
entryIterator.remove();
try {
for (Iterator<Entry<Path, List<WatchEventListener>>> listenersIterator = listeners.entrySet().iterator(); listenersIterator.hasNext();) {
Entry<Path, List<WatchEventListener>> pathListenerEntry = listenersIterator.next();
List<WatchEventListener> l = pathListenerEntry.getValue();
if (l != null) {
l.remove(entry.getKey());
}
if (l == null || l.isEmpty()) {
listenersIterator.remove();
}
}
} catch (Exception e) {
LOGGER.error("Ooops", e);
}
}
}
// cleanup...
if (classLoaderListeners.isEmpty()) {
listeners.clear();
for (WatchKey wk : keys.keySet()) {
try {
wk.cancel();
} catch (Exception e) {
LOGGER.error("Ooops", e);
}
}
try {
this.watcher.close();
} catch (IOException e) {
LOGGER.error("Ooops", e);
}
LOGGER.info("All classloaders closed, released watch service..");
try {
// Reset
this.watcher = FileSystems.getDefault().newWatchService();
} catch (IOException e) {
LOGGER.error("Ooops", e);
}
}
LOGGER.debug("All watch listeners removed for classLoader {}", classLoader);
}
/**
* Registers the given directory
*/
public void addDirectory(Path path) throws IOException {
registerAll(null, path);
}
protected abstract void registerAll(final Path watched, final Path target) throws IOException;
/**
* Process all events for keys queued to the watcher
*
* @return true if should continue
* @throws InterruptedException
*/
private boolean processEvents() throws InterruptedException {
// wait for key to be signalled
WatchKey key = watcher.poll(10, TimeUnit.MILLISECONDS);
if (key == null) {
return true;
}
PathPair dir = keys.get(key);
if (dir == null) {
LOGGER.warning("WatchKey '{}' not recognized", key);
return true;
}
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == OVERFLOW) {
LOGGER.warning("WatchKey '{}' overflowed", key);
continue;
}
// Context for directory entry event is the file name of entry
WatchEvent<Path> ev = cast(event);
Path name = ev.context();
Path child = dir.resolve(name);
LOGGER.debug("Watch event '{}' on '{}' --> {}", event.kind().name(), child, name);
dispatcher.add(ev, child);
// if directory is created, and watching recursively, then
// register it and its sub-directories
if (kind == ENTRY_CREATE) {
try {
if (Files.isDirectory(child, NOFOLLOW_LINKS)) {
registerAll(dir.getWatched(), child);
}
} catch (IOException x) {
LOGGER.warning("Unable to register events for directory {}", x, child);
}
}
}
// reset key and remove from set if directory no longer accessible
boolean valid = key.reset();
if (!valid) {
LOGGER.warning("Watcher on {} not valid, removing...", keys.get(key).getShortDescription());
keys.remove(key);
// all directories are inaccessible
if (keys.isEmpty()) {
return false;
}
if (classLoaderListeners.isEmpty()) {
for (WatchKey k : keys.keySet()) {
k.cancel();
}
return false;
}
}
return true;
}
@Override
public void run() {
runner = new Thread() {
@Override
public void run() {
try {
for (;;) {
if (stopped || !processEvents()) {
break;
}
}
} catch (InterruptedException x) {
}
}
};
runner.setDaemon(true);
runner.setName("HotSwap Watcher");
runner.start();
dispatcher.start();
}
@Override
public void stop() {
stopped = true;
}
/**
* Get a Watch event modifier. These are platform specific and hiden in sun api's
*
* @see <a href="https://github.com/HotswapProjects/HotswapAgent/issues/41">
* Issue#41</a>
* @see <a href=
* "http://stackoverflow.com/questions/9588737/is-java-7-watchservice-slow-for-anyone-else">
* Is Java 7 WatchService Slow for Anyone Else?</a>
*/
static WatchEvent.Modifier getWatchEventModifier(String claz, String field) {
try {
Class<?> c = Class.forName(claz);
Field f = c.getField(field);
return (WatchEvent.Modifier) f.get(c);
} catch (Exception e) {
return null;
}
}
}