/**
* Copyright (c) 2014-2017 by the respective copyright holders.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package org.eclipse.smarthome.core.service;
import static java.nio.file.StandardWatchEventKinds.*;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
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.text.MessageFormat;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Base class for watch queue readers
*
* @author Fabio Marini
* @author Dimitar Ivanov - use relative path in watch events. Added option to watch directory events or not
* @author Ana Dimova - reduce to a single watch thread for all class instances of {@link AbstractWatchService}
*/
public class WatchQueueReader implements Runnable {
/**
* Default logger for ESH Watch Services
*/
protected final Logger logger = LoggerFactory.getLogger(WatchQueueReader.class);
protected WatchService watchService;
private Map<WatchKey, Path> registeredKeys;
private Map<WatchKey, AbstractWatchService> keyToService;
private Thread qr;
private static final WatchQueueReader INSTANCE = new WatchQueueReader();
/**
* Perform a simple cast of given event to WatchEvent
*
* @param event the event to cast
* @return the casted event
*/
@SuppressWarnings("unchecked")
static <T> WatchEvent<T> cast(WatchEvent<?> event) {
return (WatchEvent<T>) event;
}
public static WatchQueueReader getInstance() {
return INSTANCE;
}
/**
* Builds the {@link WatchQueueReader} object that will monitor the directory changes if there is
* an {@link AbstractWatchService} that requests its functionality.
*/
private WatchQueueReader() {
registeredKeys = new HashMap<>();
keyToService = new HashMap<>();
}
/**
* Customize the queue reader to process the watch events for the given directory, provided by the watch service
*
* @param watchService the watch service, requesting the watch events for the watched directory
* @param toWatch the directory being watched by the watch service
* @param watchSubDirectories a boolean flag that specifies if the child directories of the registered directory
* will being watched by the watch service
*/
protected void customizeWatchQueueReader(AbstractWatchService watchService, Path toWatch,
boolean watchSubDirectories) {
try {
if (watchSubDirectories) {
// walk through all folders and follow symlinks
registerWithSubDirectories(watchService, toWatch);
} else {
registerDirectoryInternal(watchService, watchService.getWatchEventKinds(toWatch), toWatch);
}
} catch (NoSuchFileException e) {
logger.debug("Not watching folder '{}' as it does not exist.", toWatch);
} catch (IOException e) {
logger.warn("Cannot customize folder watcher for folder '{}'", toWatch, e);
}
}
private void registerWithSubDirectories(AbstractWatchService watchService, Path toWatch) throws IOException {
Files.walkFileTree(toWatch, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path subDir, BasicFileAttributes attrs)
throws IOException {
Kind<?>[] kinds = watchService.getWatchEventKinds(subDir);
if (kinds != null) {
registerDirectoryInternal(watchService, kinds, subDir);
}
return FileVisitResult.CONTINUE;
}
});
}
private synchronized void registerDirectoryInternal(AbstractWatchService service, Kind<?>[] kinds, Path directory) {
if (watchService == null) {
try {
watchService = FileSystems.getDefault().newWatchService();
qr = new Thread(this, "Dir Watcher");
qr.start();
} catch (IOException e) {
logger.debug("The directory '{}' was not registered in the watch service", directory, e);
return;
}
}
WatchKey registrationKey = null;
try {
registrationKey = directory.register(this.watchService, kinds);
} catch (IOException e) {
logger.debug("The directory '{}' was not registered in the watch service: {}", directory, e.getMessage());
}
if (registrationKey != null) {
registeredKeys.put(registrationKey, directory);
keyToService.put(registrationKey, service);
} else {
logger.debug("The directory '{}' was not registered in the watch service", directory);
}
}
public synchronized void stopWatchService(AbstractWatchService service) {
if (watchService != null) {
List<WatchKey> keys = new LinkedList<>();
for (WatchKey key : keyToService.keySet()) {
if (keyToService.get(key) == service) {
keys.add(key);
}
}
if (keys.size() == keyToService.size()) {
try {
watchService.close();
} catch (IOException e) {
logger.warn("Cannot deactivate folder watcher", e);
}
watchService = null;
keyToService.clear();
registeredKeys.clear();
} else {
for (WatchKey key : keys) {
key.cancel();
keyToService.remove(key);
registeredKeys.remove(key);
}
}
}
}
/*
* (non-Javadoc)
*
* @see org.eclipse.smarthome.core.service.IWatchService#activate()
*/
@Override
public void run() {
try {
for (;;) {
WatchKey key = null;
try {
key = watchService.take();
} catch (InterruptedException exc) {
logger.info(MessageFormat.format("Caught InterruptedException: {0}", exc.getLocalizedMessage()));
return;
}
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
if (kind == OVERFLOW) {
logger.warn(MessageFormat.format(
"Found an event of kind 'OVERFLOW': {0}. File system changes might have been missed.",
event));
continue;
}
Path resolvedPath = resolvePath(key, event);
if (resolvedPath != null) {
// Process the event only when a relative path to it is resolved
AbstractWatchService service = null;
synchronized (this) {
service = keyToService.get(key);
}
if (service != null) {
File f = resolvedPath.toFile();
service.processWatchEvent(event, kind, resolvedPath);
if (kind == ENTRY_CREATE && f.isDirectory() && service.watchSubDirectories()
&& service.getWatchEventKinds(resolvedPath) != null) {
registerDirectoryInternal(service, service.getWatchEventKinds(resolvedPath),
resolvedPath);
} else if (kind == ENTRY_DELETE) {
synchronized (this) {
WatchKey toCancel = null;
for (WatchKey k : registeredKeys.keySet()) {
if (registeredKeys.get(k).equals(resolvedPath)) {
toCancel = k;
break;
}
}
if (toCancel != null) {
registeredKeys.remove(toCancel);
keyToService.remove(toCancel);
toCancel.cancel();
}
}
}
}
}
}
key.reset();
}
} catch (Exception exc) {
logger.debug("ClosedWatchServiceException caught! {}. \n{} Stopping ", exc.getLocalizedMessage(),
Thread.currentThread().getName());
return;
}
}
private Path resolvePath(WatchKey key, WatchEvent<?> event) {
WatchEvent<Path> ev = cast(event);
// Context for directory entry event is the file name of entry.
Path contextPath = ev.context();
Path baseWatchedDir = null;
Path registeredPath = null;
synchronized (this) {
baseWatchedDir = keyToService.get(key).getSourcePath();
registeredPath = registeredKeys.get(key);
}
if (registeredPath != null) {
// If the path has been registered in the watch service it relative path can be resolved
// The context path is resolved by its already registered parent path
Path resolvedContextPath = registeredPath.resolve(contextPath);
// Relativize the resolved context to the directory watched (Build the relative path)
Path path = baseWatchedDir.relativize(resolvedContextPath);
// As the modification of file in subdirectory is considered a modification on the subdirectory itself, we
// will consider the defined behavior to watch the directory changes
if (baseWatchedDir.resolve(path).toFile().isDirectory()
&& !isWatchingDirectoryChanges(key, resolvedContextPath)) {
// As we have found a directory event and do not want to track directory changes - we will skip it
return null;
}
return resolvedContextPath;
}
logger.warn(
"Detected invalid WatchEvent '{}' and key '{}' for entry '{}' in not registered file or directory of '{}'",
event, key, contextPath, baseWatchedDir);
return null;
}
/**
* Tells to the queue reader if watching for the directory changes. All the watch events will be processed if the
* method returns <code>true<code>. Otherwise the events for directories will be skipped.
*
* @param key the WatchKey returned for directory, on its registration in the watch service.
* @param resolvedContextPath the path of the event (resolved to the {@link #baseWatchedDir})
*
* @return <code>true</code> if the directory events will be processed and <code>false</code> otherwise
*/
private boolean isWatchingDirectoryChanges(WatchKey key, Path resolvedContextPath) {
return keyToService.get(key).getWatchingDirectoryChanges(resolvedContextPath);
}
}