private static final Kind[] interested_types = new Kind[]{StandardWatchEventKind.ENTRY_CREATE, StandardWatchEventKind.ENTRY_DELETE, StandardWatchEventKind.ENTRY_MODIFY};
/**
* The executor service which runs the task of polling the watch service.
*/
private ExecutorService executorService;
/**
* Flag to indicate that the watch service should be stopped during
* shutdown.
*/
private volatile boolean closeWatchServiceOnShutdown = false;
/**
* Flag to indiate that the executor service should be stopped during
* shutdown.
*/
private volatile boolean shutdownExecutorServiceOnShutdown = false;
/**
* Package-protected getter (for automated testing).
*
* @return
*/
ExecutorService getExecutorService() {
return executorService;
}
/**
* Package-protected getter (for automated testing).
*
* @return
*/
WatchService getWatchService() {
return watchService;
}
/**
* The registered watch keys for each path.
*/
private final ConcurrentMap<String, WatchKey> watchKeysByPath;
/**
* The registered paths for each watch key.
*/
private final ConcurrentMap<WatchKey, String> pathsByWatchKey;
/**
* The listeners for each watch key.
*/
private final ConcurrentMap<WatchKey, Collection<SFMF4JWatchListener>> listenersByWatchKey;
/**
* Creates a new WatchServiceFileMonitorServiceImpl.
* @param watchService the watch service to use. When this argument is
* {@code null}, the watch service will be created during {@link
* #initialize()} and stopped during {@link #shutdown()}. If this argument
* is not {@code null}, then it is assumed that the watch service is running
* and will be closed elsewhere (for example, an IoC container)
* @param executorService the executor service to use. When this argument
* is {@code null}, the executor service will be created during {@link
* #initialize()} and stopped during {@link #shutdown()}. If this argument
* is not {@code null}, then it is assumed that the executor service is
* running and will be closed elsewhere (for example, in an IoC container)
*/
public WatchServiceFileMonitorServiceImpl(final WatchService watchService, final ExecutorService executorService) {
this.watchService = watchService;
this.executorService = executorService;
this.watchKeysByPath = new ConcurrentHashMap<String, WatchKey>();
this.pathsByWatchKey = new ConcurrentHashMap<WatchKey, String>();
this.listenersByWatchKey = new ConcurrentHashMap<WatchKey, Collection<SFMF4JWatchListener>>();
}
/**
* Gets the watch key for a path with lazy initialization.
* @param path the path
* @return a registered watch key for the path
* @throws IOException if lazy initialization fails
*/
private synchronized WatchKey getWatchKeyForPath(final String path) throws IOException {
WatchKey key = watchKeysByPath.get(path);
if (key == null) {
logger.debug("Lazy-instantiating watch key for path: {}", path);
key = Paths.get(path).register(watchService, interested_types);
watchKeysByPath.put(path, key);
pathsByWatchKey.put(key, path);
listenersByWatchKey.put(key, Collections.newSetFromMap(new ConcurrentHashMap<SFMF4JWatchListener, Boolean>()));
}
return key;
}
@Override
public void registerDirectoryListener(File directory, DirectoryListener directoryListener) {
String path = directory.getAbsolutePath();
try {
synchronized (this) {
WatchKey key = getWatchKeyForPath(path);
SFMF4JWatchListener listener = new SFMF4JWatchListener(directoryListener);
listenersByWatchKey.get(key).add(listener);
}
} catch (IOException ex) {
logger.error(ex.getMessage(), ex);
}
}
@Override
public void unregisterDirectoryListener(File directory, DirectoryListener directoryListener) {
String path = directory.getAbsolutePath();
synchronized (this) {
final WatchKey key = watchKeysByPath.get(path);
if (key != null) {
Collection<SFMF4JWatchListener> listeners = listenersByWatchKey.get(key);
listeners.remove(new SFMF4JWatchListener(directoryListener)); // note the equals implementation
if (listeners.isEmpty()) {
//no longer listening on that path.
cleanup(key);
} else {
logger.debug("somebody is still listening: {}", path);
}
}
}
}
/**
* Resolves a watch event with its absolute path.
* @param key the watch key (used to look up the parent path)
* @param event the event to resolve
* @return a copy of the event, with a resolved path
*/
private synchronized WatchEvent<Path> resolveEventWithCorrectPath(final WatchKey key, final WatchEvent<Path> event) {
Path correctPath = Paths.get(pathsByWatchKey.get(key));
return new ResolvedPathWatchEvent(event, correctPath);
}
/**
* Properly unregisters and removes a watch key.
* @param key the watch key
*/
@SuppressWarnings("PMD.EmptyCatchBlock")
private synchronized void cleanup(final WatchKey key) {
logger.trace("cleanUp {}", key);
try {
key.cancel();
}catch(Exception ex) {
//trap
}
Collection<SFMF4JWatchListener> listeners = listenersByWatchKey.remove(key);
if (listeners != null && !listeners.isEmpty()) {
logger.warn("Cleaning up key but listeners are still registered.");
}
String path = pathsByWatchKey.remove(key);
if (path != null) {
watchKeysByPath.remove(path);
}
}
@Override
public synchronized void initialize() {
if (watchService==null) {
logger.warn("No watch service was explicitly set. Setting one now.");
closeWatchServiceOnShutdown = true;