diff --git a/bundles/config/org.eclipse.smarthome.config.dispatch/src/main/java/org/eclipse/smarthome/config/dispatch/internal/ConfigDispatcher.java b/bundles/config/org.eclipse.smarthome.config.dispatch/src/main/java/org/eclipse/smarthome/config/dispatch/internal/ConfigDispatcher.java index fb8c16d1aa0..7b35b83e9cf 100644 --- a/bundles/config/org.eclipse.smarthome.config.dispatch/src/main/java/org/eclipse/smarthome/config/dispatch/internal/ConfigDispatcher.java +++ b/bundles/config/org.eclipse.smarthome.config.dispatch/src/main/java/org/eclipse/smarthome/config/dispatch/internal/ConfigDispatcher.java @@ -18,6 +18,7 @@ import java.nio.file.Path; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; +import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.Arrays; import java.util.Comparator; @@ -157,8 +158,9 @@ protected boolean watchSubDirectories() { * (java.nio.file.Path) */ @Override - protected void registerDirectory(Path subDir) throws IOException { - subDir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); + protected WatchKey registerDirectory(Path subDir) throws IOException { + WatchKey registrationKey = subDir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); + return registrationKey; } /* @@ -169,8 +171,9 @@ protected void registerDirectory(Path subDir) throws IOException { * (java.nio.file.WatchService, java.nio.file.Path) */ @Override - protected AbstractWatchQueueReader buildWatchQueueReader(WatchService watchService, Path toWatch) { - return new WatchQueueReader(watchService, toWatch); + protected AbstractWatchQueueReader buildWatchQueueReader(WatchService watchService, Path toWatch, + Map registredWatchKeys) { + return new WatchQueueReader(watchService, toWatch, registredWatchKeys); } private String getDefaultServiceConfigFile() { @@ -315,15 +318,15 @@ private String[] parseLine(final String filePath, final String line) { private class WatchQueueReader extends AbstractWatchQueueReader { - public WatchQueueReader(WatchService watchService, Path dir) { - super(watchService, dir); + public WatchQueueReader(WatchService watchService, Path dir, Map registeredKeys) { + super(watchService, dir, registeredKeys); } @Override protected void processWatchEvent(WatchEvent event, Kind kind, Path path) { if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) { try { - processConfigFile(new File(dir.toAbsolutePath() + File.separator + path.toString())); + processConfigFile(new File(baseWatchedDir.toAbsolutePath() + File.separator + path.toString())); } catch (IOException e) { logger.warn("Could not process config file '{}': {}", path, e); } diff --git a/bundles/core/org.eclipse.smarthome.core.test/src/test/groovy/org/eclipse/smarthome/core/service/AbstractWatchServiceTest.groovy b/bundles/core/org.eclipse.smarthome.core.test/src/test/groovy/org/eclipse/smarthome/core/service/AbstractWatchServiceTest.groovy new file mode 100644 index 00000000000..f057ae5e7ff --- /dev/null +++ b/bundles/core/org.eclipse.smarthome.core.test/src/test/groovy/org/eclipse/smarthome/core/service/AbstractWatchServiceTest.groovy @@ -0,0 +1,435 @@ +package org.eclipse.smarthome.core.service + +import static java.nio.file.StandardWatchEventKinds.* +import static org.hamcrest.CoreMatchers.* +import static org.junit.Assert.* +import static org.junit.matchers.JUnitMatchers.* + +import java.nio.file.Path +import java.nio.file.WatchEvent +import java.nio.file.WatchKey +import java.nio.file.WatchService +import java.nio.file.WatchEvent.Kind + +import org.apache.commons.lang.SystemUtils +import org.eclipse.smarthome.test.OSGiTest +import org.junit.After +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.Test + +/** + * Test for {@link AbstractWatchService}. + * + * @author Dimitar Ivanov - Initial implementation + * @author Svilen Valkanov - Tests are modified to run on different Operating Systems + */ +class AbstractWatchServiceTest extends OSGiTest { + + def static WATCHED_DIRECTORY = "watchDirectory" + + // Fail if no event has been received within the given timeout + def static NO_EVENT_TIMEOUT_IN_SECONDS; + + RelativeWatchService watchService + + @BeforeClass + static void setUpBeforeClass(){ + File watchDir = new File(WATCHED_DIRECTORY); + watchDir.mkdirs() + // set the NO_EVENT_TIMEOUT_IN_SECONDS according to the operating system used + if(SystemUtils.IS_OS_MAC_OSX) { + NO_EVENT_TIMEOUT_IN_SECONDS = 10 + } else { + NO_EVENT_TIMEOUT_IN_SECONDS = 3; + } + } + + @AfterClass + static void tearDownClass(){ + File watchedDirectory = new File(WATCHED_DIRECTORY); + watchedDirectory.deleteDir() + } + + @After + public void tearDown(){ + watchService.deactivate() + waitForAssert{assertThat watchService.watchService,is(nullValue())} + clearWatchedDir() + watchService.allFullEvents.clear() + } + + void clearWatchedDir(){ + File watchedDirectory = new File(WATCHED_DIRECTORY) + watchedDirectory.listFiles().each { File mockedFile -> + mockedFile.isFile() ? mockedFile.delete() : mockedFile.deleteDir() + } + } + + @Test + void 'AbstractWatchQueueReader processWatchEvent path in root provided with correct arguments'() { + watchService = new RelativeWatchService(WATCHED_DIRECTORY,true,false) + + // File created in the watched directory + assertByRelativePath("rootWatchFile") + } + + @Test + void 'AbstractWatchQueueReader processWatchEvent path in subdir provided with correct arguments'() { + watchService = new RelativeWatchService(WATCHED_DIRECTORY,true,false) + + // File created in a subdirectory of the watched directory + assertByRelativePath("subdir" + File.separatorChar + "subDirWatchFile") + } + + @Test + void 'AbstractWatchQueueReader processWatchEvent path in subsubdir provided with correct arguments'() { + watchService = new RelativeWatchService(WATCHED_DIRECTORY,true,false) + + // File created in a sub sub directory of the watched directory + assertByRelativePath("subDir" + File.separatorChar + "subSubDir" + File.separatorChar + "innerWatchFile") + } + + @Test + void 'same file names in root and subdir are correctly processed'(){ + watchService = new RelativeWatchService(WATCHED_DIRECTORY,true,false) + + def fileName = "duplicateFile" + def innerFileName = "duplicateDir" + File.separatorChar + fileName + + File innerfile = new File(WATCHED_DIRECTORY + File.separatorChar + innerFileName) + + // Make all the directories needed + innerfile.getParentFile().mkdirs() + + // Activate the service when the subdir is also present. Else the subdir will not be registered + watchService.activate() + waitForAssert {assertThat watchService.watchService,is(notNullValue())} + + boolean isCreated = innerfile.createNewFile() + assertThat "The file '$innerfile.absolutePath' was not created successfully", isCreated, is(true) + + // Assure that the ordering of the events will be always the same + sleep NO_EVENT_TIMEOUT_IN_SECONDS*1000 + + File file = new File(WATCHED_DIRECTORY + File.separatorChar + fileName) + isCreated = file.createNewFile() + + assertThat "The file '$file.absolutePath' was not created successfully", isCreated, is(true) + + def expectedEvents = 2 + waitForAssert{assertThat "Exactly two watch events were expected, but were: " + watchService.allFullEvents, watchService.allFullEvents.size(), is(expectedEvents)} + + FullEvent innerFileEvent = watchService.allFullEvents[0] + assertThat "The inner file '$innerfile.absolutePath' creation was not detected. All events detected: " + watchService.allFullEvents,innerFileEvent.eventKind,is(ENTRY_CREATE) + assertThat "The path of the first detected event should be for $innerFileName", innerFileEvent.eventPath.toString(), is(innerFileName) + + FullEvent fileEvent = watchService.allFullEvents[1] + assertThat "The root file '$file.absolutePath' creation was not detected. All events detected: " + watchService.allFullEvents,fileEvent.eventKind,is(ENTRY_CREATE) + assertThat "The path of the second event should be for $fileName", fileEvent.eventPath.toString(), is(fileName) + } + + @Test + void 'subdirs are registered and modifications are watched'(){ + // Watch subdirectories and their modifications + watchService = new RelativeWatchService(WATCHED_DIRECTORY,true,true) + + def subDirName = "correctlyWatchedSubDir" + def fileName = "correctSubDirInnerFile" + def innerFileName = subDirName + File.separatorChar + fileName + + File innerFile = new File(WATCHED_DIRECTORY + File.separatorChar + innerFileName) + + // Make all the subdirectories before running the service + innerFile.getParentFile().mkdirs() + + watchService.activate() + waitForAssert {assertThat watchService.watchService,is(notNullValue())} + + if(SystemUtils.IS_OS_MAC_OSX) { + sleep 1000 + } + + boolean isCreated = innerFile.createNewFile() + assertThat "The file '$innerFile.absolutePath' was not created successfully", isCreated, is(true) + + assertAllEventsAreProcessed(subDirName,innerFile,innerFileName) + } + + @Test + void 'subdirs are not registered and modifications are not watched'(){ + // Do not watch the subdirectories of the root directory + watchService = new RelativeWatchService(WATCHED_DIRECTORY,false,false) + + def innerFileName = "watchRequestSubDir"+ File.separatorChar + "watchRequestInnerFile" + + File innerFile = new File(WATCHED_DIRECTORY + File.separatorChar + innerFileName) + innerFile.getParentFile().mkdirs() + + watchService.activate() + waitForAssert {assertThat watchService.watchService,is(notNullValue())} + + // Consequent creation and deletion in order to generate any watch events for the subdirectory + boolean isCreated = innerFile.createNewFile() + assertThat "The file '$innerFile.absolutePath' was not created successfully", isCreated, is(true) + + boolean isDeleted = innerFile.delete() + assertThat "Inner file is not deleted", isDeleted, is(true) + + assertNoEventsAreProcessed() + } + + @Test + void 'subdirs are registered, but dirs modifications are not watched'() { + // Do watch the subdirectories of the root directory, but do not watch directory modifications + watchService = new RelativeWatchService(WATCHED_DIRECTORY,true,false) + + def innerFileName = "watchRequestSubDir"+ File.separatorChar + "watchRequestInnerFile" + File innerFile = new File(WATCHED_DIRECTORY + File.separatorChar + innerFileName) + // Make all the subdirectories before running the service + innerFile.getParentFile().mkdirs() + + watchService.activate() + waitForAssert {assertThat watchService.watchService,is(notNullValue())} + + boolean isCreated = innerFile.createNewFile() + assertThat "The file '$innerFile.absolutePath' was not created successfully", isCreated, is(true) + + assertFileCreateEventIsProcessed(innerFile,innerFileName) + + watchService.allFullEvents.clear(); + + assertNoEventsAreProcessed() + } + + @Test + void 'subdirs are not registered, but dirs modifications are watched'(){ + // Do not watch the subdirectories of the root directory, but watch directory modifications + watchService = new RelativeWatchService(WATCHED_DIRECTORY,false,true) + + def subDirName = "subDirModifications" + def innerFileName = subDirName + File.separatorChar + "modificationsInnerFile" + + File innerFile = new File(WATCHED_DIRECTORY + File.separatorChar + innerFileName) + innerFile.getParentFile().mkdirs() + + watchService.activate() + waitForAssert {assertThat watchService.watchService,is(notNullValue())} + + if(SystemUtils.IS_OS_MAC_OSX) { + sleep 1000 + } + + boolean isCreated = innerFile.createNewFile() + assertThat "The file '$innerFile.absolutePath' was not created successfully", isCreated, is(true) + + assertDirectoryModifyEventIsProcessed(subDirName) + + // Clear the asserted event + watchService.allFullEvents.clear() + + boolean isDeleted = innerFile.delete() + assertThat "Inner file '$innerFile.absolutePath' is not deleted", isDeleted, is(true) + + } + void assertNoEventsAreProcessed(){ + // Wait for a possible event for the maximum timeout + sleep NO_EVENT_TIMEOUT_IN_SECONDS*1000 + + assertThat "No watch events are expected, but were: " + watchService.allFullEvents, watchService.allFullEvents.size(), is(0) + } + + void assertAllEventsAreProcessed(def subDirName,def innerFile, def innerFileName){ + //This could vary across different platforms. For more information see "Platform dependencies" section in the WatchService documentation + def expectedEvents = SystemUtils.IS_OS_LINUX ? 1 : 2 + + waitForAssert({assertThat "Exactly $expectedEvents watch events were expected, but were: " + watchService.allFullEvents, watchService.allFullEvents.size(), is(expectedEvents)},30*1000) + + if(SystemUtils.IS_OS_MAC_OSX) { + FullEvent dirEvent = watchService.allFullEvents[0] + assertThat "Directory $subDirName modification was not detected. All events detected: " + watchService.allFullEvents, dirEvent.eventKind, is(ENTRY_MODIFY) + assertThat "Subdirectory was not found in the modified event", dirEvent.eventPath.toString(), is(subDirName) + + FullEvent fileEvent = watchService.allFullEvents[1] + assertThat "File '$innerFile.absolutePath' creation was not detected. All events detected: " + watchService.allFullEvents, fileEvent.eventKind, is(ENTRY_CREATE) + assertThat "File '$innerFile.absolutePath' name expected in the modified event. All events detected: " + watchService.allFullEvents, fileEvent.eventPath.toString(), is(innerFileName) + + } else { + FullEvent fileEvent = watchService.allFullEvents[0] + assertThat "File '$innerFile.absolutePath' creation was not detected. All events detected: " + watchService.allFullEvents, fileEvent.eventKind, is(ENTRY_CREATE) + assertThat "File '$innerFile.absolutePath' name expected in the modified event. All events detected: " + watchService.allFullEvents, fileEvent.eventPath.toString(), is(innerFileName) + + if(SystemUtils.IS_OS_WINDOWS) { + FullEvent dirEvent = watchService.allFullEvents[1] + assertThat "Directory $subDirName modification was not detected. All events detected: " + watchService.allFullEvents, dirEvent.eventKind, is(ENTRY_MODIFY) + assertThat "Subdirectory was not found in the modified event", dirEvent.eventPath.toString(), is(subDirName) + } + } + + } + + void assertDirectoryCraeteEventIsProcessed(def subDirName) { + //Single event for directory creation is present + def expectedEvents = 1 + waitForAssert{assertThat "Exactly $expectedEvents watch events were expected, but were: " + watchService.allFullEvents, watchService.allFullEvents.size(), is(expectedEvents)} + FullEvent event = watchService.allFullEvents[0] + assertThat "Directory $subDirName craetion was not detected. All events detected: " + watchService.allFullEvents, event.eventKind, is(ENTRY_CREATE) + assertThat "Subdirectory was not found in the creation event", event.eventPath.toString(), is(subDirName) + } + + void assertFileCreateEventIsProcessed(def innerFile, def innerFileName) { + //Single event for file creation is present + def expectedEvents = 1 + waitForAssert{assertThat "Exactly $expectedEvents watch events were expected, but were: " + watchService.allFullEvents, watchService.allFullEvents.size(), is(expectedEvents)} + FullEvent fileEvent = watchService.allFullEvents[0] + assertThat "File '$innerFile.absolutePath' creation was not detected. All events detected: " + watchService.allFullEvents, fileEvent.eventKind, is(ENTRY_CREATE) + assertThat "File '$innerFile.absolutePath' name expected in the modified event. All events detected: " + watchService.allFullEvents, fileEvent.eventPath.toString(), is(innerFileName) + } + + void assertDirectoryModifyEventIsProcessed(def subDirName) { + //Create file is not detected, only the modification event is detected + def expectedEvents = SystemUtils.IS_OS_LINUX ? 0 : 1 + waitForAssert{assertThat "Exactly $expectedEvents watch events were expected, but were: " + watchService.allFullEvents, watchService.allFullEvents.size(), is(expectedEvents)} + + if(!SystemUtils.IS_OS_LINUX){ + FullEvent dirEvent = watchService.allFullEvents[0] + assertThat "Directory $subDirName modification was not detected. All events detected: " + watchService.allFullEvents, dirEvent.eventKind, is(ENTRY_MODIFY) + assertThat "Subdirectory was not found in the modified event", dirEvent.eventPath.toString(), is(subDirName) + } else { + assertThat "No events are expected in Linux OS" + watchService.allFullEvents, watchService.allFullEvents.size(), is(0) + } + } + + void assertByRelativePath(String fileName) { + File file = new File(WATCHED_DIRECTORY + File.separatorChar + fileName) + file.getParentFile().mkdirs() + + assertThat "The file '$file.absolutePath' should not be present before the watch service activation", file.exists(), is(false) + + // We have to be sure that all the subdirectories of the watched directory are created when the watched service is activated + watchService.activate() + waitForAssert {assertThat watchService.watchService,is(notNullValue())} + + boolean isCreated = file.createNewFile() + assertThat "The file '$file.absolutePath' was not created successfully", isCreated, is(true) + + fullEventAssertionsByKind(fileName, ENTRY_CREATE, false) + + // File modified + file << "Additional content" + fullEventAssertionsByKind(fileName, ENTRY_MODIFY, false) + + // File deleted + boolean isDeleted = file.delete() + assertThat "Test file '$file.absolutePath' is not deleted", isDeleted, is(true) + fullEventAssertionsByKind(fileName, ENTRY_DELETE, true) + } + + void fullEventAssertionsByKind(String fileName, kind, osSpecific){ + waitForAssert{ + assertThat "At least one watch event is expected", + watchService.allFullEvents.size() >= 1, + is(true) + } + + if(osSpecific && kind.equals(ENTRY_DELETE)){ + // There is possibility that one more modify event is triggered on some OS + // so sleep a bit extra time + sleep 500 + cleanUpOsSpecificModifyEvent() + } + + waitForAssert {assertThat "Exactly one event of kind $kind for file $fileName is expected shortly after the file has been altered. Here are the found events: " + watchService.allFullEvents, watchService.allFullEvents.size(), is(1)} + FullEvent fullEvent = watchService.allFullEvents[0] + + assertThat "The path of the processed $kind event should be relative to the watched directory", fullEvent.eventPath.toString(), is(fileName) + assertThat "An event of corresponding kind $kind is expected for file $fileName", fullEvent.eventKind, is(kind) + assertThat "At least one watch event of kind $kind is expected", fullEvent.watchEvent.count() >= 1, is(true) + assertThat "The watch event kind should be the same as the kind provided", fullEvent.watchEvent.kind(), is(fullEvent.eventKind) + def fileNameOnly = fileName.contains(File.separatorChar.toString()) ? fileName.substring(fileName.lastIndexOf(File.separatorChar.toString()) + 1,fileName.length()) : fileName + assertThat "The watch event context have to be the file name only", fullEvent.watchEvent.context().toString(),is(fileNameOnly) + + // Clear all the asserted events + watchService.allFullEvents.clear() + } + + /** + * Cleanup the OS specific ENTRY_MODIFY event as it will not be needed for the assertion + */ + private cleanUpOsSpecificModifyEvent() { + // As the implementation of the watch events is OS specific, it can happen that when the file is deleted two events are fired - ENTRY_MODIFY followed by an ENTRY_DELETE + // This is usually observed on Windows and below is the workaround + // Related discussion in StackOverflow: http://stackoverflow.com/questions/28201283/watchservice-windows-7-when-deleting-a-file-it-fires-both-entry-modify-and-e + boolean isDeletedWithPrecedingModify = watchService.allFullEvents.size() == 2 && watchService.allFullEvents[0].eventKind.equals(ENTRY_MODIFY) + if(isDeletedWithPrecedingModify){ + // Remove the ENTRY_MODIFY element as it is not needed + watchService.allFullEvents.remove(0) + } + } + + class RelativeWatchService extends AbstractWatchService{ + + String rootWatchPath + + boolean watchSubDirs + + boolean watchDirectoryChanges + + // Synchronize list as several watcher threads can write into it + def allFullEvents = [].asSynchronized() + + RelativeWatchService(String rootPath, boolean watchSubDirectories, boolean watchDirChanges){ + rootWatchPath = rootPath + watchSubDirs = watchSubDirectories + watchDirectoryChanges = watchDirChanges + } + + + @Override + protected AbstractWatchQueueReader buildWatchQueueReader(WatchService watchServiceImpl, Path toWatch,Map registeredKeys) { + def queueReader = new AbstractWatchQueueReader(watchServiceImpl, toWatch,registeredKeys) { + @Override + protected void processWatchEvent(WatchEvent event, Kind kind, Path path) { + FullEvent fullEvent = new FullEvent(event,kind,path) + allFullEvents << fullEvent + } + }; + queueReader.setWatchingDirectoryChanges(watchDirectoryChanges) + return queueReader + + } + + @Override + protected String getSourcePath() { + return rootWatchPath + } + + @Override + protected WatchKey registerDirectory(Path path) throws IOException { + WatchKey registrationKey = path.register(watchService,ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY) + return registrationKey + } + + @Override + protected boolean watchSubDirectories() { + return watchSubDirs; + } + } + + class FullEvent{ + WatchEvent watchEvent + Kind eventKind + Path eventPath + + public FullEvent(WatchEvent event, Kind kind, Path path){ + watchEvent = event + eventKind = kind + eventPath = path + } + + @Override + public String toString() { + return "Watch Event: count " + watchEvent.count + "; kind: " + eventKind + "; path: " + eventPath; + } + } +} diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/service/AbstractWatchQueueReader.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/service/AbstractWatchQueueReader.java index 91cd853e136..35e84cd19f9 100644 --- a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/service/AbstractWatchQueueReader.java +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/service/AbstractWatchQueueReader.java @@ -15,15 +15,16 @@ import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.text.MessageFormat; +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 */ public abstract class AbstractWatchQueueReader implements Runnable { @@ -34,13 +35,16 @@ public abstract class AbstractWatchQueueReader implements Runnable { protected WatchService watchService; - protected Path dir; + protected Path baseWatchedDir; + + private Map registeredKeys; + + private boolean watchingDirectoryChanges; /** * Perform a simple cast of given event to WatchEvent * - * @param event - * the event to cast + * @param event the event to cast * @return the casted event */ @SuppressWarnings("unchecked") @@ -49,15 +53,43 @@ static WatchEvent cast(WatchEvent event) { } /** - * Build the object with the given parameters + * Build the {@link AbstractWatchQueueReader} object with the given parameters. The directory changes will be + * watched by default, e.g. watchingDirectoryChanges will be set to true (see + * {@link #setWatchingDirectoryChanges(boolean)}) + * + * @param watchService the watch service. Available to subclasses as {@link #watchService} + * @param watchedDir the base directory, watched by the watch service. Available to subclasses as + * {@link #baseWatchedDir} + * @param registeredKeys a mapping between the {@link WatchKey}s and their corresponding directories, registered + * in the watch service. + */ + public AbstractWatchQueueReader(WatchService watchService, Path watchedDir, Map registeredKeys) { + this.watchService = watchService; + this.baseWatchedDir = watchedDir; + this.registeredKeys = registeredKeys; + setWatchingDirectoryChanges(true); + } + + /** + * Build the {@link AbstractWatchQueueReader} object with the given parameters. The directory changes will be + * watched by default, e.g. watchingDirectoryChanges will be set to true (see + * {@link #setWatchingDirectoryChanges(boolean)}) * - * @param watchService - * the watch service - * @param dir + * @param watchService the watch service + * @param watchedDir the base directory, watched by the watch service. Available to subclasses as + * {@link #baseWatchedDir} + * @param registeredKeys a mapping between the {@link WatchKey}s and their corresponding directories, registered + * in the watch service. + * @param watchingDirectoryChanges whether this queue reader will be watching the directory changes when the watch + * events are processed (for more information see + * {@link #setWatchingDirectoryChanges(boolean)}). */ - public AbstractWatchQueueReader(WatchService watchService, Path dir) { + public AbstractWatchQueueReader(WatchService watchService, Path watchedDir, Map registeredKeys, + boolean watchingDirectoryChanges) { this.watchService = watchService; - this.dir = dir; + this.baseWatchedDir = watchedDir; + this.registeredKeys = registeredKeys; + setWatchingDirectoryChanges(watchingDirectoryChanges); } /* @@ -87,33 +119,85 @@ public void run() { continue; } - // Context for directory entry event is the file name of - // entry - WatchEvent ev = cast(event); - Path path = ev.context(); - - processWatchEvent(event, kind, path); + Path relativePath = resolveToRelativePath(key, event); + if (relativePath != null) { + // Process the event only when a relative path to it is resolved + processWatchEvent(event, kind, relativePath); + } } key.reset(); } } catch (ClosedWatchServiceException ecx) { - logger.debug("ClosedWatchServiceException catched! {}. \n{} Stopping ", ecx.getLocalizedMessage(), Thread - .currentThread().getName()); + logger.debug("ClosedWatchServiceException catched! {}. \n{} Stopping ", ecx.getLocalizedMessage(), + Thread.currentThread().getName()); return; } } + private Path resolveToRelativePath(WatchKey key, WatchEvent event) { + WatchEvent ev = cast(event); + // Context for directory entry event is the file name of + // entry. + Path contextPath = ev.context(); + + Path 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 (!isWatchingDirectoryChanges() && baseWatchedDir.resolve(path).toFile().isDirectory()) { + // As we have found a directory event and do not want to track directory changes - we will skip it + return null; + } + return path; + } + + logger.warn( + "Detected invalid WatchEvent '{}' and key '{}' for entry '{}' in not registered file or directory of '{}'", + event, key, contextPath, baseWatchedDir); + return null; + } + /** - * Processes the given watch event + * Processes the given watch event. Note that the kind and the number of the events for the watched directory is a + * platform dependent (see the "Platform dependencies" sections of {@link WatchService}). * - * @param event - * the watch event to perform - * @param kind - * the event's kind - * @param name - * the path of event + * @param event the watch event to be handled + * @param kind the event's kind + * @param path the path of the event (relative to the {@link #baseWatchedDir} */ protected abstract void processWatchEvent(WatchEvent event, WatchEvent.Kind kind, Path path); + + /** + * If the queue reader is watching the directory changes, all the watch events will be processed. Otherwise the + * events for directories will be skipped. + * + * @return true if the directory events will be processed and false otherwise + */ + public boolean isWatchingDirectoryChanges() { + return watchingDirectoryChanges; + } + + /** + * If the queue reader is watching the directory changes, all the watch events will be processed. Otherwise the + * events for changed directories will be skipped. For example, on some platforms an event for modified directory is + * generated when a new file is created within the directory. However, this behavior could vary a lot, depending on + * the platform (for more information see "Platform dependencies" section in the {@link WatchService} documentation) + * + * @param watchDirectoryChanges set to true if the directory events have to be processed and + * false otherwise + */ + public final void setWatchingDirectoryChanges(boolean watchDirectoryChanges) { + this.watchingDirectoryChanges = watchDirectoryChanges; + } + } \ No newline at end of file diff --git a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/service/AbstractWatchService.java b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/service/AbstractWatchService.java index 51480ba43ad..bdaac607efd 100644 --- a/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/service/AbstractWatchService.java +++ b/bundles/core/org.eclipse.smarthome.core/src/main/java/org/eclipse/smarthome/core/service/AbstractWatchService.java @@ -1,3 +1,4 @@ + /** * Copyright (c) 2014-2016 by the respective copyright holders. * All rights reserved. This program and the accompanying materials @@ -15,9 +16,12 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.SimpleFileVisitor; +import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; @@ -30,6 +34,7 @@ * >java docs for more details * * @author Fabio Marini + * @author Dimitar Ivanov - added javadoc; introduced WatchKey to directory mapping for the queue reader * */ public abstract class AbstractWatchService { @@ -76,25 +81,26 @@ protected void initializeWatchService() { if (StringUtils.isNotBlank(pathToWatch)) { Path toWatch = Paths.get(pathToWatch); try { + final Map registeredWatchKeys = new HashMap<>(); if (watchSubDirectories()) { watchService = FileSystems.getDefault().newWatchService(); // walk through all folders and follow symlinks - Files.walkFileTree(toWatch, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, - new SimpleFileVisitor() { - @Override - public FileVisitResult preVisitDirectory(Path subDir, BasicFileAttributes attrs) - throws IOException { - registerDirectory(subDir); - return FileVisitResult.CONTINUE; - } - }); + Files.walkFileTree(toWatch, EnumSet.of(FileVisitOption.FOLLOW_LINKS), Integer.MAX_VALUE, + new SimpleFileVisitor() { + @Override + public FileVisitResult preVisitDirectory(Path subDir, BasicFileAttributes attrs) + throws IOException { + registerDirectoryInternal(subDir, registeredWatchKeys); + return FileVisitResult.CONTINUE; + } + }); } else { watchService = toWatch.getFileSystem().newWatchService(); - registerDirectory(toWatch); + registerDirectoryInternal(toWatch, registeredWatchKeys); } - AbstractWatchQueueReader reader = buildWatchQueueReader(watchService, toWatch); + AbstractWatchQueueReader reader = buildWatchQueueReader(watchService, toWatch, registeredWatchKeys); Thread qr = new Thread(reader, "Dir Watcher"); qr.start(); @@ -104,41 +110,64 @@ public FileVisitResult preVisitDirectory(Path subDir, BasicFileAttributes attrs) } } + private void registerDirectoryInternal(Path directory, Map registredWatchKeys) throws IOException { + WatchKey registrationKey = registerDirectory(directory); + if (registrationKey != null) { + registredWatchKeys.put(registrationKey, directory); + } else { + logger.info("The directory '{}' was not registered in the watch service", directory); + } + } + + /** + * This method will close the {@link #watchService}. + */ protected void stopWatchService() { - if(watchService != null) { - try { - watchService.close(); - } catch (IOException e) { - logger.warn("Cannot deactivate folder watcher", e); - } - - watchService = null; - } + if (watchService != null) { + try { + watchService.close(); + } catch (IOException e) { + logger.warn("Cannot deactivate folder watcher", e); + } + + watchService = null; + } } /** + * Build a queue reader to process the watch events, provided by the watch service for the given directory * - * @param watchService - * @param toWatch - * @return + * @param watchService the watch service, providing the watch events for the watched directory + * @param toWatch the directory being watched by the watch service + * @param registredWatchKeys a mapping between the registered directories and their {@link WatchKey registration + * keys}. + * @return the concrete queue reader */ - protected abstract AbstractWatchQueueReader buildWatchQueueReader(WatchService watchService, Path toWatch); + protected abstract AbstractWatchQueueReader buildWatchQueueReader(WatchService watchService, Path toWatch, + Map registredWatchKeys); /** - * - * @return + * @return the path to be watched as a {@link String}. The returned path should be applicable for creating a + * {@link Path} with the {@link Paths#get(String, String...)} method. */ protected abstract String getSourcePath(); /** + * Determines whether the subdirectories of the source path (determined by the {@link #getSourcePath()}) will be + * watched or not. * - * @return + * @return true if the subdirectories will be watched and false if only the source path + * (determined by the {@link #getSourcePath()}) will be watched */ protected abstract boolean watchSubDirectories(); /** - * @param subDir - * @throws IOException + * Registers a directory to be watched by the watch service. The {@link WatchKey} of the registration should be + * provided. + * + * @param directory the directory, which will be registered in the watch service + * @return The {@link WatchKey} of the registration or null if no registration has been done. + * @throws IOException if an error occurs while processing the given path */ - protected abstract void registerDirectory(Path subDir) throws IOException; + protected abstract WatchKey registerDirectory(Path directory) throws IOException; } diff --git a/bundles/model/org.eclipse.smarthome.model.core/src/main/java/org/eclipse/smarthome/model/core/internal/folder/FolderObserver.java b/bundles/model/org.eclipse.smarthome.model.core/src/main/java/org/eclipse/smarthome/model/core/internal/folder/FolderObserver.java index 115702870ea..db973dead72 100644 --- a/bundles/model/org.eclipse.smarthome.model.core/src/main/java/org/eclipse/smarthome/model/core/internal/folder/FolderObserver.java +++ b/bundles/model/org.eclipse.smarthome.model.core/src/main/java/org/eclipse/smarthome/model/core/internal/folder/FolderObserver.java @@ -15,6 +15,7 @@ import java.nio.file.Path; import java.nio.file.WatchEvent; import java.nio.file.WatchEvent.Kind; +import java.nio.file.WatchKey; import java.nio.file.WatchService; import java.util.Dictionary; import java.util.Enumeration; @@ -97,8 +98,9 @@ private void processIgnoredFiles(String extension) { } @Override - protected AbstractWatchQueueReader buildWatchQueueReader(WatchService watchService, Path toWatch) { - return new WatchQueueReader(watchService, toWatch, folderFileExtMap, modelRepo); + protected AbstractWatchQueueReader buildWatchQueueReader(WatchService watchService, Path toWatch, + Map registeredKeys) { + return new WatchQueueReader(watchService, toWatch, registeredKeys, folderFileExtMap, modelRepo); } @Override @@ -112,13 +114,15 @@ protected boolean watchSubDirectories() { } @Override - protected void registerDirectory(Path subDir) throws IOException { + protected WatchKey registerDirectory(Path subDir) throws IOException { if (subDir != null && MapUtils.isNotEmpty(folderFileExtMap)) { String folderName = subDir.getFileName().toString(); if (folderFileExtMap.containsKey(folderName)) { - subDir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); + WatchKey registrationKey = subDir.register(watchService, ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY); + return registrationKey; } } + return null; } private static class WatchQueueReader extends AbstractWatchQueueReader { @@ -127,9 +131,10 @@ private static class WatchQueueReader extends AbstractWatchQueueReader { private ModelRepository modelRepo = null; - public WatchQueueReader(WatchService watchService, Path dirToWatch, Map folderFileExtMap, + public WatchQueueReader(WatchService watchService, Path dirToWatch, Map registeredKeys, + Map folderFileExtMap, ModelRepository modelRepo) { - super(watchService, dirToWatch); + super(watchService, dirToWatch,registeredKeys); this.folderFileExtMap = folderFileExtMap; this.modelRepo = modelRepo; @@ -137,7 +142,7 @@ public WatchQueueReader(WatchService watchService, Path dirToWatch, Map event, Kind kind, Path path) { - File toCheck = getFileByFileExtMap(folderFileExtMap, path.toString()); + File toCheck = getFileByFileExtMap(folderFileExtMap, path.getFileName().toString()); if (toCheck != null) { checkFile(modelRepo, toCheck, kind); }