diff --git a/bom/compile/pom.xml b/bom/compile/pom.xml index 26479ce8a49..023dc520f3d 100644 --- a/bom/compile/pom.xml +++ b/bom/compile/pom.xml @@ -338,6 +338,25 @@ compile + + + org.openhab.osgiify + io.methvin.directory-watcher + 0.17.1 + compile + + + net.java.dev.jna + jna + 5.12.1 + compile + + + net.java.dev.jna + jna-platform + 5.12.1 + compile + diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index a7e3e3d0fcb..04c41d83789 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -1039,6 +1039,26 @@ 3.27.0-GA compile + + + + org.openhab.osgiify + io.methvin.directory-watcher + 0.17.1 + compile + + + net.java.dev.jna + jna + 5.12.1 + compile + + + net.java.dev.jna + jna-platform + 5.12.1 + compile + diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/DefaultScriptFileWatcher.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/DefaultScriptFileWatcher.java index 4d516b2de64..a962b2ff866 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/DefaultScriptFileWatcher.java +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/DefaultScriptFileWatcher.java @@ -19,9 +19,9 @@ import org.openhab.core.automation.module.script.rulesupport.loader.AbstractScriptFileWatcher; import org.openhab.core.service.ReadyService; import org.openhab.core.service.StartLevelService; +import org.openhab.core.service.WatchService; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; /** @@ -37,20 +37,10 @@ public class DefaultScriptFileWatcher extends AbstractScriptFileWatcher { private static final String FILE_DIRECTORY = "automation" + File.separator + "jsr223"; @Activate - public DefaultScriptFileWatcher(final @Reference ScriptEngineManager manager, - final @Reference ReadyService readyService, final @Reference StartLevelService startLevelService) { - super(manager, readyService, startLevelService, FILE_DIRECTORY); - } - - @Activate - @Override - public void activate() { - super.activate(); - } - - @Deactivate - @Override - public void deactivate() { - super.deactivate(); + public DefaultScriptFileWatcher( + final @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService, + final @Reference ScriptEngineManager manager, final @Reference ReadyService readyService, + final @Reference StartLevelService startLevelService) { + super(watchService, manager, readyService, startLevelService, FILE_DIRECTORY, true); } } diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java index b1e8b23b8bd..f95eda44054 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTracker.java @@ -12,23 +12,21 @@ */ package org.openhab.core.automation.module.script.rulesupport.loader; -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 org.openhab.core.service.WatchService.Kind.CREATE; +import static org.openhab.core.service.WatchService.Kind.DELETE; +import static org.openhab.core.service.WatchService.Kind.MODIFY; import java.io.File; import java.nio.file.Path; -import java.nio.file.WatchEvent; import java.util.HashSet; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.automation.module.script.ScriptDependencyTracker; import org.openhab.core.automation.module.script.rulesupport.internal.loader.BidiSetBag; -import org.openhab.core.service.AbstractWatchService; +import org.openhab.core.service.WatchService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,54 +39,34 @@ * @author Jan N. Klug - Refactored to OSGi service */ @NonNullByDefault -public abstract class AbstractScriptDependencyTracker implements ScriptDependencyTracker { +public abstract class AbstractScriptDependencyTracker + implements ScriptDependencyTracker, WatchService.WatchEventListener { private final Logger logger = LoggerFactory.getLogger(AbstractScriptDependencyTracker.class); - protected final String libraryPath; + protected final Path libraryPath; private final Set dependencyChangeListeners = ConcurrentHashMap.newKeySet(); private final BidiSetBag scriptToLibs = new BidiSetBag<>(); - private @Nullable AbstractWatchService dependencyWatchService; + private final WatchService watchService; - public AbstractScriptDependencyTracker(final String libraryPath) { - this.libraryPath = libraryPath; - } + public AbstractScriptDependencyTracker(WatchService watchService, final String libraryPath) { + this.libraryPath = Path.of(libraryPath); + this.watchService = watchService; - public void activate() { - AbstractWatchService dependencyWatchService = createDependencyWatchService(); - dependencyWatchService.activate(); - this.dependencyWatchService = dependencyWatchService; + watchService.registerListener(this, this.libraryPath); } public void deactivate() { - AbstractWatchService dependencyWatchService = this.dependencyWatchService; - if (dependencyWatchService != null) { - dependencyWatchService.deactivate(); - } + watchService.unregisterListener(this); } - protected AbstractWatchService createDependencyWatchService() { - return new AbstractWatchService(libraryPath) { - @Override - protected boolean watchSubDirectories() { - return true; - } - - @Override - protected WatchEvent.Kind @Nullable [] getWatchEventKinds(Path path) { - return new WatchEvent.Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; - } - - @Override - protected void processWatchEvent(WatchEvent watchEvent, WatchEvent.Kind kind, Path path) { - File file = path.toFile(); - if (!file.isHidden() && (kind.equals(ENTRY_DELETE) - || (file.canRead() && (kind.equals(ENTRY_CREATE) || kind.equals(ENTRY_MODIFY))))) { - dependencyChanged(file.getPath()); - } - } - }; + @Override + public void processWatchEvent(WatchService.Kind kind, Path path) { + File file = path.toFile(); + if (!file.isHidden() && (kind == DELETE || (file.canRead() && (kind == CREATE || kind == MODIFY)))) { + dependencyChanged(file.getPath()); + } } protected void dependencyChanged(String dependency) { diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptFileWatcher.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptFileWatcher.java index 6f1b2dae07e..793c570d325 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptFileWatcher.java +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptFileWatcher.java @@ -12,7 +12,9 @@ */ package org.openhab.core.automation.module.script.rulesupport.loader; -import static java.nio.file.StandardWatchEventKinds.*; +import static org.openhab.core.service.WatchService.Kind.CREATE; +import static org.openhab.core.service.WatchService.Kind.DELETE; +import static org.openhab.core.service.WatchService.Kind.MODIFY; import java.io.File; import java.io.IOException; @@ -20,8 +22,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.WatchEvent; -import java.nio.file.WatchEvent.Kind; import java.util.Collection; import java.util.List; import java.util.Map; @@ -48,11 +48,11 @@ import org.openhab.core.automation.module.script.ScriptEngineManager; import org.openhab.core.automation.module.script.rulesupport.internal.loader.ScriptFileReference; import org.openhab.core.common.NamedThreadFactory; -import org.openhab.core.service.AbstractWatchService; import org.openhab.core.service.ReadyMarker; import org.openhab.core.service.ReadyMarkerFilter; import org.openhab.core.service.ReadyService; import org.openhab.core.service.StartLevelService; +import org.openhab.core.service.WatchService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -67,7 +67,7 @@ * @author Jan N. Klug - Refactored dependency tracking to script engine factories */ @NonNullByDefault -public abstract class AbstractScriptFileWatcher extends AbstractWatchService implements ReadyService.ReadyTracker, +public abstract class AbstractScriptFileWatcher implements WatchService.WatchEventListener, ReadyService.ReadyTracker, ScriptDependencyTracker.Listener, ScriptEngineManager.FactoryChangeListener, ScriptFileWatcher { private static final Set EXCLUDED_FILE_EXTENSIONS = Set.of("txt", "old", "example", "backup", "md", "swp", @@ -82,6 +82,9 @@ public abstract class AbstractScriptFileWatcher extends AbstractWatchService imp private final ScriptEngineManager manager; private final ReadyService readyService; + private final WatchService watchService; + private final Path watchPath; + private final boolean watchSubDirectories; protected ScheduledExecutorService scheduler; @@ -89,13 +92,16 @@ public abstract class AbstractScriptFileWatcher extends AbstractWatchService imp private final Map scriptLockMap = new ConcurrentHashMap<>(); private final CompletableFuture<@Nullable Void> initialized = new CompletableFuture<>(); - private volatile int currentStartLevel = 0; + private volatile int currentStartLevel; - public AbstractScriptFileWatcher(final ScriptEngineManager manager, final ReadyService readyService, - final StartLevelService startLevelService, final String fileDirectory) { - super(OpenHAB.getConfigFolder() + File.separator + fileDirectory); + public AbstractScriptFileWatcher(final WatchService watchService, final ScriptEngineManager manager, + final ReadyService readyService, final StartLevelService startLevelService, final String fileDirectory, + boolean watchSubDirectories) { + this.watchService = watchService; this.manager = manager; this.readyService = readyService; + this.watchSubDirectories = watchSubDirectories; + this.watchPath = Path.of(OpenHAB.getConfigFolder()).resolve(fileDirectory); manager.addFactoryChangeListener(this); readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE)); @@ -108,13 +114,6 @@ public AbstractScriptFileWatcher(final ScriptEngineManager manager, final ReadyS } } - @Override - public void activate() { - // TODO: needed to initialize underlying AbstractWatchService, should be removed when we switch to PR - // openhab-core#3004 - super.activate(); - } - /** * Can be overridden by subclasses (e.g. for testing) * @@ -124,11 +123,23 @@ protected ScheduledExecutorService getScheduler() { return Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("scriptwatcher")); } - @Override + public void activate() { + if (!Files.exists(watchPath)) { + try { + Files.createDirectories(watchPath); + } catch (IOException e) { + logger.warn("Failed to create watched directory: {}", watchPath); + } + } else if (!Files.isDirectory(watchPath)) { + logger.warn("Trying to watch directory {}, however it is a file", watchPath); + } + watchService.registerListener(this, watchPath, watchSubDirectories); + } + public void deactivate() { + watchService.unregisterListener(this); manager.removeFactoryChangeListener(this); readyService.unregisterTracker(this); - super.deactivate(); CompletableFuture.allOf( Set.copyOf(scriptMap.keySet()).stream().map(this::removeFile).toArray(CompletableFuture[]::new)) @@ -200,22 +211,12 @@ private List listFiles(Path path, boolean includeSubDirectory) { } @Override - protected boolean watchSubDirectories() { - return true; - } - - @Override - protected Kind @Nullable [] getWatchEventKinds(@Nullable Path subDir) { - return new Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; - } - - @Override - protected void processWatchEvent(@Nullable WatchEvent event, @Nullable Kind kind, @Nullable Path path) { + public void processWatchEvent(WatchService.Kind kind, Path path) { File file = path.toFile(); if (!file.isHidden()) { - if (ENTRY_DELETE.equals(kind)) { + if (kind == DELETE) { if (file.isDirectory()) { - if (watchSubDirectories()) { + if (watchSubDirectories) { synchronized (this) { String prefix = path.getParent().toString(); Set toRemove = scriptMap.keySet().stream().filter(ref -> ref.startsWith(prefix)) @@ -228,8 +229,8 @@ protected void processWatchEvent(@Nullable WatchEvent event, @Nullable Kind initialized.complete(null)); + addFiles(listFiles(directory.toPath(), watchSubDirectories)).thenRun(() -> initialized.complete(null)); } @Override diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTrackerTest.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTrackerTest.java index f7d317e5033..4d336b2f450 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTrackerTest.java +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptDependencyTrackerTest.java @@ -12,9 +12,6 @@ */ package org.openhab.core.automation.module.script.rulesupport.loader; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.notNullValue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -24,13 +21,16 @@ import java.nio.file.Path; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.openhab.core.automation.module.script.ScriptDependencyTracker; -import org.openhab.core.service.AbstractWatchService; +import org.openhab.core.service.WatchService; /** * The {@link AbstractScriptDependencyTrackerTest} contains tests for the {@link AbstractScriptDependencyTracker} @@ -38,32 +38,19 @@ * @author Jan N. Klug - Initial contribution */ @NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class AbstractScriptDependencyTrackerTest { private static final String WATCH_DIR = "test"; - private @Nullable AbstractWatchService dependencyWatchService; - private @NonNullByDefault({}) AbstractScriptDependencyTracker scriptDependencyTracker; + private @Mock @NonNullByDefault({}) WatchService watchServiceMock; @BeforeEach public void setup() { - scriptDependencyTracker = new AbstractScriptDependencyTracker(WATCH_DIR) { - - @Override - protected AbstractWatchService createDependencyWatchService() { - AbstractWatchService dependencyWatchService = Mockito.spy(super.createDependencyWatchService()); - AbstractScriptDependencyTrackerTest.this.dependencyWatchService = dependencyWatchService; - return dependencyWatchService; - } - - @Override - public void dependencyChanged(String dependency) { - super.dependencyChanged(dependency); - } + scriptDependencyTracker = new AbstractScriptDependencyTracker(watchServiceMock, WATCH_DIR) { }; - - scriptDependencyTracker.activate(); } @AfterEach @@ -73,18 +60,14 @@ public void tearDown() { @Test public void testScriptLibraryWatcherIsCreatedAndActivated() { - assertThat(dependencyWatchService, is(notNullValue())); - - assertThat(dependencyWatchService.getSourcePath(), is(Path.of(WATCH_DIR))); - - verify(dependencyWatchService).activate(); + verify(watchServiceMock).registerListener(eq(scriptDependencyTracker), eq(Path.of(WATCH_DIR))); } @Test public void testScriptLibraryWatchersIsDeactivatedOnShutdown() { scriptDependencyTracker.deactivate(); - verify(dependencyWatchService).deactivate(); + verify(watchServiceMock).unregisterListener(eq(scriptDependencyTracker)); } @Test diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptFileWatcherTest.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptFileWatcherTest.java index a2f8e91d210..2dd471f36c9 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptFileWatcherTest.java +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/test/java/org/openhab/core/automation/module/script/rulesupport/loader/AbstractScriptFileWatcherTest.java @@ -19,6 +19,9 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import static org.openhab.core.OpenHAB.CONFIG_DIR_PROG_ARGUMENT; +import static org.openhab.core.service.WatchService.Kind.CREATE; +import static org.openhab.core.service.WatchService.Kind.DELETE; +import static org.openhab.core.service.WatchService.Kind.MODIFY; import java.io.File; import java.io.IOException; @@ -50,6 +53,7 @@ import org.openhab.core.service.ReadyMarker; import org.openhab.core.service.ReadyService; import org.openhab.core.service.StartLevelService; +import org.openhab.core.service.WatchService; import org.openhab.core.test.java.JavaTest; import org.opentest4j.AssertionFailedError; @@ -64,13 +68,13 @@ @MockitoSettings(strictness = Strictness.LENIENT) class AbstractScriptFileWatcherTest extends JavaTest { - private boolean watchSubDirectories = true; private @NonNullByDefault({}) AbstractScriptFileWatcher scriptFileWatcher; private @Mock @NonNullByDefault({}) ScriptEngineManager scriptEngineManagerMock; private @Mock @NonNullByDefault({}) ScriptDependencyTracker scriptDependencyTrackerMock; private @Mock @NonNullByDefault({}) StartLevelService startLevelServiceMock; private @Mock @NonNullByDefault({}) ReadyService readyServiceMock; + private @Mock @NonNullByDefault({}) WatchService watchServiceMock; protected @NonNullByDefault({}) @TempDir Path tempScriptDir; @@ -87,8 +91,6 @@ public void setUp() { // ensure initialize is not called on initialization when(startLevelServiceMock.getStartLevel()).thenAnswer(invocation -> currentStartLevel); - - scriptFileWatcher = createScriptFileWatcher(); } @AfterEach @@ -98,6 +100,7 @@ public void tearDown() { @Test public void testLoadOneDefaultFileAlreadyStarted() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -107,13 +110,14 @@ public void testLoadOneDefaultFileAlreadyStarted() { Path p = getFile("script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); + scriptFileWatcher.processWatchEvent(CREATE, p); verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p.toString()); } @Test public void testSubDirectoryIncludedInInitialImport() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -134,12 +138,13 @@ public void testSubDirectoryIncludedInInitialImport() { @Test public void testSubDirectoryIgnoredInInitialImport() { + scriptFileWatcher = createScriptFileWatcher(false); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); when(scriptEngineManagerMock.createScriptEngine(anyString(), anyString())).thenReturn(scriptEngineContainer); when(scriptEngineManagerMock.loadScript(any(), any())).thenReturn(true); - watchSubDirectories = false; + Path p0 = getFile("script.js"); Path p1 = getFile("dir/script.js"); @@ -154,6 +159,7 @@ public void testSubDirectoryIgnoredInInitialImport() { @Test public void testLoadOneDefaultFileWaitUntilStarted() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -162,7 +168,7 @@ public void testLoadOneDefaultFileWaitUntilStarted() { updateStartLevel(20); Path p = getFile("script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); + scriptFileWatcher.processWatchEvent(CREATE, p); awaitEmptyQueue(); @@ -179,6 +185,7 @@ public void testLoadOneDefaultFileWaitUntilStarted() { @Test public void testLoadOneCustomFileWaitUntilStarted() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -188,7 +195,7 @@ public void testLoadOneCustomFileWaitUntilStarted() { updateStartLevel(50); Path p = getFile("script.sl60.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); + scriptFileWatcher.processWatchEvent(CREATE, p); awaitEmptyQueue(); @@ -205,6 +212,7 @@ public void testLoadOneCustomFileWaitUntilStarted() { @Test public void testLoadTwoCustomFilesDifferentStartLevels() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -214,8 +222,8 @@ public void testLoadTwoCustomFilesDifferentStartLevels() { Path p1 = getFile("script.sl70.js"); Path p2 = getFile("script.sl50.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p1); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p2); + scriptFileWatcher.processWatchEvent(CREATE, p1); + scriptFileWatcher.processWatchEvent(CREATE, p2); awaitEmptyQueue(); @@ -245,6 +253,7 @@ public void testLoadTwoCustomFilesDifferentStartLevels() { @Test public void testLoadTwoCustomFilesAlternativePatternDifferentStartLevels() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -252,9 +261,9 @@ public void testLoadTwoCustomFilesAlternativePatternDifferentStartLevels() { when(scriptEngineManagerMock.loadScript(any(), any())).thenReturn(true); Path p1 = getFile("sl70/script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p1); + scriptFileWatcher.processWatchEvent(CREATE, p1); Path p2 = getFile("sl50/script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p2); + scriptFileWatcher.processWatchEvent(CREATE, p2); // verify not yet called verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString()); @@ -278,6 +287,7 @@ public void testLoadTwoCustomFilesAlternativePatternDifferentStartLevels() { @Test public void testLoadOneDefaultFileDelayedSupport() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(false); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -286,7 +296,7 @@ public void testLoadOneDefaultFileDelayedSupport() { updateStartLevel(100); Path p = getFile("script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); + scriptFileWatcher.processWatchEvent(CREATE, p); // verify not yet called but checked waitForAssert(() -> verify(scriptEngineManagerMock).isSupported(anyString())); @@ -304,6 +314,7 @@ public void testLoadOneDefaultFileDelayedSupport() { @Test public void testOrderingWithinSingleStartLevel() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -312,11 +323,11 @@ public void testOrderingWithinSingleStartLevel() { updateStartLevel(50); Path p64 = getFile("script.sl64.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p64); + scriptFileWatcher.processWatchEvent(CREATE, p64); Path p66 = getFile("script.sl66.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p66); + scriptFileWatcher.processWatchEvent(CREATE, p66); Path p65 = getFile("script.sl65.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p65); + scriptFileWatcher.processWatchEvent(CREATE, p65); updateStartLevel(70); @@ -331,6 +342,7 @@ public void testOrderingWithinSingleStartLevel() { @Test public void testOrderingStartlevelFolders() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -338,11 +350,11 @@ public void testOrderingStartlevelFolders() { when(scriptEngineManagerMock.loadScript(any(), any())).thenReturn(true); Path p50 = getFile("a_script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p50); + scriptFileWatcher.processWatchEvent(CREATE, p50); Path p40 = getFile("sl40/b_script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p40); + scriptFileWatcher.processWatchEvent(CREATE, p40); Path p30 = getFile("sl30/script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p30); + scriptFileWatcher.processWatchEvent(CREATE, p30); awaitEmptyQueue(); @@ -358,6 +370,7 @@ public void testOrderingStartlevelFolders() { @Test public void testReloadActiveWhenDependencyChanged() { + scriptFileWatcher = createScriptFileWatcher(true); ScriptEngineFactory scriptEngineFactoryMock = mock(ScriptEngineFactory.class); when(scriptEngineFactoryMock.getDependencyTracker()).thenReturn(scriptDependencyTrackerMock); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); @@ -370,7 +383,7 @@ public void testReloadActiveWhenDependencyChanged() { Path p = getFile("script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); + scriptFileWatcher.processWatchEvent(CREATE, p); awaitEmptyQueue(); @@ -385,6 +398,7 @@ public void testReloadActiveWhenDependencyChanged() { @Test public void testNotReloadInactiveWhenDependencyChanged() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -394,7 +408,7 @@ public void testNotReloadInactiveWhenDependencyChanged() { Path p = getFile("script.js"); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); + scriptFileWatcher.processWatchEvent(CREATE, p); awaitEmptyQueue(); @@ -407,6 +421,7 @@ public void testNotReloadInactiveWhenDependencyChanged() { @Test public void testRemoveBeforeReAdd() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -419,12 +434,12 @@ public void testRemoveBeforeReAdd() { InOrder inOrder = inOrder(scriptEngineManagerMock); String scriptIdentifier = ScriptFileReference.getScriptIdentifier(p); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); + scriptFileWatcher.processWatchEvent(CREATE, p); awaitEmptyQueue(); inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", scriptIdentifier); - scriptFileWatcher.processWatchEvent(null, ENTRY_MODIFY, p); + scriptFileWatcher.processWatchEvent(MODIFY, p); awaitEmptyQueue(); inOrder.verify(scriptEngineManagerMock, timeout(10000)).removeEngine(scriptIdentifier); @@ -433,6 +448,7 @@ public void testRemoveBeforeReAdd() { @Test public void testDirectoryAdded() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -444,7 +460,7 @@ public void testDirectoryAdded() { Path p2 = getFile("dir/script2.js"); Path d = p1.getParent(); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, d); + scriptFileWatcher.processWatchEvent(CREATE, d); awaitEmptyQueue(); @@ -454,6 +470,7 @@ public void testDirectoryAdded() { @Test public void testDirectoryAddedSubDirIncluded() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -465,7 +482,7 @@ public void testDirectoryAddedSubDirIncluded() { Path p2 = getFile("dir/sub/script.js"); Path d = p1.getParent(); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, d); + scriptFileWatcher.processWatchEvent(CREATE, d); awaitEmptyQueue(); @@ -477,19 +494,19 @@ public void testDirectoryAddedSubDirIncluded() { @Test public void testDirectoryAddedSubDirIgnored() { + scriptFileWatcher = createScriptFileWatcher(false); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); when(scriptEngineManagerMock.createScriptEngine(anyString(), anyString())).thenReturn(scriptEngineContainer); when(scriptEngineManagerMock.loadScript(any(), any())).thenReturn(true); - watchSubDirectories = false; updateStartLevel(100); Path p1 = getFile("dir/script.js"); Path p2 = getFile("dir/sub/script.js"); Path d = p1.getParent(); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, d); + scriptFileWatcher.processWatchEvent(CREATE, d); awaitEmptyQueue(); @@ -500,6 +517,7 @@ public void testDirectoryAddedSubDirIgnored() { @Test public void testSortsAllFilesInNewDirectory() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -511,7 +529,7 @@ public void testSortsAllFilesInNewDirectory() { Path p10 = getFile("dir/script2.sl10.js"); Path d = p10.getParent(); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, d); + scriptFileWatcher.processWatchEvent(CREATE, d); awaitEmptyQueue(); @@ -522,6 +540,7 @@ public void testSortsAllFilesInNewDirectory() { @Test public void testDirectoryRemoved() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -533,9 +552,9 @@ public void testDirectoryRemoved() { Path p2 = getFile("dir/script2.js"); Path d = p1.getParent(); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p1); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p2); - scriptFileWatcher.processWatchEvent(null, ENTRY_DELETE, d); + scriptFileWatcher.processWatchEvent(CREATE, p1); + scriptFileWatcher.processWatchEvent(CREATE, p2); + scriptFileWatcher.processWatchEvent(DELETE, d); awaitEmptyQueue(); @@ -547,6 +566,7 @@ public void testDirectoryRemoved() { @Test public void testScriptEngineRemovedOnFailedLoad() { + scriptFileWatcher = createScriptFileWatcher(true); when(scriptEngineManagerMock.isSupported("js")).thenReturn(true); ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class); when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class)); @@ -558,7 +578,7 @@ public void testScriptEngineRemovedOnFailedLoad() { when(scriptEngineContainer.getIdentifier()).thenReturn(ScriptFileReference.getScriptIdentifier(p)); - scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); + scriptFileWatcher.processWatchEvent(CREATE, p); awaitEmptyQueue(); @@ -572,6 +592,7 @@ public void testScriptEngineRemovedOnFailedLoad() { @Test public void testIfInitializedForEarlyInitialization() { + scriptFileWatcher = createScriptFileWatcher(true); CompletableFuture initialized = scriptFileWatcher.ifInitialized(); assertThat(initialized.isDone(), is(false)); @@ -582,6 +603,7 @@ public void testIfInitializedForEarlyInitialization() { @Test public void testIfInitializedForLateInitialization() { + scriptFileWatcher = createScriptFileWatcher(true); when(startLevelServiceMock.getStartLevel()).thenReturn(StartLevelService.STARTLEVEL_RULEENGINE); AbstractScriptFileWatcher watcher = createScriptFileWatcher(); @@ -622,17 +644,17 @@ private void updateStartLevel(int level) { } private AbstractScriptFileWatcher createScriptFileWatcher() { - return new AbstractScriptFileWatcher(scriptEngineManagerMock, readyServiceMock, startLevelServiceMock, "") { + return createScriptFileWatcher(false); + } + + private AbstractScriptFileWatcher createScriptFileWatcher(boolean watchSubDirectories) { + return new AbstractScriptFileWatcher(watchServiceMock, scriptEngineManagerMock, readyServiceMock, + startLevelServiceMock, "", watchSubDirectories) { @Override protected ScheduledExecutorService getScheduler() { return new CountingScheduledExecutor(atomicInteger); } - - @Override - protected boolean watchSubDirectories() { - return watchSubDirectories; - } }; } diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/AutomationWatchService.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/AutomationWatchService.java index a61325033b0..cfa59acf274 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/AutomationWatchService.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/AutomationWatchService.java @@ -12,16 +12,11 @@ */ package org.openhab.core.automation.internal.provider.file; -import static java.nio.file.StandardWatchEventKinds.*; - import java.io.File; import java.nio.file.Path; -import java.nio.file.WatchEvent; -import java.nio.file.WatchEvent.Kind; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.service.AbstractWatchService; +import org.openhab.core.service.WatchService; /** * This class is an implementation of {@link AbstractWatchService} which is responsible for tracking changes in file @@ -33,30 +28,35 @@ */ @SuppressWarnings("rawtypes") @NonNullByDefault -public class AutomationWatchService extends AbstractWatchService { +public class AutomationWatchService implements WatchService.WatchEventListener { + private final WatchService watchService; + private final Path watchingDir; private AbstractFileProvider provider; - public AutomationWatchService(AbstractFileProvider provider, String watchingDir) { - super(watchingDir); + public AutomationWatchService(AbstractFileProvider provider, WatchService watchService, String watchingDir) { + this.watchService = watchService; + this.watchingDir = Path.of(watchingDir); this.provider = provider; } - @Override - protected boolean watchSubDirectories() { - return true; + public void activate() { + watchService.registerListener(this, watchingDir); } - @Override - protected Kind @Nullable [] getWatchEventKinds(Path subDir) { - return new Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; + public void deactivate() { + watchService.unregisterListener(this); + } + + public Path getSourcePath() { + return watchingDir; } @Override - protected void processWatchEvent(WatchEvent event, Kind kind, Path path) { + public void processWatchEvent(WatchService.Kind kind, Path path) { File file = path.toFile(); if (!file.isHidden()) { - if (ENTRY_DELETE.equals(kind)) { + if (kind == WatchService.Kind.DELETE) { provider.removeResources(file); } else if (file.canRead()) { provider.importResources(file); diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/ModuleTypeFileProviderWatcher.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/ModuleTypeFileProviderWatcher.java index ebc442ae428..4a78cd492ba 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/ModuleTypeFileProviderWatcher.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/ModuleTypeFileProviderWatcher.java @@ -18,6 +18,8 @@ import org.openhab.core.automation.parser.Parser; import org.openhab.core.automation.type.ModuleType; import org.openhab.core.automation.type.ModuleTypeProvider; +import org.openhab.core.service.WatchService; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; @@ -32,9 +34,17 @@ @Component(immediate = true, service = ModuleTypeProvider.class) public class ModuleTypeFileProviderWatcher extends ModuleTypeFileProvider { + private final WatchService watchService; + + @Activate + public ModuleTypeFileProviderWatcher( + @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) { + this.watchService = watchService; + } + @Override protected void initializeWatchService(String watchingDir) { - WatchServiceUtil.initializeWatchService(watchingDir, this); + WatchServiceUtil.initializeWatchService(watchingDir, this, watchService); } @Override diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/TemplateFileProviderWatcher.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/TemplateFileProviderWatcher.java index c464faaca9f..aa01bb79d53 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/TemplateFileProviderWatcher.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/TemplateFileProviderWatcher.java @@ -19,6 +19,8 @@ import org.openhab.core.automation.template.RuleTemplate; import org.openhab.core.automation.template.RuleTemplateProvider; import org.openhab.core.automation.template.TemplateProvider; +import org.openhab.core.service.WatchService; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; @@ -33,9 +35,17 @@ @Component(immediate = true, service = RuleTemplateProvider.class) public class TemplateFileProviderWatcher extends TemplateFileProvider { + private final WatchService watchService; + + @Activate + public TemplateFileProviderWatcher( + @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) { + this.watchService = watchService; + } + @Override protected void initializeWatchService(String watchingDir) { - WatchServiceUtil.initializeWatchService(watchingDir, this); + WatchServiceUtil.initializeWatchService(watchingDir, this, watchService); } @Override diff --git a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/WatchServiceUtil.java b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/WatchServiceUtil.java index 87e16c4cf32..69095d27c6b 100644 --- a/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/WatchServiceUtil.java +++ b/bundles/org.openhab.core.automation/src/main/java/org/openhab/core/automation/internal/provider/file/WatchServiceUtil.java @@ -18,6 +18,7 @@ import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.service.WatchService; /** * This class isolates the java 1.7 functionality which tracks the file system changes. @@ -30,7 +31,8 @@ public class WatchServiceUtil { private static final Map> WATCH_SERVICES = new HashMap<>(); - public static void initializeWatchService(String watchingDir, AbstractFileProvider provider) { + public static void initializeWatchService(String watchingDir, AbstractFileProvider provider, + WatchService watchService) { AutomationWatchService aws = null; synchronized (WATCH_SERVICES) { Map watchers = WATCH_SERVICES.get(provider); @@ -39,7 +41,7 @@ public static void initializeWatchService(String watchingDir, AbstractFileProvid WATCH_SERVICES.put(provider, watchers); } if (watchers.get(watchingDir) == null) { - aws = new AutomationWatchService(provider, watchingDir); + aws = new AutomationWatchService(provider, watchService, watchingDir); watchers.put(watchingDir, aws); } } @@ -63,9 +65,7 @@ public static void deactivateWatchService(String watchingDir, AbstractFileProvid if (aws != null) { aws.deactivate(); Path sourcePath = aws.getSourcePath(); - if (sourcePath != null) { - provider.removeResources(sourcePath.toFile()); - } + provider.removeResources(sourcePath.toFile()); } } } diff --git a/bundles/org.openhab.core.config.dispatch/src/main/java/org/openhab/core/config/dispatch/internal/ConfigDispatcherFileWatcher.java b/bundles/org.openhab.core.config.dispatch/src/main/java/org/openhab/core/config/dispatch/internal/ConfigDispatcherFileWatcher.java index 0d6749f4f71..01aba7849b6 100644 --- a/bundles/org.openhab.core.config.dispatch/src/main/java/org/openhab/core/config/dispatch/internal/ConfigDispatcherFileWatcher.java +++ b/bundles/org.openhab.core.config.dispatch/src/main/java/org/openhab/core/config/dispatch/internal/ConfigDispatcherFileWatcher.java @@ -12,21 +12,20 @@ */ package org.openhab.core.config.dispatch.internal; -import static java.nio.file.StandardWatchEventKinds.*; - import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.WatchEvent; -import java.nio.file.WatchEvent.Kind; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.OpenHAB; -import org.openhab.core.service.AbstractWatchService; +import org.openhab.core.service.WatchService; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Watches file-system events and passes them to our {@link ConfigDispatcher} @@ -36,69 +35,52 @@ */ @Component(immediate = true) @NonNullByDefault -public class ConfigDispatcherFileWatcher extends AbstractWatchService { - - /** The program argument name for setting the service config directory path */ +public class ConfigDispatcherFileWatcher implements WatchService.WatchEventListener { public static final String SERVICEDIR_PROG_ARGUMENT = "openhab.servicedir"; /** The default folder name of the configuration folder of services */ public static final String SERVICES_FOLDER = "services"; + private final Logger logger = LoggerFactory.getLogger(ConfigDispatcherFileWatcher.class); private final ConfigDispatcher configDispatcher; + private final WatchService watchService; @Activate - public ConfigDispatcherFileWatcher(final @Reference ConfigDispatcher configDispatcher) { - super(getPathToWatch()); + public ConfigDispatcherFileWatcher(final @Reference ConfigDispatcher configDispatcher, + final @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) { this.configDispatcher = configDispatcher; - } - private static String getPathToWatch() { - String progArg = System.getProperty(SERVICEDIR_PROG_ARGUMENT); - if (progArg != null) { - return OpenHAB.getConfigFolder() + File.separator + progArg; - } else { - return OpenHAB.getConfigFolder() + File.separator + SERVICES_FOLDER; - } - } + String servicesFolder = System.getProperty(SERVICEDIR_PROG_ARGUMENT, SERVICES_FOLDER); - @Activate - @Override - public void activate() { - super.activate(); - configDispatcher.processConfigFile(getSourcePath().toFile()); + this.watchService = watchService; + + watchService.registerListener(this, Path.of(servicesFolder)); + configDispatcher.processConfigFile(Path.of(OpenHAB.getConfigFolder(), servicesFolder).toFile()); } @Deactivate - @Override public void deactivate() { - super.deactivate(); - } - - @Override - protected boolean watchSubDirectories() { - return false; + watchService.unregisterListener(this); } @Override - protected Kind @Nullable [] getWatchEventKinds(Path subDir) { - return new Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; - } - - @Override - protected void processWatchEvent(WatchEvent event, Kind kind, Path path) { - if (kind == ENTRY_CREATE || kind == ENTRY_MODIFY) { - File f = path.toFile(); - if (!f.isHidden() && f.getName().endsWith(".cfg")) { - configDispatcher.processConfigFile(f); - } - } else if (kind == ENTRY_DELETE) { - // Detect if a service specific configuration file was removed. We want to - // notify the service in this case with an updated empty configuration. - File configFile = path.toFile(); - if (configFile.isHidden() || configFile.isDirectory() || !configFile.getName().endsWith(".cfg")) { - return; + public void processWatchEvent(WatchService.Kind kind, Path path) { + try { + if (kind == WatchService.Kind.CREATE || kind == WatchService.Kind.MODIFY) { + if (!Files.isHidden(path) && path.toString().endsWith(".cfg")) { + configDispatcher.processConfigFile(path.toFile()); + } + } else if (kind == WatchService.Kind.DELETE) { + // Detect if a service specific configuration file was removed. We want to + // notify the service in this case with an updated empty configuration. + File configFile = path.toFile(); + if (Files.isHidden(path) || Files.isDirectory(path) || !path.toString().endsWith(".cfg")) { + return; + } + configDispatcher.fileRemoved(configFile.getAbsolutePath()); } - configDispatcher.fileRemoved(configFile.getAbsolutePath()); + } catch (IOException e) { + logger.error("Failed to process watch event {} for {}", kind, path); } } } diff --git a/bundles/org.openhab.core.config.dispatch/src/test/java/org/openhab/core/config/dispatch/internal/ConfigDispatcherFileWatcherTest.java b/bundles/org.openhab.core.config.dispatch/src/test/java/org/openhab/core/config/dispatch/internal/ConfigDispatcherFileWatcherTest.java index dc7f4e4aea4..c42154be78c 100644 --- a/bundles/org.openhab.core.config.dispatch/src/test/java/org/openhab/core/config/dispatch/internal/ConfigDispatcherFileWatcherTest.java +++ b/bundles/org.openhab.core.config.dispatch/src/test/java/org/openhab/core/config/dispatch/internal/ConfigDispatcherFileWatcherTest.java @@ -14,19 +14,15 @@ import static org.mockito.Mockito.*; -import java.io.File; import java.nio.file.Path; -import java.nio.file.StandardWatchEventKinds; -import java.nio.file.WatchEvent; -import java.nio.file.WatchEvent.Kind; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.openhab.core.service.WatchService; /** * @author Stefan Triller - Initial contribution @@ -35,95 +31,62 @@ @NonNullByDefault public class ConfigDispatcherFileWatcherTest { - private @NonNullByDefault({}) TestConfigDispatcherFileWatcher configDispatcherFileWatcher; + private @NonNullByDefault({}) ConfigDispatcherFileWatcher configDispatcherFileWatcher; private @Mock @NonNullByDefault({}) ConfigDispatcher configDispatcherMock; + private @Mock @NonNullByDefault({}) WatchService watchService; @BeforeEach - public void setUp() throws Exception { - configDispatcherFileWatcher = new TestConfigDispatcherFileWatcher(configDispatcherMock); + public void setUp() { + configDispatcherFileWatcher = new ConfigDispatcherFileWatcher(configDispatcherMock, watchService); + verify(configDispatcherMock).processConfigFile(any()); } @Test public void configurationFileCreated() { - String path = "myPath.cfg"; - configDispatcherFileWatcher.processWatchEvent(new TestWatchEvent(), StandardWatchEventKinds.ENTRY_CREATE, - new File(path).toPath()); + Path path = Path.of("myPath.cfg"); + configDispatcherFileWatcher.processWatchEvent(WatchService.Kind.CREATE, path); - verify(configDispatcherMock).processConfigFile(new File(path)); + verify(configDispatcherMock).processConfigFile(path.toFile()); } @Test public void configurationFileModified() { - String path = "myPath.cfg"; - configDispatcherFileWatcher.processWatchEvent(new TestWatchEvent(), StandardWatchEventKinds.ENTRY_MODIFY, - new File(path).toPath()); + Path path = Path.of("myPath.cfg"); + configDispatcherFileWatcher.processWatchEvent(WatchService.Kind.MODIFY, path); - verify(configDispatcherMock).processConfigFile(new File(path)); + verify(configDispatcherMock).processConfigFile(path.toFile()); } @Test public void nonConfigurationFileCreated() { - String path = "myPath"; - configDispatcherFileWatcher.processWatchEvent(new TestWatchEvent(), StandardWatchEventKinds.ENTRY_CREATE, - new File(path).toPath()); + Path path = Path.of("myPath"); + configDispatcherFileWatcher.processWatchEvent(WatchService.Kind.CREATE, path); - verifyNoInteractions(configDispatcherMock); + verifyNoMoreInteractions(configDispatcherMock); } @Test public void nonConfigurationFileModified() { - String path = "myPath"; - configDispatcherFileWatcher.processWatchEvent(new TestWatchEvent(), StandardWatchEventKinds.ENTRY_MODIFY, - new File(path).toPath()); + Path path = Path.of("myPath"); + configDispatcherFileWatcher.processWatchEvent(WatchService.Kind.MODIFY, path); - verifyNoInteractions(configDispatcherMock); + verifyNoMoreInteractions(configDispatcherMock); } @Test public void configurationFileRemoved() { - String path = "myPath.cfg"; - configDispatcherFileWatcher.processWatchEvent(new TestWatchEvent(), StandardWatchEventKinds.ENTRY_DELETE, - new File(path).toPath()); + Path path = Path.of("myPath.cfg"); + configDispatcherFileWatcher.processWatchEvent(WatchService.Kind.DELETE, path); - verify(configDispatcherMock).fileRemoved(new File(path).getAbsolutePath()); + verify(configDispatcherMock).fileRemoved(path.toAbsolutePath().toString()); } @Test public void nonConfigurationFileRemoved() { - String path = "myPath"; - configDispatcherFileWatcher.processWatchEvent(new TestWatchEvent(), StandardWatchEventKinds.ENTRY_DELETE, - new File(path).toPath()); + Path path = Path.of("myPath"); + configDispatcherFileWatcher.processWatchEvent(WatchService.Kind.DELETE, path); - verifyNoInteractions(configDispatcherMock); - } - - public static class TestConfigDispatcherFileWatcher extends ConfigDispatcherFileWatcher { - public TestConfigDispatcherFileWatcher(ConfigDispatcher configDispatcher) { - super(configDispatcher); - } - - @Override - protected void processWatchEvent(WatchEvent event, Kind kind, Path path) { - super.processWatchEvent(event, kind, path); - } - } - - private static class TestWatchEvent implements WatchEvent<@Nullable Path> { - - @Override - public Kind<@Nullable Path> kind() { - return StandardWatchEventKinds.ENTRY_CREATE; - } - - @Override - public int count() { - return 0; - } - - @Override - public @Nullable Path context() { - return null; - } + verifyNoMoreInteractions(configDispatcherMock); } } diff --git a/bundles/org.openhab.core.model.core/pom.xml b/bundles/org.openhab.core.model.core/pom.xml index 5e07ce56609..3787008343f 100644 --- a/bundles/org.openhab.core.model.core/pom.xml +++ b/bundles/org.openhab.core.model.core/pom.xml @@ -20,6 +20,11 @@ org.openhab.core.config.core ${project.version} + + org.openhab.core.bundles + org.openhab.core.test + ${project.version} + diff --git a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserver.java b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserver.java index 73044e1b2ce..7d3cfacd7f0 100644 --- a/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserver.java +++ b/bundles/org.openhab.core.model.core/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserver.java @@ -13,6 +13,8 @@ package org.openhab.core.model.core.internal.folder; import static java.nio.file.StandardWatchEventKinds.*; +import static org.openhab.core.service.WatchService.Kind.CREATE; +import static org.openhab.core.service.WatchService.Kind.MODIFY; import java.io.File; import java.io.FilenameFilter; @@ -20,14 +22,11 @@ import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.WatchEvent; -import java.nio.file.WatchEvent.Kind; import java.util.Arrays; import java.util.Dictionary; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Set; @@ -39,9 +38,9 @@ import org.openhab.core.OpenHAB; import org.openhab.core.model.core.ModelParser; import org.openhab.core.model.core.ModelRepository; -import org.openhab.core.service.AbstractWatchService; import org.openhab.core.service.ReadyMarker; import org.openhab.core.service.ReadyService; +import org.openhab.core.service.WatchService; import org.osgi.service.component.ComponentContext; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; @@ -63,7 +62,8 @@ */ @NonNullByDefault @Component(name = "org.openhab.core.folder", immediate = true, configurationPid = "org.openhab.folder", configurationPolicy = ConfigurationPolicy.REQUIRE) -public class FolderObserver extends AbstractWatchService { +public class FolderObserver implements WatchService.WatchEventListener { + private final WatchService watchService; private Logger logger = LoggerFactory.getLogger(FolderObserver.class); /* the model repository is provided as a service */ @@ -85,11 +85,11 @@ public class FolderObserver extends AbstractWatchService { private final Map nameFileMap = new HashMap<>(); @Activate - public FolderObserver(final @Reference ModelRepository modelRepo, final @Reference ReadyService readyService) { - super(OpenHAB.getConfigFolder()); - + public FolderObserver(final @Reference ModelRepository modelRepo, final @Reference ReadyService readyService, + final @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) { this.modelRepository = modelRepo; this.readyService = readyService; + this.watchService = watchService; } @Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policy = ReferencePolicy.DYNAMIC) @@ -106,7 +106,7 @@ protected void removeModelParser(ModelParser modelParser) { parsers.remove(modelParser.getExtension()); Set removed = modelRepository.removeAllModelsOfType(modelParser.getExtension()); - ignoredFiles.addAll(removed.stream().map(name -> nameFileMap.get(name)).collect(Collectors.toSet())); + ignoredFiles.addAll(removed.stream().map(nameFileMap::get).collect(Collectors.toSet())); } @Activate @@ -133,16 +133,15 @@ public void activate(ComponentContext ctx) { } } + watchService.registerListener(this, folderFileExtMap.keySet().stream().map(Path::of).toList()); addModelsToRepo(); - super.activate(); this.activated = true; } - @Override @Deactivate public void deactivate() { + watchService.unregisterListener(this); this.activated = false; - super.deactivate(); deleteModelsFromRepo(); this.ignoredFiles.clear(); this.folderFileExtMap.clear(); @@ -154,52 +153,25 @@ private void processIgnoredFiles(String extension) { Set clonedSet = new HashSet<>(this.ignoredFiles); for (File file : clonedSet) { if (extension.equals(getExtension(file.getPath()))) { - checkFile(modelRepository, file, ENTRY_CREATE); + checkFile(modelRepository, file, CREATE); this.ignoredFiles.remove(file); } } } - @Override - protected boolean watchSubDirectories() { - return true; - } - - @Override - protected Kind @Nullable [] getWatchEventKinds(Path directory) { - if (directory != null && isNotEmpty(folderFileExtMap)) { - String folderName = directory.getFileName().toString(); - if (folderFileExtMap.containsKey(folderName)) { - return new Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; - } - } - return null; - } - - private boolean isEmpty(final Map map) { - return map == null || map.isEmpty(); - } - - private boolean isNotEmpty(final Map map) { - return !isEmpty(map); - } - private void addModelsToRepo() { - if (isNotEmpty(folderFileExtMap)) { - Iterator iterator = folderFileExtMap.keySet().iterator(); - while (iterator.hasNext()) { - String folderName = iterator.next(); - + if (!folderFileExtMap.isEmpty()) { + for (String folderName : folderFileExtMap.keySet()) { final String[] validExtension = folderFileExtMap.get(folderName); if (validExtension != null && validExtension.length > 0) { File folder = getFile(folderName); File[] files = folder.listFiles(new FileExtensionsFilter(validExtension)); - if (files != null && files.length > 0) { + if (files != null) { for (File file : files) { // we omit parsing of hidden files possibly created by editors or operating systems if (!file.isHidden()) { - checkFile(modelRepository, file, ENTRY_CREATE); + checkFile(modelRepository, file, CREATE); } } } @@ -222,7 +194,7 @@ private void deleteModelsFromRepo() { } } - protected class FileExtensionsFilter implements FilenameFilter { + protected static class FileExtensionsFilter implements FilenameFilter { private final String[] validExtensions; @@ -232,11 +204,9 @@ public FileExtensionsFilter(String[] validExtensions) { @Override public boolean accept(@NonNullByDefault({}) File dir, @NonNullByDefault({}) String name) { - if (validExtensions != null && validExtensions.length > 0) { - for (String extension : validExtensions) { - if (name.toLowerCase().endsWith("." + extension)) { - return true; - } + for (String extension : validExtensions) { + if (name.toLowerCase().endsWith("." + extension)) { + return true; } } return false; @@ -244,42 +214,37 @@ public boolean accept(@NonNullByDefault({}) File dir, @NonNullByDefault({}) Stri } @SuppressWarnings("rawtypes") - private void checkFile(final ModelRepository modelRepository, final File file, final Kind kind) { - if (file != null) { - try { - synchronized (FolderObserver.class) { - if ((kind == ENTRY_CREATE || kind == ENTRY_MODIFY)) { - if (parsers.contains(getExtension(file.getName()))) { - try (InputStream inputStream = Files.newInputStream(file.toPath())) { - nameFileMap.put(file.getName(), file); - modelRepository.addOrRefreshModel(file.getName(), inputStream); - } catch (IOException e) { - logger.warn("Error while opening file during update: {}", file.getAbsolutePath()); - } - } else { - ignoredFiles.add(file); + private void checkFile(final ModelRepository modelRepository, final File file, final WatchService.Kind kind) { + try { + synchronized (FolderObserver.class) { + if ((kind == CREATE || kind == MODIFY)) { + if (parsers.contains(getExtension(file.getName()))) { + try (InputStream inputStream = Files.newInputStream(file.toPath())) { + nameFileMap.put(file.getName(), file); + modelRepository.addOrRefreshModel(file.getName(), inputStream); + } catch (IOException e) { + logger.warn("Error while opening file during update: {}", file.getAbsolutePath()); } - } else if (kind == ENTRY_DELETE) { - modelRepository.removeModel(file.getName()); - nameFileMap.remove(file.getName()); + } else { + ignoredFiles.add(file); } + } else if (kind == WatchService.Kind.DELETE) { + modelRepository.removeModel(file.getName()); + nameFileMap.remove(file.getName()); } - } catch (Exception e) { - logger.error("Error handling update of file '{}': {}.", file.getAbsolutePath(), e.getMessage(), e); } + } catch (Exception e) { + logger.error("Error handling update of file '{}': {}.", file.getAbsolutePath(), e.getMessage(), e); } } private @Nullable File getFileByFileExtMap(Map folderFileExtMap, String filename) { - if (filename != null && !filename.trim().isEmpty() && isNotEmpty(folderFileExtMap)) { + if (!filename.trim().isEmpty() && !folderFileExtMap.isEmpty()) { String extension = getExtension(filename); if (extension != null && !extension.trim().isEmpty()) { Set> entries = folderFileExtMap.entrySet(); - Iterator> iterator = entries.iterator(); - while (iterator.hasNext()) { - Entry entry = iterator.next(); - - if (Arrays.stream(entry.getValue()).anyMatch(extension::equals)) { + for (Entry entry : entries) { + if (Arrays.asList(entry.getValue()).contains(extension)) { return new File(getFile(entry.getKey()) + File.separator + filename); } } @@ -296,7 +261,7 @@ private void checkFile(final ModelRepository modelRepository, final File file, f * the file name to get the {@link File} for * @return the corresponding {@link File} */ - private File getFile(String filename) { + protected File getFile(String filename) { return new File(OpenHAB.getConfigFolder() + File.separator + filename); } @@ -307,12 +272,16 @@ private File getFile(String filename) { * the file name to get the extension * @return the file's extension */ - public String getExtension(String filename) { - return filename.substring(filename.lastIndexOf(".") + 1); + public @Nullable String getExtension(String filename) { + if (filename.contains(".")) { + return filename.substring(filename.lastIndexOf(".") + 1); + } else { + return null; + } } @Override - protected void processWatchEvent(WatchEvent event, Kind kind, Path path) { + public void processWatchEvent(WatchService.Kind kind, Path path) { File toCheck = getFileByFileExtMap(folderFileExtMap, path.getFileName().toString()); if (toCheck != null && !toCheck.isHidden()) { checkFile(modelRepository, toCheck, kind); diff --git a/itests/org.openhab.core.model.core.tests/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserverTest.java b/bundles/org.openhab.core.model.core/src/test/java/org/openhab/core/model/core/internal/folder/FolderObserverTest.java similarity index 53% rename from itests/org.openhab.core.model.core.tests/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserverTest.java rename to bundles/org.openhab.core.model.core/src/test/java/org/openhab/core/model/core/internal/folder/FolderObserverTest.java index 9a5fe2d5131..0fbed211b43 100644 --- a/itests/org.openhab.core.model.core.tests/src/main/java/org/openhab/core/model/core/internal/folder/FolderObserverTest.java +++ b/bundles/org.openhab.core.model.core/src/test/java/org/openhab/core/model/core/internal/folder/FolderObserverTest.java @@ -14,12 +14,18 @@ import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static org.openhab.core.service.WatchService.Kind.CREATE; +import static org.openhab.core.service.WatchService.Kind.MODIFY; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -29,25 +35,20 @@ import java.util.Dictionary; import java.util.Hashtable; import java.util.List; -import java.util.Set; import java.util.stream.Stream; -import org.eclipse.emf.ecore.EObject; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.openhab.core.OpenHAB; import org.openhab.core.model.core.ModelParser; import org.openhab.core.model.core.ModelRepository; -import org.openhab.core.model.core.ModelRepositoryChangeListener; -import org.openhab.core.service.AbstractWatchService; import org.openhab.core.service.ReadyService; -import org.openhab.core.test.java.JavaOSGiTest; +import org.openhab.core.service.WatchService; +import org.openhab.core.test.java.JavaTest; import org.osgi.service.component.ComponentContext; /** @@ -55,76 +56,48 @@ * to check if {@link FolderObserver} invokes the correct {@link ModelRepository}'s methods * with correct arguments when certain events in the watched directory are triggered. * - * {@link AbstractWatchService#initializeWatchService} method is called in the - * {@link FolderObserver#updated} method and initializing a new WatchService - * is related to creating a new {@link AbstractWatchQueueReader} which starts in a new Thread. - * Since we rely on the {@link AbstractWatchQueueReader} to "listen" for changes, we have to - * be sure that it has been started. - * Based on that putting the current Thread to sleep after each invocation of - * {@link FolderObserver#updated} method is necessary. - * On the other hand, creating, modifying and deleting files and folders causes invocation - * of {@link AbstractWatchQueueReader#processWatchEvent} method. That method is called asynchronously - * and we do not know exactly when the event will be handled (it is OS specific). - * Since the assertions in the tests depend on handling the events, - * putting the current Thread to sleep after the file operations is also necessary. - * * @author Mihaela Memova - Initial contribution * @author Stefan Triller - added hidden file test * @author Simon Kaufmann - ported to Java + * @author Jan N. Klug - Refactored to UnitTest */ @ExtendWith(MockitoExtension.class) @NonNullByDefault -public class FolderObserverTest extends JavaOSGiTest { - +public class FolderObserverTest extends JavaTest { private static final boolean IS_OS_WINDOWS = System.getProperty("os.name").startsWith("Windows"); private static final File WATCHED_DIRECTORY = new File("watcheddir"); private static final File UNWATCHED_DIRECTORY = new File("unwatcheddir"); private static final String EXISTING_SUBDIR_NAME = "existingsubdir"; private static final File EXISTING_SUBDIR_PATH = new File(WATCHED_DIRECTORY, EXISTING_SUBDIR_NAME); - - private static final int WAIT_EVENT_TO_BE_HANDLED = 500; - private static final String MOCK_MODEL_TO_BE_REMOVED = "MockFileInModelForDeletion.java"; private static final String INITIAL_FILE_CONTENT = "Initial content"; private @NonNullByDefault({}) Dictionary configProps; - private @NonNullByDefault({}) String defaultWatchedDir; private @NonNullByDefault({}) FolderObserver folderObserver; - private @NonNullByDefault({}) ModelRepoDummy modelRepo; + private @Mock @NonNullByDefault({}) ModelRepository modelRepoMock; private @Mock @NonNullByDefault({}) ModelParser modelParserMock; private @Mock @NonNullByDefault({}) ReadyService readyServiceMock; + private @Mock @NonNullByDefault({}) WatchService watchServiceMock; private @Mock @NonNullByDefault({}) ComponentContext contextMock; @BeforeEach - public void beforeEach() { + public void beforeEach() throws IOException, InterruptedException { configProps = new Hashtable<>(); - setupWatchedDirectory(); - setUpServices(); - } - - /** - * The main configuration folder's path is saved in the defaultWatchedDir variable - * in order to be restored after all the tests are finished. - * For the purpose of the FolderObserverTest class a new folder is created. - * Its path is set to the OpenHAB.CONFIG_DIR_PROG_ARGUMENT property. - */ - private void setupWatchedDirectory() { - defaultWatchedDir = System.getProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT); WATCHED_DIRECTORY.mkdirs(); - System.setProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, WATCHED_DIRECTORY.getPath()); EXISTING_SUBDIR_PATH.mkdirs(); - } - private void setUpServices() { when(modelParserMock.getExtension()).thenReturn("java"); when(contextMock.getProperties()).thenReturn(configProps); - modelRepo = new ModelRepoDummy(); - - folderObserver = new FolderObserver(modelRepo, readyServiceMock); + folderObserver = new FolderObserver(modelRepoMock, readyServiceMock, watchServiceMock) { + @Override + protected File getFile(String filename) { + return new File(WATCHED_DIRECTORY + File.separator + filename); + } + }; folderObserver.addModelParser(modelParserMock); } @@ -137,16 +110,10 @@ private void setUpServices() { @AfterEach public void tearDown() throws Exception { folderObserver.deactivate(); + try (Stream walk = Files.walk(WATCHED_DIRECTORY.toPath())) { walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); } - - modelRepo.clean(); - if (defaultWatchedDir != null) { - System.setProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, defaultWatchedDir); - } else { - System.clearProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT); - } } /** @@ -164,21 +131,13 @@ public void testCreation() throws Exception { folderObserver.activate(contextMock); File file = new File(EXISTING_SUBDIR_PATH, "NewlyCreatedMockFile." + validExtension); - file.createNewFile(); - - /* - * In some OS, like MacOS, creating an empty file is not related to sending an ENTRY_CREATE event. - * So, it's necessary to put some initial content in that file. - */ - if (!IS_OS_WINDOWS) { - Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8); - } + Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8, StandardOpenOption.CREATE); waitForAssert(() -> assertThat(file.exists(), is(true))); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(true)), DFL_TIMEOUT * 2, - DFL_SLEEP_TIME); - waitForAssert(() -> assertThat(modelRepo.isRemoveModelMethodCalled, is(false))); - waitForAssert(() -> assertThat(modelRepo.calledFileName, is(file.getName()))); + folderObserver.processWatchEvent(CREATE, file.toPath()); + + verify(modelRepoMock).addOrRefreshModel(eq(file.getName()), any()); + verifyNoMoreInteractions(modelRepoMock); } /** @@ -197,37 +156,18 @@ public void testModification() throws Exception { folderObserver.activate(contextMock); File file = new File(EXISTING_SUBDIR_PATH, "MockFileForModification." + validExtension); - file.createNewFile(); - - /* - * In some OS, like MacOS, creating an empty file is not related to sending an ENTRY_CREATE event. So, it's - * necessary to put some initial content in that file. - */ - if (!IS_OS_WINDOWS) { - Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8, StandardOpenOption.APPEND); - } + Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8, StandardOpenOption.CREATE); waitForAssert(() -> assertThat(file.exists(), is(true))); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(true)), DFL_TIMEOUT * 2, - DFL_SLEEP_TIME); - - modelRepo.clean(); + folderObserver.processWatchEvent(CREATE, file.toPath()); String text = "Additional content"; Files.writeString(file.toPath(), text, StandardCharsets.UTF_8, StandardOpenOption.APPEND); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(true)), DFL_TIMEOUT * 2, - DFL_SLEEP_TIME); - waitForAssert(() -> assertThat(modelRepo.calledFileName, is(file.getName()))); + folderObserver.processWatchEvent(MODIFY, file.toPath()); - String finalFileContent; - if (!IS_OS_WINDOWS) { - finalFileContent = INITIAL_FILE_CONTENT + text; - } else { - finalFileContent = text; - } - - waitForAssert(() -> assertThat(modelRepo.fileContent, is(finalFileContent))); + verify(modelRepoMock, times(2)).addOrRefreshModel(eq(file.getName()), any()); + verifyNoMoreInteractions(modelRepoMock); } /** @@ -245,12 +185,12 @@ public void testCreationUntrackedExtension() throws Exception { folderObserver.activate(contextMock); File file = new File(EXISTING_SUBDIR_PATH, "NewlyCreatedMockFile." + noParserExtension); - file.createNewFile(); - - Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); + Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8, StandardOpenOption.CREATE); waitForAssert(() -> assertThat(file.exists(), is(true))); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(false))); - waitForAssert(() -> assertThat(modelRepo.isRemoveModelMethodCalled, is(false))); + + folderObserver.processWatchEvent(CREATE, file.toPath()); + + verifyNoInteractions(modelRepoMock); } /** @@ -266,12 +206,11 @@ public void testCreationUntrackedDirectory() throws Exception { folderObserver.activate(contextMock); File file = new File(EXISTING_SUBDIR_PATH, "NewlyCreatedMockFile.java"); - file.createNewFile(); - - Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); + Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8, StandardOpenOption.CREATE); waitForAssert(() -> assertThat(file.exists(), is(true))); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(false))); - waitForAssert(() -> assertThat(modelRepo.isRemoveModelMethodCalled, is(false))); + folderObserver.processWatchEvent(CREATE, file.toPath()); + + verifyNoInteractions(modelRepoMock); } /** @@ -281,17 +220,15 @@ public void testCreationUntrackedDirectory() throws Exception { */ @Test public void testShutdown() { + when(modelRepoMock.getAllModelNamesOfType(EXISTING_SUBDIR_NAME)).thenReturn(List.of(MOCK_MODEL_TO_BE_REMOVED)); configProps.put(EXISTING_SUBDIR_NAME, "java,txt,jpg"); folderObserver.activate(contextMock); - modelRepo.clean(); - folderObserver.deactivate(); configProps.remove(EXISTING_SUBDIR_NAME); folderObserver.activate(contextMock); - waitForAssert(() -> assertThat(modelRepo.isRemoveModelMethodCalled, is(true))); - waitForAssert(() -> assertThat(modelRepo.calledFileName, is(MOCK_MODEL_TO_BE_REMOVED))); + verify(modelRepoMock).removeModel(MOCK_MODEL_TO_BE_REMOVED); } /** @@ -302,9 +239,7 @@ public void testNonExisting() throws Exception { configProps.put("nonExistingSubdir", "txt,jpg,java"); folderObserver.activate(contextMock); - Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(false))); - waitForAssert(() -> assertThat(modelRepo.isRemoveModelMethodCalled, is(false))); + verifyNoInteractions(modelRepoMock); } /** @@ -323,24 +258,25 @@ public void testCreationNoExtensions() throws Exception { folderObserver.activate(contextMock); File file = new File(WATCHED_DIRECTORY, Paths.get(subdir, "MockFileInNoExtSubDir.txt").toString()); - file.createNewFile(); - - Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); + Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8, StandardOpenOption.CREATE); waitForAssert(() -> assertThat(file.exists(), is(true))); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(false))); - waitForAssert(() -> assertThat(modelRepo.isRemoveModelMethodCalled, is(false))); + + folderObserver.processWatchEvent(CREATE, file.toPath()); + + verifyNoInteractions(modelRepoMock); } @Test public void testException() throws Exception { - ModelRepoDummy modelRepo = new ModelRepoDummy() { + when(modelRepoMock.addOrRefreshModel(any(), any())).thenThrow(new IllegalStateException("intentional failure")); + + FolderObserver localFolderObserver = new FolderObserver(modelRepoMock, readyServiceMock, watchServiceMock) { @Override - public boolean addOrRefreshModel(String name, InputStream inputStream) { - super.addOrRefreshModel(name, inputStream); - throw new IllegalStateException("intentional failure."); + protected File getFile(String filename) { + return new File(WATCHED_DIRECTORY + File.separator + filename); } }; - FolderObserver localFolderObserver = new FolderObserver(modelRepo, readyServiceMock); + localFolderObserver.addModelParser(modelParserMock); String validExtension = "java"; @@ -348,20 +284,15 @@ public boolean addOrRefreshModel(String name, InputStream inputStream) { localFolderObserver.activate(contextMock); File mockFileWithValidExt = new File(EXISTING_SUBDIR_PATH, "MockFileForModification." + validExtension); - mockFileWithValidExt.createNewFile(); - if (!IS_OS_WINDOWS) { - Files.writeString(mockFileWithValidExt.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8); - } + Files.writeString(mockFileWithValidExt.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8, + StandardOpenOption.CREATE); + localFolderObserver.processWatchEvent(CREATE, mockFileWithValidExt.toPath()); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(true)), DFL_TIMEOUT * 2, - DFL_SLEEP_TIME); - - modelRepo.clean(); Files.writeString(mockFileWithValidExt.toPath(), "Additional content", StandardCharsets.UTF_8, StandardOpenOption.APPEND); + localFolderObserver.processWatchEvent(MODIFY, mockFileWithValidExt.toPath()); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(true)), DFL_TIMEOUT * 2, - DFL_SLEEP_TIME); + verify(modelRepoMock, times(2)).addOrRefreshModel(eq(mockFileWithValidExt.getName()), any()); } /** @@ -381,13 +312,8 @@ public void testHiddenFile() throws Exception { String filename = ".HiddenNewlyCreatedMockFile." + validExtension; if (!IS_OS_WINDOWS) { - /* - * In some OS, like MacOS, creating an empty file is not related to sending an ENTRY_CREATE event. - * So, it's necessary to put some initial content in that file. - */ File file = new File(EXISTING_SUBDIR_PATH, filename); - file.createNewFile(); - Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8); + Files.writeString(file.toPath(), INITIAL_FILE_CONTENT, StandardCharsets.UTF_8, StandardOpenOption.CREATE); } else { /* * In windows a hidden file cannot be created with a single api call. @@ -402,81 +328,13 @@ public void testHiddenFile() throws Exception { Files.setAttribute(file.toPath(), "dos:hidden", true); try { Files.move(file.toPath(), EXISTING_SUBDIR_PATH.toPath()); - } catch (java.nio.file.FileAlreadyExistsException e) { + } catch (java.nio.file.FileAlreadyExistsException ignored) { } try (Stream walk = Files.walk(UNWATCHED_DIRECTORY.toPath())) { walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); } } - - Thread.sleep(WAIT_EVENT_TO_BE_HANDLED); - waitForAssert(() -> assertThat(modelRepo.isAddOrRefreshModelMethodCalled, is(false))); - } - - private static class ModelRepoDummy implements ModelRepository { - - public boolean isAddOrRefreshModelMethodCalled = false; - public boolean isRemoveModelMethodCalled = false; - public @Nullable String calledFileName; - public @Nullable String fileContent; - - @Override - public boolean addOrRefreshModel(String name, InputStream inputStream) { - calledFileName = name; - isAddOrRefreshModelMethodCalled = true; - try { - fileContent = new String(inputStream.readAllBytes(), StandardCharsets.UTF_8); - inputStream.close(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } - return true; - } - - @Override - public boolean removeModel(String name) { - calledFileName = name; - isRemoveModelMethodCalled = true; - return true; - } - - /** - * This method is invoked when a model is about to be deleted. - * For the purposes of the FolderObserverTest class it is overridden and - * it returns an array of exactly one model name. - */ - @Override - public Iterable getAllModelNamesOfType(String modelType) { - return List.of(MOCK_MODEL_TO_BE_REMOVED); - } - - @Override - public void reloadAllModelsOfType(String modelType) { - } - - @Override - public void addModelRepositoryChangeListener(ModelRepositoryChangeListener listener) { - } - - @Override - public void removeModelRepositoryChangeListener(ModelRepositoryChangeListener listener) { - } - - @Override - public @Nullable EObject getModel(String name) { - return null; - } - - public void clean() { - isAddOrRefreshModelMethodCalled = false; - isRemoveModelMethodCalled = false; - calledFileName = null; - fileContent = null; - } - - @Override - public Set removeAllModelsOfType(String modelType) { - return Set.of(); - } + folderObserver.processWatchEvent(CREATE, Path.of(EXISTING_SUBDIR_PATH.getName(), filename)); + verifyNoInteractions(modelRepoMock); } } diff --git a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationProvider.java b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationProvider.java index 83baf20c67e..a494039f5d1 100644 --- a/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationProvider.java +++ b/bundles/org.openhab.core.transform/src/main/java/org/openhab/core/transform/FileTransformationProvider.java @@ -15,7 +15,6 @@ import static org.openhab.core.transform.Transformation.FUNCTION; import java.io.IOException; -import java.nio.file.FileVisitOption; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardWatchEventKinds; @@ -28,11 +27,13 @@ import java.util.regex.Pattern; import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; import org.openhab.core.OpenHAB; import org.openhab.core.common.registry.ProviderChangeListener; -import org.openhab.core.service.AbstractWatchService; +import org.openhab.core.service.WatchService; +import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -44,7 +45,7 @@ */ @NonNullByDefault @Component(service = TransformationProvider.class, immediate = true) -public class FileTransformationProvider extends AbstractWatchService implements TransformationProvider { +public class FileTransformationProvider implements WatchService.WatchEventListener, TransformationProvider { private static final WatchEvent.Kind[] WATCH_EVENTS = { StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY }; private static final Set IGNORED_EXTENSIONS = Set.of("txt", "swp"); @@ -58,26 +59,35 @@ public class FileTransformationProvider extends AbstractWatchService implements private final Set> listeners = ConcurrentHashMap.newKeySet(); private final Map transformationConfigurations = new ConcurrentHashMap<>(); private final Path transformationPath; + private final WatchService watchService; - public FileTransformationProvider() { - this(TRANSFORMATION_PATH); + @Activate + public FileTransformationProvider( + @Reference(target = WatchService.CONFIG_WATCHER_FILTER) WatchService watchService) { + this(watchService, TRANSFORMATION_PATH); } // constructor package private used for testing - FileTransformationProvider(Path transformationPath) { - super(transformationPath.toString()); + FileTransformationProvider(WatchService watchService, Path transformationPath) { this.transformationPath = transformationPath; + this.watchService = watchService; + watchService.registerListener(this, transformationPath); // read initial contents try { - Files.walk(transformationPath, FileVisitOption.FOLLOW_LINKS).filter(Files::isRegularFile) - .forEach(f -> processPath(StandardWatchEventKinds.ENTRY_CREATE, f)); + Files.walk(transformationPath).filter(Files::isRegularFile) + .forEach(f -> processPath(WatchService.Kind.CREATE, f)); } catch (IOException e) { logger.warn("Could not list files in '{}', transformation configurations might be missing: {}", transformationPath, e.getMessage()); } } + @Deactivate + public void deactivate() { + watchService.unregisterListener(this); + } + @Override public void addProviderChangeListener(ProviderChangeListener listener) { listeners.add(listener); @@ -93,30 +103,15 @@ public Collection getAll() { return transformationConfigurations.values(); } - @Override - protected boolean watchSubDirectories() { - return true; - } - - @Override - protected WatchEvent.Kind @Nullable [] getWatchEventKinds(Path directory) { - return WATCH_EVENTS; - } - - @Override - protected void processWatchEvent(WatchEvent event, WatchEvent.Kind kind, Path path) { - processPath(kind, path); - } - - private void processPath(WatchEvent.Kind kind, Path path) { - if (StandardWatchEventKinds.ENTRY_DELETE.equals(kind)) { + private void processPath(WatchService.Kind kind, Path path) { + if (kind == WatchService.Kind.DELETE) { Transformation oldElement = transformationConfigurations.remove(path); if (oldElement != null) { logger.trace("Removed configuration from file '{}", path); listeners.forEach(listener -> listener.removed(this, oldElement)); } - } else if (Files.isRegularFile(path) && (StandardWatchEventKinds.ENTRY_CREATE.equals(kind) - || StandardWatchEventKinds.ENTRY_MODIFY.equals(kind))) { + } else if (Files.isRegularFile(path) + && (kind == WatchService.Kind.CREATE || kind == WatchService.Kind.MODIFY)) { try { String fileName = path.getFileName().toString(); Matcher m = FILENAME_PATTERN.matcher(fileName); @@ -152,4 +147,9 @@ private void processPath(WatchEvent.Kind kind, Path path) { logger.trace("Skipping {} event for '{}' - not a regular file", kind, path); } } + + @Override + public void processWatchEvent(WatchService.Kind kind, Path path) { + processPath(kind, path); + } } diff --git a/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/FileTransformationProviderTest.java b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/FileTransformationProviderTest.java index 430c054fb88..257ae3e8240 100644 --- a/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/FileTransformationProviderTest.java +++ b/bundles/org.openhab.core.transform/src/test/java/org/openhab/core/transform/FileTransformationProviderTest.java @@ -25,8 +25,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.StandardWatchEventKinds; -import java.nio.file.WatchEvent; import java.util.Map; import java.util.stream.Stream; @@ -42,6 +40,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.openhab.core.common.registry.ProviderChangeListener; +import org.openhab.core.service.WatchService; /** * The {@link FileTransformationProviderTest} includes tests for the @@ -61,7 +60,7 @@ public class FileTransformationProviderTest { private static final String ADDED_CONTENT = "added"; private static final String ADDED_FILENAME = ADDED_CONTENT + "." + FOO_TYPE; - private @Mock @NonNullByDefault({}) WatchEvent watchEventMock; + private @Mock @NonNullByDefault({}) WatchService watchService; private @Mock @NonNullByDefault({}) ProviderChangeListener<@NonNull Transformation> listenerMock; private @NonNullByDefault({}) FileTransformationProvider provider; @@ -71,10 +70,11 @@ public class FileTransformationProviderTest { public void setup() throws IOException { // create directory targetPath = Files.createTempDirectory("fileTest"); + // set initial content Files.write(targetPath.resolve(INITIAL_FILENAME), INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8)); - provider = new FileTransformationProvider(targetPath); + provider = new FileTransformationProvider(watchService, targetPath); provider.addProviderChangeListener(listenerMock); } @@ -100,7 +100,7 @@ public void testAddingConfigurationIsPropagated() throws IOException { Transformation addedConfiguration = new Transformation(ADDED_FILENAME, ADDED_FILENAME, FOO_TYPE, Map.of(FUNCTION, ADDED_CONTENT)); - provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_CREATE, path); + provider.processWatchEvent(WatchService.Kind.CREATE, path); // assert registry is notified and internal cache updated Mockito.verify(listenerMock).added(provider, addedConfiguration); @@ -114,7 +114,7 @@ public void testUpdatingConfigurationIsPropagated() throws IOException { Transformation updatedConfiguration = new Transformation(INITIAL_FILENAME, INITIAL_FILENAME, FOO_TYPE, Map.of(FUNCTION, "updated")); - provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_MODIFY, path); + provider.processWatchEvent(WatchService.Kind.MODIFY, path); Mockito.verify(listenerMock).updated(provider, INITIAL_CONFIGURATION, updatedConfiguration); assertThat(provider.getAll(), contains(updatedConfiguration)); @@ -125,7 +125,7 @@ public void testUpdatingConfigurationIsPropagated() throws IOException { public void testDeletingConfigurationIsPropagated() { Path path = targetPath.resolve(INITIAL_FILENAME); - provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_DELETE, path); + provider.processWatchEvent(WatchService.Kind.DELETE, path); Mockito.verify(listenerMock).removed(provider, INITIAL_CONFIGURATION); assertThat(provider.getAll(), not(contains(INITIAL_CONFIGURATION))); @@ -140,7 +140,7 @@ public void testLanguageIsProperlyParsed() throws IOException { Transformation expected = new Transformation(fileName, fileName, FOO_TYPE, Map.of(FUNCTION, INITIAL_CONTENT)); - provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_CREATE, path); + provider.processWatchEvent(WatchService.Kind.CREATE, path); assertThat(provider.getAll(), hasItem(expected)); } @@ -148,8 +148,8 @@ public void testLanguageIsProperlyParsed() throws IOException { public void testMissingExtensionIsIgnored() throws IOException { Path path = targetPath.resolve("extensionMissing"); Files.write(path, INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8)); - provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_CREATE, path); - provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_MODIFY, path); + provider.processWatchEvent(WatchService.Kind.CREATE, path); + provider.processWatchEvent(WatchService.Kind.MODIFY, path); Mockito.verify(listenerMock, never()).added(any(), any()); Mockito.verify(listenerMock, never()).updated(any(), any(), any()); @@ -159,8 +159,8 @@ public void testMissingExtensionIsIgnored() throws IOException { public void testIgnoredExtensionIsIgnored() throws IOException { Path path = targetPath.resolve("extensionIgnore.txt"); Files.write(path, INITIAL_CONTENT.getBytes(StandardCharsets.UTF_8)); - provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_CREATE, path); - provider.processWatchEvent(watchEventMock, StandardWatchEventKinds.ENTRY_MODIFY, path); + provider.processWatchEvent(WatchService.Kind.CREATE, path); + provider.processWatchEvent(WatchService.Kind.MODIFY, path); Mockito.verify(listenerMock, never()).added(any(), any()); Mockito.verify(listenerMock, never()).updated(any(), any(), any()); diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceFactoryImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceFactoryImpl.java new file mode 100644 index 00000000000..e4d51974a2d --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceFactoryImpl.java @@ -0,0 +1,117 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.service; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Dictionary; +import java.util.Hashtable; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.OpenHAB; +import org.openhab.core.service.WatchService; +import org.openhab.core.service.WatchServiceFactory; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.cm.Configuration; +import org.osgi.service.cm.ConfigurationAdmin; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link WatchServiceFactoryImpl} is a + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = WatchServiceFactory.class) +public class WatchServiceFactoryImpl implements WatchServiceFactory { + private final Logger logger = LoggerFactory.getLogger(WatchServiceFactoryImpl.class); + + private final ConfigurationAdmin cm; + + @Activate + public WatchServiceFactoryImpl(@Reference ConfigurationAdmin cm) { + this.cm = cm; + + // make sure we start with a clean configuration. + clearConfigurationAdmin(); + + createWatchService(WatchService.CONFIG_WATCHER_NAME, Path.of(OpenHAB.getConfigFolder())); + } + + @Deactivate + public void deactivate() { + clearConfigurationAdmin(); + } + + @Override + public void createWatchService(String name, Path basePath) { + try { + String filter = "(&(name=" + name + ")" + "(service.factoryPid=" + WatchService.SERVICE_PID + "))"; + Configuration[] configurations = cm.listConfigurations(filter); + + if (configurations == null || configurations.length == 0) { + Configuration config = cm.createFactoryConfiguration(WatchService.SERVICE_PID, "?"); + Dictionary map = new Hashtable<>(); + + map.put("name", name); + map.put("path", basePath.toString()); + config.update(map); + } else { + Configuration config = configurations[0]; + Dictionary map = config.getProperties(); + map.put("name", name); + map.put("path", basePath.toString()); + config.update(map); + } + } catch (IOException | InvalidSyntaxException e1) { + logger.error("Failed to create configuration with name `{}' and path '{}'", name, basePath); + } + } + + @Override + public void removeWatchService(String name) { + try { + String filter = "(&(name=" + name + ")" + "(service.factoryPid=" + WatchService.SERVICE_PID + "))"; + Configuration[] configurations = this.cm.listConfigurations(filter); + if (configurations != null) { + configurations[0].delete(); + } + } catch (IOException | InvalidSyntaxException e) { + logger.error("Failed to remove configuration with name '{}", name); + } + } + + private void clearConfigurationAdmin() { + try { + String filter = "(service.factoryPid=" + WatchService.SERVICE_PID + ")"; + Configuration[] configurations = this.cm.listConfigurations(filter); + if (configurations != null) { + for (Configuration configuration : configurations) { + try { + configuration.delete(); + } catch (IOException e) { + logger.error("Failed to remove configuration with name '{}", + configuration.getProperties().get("name")); + } + } + } + } catch (IOException | InvalidSyntaxException e) { + logger.error("Failed to remove services."); + } + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceImpl.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceImpl.java new file mode 100644 index 00000000000..44ab54f0baf --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/internal/service/WatchServiceImpl.java @@ -0,0 +1,223 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.service; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Predicate; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.service.WatchService; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.service.component.annotations.Activate; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.ConfigurationPolicy; +import org.osgi.service.component.annotations.Deactivate; +import org.osgi.service.component.annotations.Modified; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.methvin.watcher.DirectoryChangeEvent; +import io.methvin.watcher.DirectoryChangeListener; +import io.methvin.watcher.DirectoryWatcher; + +/** + * The {@link WatchServiceImpl} is the implementation of the {@link WatchService} + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@Component(immediate = true, service = WatchService.class, configurationPid = WatchService.SERVICE_PID, configurationPolicy = ConfigurationPolicy.REQUIRE) +public class WatchServiceImpl implements WatchService, DirectoryChangeListener { + + public @interface WatchServiceConfiguration { + String name() default ""; + + String path() default ""; + } + + private final Logger logger = LoggerFactory.getLogger(WatchServiceImpl.class); + + private final List dirPathListeners = new CopyOnWriteArrayList<>(); + private final List subDirPathListeners = new CopyOnWriteArrayList<>(); + private final ExecutorService executor; + private final String name; + private final BundleContext bundleContext; + + private @Nullable Path basePath; + private @Nullable DirectoryWatcher dirWatcher; + private @Nullable ServiceRegistration reg; + + @Activate + public WatchServiceImpl(WatchServiceConfiguration config, BundleContext bundleContext) throws IOException { + this.bundleContext = bundleContext; + if (config.name().isBlank()) { + throw new IllegalArgumentException("service name must not be blank"); + } + + this.name = config.name(); + executor = Executors.newSingleThreadExecutor(r -> new Thread(r, name)); + + modified(config); + } + + @Modified + public void modified(WatchServiceConfiguration config) throws IOException { + logger.trace("Trying to setup WatchService '{}' with path '{}'", config.name(), config.path()); + + Path basePath = Path.of(config.path()).toAbsolutePath(); + + if (basePath.equals(this.basePath)) { + return; + } + + this.basePath = basePath; + + try { + closeWatcherAndUnregister(); + + if (!Files.exists(basePath)) { + logger.info("Watch directory '{}' does not exists. Trying to create it.", basePath); + Files.createDirectories(basePath); + } + + DirectoryWatcher newDirWatcher = DirectoryWatcher.builder().listener(this).path(basePath).build(); + CompletableFuture + .runAsync( + () -> newDirWatcher.watchAsync(executor) + .thenRun(() -> logger.debug("WatchService '{}' has been shut down.", name)), + ThreadPoolManager.getScheduledPool(ThreadPoolManager.THREAD_POOL_NAME_COMMON)) + .thenRun(this::registerWatchService); + this.dirWatcher = newDirWatcher; + } catch (NoSuchFileException e) { + // log message here, otherwise it'll be swallowed by the call to newInstance in the factory + // also re-throw the exception to indicate that we failed + logger.warn("Could not instantiate WatchService '{}', directory '{}' is missing.", name, e.getMessage()); + throw e; + } catch (IOException e) { + // log message here, otherwise it'll be swallowed by the call to newInstance in the factory + // also re-throw the exception to indicate that we failed + logger.warn("Could not instantiate WatchService '{}':", name, e); + throw e; + } + } + + @Deactivate + public void deactivate() { + try { + closeWatcherAndUnregister(); + executor.shutdown(); + } catch (IOException e) { + logger.warn("Failed to shutdown WatchService '{}'", name, e); + } + } + + private void registerWatchService() { + Dictionary properties = new Hashtable<>(); + properties.put(WatchService.SERVICE_PROPERTY_NAME, name); + this.reg = bundleContext.registerService(WatchService.class, this, properties); + logger.debug("WatchService '{}' completed initialization and registered itself as service.", name); + } + + private void closeWatcherAndUnregister() throws IOException { + DirectoryWatcher localDirWatcher = this.dirWatcher; + if (localDirWatcher != null) { + localDirWatcher.close(); + this.dirWatcher = null; + } + + ServiceRegistration localReg = this.reg; + if (localReg != null) { + localReg.unregister(); + this.reg = null; + } + } + + @Override + public void registerListener(WatchEventListener watchEventListener, List paths, boolean withSubDirectories) { + Path basePath = this.basePath; + if (basePath == null) { + throw new IllegalStateException("Trying to register listener before initialization complete."); + } + for (Path path : paths) { + Path absolutePath = path.isAbsolute() ? path : basePath.resolve(path).toAbsolutePath(); + if (absolutePath.startsWith(basePath)) { + if (withSubDirectories) { + subDirPathListeners.add(new Listener(absolutePath, watchEventListener)); + } else { + dirPathListeners.add(new Listener(absolutePath, watchEventListener)); + } + } else { + logger.warn("Tried to add path '{}' to listener '{}', but the base path of this listener is '{}'", path, + name, basePath); + } + } + } + + @Override + public void unregisterListener(WatchEventListener watchEventListener) { + subDirPathListeners.removeIf(Listener.isListener(watchEventListener)); + dirPathListeners.removeIf(Listener.isListener(watchEventListener)); + } + + @Override + public void onEvent(@Nullable DirectoryChangeEvent directoryChangeEvent) throws IOException { + if (directoryChangeEvent == null || directoryChangeEvent.isDirectory() + || directoryChangeEvent.eventType() == DirectoryChangeEvent.EventType.OVERFLOW) { + // exit early, we are neither interested in directory events nor in OVERFLOW events + return; + } + + Path path = directoryChangeEvent.path(); + Kind kind = switch (directoryChangeEvent.eventType()) { + case CREATE -> Kind.CREATE; + case MODIFY -> Kind.MODIFY; + case DELETE -> Kind.DELETE; + case OVERFLOW -> Kind.OVERFLOW; + }; + + subDirPathListeners.stream().filter(isChildOf(path)).forEach(l -> l.notify(path, kind)); + dirPathListeners.stream().filter(isDirectChildOf(path)).forEach(l -> l.notify(path, kind)); + } + + public static Predicate isChildOf(Path path) { + return l -> path.startsWith(l.rootPath); + } + + public static Predicate isDirectChildOf(Path path) { + return l -> path.startsWith(l.rootPath) && l.rootPath.relativize(path).getNameCount() == 1; + } + + private record Listener(Path rootPath, WatchEventListener watchEventListener) { + + void notify(Path path, Kind kind) { + watchEventListener.processWatchEvent(kind, rootPath.relativize(path)); + } + + static Predicate isListener(WatchEventListener watchEventListener) { + return l -> watchEventListener.equals(l.watchEventListener); + } + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/service/AbstractWatchService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/service/AbstractWatchService.java deleted file mode 100644 index bc4906c5c5e..00000000000 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/service/AbstractWatchService.java +++ /dev/null @@ -1,115 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.core.service; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.WatchEvent; -import java.nio.file.WatchEvent.Kind; -import java.nio.file.WatchKey; -import java.nio.file.WatchService; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; - -/** - * Base class for OSGI services that access to file system by Java WatchService. - * - * See the WatchService java docs - * for more details - * - * @author Fabio Marini - Initial contribution - * @author Dimitar Ivanov - added javadoc; introduced WatchKey to directory mapping for the queue reader - * @author Ana Dimova - reduce to a single watch thread for all class instances of {@link AbstractWatchService} - * @author Jan N. Klug - add null annotations - */ -@NonNullByDefault -public abstract class AbstractWatchService { - protected @Nullable String pathToWatch; - - /** - * The queue reader - */ - protected WatchQueueReader watchQueueReader = WatchQueueReader.getInstance(); - - protected AbstractWatchService(String pathToWatch) { - this.pathToWatch = pathToWatch; - } - - /** - * Change the watch directory for this WatchService - * - * @param pathToWatch the new path - */ - protected void changeWatchDirectory(String pathToWatch) { - deactivate(); - this.pathToWatch = pathToWatch; - activate(); - } - - /** - * Method to call on service activation - */ - public void activate() { - Path pathToWatch = getSourcePath(); - if (pathToWatch != null) { - watchQueueReader.customizeWatchQueueReader(this, pathToWatch, watchSubDirectories()); - } - } - - /** - * Method to call on service deactivation - */ - @SuppressWarnings("unused") - public void deactivate() { - watchQueueReader.stopWatchService(this); - } - - /** - * @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. - */ - public @Nullable Path getSourcePath() { - String pathToWatch = this.pathToWatch; - return pathToWatch == null || pathToWatch.isBlank() ? null : Paths.get(pathToWatch); - } - - /** - * Determines whether the subdirectories of the source path (determined by the {@link #getSourcePath()}) will be - * watched or not. - * - * @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(); - - /** - * Provides the {@link WatchKey}s for the registration of the directory, which will be registered in the watch - * service. - * - * @param directory the directory, which will be registered in the watch service - * @return The array of {@link WatchKey}s for the registration or null if no registration has been - * done. - */ - protected abstract Kind @Nullable [] getWatchEventKinds(Path directory); - - /** - * 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 be handled - * @param kind the event's kind - * @param path the path of the event (resolved to the {@link #pathToWatch}) - */ - protected abstract void processWatchEvent(WatchEvent event, Kind kind, Path path); -} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchQueueReader.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchQueueReader.java deleted file mode 100644 index 43f724bc4c9..00000000000 --- a/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchQueueReader.java +++ /dev/null @@ -1,490 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.core.service; - -import static java.nio.file.StandardWatchEventKinds.*; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.AccessDeniedException; -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.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.EnumSet; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; -import java.util.stream.Collectors; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.core.common.ThreadPoolManager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Base class for watch queue readers - * - * @author Fabio Marini - Initial contribution - * @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} - * @author Jan N. Klug - allow multiple listeners for the same directory and add null annotations - */ -@NonNullByDefault -public class WatchQueueReader implements Runnable { - private static final String THREAD_POOL_NAME = "file-processing"; - private static final int PROCESSING_DELAY = 1000; // ms - - protected final Logger logger = LoggerFactory.getLogger(WatchQueueReader.class); - private final ScheduledExecutorService scheduler = ThreadPoolManager.getScheduledPool(THREAD_POOL_NAME); - - protected @Nullable WatchService watchService; - - private final Map registeredKeys = new HashMap<>(); - private final Map> keyToService = new ConcurrentHashMap<>(); - private final Map> hashes = new ConcurrentHashMap<>(); - private final List notifications = new CopyOnWriteArrayList<>(); - - private @Nullable 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 WatchEvent cast(WatchEvent event) { - return (WatchEvent) event; - } - - public static WatchQueueReader getInstance() { - return INSTANCE; - } - - private WatchQueueReader() { - // prevent instantiation - } - - // used for testing to check if properly terminated - @Nullable - WatchService getWatchService() { - return watchService; - } - - /** - * 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<>() { - @Override - public FileVisitResult preVisitDirectory(@Nullable Path subDir, - @Nullable BasicFileAttributes attrs) { - if (subDir != null) { - Kind[] kinds = watchService.getWatchEventKinds(subDir); - registerDirectoryInternal(watchService, kinds, subDir); - } - return FileVisitResult.CONTINUE; - } - - @Override - public FileVisitResult visitFileFailed(@Nullable Path file, @Nullable IOException exc) { - if (exc instanceof AccessDeniedException) { - logger.warn("Access to folder '{}' was denied, therefore skipping it.", - file != null ? file.toAbsolutePath() : null); - } - return FileVisitResult.SKIP_SUBTREE; - } - }); - } - - private synchronized void registerDirectoryInternal(AbstractWatchService service, Kind @Nullable [] kinds, - Path directory) { - WatchService watchService = this.watchService; - if (watchService == null) { - try { - watchService = FileSystems.getDefault().newWatchService(); - this.watchService = watchService; - Thread qr = new Thread(this, "openHAB Dir Watcher"); - this.qr = qr; - qr.start(); - } catch (IOException e) { - logger.debug("The directory '{}' was not registered in the watch service", directory, e); - return; - } - } - WatchKey registrationKey = null; - if (kinds == null) { - return; - } - try { - registrationKey = directory.register(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); - Set services = Objects - .requireNonNull(keyToService.computeIfAbsent(registrationKey, k -> new HashSet<>())); - services.add(service); - } else { - logger.debug("The directory '{}' was not registered in the watch service", directory); - } - } - - public synchronized void stopWatchService(AbstractWatchService service) { - List keysToRemove = new LinkedList<>(); - for (Map.Entry> entry : keyToService.entrySet()) { - Set services = entry.getValue(); - services.remove(service); - if (services.isEmpty()) { - keysToRemove.add(entry.getKey()); - } - } - for (Notification notification : notifications) { - if (notification.service.equals(service)) { - notification.future.cancel(true); - notifications.remove(notification); - } - } - if (keysToRemove.size() == keyToService.size()) { - try { - WatchService watchService = this.watchService; - if (watchService != null) { - watchService.close(); - - Thread qr = this.qr; - if (qr != null) { - qr.interrupt(); - this.qr = null; - } - - this.watchService = null; - } - } catch (IOException e) { - logger.warn("Cannot deactivate folder watcher", e); - } - keyToService.clear(); - registeredKeys.clear(); - hashes.clear(); - notifications.forEach(notification -> notification.future.cancel(true)); - notifications.clear(); - } else { - for (WatchKey key : keysToRemove) { - key.cancel(); - keyToService.remove(key); - registeredKeys.remove(key); - hashes.remove(service); - } - } - } - - @Override - public void run() { - while (!Thread.currentThread().isInterrupted()) { - try { - WatchKey key = null; - WatchService watchService = this.watchService; - if (watchService != null) { - key = watchService.take(); - } - - if (key == null) { - continue; - } - for (WatchEvent event : key.pollEvents()) { - Kind kind = event.kind(); - if (OVERFLOW.equals(kind)) { - logger.warn( - "Found an event of kind 'OVERFLOW': {}. 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 - Set services; - synchronized (this) { - services = keyToService.get(key); - } - if (services != null) { - File f = resolvedPath.toFile(); - if (ENTRY_MODIFY.equals(kind) && f.isDirectory()) { - logger.trace("Skipping modification event for directory: {}", f); - } else { - if (ENTRY_MODIFY.equals(kind)) { - processModificationEvent(key, event, resolvedPath, services); - } else { - services.forEach(s -> s.processWatchEvent(event, kind, resolvedPath)); - } - } - if (ENTRY_CREATE.equals(kind) && f.isDirectory()) { - for (AbstractWatchService service : services) { - if (service.watchSubDirectories() - && service.getWatchEventKinds(resolvedPath) != null) { - registerDirectoryInternal(service, service.getWatchEventKinds(resolvedPath), - resolvedPath); - } - } - } else if (ENTRY_DELETE.equals(kind)) { - synchronized (this) { - WatchKey toCancel = null; - for (Map.Entry entry : registeredKeys.entrySet()) { - if (entry.getValue().equals(resolvedPath)) { - toCancel = entry.getKey(); - break; - } - } - if (toCancel != null) { - registeredKeys.remove(toCancel); - keyToService.remove(toCancel); - toCancel.cancel(); - } - - services.forEach(service -> forgetChecksum(service, resolvedPath)); - notifications.forEach(notification -> { - if (notification.path.equals(resolvedPath)) { - notification.future.cancel(true); - notifications.remove(notification); - } - }); - } - } - } - } - } - - key.reset(); - } catch (InterruptedException exc) { - logger.debug("Caught InterruptedException. Shutting down."); - return; - } catch (Exception exc) { - logger.debug("Exception caught in WatchQueueReader. Restarting.", exc); - } - } - } - - /** - * Schedules forwarding of the event to the listeners (if applicable). - * - * By delaying the forwarding, duplicate modification events and those where the actual file-content is not - * consistent or empty in between will get skipped and the file system gets a chance to "settle" before the - * framework is going to act on it. - * - * Also, modification events are received for meta-data changes (e.g. last modification timestamp or file - * permissions). They are filtered out by comparing the checksums of the file's content. - * - * See also thisdiscussion - * on Stack Overflow. - * - * @param key the {@link WatchKey} - * @param event the {@link WatchEvent} itself - * @param resolvedPath the resolved {@link Path} for this event - * @param services the {@link AbstractWatchService}s that subscribe to this event - */ - private void processModificationEvent(WatchKey key, WatchEvent event, Path resolvedPath, - Set services) { - synchronized (notifications) { - for (AbstractWatchService service : services) { - logger.trace("Modification event for {} ", resolvedPath); - removeScheduledNotifications(key, service, resolvedPath, true); - ScheduledFuture future = scheduler.schedule(() -> { - logger.trace("Executing job for {}", resolvedPath); - if (removeScheduledNotifications(key, service, resolvedPath, false)) { - logger.trace("Job removed itself for {}", resolvedPath); - } else { - logger.trace("Job couldn't find itself for {}", resolvedPath); - } - if (checkAndTrackContent(service, resolvedPath)) { - service.processWatchEvent(event, event.kind(), resolvedPath); - } else { - logger.trace("File content '{}' has not changed, skipping modification event", resolvedPath); - } - }, PROCESSING_DELAY, TimeUnit.MILLISECONDS); - logger.trace("Scheduled processing of {}", resolvedPath); - notifications.add(new Notification(key, service, resolvedPath, future)); - } - } - } - - private boolean removeScheduledNotifications(WatchKey key, AbstractWatchService service, Path path, - boolean cancelJob) { - Set notifications = this.notifications.stream().filter(f -> f.matches(key, service, path)) - .collect(Collectors.toSet()); - if (notifications.size() > 1) { - logger.warn("Found more than one notification for {} / {} / {}. This is a bug.", key, service, path); - } - if (cancelJob) { - notifications.forEach(notification -> notification.future.cancel(true)); - } - this.notifications.removeAll(notifications); - return !notifications.isEmpty(); - } - - private @Nullable Path resolvePath(WatchKey key, WatchEvent event) { - WatchEvent ev = cast(event); - // Context for directory entry event is the file name of entry. - Path contextPath = ev.context(); - List baseWatchedDir; - Path registeredPath; - synchronized (this) { - baseWatchedDir = keyToService.getOrDefault(key, Set.of()).stream().map(AbstractWatchService::getSourcePath) - .filter(Objects::nonNull).map(Objects::requireNonNull).collect(Collectors.toList()); - 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 - return registeredPath.resolve(contextPath); - } - - logger.warn( - "Detected invalid WatchEvent '{}' and key '{}' for entry '{}' in not registered file or directory of '{}'", - event, key, contextPath, baseWatchedDir); - return null; - } - - private byte @Nullable [] hash(Path path) { - try { - MessageDigest digester = MessageDigest.getInstance("SHA-256"); - if (!Files.exists(path)) { - return null; - } - try (InputStream is = Files.newInputStream(path)) { - byte[] buffer = new byte[4069]; - int read; - do { - read = is.read(buffer); - if (read > 0) { - digester.update(buffer, 0, read); - } - } while (read != -1); - } - return digester.digest(); - } catch (NoSuchAlgorithmException | IOException e) { - logger.debug("Error calculating the hash of file {}", path, e); - return null; - } - } - - /** - * Calculate a checksum of the given file and report back whether it has changed since the last time. - * - * @param service the service determining the scope - * @param resolvedPath the file path - * @return {@code true} if the file content has changed since the last call to this method - */ - private boolean checkAndTrackContent(AbstractWatchService service, Path resolvedPath) { - byte[] newHash = hash(resolvedPath); - if (newHash == null) { - return true; - } - Map keyHashes = Objects.requireNonNull(hashes.computeIfAbsent(service, s -> new HashMap<>())); - byte[] oldHash = keyHashes.put(resolvedPath, newHash); - return oldHash == null || !Arrays.equals(oldHash, newHash); - } - - private void forgetChecksum(AbstractWatchService service, Path resolvedPath) { - Map keyHashes = hashes.get(service); - if (keyHashes != null) { - keyHashes.remove(resolvedPath); - } - } - - /** - * The {@link Notification} stores the information of a single notification - */ - private static class Notification { - public final WatchKey key; - public final AbstractWatchService service; - public final Path path; - public final ScheduledFuture future; - - private Notification(WatchKey key, AbstractWatchService service, Path path, ScheduledFuture future) { - this.key = key; - this.service = service; - this.path = path; - this.future = future; - } - - public boolean matches(WatchKey key, AbstractWatchService service, Path path) { - return this.key.equals(key) && this.service.equals(service) && this.path.equals(path); - } - - @Override - public boolean equals(@Nullable Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Notification notification = (Notification) o; - return key.equals(notification.key) && service.equals(notification.service) - && path.equals(notification.path) && future.equals(notification.future); - } - - @Override - public int hashCode() { - return Objects.hash(key, service, path, future); - } - } -} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchService.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchService.java new file mode 100644 index 00000000000..f0fdf7a8d81 --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchService.java @@ -0,0 +1,126 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.service; + +import java.nio.file.Path; +import java.util.List; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link WatchService} defines the interface for a general watch service. It allows registering + * listeners for subdirectories of the openHAB configuration directory. The reported path in the event is relative to + * the registered path. Watch services are created by {@link WatchServiceFactory#createWatchService(String, Path)}. + * + * For files in the openHAB configuration folder a watch service with the name {@link WatchService#CONFIG_WATCHER_NAME} + * is registered. For convenience, an OSGi target filter for referencing this watch service is provided + * {@link WatchService#CONFIG_WATCHER_FILTER}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface WatchService { + String SERVICE_PID = "org.openhab.core.service.WatchService"; + String SERVICE_PROPERTY_DIR = "watchservice.dir"; + String SERVICE_PROPERTY_NAME = "watchservice.name"; + String CONFIG_WATCHER_NAME = "configWatcher"; + String CONFIG_WATCHER_FILTER = "(" + SERVICE_PROPERTY_NAME + "=" + CONFIG_WATCHER_NAME + ")"; + + /** + * Register a listener for this {@link WatchService} + * + * The given listener will be notified about all events related to files and directories (including subdirectories) + * in the given {@link Path} + *

+ * Listeners must unregister themselves before they are disposed + * + * @param watchEventListener the listener for this configuration + * @param path a {@link Path} that the listener is interested in, relative to the base path of the watch service + */ + default void registerListener(WatchEventListener watchEventListener, Path path) { + registerListener(watchEventListener, List.of(path), true); + } + + /** + * Register a listener for this {@link WatchService} + * + * The given listener will be notified about all events related to files and directories (including subdirectories) + * in the given {@link Path} + *

+ * Listeners must unregister themselves before they are disposed + * + * @param watchEventListener the listener for this configuration + * @param paths a list of {@link Path} that the listener is interested in, relative to the base path of the watch + * service + */ + default void registerListener(WatchEventListener watchEventListener, List paths) { + registerListener(watchEventListener, paths, true); + } + + /** + * Register a listener for this {@link WatchService} + * + * The given listener will be notified about all events related to files and directories in the given {@link Path} + *

+ * Listeners must unregister themselves before they are disposed + * + * @param watchEventListener the listener for this configuration + * @param path the{@link Path} that the listener is interested in, relative to the base path of the watch service + * @param withSubDirectories whether subdirectories of the given path should also be watched + */ + default void registerListener(WatchEventListener watchEventListener, Path path, boolean withSubDirectories) { + registerListener(watchEventListener, List.of(path), withSubDirectories); + } + + /** + * Register a listener for this {@link WatchService} + * + * The given listener will be notified about all events related to files and directories in the given list of + * {@link Path} + *

+ * Listeners must unregister themselves before they are disposed + * + * @param watchEventListener the listener for this configuration + * @param paths a list of {@link Path} that the listener is interested in, relative to the base path of the watch + * service + * @param withSubDirectories whether subdirectories of the given paths should also be watched + */ + void registerListener(WatchEventListener watchEventListener, List paths, boolean withSubDirectories); + + /** + * Unregister a listener from this {@link WatchService} + * + * The listener will no longer be notified of watch events + * + * @param watchEventListener the listener to unregister + */ + void unregisterListener(WatchEventListener watchEventListener); + + @FunctionalInterface + interface WatchEventListener { + /** + * Notify Listener about watch event + * + * @param kind the {@link Kind} of this event + * @param path the relative path of the file associated with this event + */ + void processWatchEvent(Kind kind, Path path); + } + + enum Kind { + CREATE, + MODIFY, + DELETE, + OVERFLOW + } +} diff --git a/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchServiceFactory.java b/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchServiceFactory.java new file mode 100644 index 00000000000..fea74a0717a --- /dev/null +++ b/bundles/org.openhab.core/src/main/java/org/openhab/core/service/WatchServiceFactory.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.service; + +import java.io.IOException; +import java.nio.file.Path; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link WatchServiceFactory} is used to create {@link WatchService} instances. + * + * For files in the openHAB configuration folder a watch service with the name {@link WatchService#CONFIG_WATCHER_NAME} + * is registered. For convenience, an OSGi target filter for referencing this watch service is provided + * {@link WatchService#CONFIG_WATCHER_FILTER}. + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +public interface WatchServiceFactory { + + /** + * Create a new {@link WatchService} service component with the given name and path or return the already existing + * instance if a {@link WatchService} with the given name was created before. + * + * @param name the name of the service to create/get (must follow the conventions of an OSGi service name) + * @param basePath the base path of the watch service (path is created if it does not exist) + * @return a {@link WatchService} with the given configuration + * @throws IOException if the {@link WatchService} could not be instantiated + */ + void createWatchService(String name, Path basePath) throws IOException; + + /** + * Dispose the {@link WatchService} service component + * + * @param name the name of the {@link WatchService} + */ + void removeWatchService(String name); +} diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/service/WatchServiceImplTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/service/WatchServiceImplTest.java new file mode 100644 index 00000000000..ef1c6ee07cb --- /dev/null +++ b/bundles/org.openhab.core/src/test/java/org/openhab/core/internal/service/WatchServiceImplTest.java @@ -0,0 +1,223 @@ +/** + * Copyright (c) 2010-2023 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.internal.service; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ExecutorService; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.openhab.core.JavaTest; +import org.openhab.core.OpenHAB; +import org.openhab.core.common.ThreadPoolManager; +import org.openhab.core.service.WatchService; +import org.openhab.core.service.WatchService.Kind; +import org.osgi.framework.BundleContext; + +/** + * The {@link WatchServiceImplTest} is a + * + * @author Jan N. Klug - Initial contribution + */ +@NonNullByDefault +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class WatchServiceImplTest extends JavaTest { + private static final String SUB_DIR_PATH_NAME = "subDir"; + private static final String TEST_FILE_NANE = "testFile"; + + private @NonNullByDefault({}) String systemConfDirProperty; + + private @NonNullByDefault({}) WatchServiceImpl.WatchServiceConfiguration configurationMock; + + private @NonNullByDefault({}) WatchServiceImpl watchService; + private @NonNullByDefault({}) Path rootPath; + private @NonNullByDefault({}) Path subDirPath; + private @NonNullByDefault({}) TestWatchEventListener listener; + + @BeforeEach + public void setup() throws IOException { + // store property so we can restore later + systemConfDirProperty = System.getProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT); + + rootPath = Files.createDirectories(Path.of("target", "test-watcher")); + subDirPath = Files.createDirectories(rootPath.resolve(SUB_DIR_PATH_NAME)); + ExecutorService ex = ThreadPoolManager.getScheduledPool("file-processing"); + System.setProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, rootPath.toString()); + + when(configurationMock.name()).thenReturn("unnamed"); + when(configurationMock.path()).thenReturn(""); + + watchService = new WatchServiceImpl(configurationMock, mock(BundleContext.class)); + listener = new TestWatchEventListener(); + } + + @AfterEach + public void tearDown() throws IOException { + watchService.deactivate(); + System.setProperty(OpenHAB.CONFIG_DIR_PROG_ARGUMENT, systemConfDirProperty); + } + + @Test + private void testFileInWatchedDir() throws IOException, InterruptedException { + watchService.registerListener(listener, Path.of(""), false); + + Path testFile = rootPath.resolve(TEST_FILE_NANE); + Path relativeTestFilePath = Path.of(TEST_FILE_NANE); + + Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); + assertEvent(relativeTestFilePath, Kind.CREATE); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertEvent(relativeTestFilePath, Kind.MODIFY); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertNoEvent(); + + Files.delete(testFile); + assertEvent(relativeTestFilePath, Kind.DELETE); + } + + @Test + private void testFileInWatchedSubDir() throws IOException, InterruptedException { + // listener is listening to root and sub-dir + watchService.registerListener(listener, Path.of(""), false); + + Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NANE); + Path relativeTestFilePath = Path.of(SUB_DIR_PATH_NAME, TEST_FILE_NANE); + + Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); + assertEvent(relativeTestFilePath, Kind.CREATE); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertEvent(relativeTestFilePath, Kind.MODIFY); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertNoEvent(); + + Files.delete(testFile); + assertEvent(relativeTestFilePath, Kind.DELETE); + } + + @Test + private void testFileInWatchedSubDir2() throws IOException, InterruptedException { + // listener is only listening to sub-dir of root + watchService.registerListener(listener, Path.of(SUB_DIR_PATH_NAME), false); + + Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NANE); + Path relativeTestFilePath = Path.of(TEST_FILE_NANE); + + Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); + assertEvent(relativeTestFilePath, Kind.CREATE); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertEvent(relativeTestFilePath, Kind.MODIFY); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertNoEvent(); + + Files.delete(testFile); + assertEvent(relativeTestFilePath, Kind.DELETE); + } + + @Test + private void testFileInUnwatchedSubDir() throws IOException, InterruptedException { + watchService.registerListener(listener, Path.of(""), false); + + Path testFile = rootPath.resolve(SUB_DIR_PATH_NAME).resolve(TEST_FILE_NANE); + + Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); + assertNoEvent(); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertNoEvent(); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertNoEvent(); + + Files.delete(testFile); + assertNoEvent(); + } + + @Test + private void testNewSubDirAlsoWatched() throws IOException, InterruptedException { + watchService.registerListener(listener, Path.of(""), false); + + Path subDirSubDir = Files.createDirectories(rootPath.resolve(SUB_DIR_PATH_NAME).resolve(SUB_DIR_PATH_NAME)); + assertNoEvent(); + + Path testFile = subDirSubDir.resolve(TEST_FILE_NANE); + Path relativeTestFilePath = testFile.relativize(rootPath); + + Files.writeString(testFile, "initial content", StandardCharsets.UTF_8); + assertEvent(relativeTestFilePath, Kind.CREATE); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertEvent(relativeTestFilePath, Kind.MODIFY); + + Files.writeString(testFile, "modified content", StandardCharsets.UTF_8); + assertNoEvent(); + + Files.delete(testFile); + assertEvent(relativeTestFilePath, Kind.DELETE); + + Files.delete(subDirSubDir); + assertNoEvent(); + } + + private void assertNoEvent() throws InterruptedException { + Thread.sleep(5000); + + assertThat(listener.events, empty()); + } + + private void assertEvent(Path path, Kind kind) throws InterruptedException { + waitForAssert(() -> assertThat(listener.events, not(empty()))); + Thread.sleep(500); + + assertThat(listener.events, hasSize(1)); + assertThat(listener.events, hasItem(new Event(path, kind))); + listener.events.clear(); + } + + private class TestWatchEventListener implements WatchService.WatchEventListener { + List events = new CopyOnWriteArrayList<>(); + + @Override + public void processWatchEvent(Kind kind, Path path) { + events.add(new Event(path, kind)); + } + } + + record Event(Path path, Kind kind) { + } +} diff --git a/bundles/org.openhab.core/src/test/java/org/openhab/core/service/AbstractWatchServiceTest.java b/bundles/org.openhab.core/src/test/java/org/openhab/core/service/AbstractWatchServiceTest.java deleted file mode 100644 index d6f9dbf8c7e..00000000000 --- a/bundles/org.openhab.core/src/test/java/org/openhab/core/service/AbstractWatchServiceTest.java +++ /dev/null @@ -1,340 +0,0 @@ -/** - * Copyright (c) 2010-2023 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.core.service; - -import static java.nio.file.StandardWatchEventKinds.*; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.nio.file.StandardOpenOption; -import java.nio.file.WatchEvent; -import java.nio.file.WatchEvent.Kind; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Stream; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.openhab.core.JavaTest; - -/** - * Test for {@link AbstractWatchService}. - * - * @author Dimitar Ivanov - Initial contribution - * @author Svilen Valkanov - Tests are modified to run on different Operating Systems - * @author Ana Dimova - reduce to a single watch thread for all class instances - * @author Simon Kaufmann - ported it from Groovy to Java - */ -@NonNullByDefault -public class AbstractWatchServiceTest extends JavaTest { - - private static final String WATCHED_DIRECTORY = "watchDirectory"; - - // Fail if no event has been received within the given timeout - private static int noEventTimeoutInSeconds; - - private @NonNullByDefault({}) RelativeWatchService watchService; - - @BeforeAll - public static void setUpBeforeClass() { - // set the NO_EVENT_TIMEOUT_IN_SECONDS according to the operating system used - if (System.getProperty("os.name").startsWith("Mac OS X")) { - noEventTimeoutInSeconds = 15; - } else { - noEventTimeoutInSeconds = 3; - } - } - - @BeforeEach - public void setup() { - File watchDir = new File(WATCHED_DIRECTORY); - watchDir.mkdirs(); - } - - @AfterEach - public void tearDown() throws Exception { - watchService.deactivate(); - final Path watchedDirectory = Paths.get(WATCHED_DIRECTORY); - if (Files.exists(watchedDirectory)) { - try (Stream walk = Files.walk(watchedDirectory)) { - walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); - } - } - watchService.allFullEvents.clear(); - } - - @Test - public void testInRoot() throws Exception { - watchService = new RelativeWatchService(WATCHED_DIRECTORY, true); - - // File created in the watched directory - assertByRelativePath("rootWatchFile"); - } - - @Test - public void testInSub() throws Exception { - watchService = new RelativeWatchService(WATCHED_DIRECTORY, true); - - // File created in a subdirectory of the watched directory - assertByRelativePath("subDir" + File.separatorChar + "subDirWatchFile"); - } - - @Test - public void testInSubSub() throws Exception { - watchService = new RelativeWatchService(WATCHED_DIRECTORY, true); - - // File created in a sub sub directory of the watched directory - assertByRelativePath("subDir" + File.separatorChar + "subSubDir" + File.separatorChar + "innerWatchFile"); - } - - @Test - public void testIdenticalNames() throws Exception { - watchService = new RelativeWatchService(WATCHED_DIRECTORY, true); - - String fileName = "duplicateFile"; - String innerFileName = "duplicateDir" + File.separatorChar + fileName; - - File innerfile = new File(WATCHED_DIRECTORY + File.separatorChar + innerFileName); - innerfile.getParentFile().mkdirs(); - - // Activate the service when the subdir is also present. Else the subdir will not be registered - watchService.activate(); - - innerfile.createNewFile(); - - // Assure that the ordering of the events will be always the same - Thread.sleep(noEventTimeoutInSeconds * 1000); - - new File(WATCHED_DIRECTORY + File.separatorChar + fileName).createNewFile(); - - assertEventCount(2); - - FullEvent innerFileEvent = watchService.allFullEvents.get(0); - assertThat(innerFileEvent.eventKind, is(ENTRY_CREATE)); - assertThat(innerFileEvent.eventPath.toString(), is(WATCHED_DIRECTORY + File.separatorChar + innerFileName)); - - FullEvent fileEvent = watchService.allFullEvents.get(1); - assertThat(fileEvent.eventKind, is(ENTRY_CREATE)); - assertThat(fileEvent.eventPath.toString(), is(WATCHED_DIRECTORY + File.separatorChar + fileName)); - } - - @Test - public void testExcludeSubdirs() throws Exception { - // Do not watch the subdirectories of the root directory - watchService = new RelativeWatchService(WATCHED_DIRECTORY, false); - - String innerFileName = "watchRequestSubDir" + File.separatorChar + "watchRequestInnerFile"; - - File innerFile = new File(WATCHED_DIRECTORY + File.separatorChar + innerFileName); - innerFile.getParentFile().mkdirs(); - - watchService.activate(); - - // Consequent creation and deletion in order to generate any watch events for the subdirectory - innerFile.createNewFile(); - innerFile.delete(); - - assertNoEventsAreProcessed(); - } - - @Test - public void testIncludeSubdirs() throws Exception { - // Do watch the subdirectories of the root directory - watchService = new RelativeWatchService(WATCHED_DIRECTORY, true); - - String 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(); - - innerFile.createNewFile(); - assertFileCreateEventIsProcessed(innerFile, innerFileName); - - watchService.allFullEvents.clear(); - assertNoEventsAreProcessed(); - } - - @Test - public void testChangeDirectory() { - WatchQueueReader watchQueueReaderMock = mock(WatchQueueReader.class); - watchService = new RelativeWatchService("foo", false); - watchService.setWatchQueueReader(watchQueueReaderMock); - watchService.activate(); - verify(watchQueueReaderMock).customizeWatchQueueReader(watchService, Path.of("foo"), false); - watchService.changeWatchDirectory("bar"); - verify(watchQueueReaderMock).stopWatchService(watchService); - verify(watchQueueReaderMock).customizeWatchQueueReader(watchService, Path.of("bar"), false); - } - - private void assertNoEventsAreProcessed() throws Exception { - // Wait for a possible event for the maximum timeout - Thread.sleep(noEventTimeoutInSeconds * 1000); - - assertEventCount(0); - } - - private void assertFileCreateEventIsProcessed(File innerFile, String innerFileName) { - // Single event for file creation is present - assertEventCount(1); - FullEvent fileEvent = watchService.allFullEvents.get(0); - assertThat(fileEvent.eventKind, is(ENTRY_CREATE)); - assertThat(fileEvent.eventPath.toString(), is(WATCHED_DIRECTORY + File.separatorChar + innerFileName)); - } - - private void assertByRelativePath(String fileName) throws Exception { - File file = new File(WATCHED_DIRECTORY + File.separatorChar + fileName); - file.getParentFile().mkdirs(); - - assertThat(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(); - - file.createNewFile(); - fullEventAssertionsByKind(fileName, ENTRY_CREATE, false); - - // File modified - Files.writeString(file.toPath(), "Additional content", StandardOpenOption.APPEND); - fullEventAssertionsByKind(fileName, ENTRY_MODIFY, false); - - // File modified but identical content - Files.writeString(file.toPath(), "Additional content", StandardOpenOption.TRUNCATE_EXISTING); - assertNoEventsAreProcessed(); - - // File deleted - file.delete(); - fullEventAssertionsByKind(fileName, ENTRY_DELETE, true); - } - - private void assertEventCount(int expected) { - try { - waitForAssert(() -> assertThat(watchService.allFullEvents.size(), is(expected))); - } catch (AssertionError e) { - watchService.allFullEvents.forEach(event -> event.toString()); - throw e; - } - } - - private void fullEventAssertionsByKind(String fileName, Kind kind, boolean osSpecific) throws Exception { - waitForAssert(() -> assertThat(!watchService.allFullEvents.isEmpty(), is(true)), JavaTest.DFL_TIMEOUT * 2, - JavaTest.DFL_SLEEP_TIME); - - if (osSpecific && ENTRY_DELETE.equals(kind)) { - // There is possibility that one more modify event is triggered on some OS - // so sleep a bit extra time - Thread.sleep(500); - cleanUpOsSpecificModifyEvent(); - } - - assertEventCount(1); - FullEvent fullEvent = watchService.allFullEvents.get(0); - - assertThat(fullEvent.eventPath.toString(), is(WATCHED_DIRECTORY + File.separatorChar + fileName)); - assertThat(fullEvent.eventKind, is(kind)); - assertThat(fullEvent.watchEvent.count() >= 1, is(true)); - assertThat(fullEvent.watchEvent.kind(), is(fullEvent.eventKind)); - String fileNameOnly = fileName.contains(File.separatorChar + "") - ? fileName.substring(fileName.lastIndexOf(File.separatorChar) + 1, fileName.length()) - : fileName; - assertThat(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 void 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 - && ENTRY_MODIFY.equals(watchService.allFullEvents.get(0).eventKind); - if (isDeletedWithPrecedingModify) { - // Remove the ENTRY_MODIFY element as it is not needed - watchService.allFullEvents.remove(0); - } - } - - private static class RelativeWatchService extends AbstractWatchService { - - boolean watchSubDirs; - - // Synchronize list as several watcher threads can write into it - public volatile List allFullEvents = new CopyOnWriteArrayList<>(); - - RelativeWatchService(String rootPath, boolean watchSubDirectories) { - super(rootPath); - watchSubDirs = watchSubDirectories; - } - - /** - * Inject a mocked WatchQueueReader - * - * @param watchQueueReader the mock - */ - public void setWatchQueueReader(WatchQueueReader watchQueueReader) { - this.watchQueueReader = watchQueueReader; - } - - @Override - protected void processWatchEvent(WatchEvent event, Kind kind, Path path) { - FullEvent fullEvent = new FullEvent(event, kind, path); - allFullEvents.add(fullEvent); - } - - @Override - protected Kind @Nullable [] getWatchEventKinds(Path subDir) { - return new Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; - } - - @Override - protected boolean watchSubDirectories() { - return watchSubDirs; - } - } - - private static 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/features/karaf/openhab-core/src/main/feature/feature.xml b/features/karaf/openhab-core/src/main/feature/feature.xml index b60919acc62..b5c7d7e7217 100644 --- a/features/karaf/openhab-core/src/main/feature/feature.xml +++ b/features/karaf/openhab-core/src/main/feature/feature.xml @@ -46,6 +46,9 @@ mvn:org.openhab.core.bundles/org.openhab.core.config.dispatch/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.config.xml/${project.version} mvn:org.openhab.core.bundles/org.openhab.core/${project.version} + mvn:org.openhab.osgiify/io.methvin.directory-watcher/0.17.1 + mvn:net.java.dev.jna/jna/5.12.1 + mvn:net.java.dev.jna/jna-platform/5.12.1 openhab-core-storage-json mvn:org.openhab.core.bundles/org.openhab.core.addon.xml/${project.version} mvn:org.openhab.core.bundles/org.openhab.core.ephemeris/${project.version} diff --git a/itests/org.openhab.core.addon.xml.tests/itest.bndrun b/itests/org.openhab.core.addon.xml.tests/itest.bndrun index 9739940edaa..eaadf8281a4 100644 --- a/itests/org.openhab.core.addon.xml.tests/itest.bndrun +++ b/itests/org.openhab.core.addon.xml.tests/itest.bndrun @@ -54,8 +54,10 @@ Fragment-Host: org.openhab.core.addon.xml biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ - org.openhab.core.addon.xml;version='[4.0.0,4.0.1)',\ - org.openhab.core.addon.xml.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.xml;version='[4.0.0,4.0.1)',\ - org.openhab.core.test;version='[4.0.0,4.0.1)' + org.openhab.core.test;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.openhab.core.addon.xml;version='[4.0.0,4.0.1)',\ + org.openhab.core.addon.xml.tests;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.auth.oauth2client.tests/itest.bndrun b/itests/org.openhab.core.auth.oauth2client.tests/itest.bndrun index 74bcd28658b..2b90fad930e 100644 --- a/itests/org.openhab.core.auth.oauth2client.tests/itest.bndrun +++ b/itests/org.openhab.core.auth.oauth2client.tests/itest.bndrun @@ -62,4 +62,6 @@ Fragment-Host: org.openhab.core.auth.oauth2client org.openhab.core.auth.oauth2client.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.io.net;version='[4.0.0,4.0.1)',\ - org.openhab.core.test;version='[4.0.0,4.0.1)' + org.openhab.core.test;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)' diff --git a/itests/org.openhab.core.automation.integration.tests/itest.bndrun b/itests/org.openhab.core.automation.integration.tests/itest.bndrun index d5cfeccd370..5df7bcaffd9 100644 --- a/itests/org.openhab.core.automation.integration.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.integration.tests/itest.bndrun @@ -48,6 +48,7 @@ Fragment-Host: org.openhab.core.automation ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.automation;version='[4.0.0,4.0.1)',\ org.openhab.core.automation.integration.tests;version='[4.0.0,4.0.1)',\ @@ -56,4 +57,7 @@ Fragment-Host: org.openhab.core.automation org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.automation.module.core.tests/itest.bndrun b/itests/org.openhab.core.automation.module.core.tests/itest.bndrun index 0642b71dad5..e26acb2bc0a 100644 --- a/itests/org.openhab.core.automation.module.core.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.module.core.tests/itest.bndrun @@ -48,6 +48,7 @@ Fragment-Host: org.openhab.core.automation ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.automation;version='[4.0.0,4.0.1)',\ org.openhab.core.automation.module.core.tests;version='[4.0.0,4.0.1)',\ @@ -56,4 +57,7 @@ Fragment-Host: org.openhab.core.automation org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.automation.module.script.tests/itest.bndrun b/itests/org.openhab.core.automation.module.script.tests/itest.bndrun index 08e890ceb21..9832781cdd6 100644 --- a/itests/org.openhab.core.automation.module.script.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.module.script.tests/itest.bndrun @@ -48,6 +48,7 @@ Fragment-Host: org.openhab.core.automation.module.script ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.automation;version='[4.0.0,4.0.1)',\ org.openhab.core.automation.module.script;version='[4.0.0,4.0.1)',\ @@ -58,4 +59,7 @@ Fragment-Host: org.openhab.core.automation.module.script org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ org.openhab.core.transform;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun b/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun index 53281b1ac76..27874aa4a71 100644 --- a/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.module.timer.tests/itest.bndrun @@ -48,6 +48,7 @@ Fragment-Host: org.openhab.core.automation ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.automation;version='[4.0.0,4.0.1)',\ org.openhab.core.automation.module.timer.tests;version='[4.0.0,4.0.1)',\ @@ -56,4 +57,7 @@ Fragment-Host: org.openhab.core.automation org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.automation.tests/itest.bndrun b/itests/org.openhab.core.automation.tests/itest.bndrun index c5501b38fa9..5964f7eae71 100644 --- a/itests/org.openhab.core.automation.tests/itest.bndrun +++ b/itests/org.openhab.core.automation.tests/itest.bndrun @@ -48,6 +48,7 @@ Fragment-Host: org.openhab.core.automation ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.automation;version='[4.0.0,4.0.1)',\ org.openhab.core.automation.tests;version='[4.0.0,4.0.1)',\ @@ -56,4 +57,7 @@ Fragment-Host: org.openhab.core.automation org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.config.core.tests/itest.bndrun b/itests/org.openhab.core.config.core.tests/itest.bndrun index 2bb1b54a4cc..66f9fffc1b5 100644 --- a/itests/org.openhab.core.config.core.tests/itest.bndrun +++ b/itests/org.openhab.core.config.core.tests/itest.bndrun @@ -52,8 +52,12 @@ Fragment-Host: org.openhab.core.config.core ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun index 7bf217cabe2..9148a1b9e41 100644 --- a/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun +++ b/itests/org.openhab.core.config.discovery.mdns.tests/itest.bndrun @@ -62,4 +62,6 @@ Fragment-Host: org.openhab.core.config.discovery.mdns org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.io.transport.mdns;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ - org.openhab.core.thing;version='[4.0.0,4.0.1)' + org.openhab.core.thing;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)' diff --git a/itests/org.openhab.core.config.discovery.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.tests/itest.bndrun index 3e4696ae33e..d57281c5c51 100644 --- a/itests/org.openhab.core.config.discovery.tests/itest.bndrun +++ b/itests/org.openhab.core.config.discovery.tests/itest.bndrun @@ -65,4 +65,6 @@ Fragment-Host: org.openhab.core.config.discovery org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ - org.openhab.core.thing.xml;version='[4.0.0,4.0.1)' + org.openhab.core.thing.xml;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)' diff --git a/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun index 8855441cbfb..fd75e2d6a20 100644 --- a/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun +++ b/itests/org.openhab.core.config.discovery.usbserial.linuxsysfs.tests/itest.bndrun @@ -62,4 +62,6 @@ Fragment-Host: org.openhab.core.config.discovery.usbserial.linuxsysfs org.openhab.core.config.discovery.usbserial.linuxsysfs.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ - org.openhab.core.thing;version='[4.0.0,4.0.1)' + org.openhab.core.thing;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)' diff --git a/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun b/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun index 0b72b3e931e..f9d8036023a 100644 --- a/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun +++ b/itests/org.openhab.core.config.discovery.usbserial.tests/itest.bndrun @@ -70,4 +70,6 @@ Provide-Capability: \ org.openhab.core.config.discovery.usbserial.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ - org.openhab.core.thing;version='[4.0.0,4.0.1)' + org.openhab.core.thing;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)' diff --git a/itests/org.openhab.core.config.dispatch.tests/itest.bndrun b/itests/org.openhab.core.config.dispatch.tests/itest.bndrun index 34f9e0dd014..9a64694169d 100644 --- a/itests/org.openhab.core.config.dispatch.tests/itest.bndrun +++ b/itests/org.openhab.core.config.dispatch.tests/itest.bndrun @@ -52,4 +52,6 @@ Fragment-Host: org.openhab.core.config.dispatch org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.dispatch;version='[4.0.0,4.0.1)',\ org.openhab.core.config.dispatch.tests;version='[4.0.0,4.0.1)',\ - org.openhab.core.test;version='[4.0.0,4.0.1)' + org.openhab.core.test;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)' diff --git a/itests/org.openhab.core.config.xml.tests/itest.bndrun b/itests/org.openhab.core.config.xml.tests/itest.bndrun index 4cb4302e67d..c7aebe06bd9 100644 --- a/itests/org.openhab.core.config.xml.tests/itest.bndrun +++ b/itests/org.openhab.core.config.xml.tests/itest.bndrun @@ -47,9 +47,13 @@ Fragment-Host: org.openhab.core.config.xml ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.xml;version='[4.0.0,4.0.1)',\ org.openhab.core.config.xml.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.ephemeris.tests/itest.bndrun b/itests/org.openhab.core.ephemeris.tests/itest.bndrun index 254909589fc..f747502b14d 100644 --- a/itests/org.openhab.core.ephemeris.tests/itest.bndrun +++ b/itests/org.openhab.core.ephemeris.tests/itest.bndrun @@ -55,10 +55,14 @@ feature.openhab-config: \ ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.xml;version='[4.0.0,4.0.1)',\ org.openhab.core.ephemeris;version='[4.0.0,4.0.1)',\ org.openhab.core.ephemeris.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.io.rest.core.tests/itest.bndrun b/itests/org.openhab.core.io.rest.core.tests/itest.bndrun index b1d832b9362..5368649a7f1 100644 --- a/itests/org.openhab.core.io.rest.core.tests/itest.bndrun +++ b/itests/org.openhab.core.io.rest.core.tests/itest.bndrun @@ -45,7 +45,6 @@ Fragment-Host: org.openhab.core.io.rest.core org.glassfish.hk2.external.javax.inject;version='[2.4.0,2.4.1)',\ tech.units.indriya;version='[2.1.2,2.1.3)',\ uom-lib-common;version='[2.1.0,2.1.1)',\ - com.fasterxml.woodstox.woodstox-core;version='[6.2.6,6.2.7)',\ org.apache.cxf.cxf-core;version='[3.4.5,3.4.6)',\ org.apache.cxf.cxf-rt-frontend-jaxrs;version='[3.4.5,3.4.6)',\ org.apache.cxf.cxf-rt-rs-client;version='[3.4.5,3.4.6)',\ @@ -85,10 +84,10 @@ Fragment-Host: org.openhab.core.io.rest.core org.ops4j.pax.web.pax-web-spi;version='[7.3.25,7.3.26)',\ ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ + junit-jupiter-params;version='[5.8.1,5.8.2)',\ org.osgi.service.cm;version='[1.6.0,1.6.1)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ com.google.gson;version='[2.9.1,2.9.2)',\ - junit-jupiter-params;version='[5.8.1,5.8.2)',\ org.objectweb.asm;version='[9.4.0,9.4.1)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ @@ -101,4 +100,7 @@ Fragment-Host: org.openhab.core.io.rest.core org.openhab.core.semantics;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ - org.openhab.core.transform;version='[4.0.0,4.0.1)' + org.openhab.core.transform;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + com.fasterxml.woodstox.woodstox-core;version='[6.4.0,6.4.1)' diff --git a/itests/org.openhab.core.model.core.tests/.classpath b/itests/org.openhab.core.model.core.tests/.classpath deleted file mode 100644 index 9e55698cddc..00000000000 --- a/itests/org.openhab.core.model.core.tests/.classpath +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/itests/org.openhab.core.model.core.tests/.project b/itests/org.openhab.core.model.core.tests/.project deleted file mode 100644 index 2d77187b725..00000000000 --- a/itests/org.openhab.core.model.core.tests/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - org.openhab.core.model.core.tests - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.m2e.core.maven2Builder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.m2e.core.maven2Nature - - diff --git a/itests/org.openhab.core.model.core.tests/NOTICE b/itests/org.openhab.core.model.core.tests/NOTICE deleted file mode 100644 index 6c17d0d8a45..00000000000 --- a/itests/org.openhab.core.model.core.tests/NOTICE +++ /dev/null @@ -1,14 +0,0 @@ -This content is produced and maintained by the openHAB project. - -* Project home: https://www.openhab.org - -== Declared Project Licenses - -This program and the accompanying materials are made available under the terms -of the Eclipse Public License 2.0 which is available at -https://www.eclipse.org/legal/epl-2.0/. - -== Source Code - -https://github.com/openhab/openhab-core - diff --git a/itests/org.openhab.core.model.core.tests/itest.bndrun b/itests/org.openhab.core.model.core.tests/itest.bndrun deleted file mode 100644 index 0dd9e71b735..00000000000 --- a/itests/org.openhab.core.model.core.tests/itest.bndrun +++ /dev/null @@ -1,114 +0,0 @@ --include: ../itest-common.bndrun - -Bundle-SymbolicName: ${project.artifactId} -Fragment-Host: org.openhab.core.model.core - --runrequires: bnd.identity;id='org.openhab.core.model.core.tests' - -# -# done -# --runbundles: \ - org.antlr.runtime;version='[3.2.0,3.2.1)',\ - org.apache.felix.http.servlet-api;version='[1.1.2,1.1.3)',\ - org.glassfish.hk2.external.aopalliance-repackaged;version='[2.4.0,2.4.1)',\ - org.glassfish.hk2.external.javax.inject;version='[2.4.0,2.4.1)',\ - org.osgi.service.event;version='[1.4.0,1.4.1)',\ - org.apache.servicemix.specs.annotation-api-1.3;version='[1.3.0,1.3.1)',\ - org.eclipse.equinox.event;version='[1.4.300,1.4.301)',\ - com.google.guava.failureaccess;version='[1.0.1,1.0.2)',\ - org.hamcrest;version='[2.2.0,2.2.1)',\ - org.opentest4j;version='[1.2.0,1.2.1)',\ - com.sun.xml.bind.jaxb-osgi;version='[2.3.3,2.3.4)',\ - jakarta.xml.bind-api;version='[2.3.3,2.3.4)',\ - org.apache.servicemix.specs.activation-api-1.2.1;version='[1.2.1,1.2.2)',\ - org.eclipse.emf.common;version='[2.17.0,2.17.1)',\ - org.eclipse.emf.ecore;version='[2.20.0,2.20.1)',\ - org.eclipse.emf.ecore.xmi;version='[2.16.0,2.16.1)',\ - org.glassfish.hk2.osgi-resource-locator;version='[1.0.3,1.0.4)',\ - com.google.guava;version='[30.1.0,30.1.1)',\ - jakarta.annotation-api;version='[2.0.0,2.0.1)',\ - jakarta.inject.jakarta.inject-api;version='[2.0.0,2.0.1)',\ - javax.measure.unit-api;version='[2.1.2,2.1.3)',\ - org.osgi.service.cm;version='[1.6.0,1.6.1)',\ - tech.units.indriya;version='[2.1.2,2.1.3)',\ - uom-lib-common;version='[2.1.0,2.1.1)',\ - si-units;version='[2.1.0,2.1.1)',\ - si.uom.si-quantity;version='[2.1.0,2.1.1)',\ - junit-jupiter-api;version='[5.8.1,5.8.2)',\ - junit-jupiter-engine;version='[5.8.1,5.8.2)',\ - junit-platform-commons;version='[1.8.1,1.8.2)',\ - junit-platform-engine;version='[1.8.1,1.8.2)',\ - junit-platform-launcher;version='[1.8.1,1.8.2)',\ - net.bytebuddy.byte-buddy;version='[1.12.1,1.12.2)',\ - net.bytebuddy.byte-buddy-agent;version='[1.12.1,1.12.2)',\ - org.mockito.junit-jupiter;version='[4.1.0,4.1.1)',\ - org.mockito.mockito-core;version='[4.1.0,4.1.1)',\ - org.objenesis;version='[3.2.0,3.2.1)',\ - org.apache.felix.scr;version='[2.1.30,2.1.31)',\ - org.osgi.util.function;version='[1.2.0,1.2.1)',\ - org.osgi.util.promise;version='[1.2.0,1.2.1)',\ - com.google.inject;version='[5.0.1,5.0.2)',\ - jollyday;version='[0.5.10,0.5.11)',\ - org.objectweb.asm.commons;version='[9.0.0,9.0.1)',\ - org.objectweb.asm.tree;version='[9.0.0,9.0.1)',\ - org.threeten.extra;version='[1.5.0,1.5.1)',\ - org.apache.xbean.bundleutils;version='[4.21.0,4.21.1)',\ - org.apache.xbean.finder;version='[4.21.0,4.21.1)',\ - org.eclipse.jetty.client;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.http;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.io;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.security;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.server;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.servlet;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.util;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.util.ajax;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.websocket.api;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.websocket.client;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.websocket.common;version='[9.4.46,9.4.47)',\ - org.eclipse.jetty.xml;version='[9.4.46,9.4.47)',\ - org.ops4j.pax.logging.pax-logging-api;version='[2.0.16,2.0.17)',\ - org.ops4j.pax.web.pax-web-api;version='[7.3.25,7.3.26)',\ - org.ops4j.pax.web.pax-web-jetty;version='[7.3.25,7.3.26)',\ - org.ops4j.pax.web.pax-web-runtime;version='[7.3.25,7.3.26)',\ - org.ops4j.pax.web.pax-web-spi;version='[7.3.25,7.3.26)',\ - ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ - ch.qos.logback.core;version='[1.2.11,1.2.12)',\ - biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ - com.google.gson;version='[2.9.1,2.9.2)',\ - io.github.classgraph;version='[4.8.149,4.8.150)',\ - org.apache.log4j;version='[1.2.19,1.2.20)',\ - org.eclipse.equinox.common;version='[3.16.200,3.16.201)',\ - org.eclipse.xtend.lib;version='[2.29.0,2.29.1)',\ - org.eclipse.xtend.lib.macro;version='[2.29.0,2.29.1)',\ - org.eclipse.xtext;version='[2.29.0,2.29.1)',\ - org.eclipse.xtext.common.types;version='[2.29.0,2.29.1)',\ - org.eclipse.xtext.util;version='[2.29.0,2.29.1)',\ - org.eclipse.xtext.xbase;version='[2.29.0,2.29.1)',\ - org.eclipse.xtext.xbase.lib;version='[2.29.0,2.29.1)',\ - org.objectweb.asm;version='[9.4.0,9.4.1)',\ - org.openhab.core;version='[4.0.0,4.0.1)',\ - org.openhab.core.audio;version='[4.0.0,4.0.1)',\ - org.openhab.core.automation;version='[4.0.0,4.0.1)',\ - org.openhab.core.automation.module.script;version='[4.0.0,4.0.1)',\ - org.openhab.core.automation.module.script.rulesupport;version='[4.0.0,4.0.1)',\ - org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ - org.openhab.core.ephemeris;version='[4.0.0,4.0.1)',\ - org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ - org.openhab.core.io.http;version='[4.0.0,4.0.1)',\ - org.openhab.core.io.net;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.core;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.core.tests;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.item;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.persistence;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.rule;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.script;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.script.runtime;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.sitemap;version='[4.0.0,4.0.1)',\ - org.openhab.core.model.thing;version='[4.0.0,4.0.1)',\ - org.openhab.core.persistence;version='[4.0.0,4.0.1)',\ - org.openhab.core.semantics;version='[4.0.0,4.0.1)',\ - org.openhab.core.test;version='[4.0.0,4.0.1)',\ - org.openhab.core.thing;version='[4.0.0,4.0.1)',\ - org.openhab.core.transform;version='[4.0.0,4.0.1)',\ - org.openhab.core.voice;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.model.core.tests/pom.xml b/itests/org.openhab.core.model.core.tests/pom.xml deleted file mode 100644 index d7cba644591..00000000000 --- a/itests/org.openhab.core.model.core.tests/pom.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - 4.0.0 - - - org.openhab.core.itests - org.openhab.core.reactor.itests - 4.0.0-SNAPSHOT - - - org.openhab.core.model.core.tests - - openHAB Core :: Integration Tests :: Model Core Tests - - - - org.openhab.core.bom - org.openhab.core.bom.compile-model - pom - - - - diff --git a/itests/org.openhab.core.model.item.tests/itest.bndrun b/itests/org.openhab.core.model.item.tests/itest.bndrun index 45214d26624..dfcd3d7b78e 100644 --- a/itests/org.openhab.core.model.item.tests/itest.bndrun +++ b/itests/org.openhab.core.model.item.tests/itest.bndrun @@ -109,4 +109,8 @@ Fragment-Host: org.openhab.core.model.item org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ org.openhab.core.transform;version='[4.0.0,4.0.1)',\ - org.openhab.core.voice;version='[4.0.0,4.0.1)' + org.openhab.core.voice;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.openhab.core.model.sitemap.runtime;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.model.rule.tests/itest.bndrun b/itests/org.openhab.core.model.rule.tests/itest.bndrun index 4f107073fd7..b1b769d9a24 100644 --- a/itests/org.openhab.core.model.rule.tests/itest.bndrun +++ b/itests/org.openhab.core.model.rule.tests/itest.bndrun @@ -110,5 +110,9 @@ Fragment-Host: org.openhab.core.model.rule.runtime org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ org.openhab.core.transform;version='[4.0.0,4.0.1)',\ - org.openhab.core.voice;version='[4.0.0,4.0.1)' + org.openhab.core.voice;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.openhab.core.model.sitemap.runtime;version='[4.0.0,4.0.1)' -runblacklist: bnd.identity;id='jakarta.activation-api' diff --git a/itests/org.openhab.core.model.script.tests/itest.bndrun b/itests/org.openhab.core.model.script.tests/itest.bndrun index 82e445df389..c2720a97089 100644 --- a/itests/org.openhab.core.model.script.tests/itest.bndrun +++ b/itests/org.openhab.core.model.script.tests/itest.bndrun @@ -111,4 +111,7 @@ Fragment-Host: org.openhab.core.model.script org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ org.openhab.core.transform;version='[4.0.0,4.0.1)',\ - org.openhab.core.voice;version='[4.0.0,4.0.1)' + org.openhab.core.voice;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.openhab.core.model.sitemap.runtime;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.model.thing.tests/itest.bndrun b/itests/org.openhab.core.model.thing.tests/itest.bndrun index f28f44830f0..f69471f4018 100644 --- a/itests/org.openhab.core.model.thing.tests/itest.bndrun +++ b/itests/org.openhab.core.model.thing.tests/itest.bndrun @@ -122,4 +122,8 @@ Fragment-Host: org.openhab.core.model.thing org.openhab.core.thing;version='[4.0.0,4.0.1)',\ org.openhab.core.thing.xml;version='[4.0.0,4.0.1)',\ org.openhab.core.transform;version='[4.0.0,4.0.1)',\ - org.openhab.core.voice;version='[4.0.0,4.0.1)' + org.openhab.core.voice;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.openhab.core.model.sitemap.runtime;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.storage.json.tests/itest.bndrun b/itests/org.openhab.core.storage.json.tests/itest.bndrun index c82b22f5954..8833d48740a 100644 --- a/itests/org.openhab.core.storage.json.tests/itest.bndrun +++ b/itests/org.openhab.core.storage.json.tests/itest.bndrun @@ -46,6 +46,7 @@ Fragment-Host: org.openhab.core.storage.json ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ @@ -53,4 +54,7 @@ Fragment-Host: org.openhab.core.storage.json org.openhab.core.storage.json.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' \ No newline at end of file diff --git a/itests/org.openhab.core.tests/itest.bndrun b/itests/org.openhab.core.tests/itest.bndrun index e2cf02b0969..c929f41647a 100644 --- a/itests/org.openhab.core.tests/itest.bndrun +++ b/itests/org.openhab.core.tests/itest.bndrun @@ -51,7 +51,11 @@ Fragment-Host: org.openhab.core ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.tests;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)' diff --git a/itests/org.openhab.core.thing.tests/itest.bndrun b/itests/org.openhab.core.thing.tests/itest.bndrun index b899b7ee63d..3a0fd4dfffa 100644 --- a/itests/org.openhab.core.thing.tests/itest.bndrun +++ b/itests/org.openhab.core.thing.tests/itest.bndrun @@ -59,6 +59,7 @@ Fragment-Host: org.openhab.core.thing ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.xml;version='[4.0.0,4.0.1)',\ @@ -67,4 +68,5 @@ Fragment-Host: org.openhab.core.thing org.openhab.core.thing;version='[4.0.0,4.0.1)',\ org.openhab.core.thing.tests;version='[4.0.0,4.0.1)',\ org.openhab.core.thing.xml;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)' diff --git a/itests/org.openhab.core.thing.xml.tests/itest.bndrun b/itests/org.openhab.core.thing.xml.tests/itest.bndrun index c588111615c..db90d908d0c 100644 --- a/itests/org.openhab.core.thing.xml.tests/itest.bndrun +++ b/itests/org.openhab.core.thing.xml.tests/itest.bndrun @@ -51,11 +51,15 @@ Fragment-Host: org.openhab.core.thing.xml biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ com.google.gson;version='[2.9.1,2.9.2)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ - org.openhab.core.addon.xml;version='[4.0.0,4.0.1)',\ org.openhab.core.config.core;version='[4.0.0,4.0.1)',\ org.openhab.core.config.xml;version='[4.0.0,4.0.1)',\ org.openhab.core.io.console;version='[4.0.0,4.0.1)',\ org.openhab.core.test;version='[4.0.0,4.0.1)',\ org.openhab.core.thing;version='[4.0.0,4.0.1)',\ org.openhab.core.thing.xml;version='[4.0.0,4.0.1)',\ - org.openhab.core.thing.xml.tests;version='[4.0.0,4.0.1)' + org.openhab.core.thing.xml.tests;version='[4.0.0,4.0.1)',\ + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)',\ + org.apache.felix.configadmin;version='[1.9.24,1.9.25)',\ + org.osgi.service.cm;version='[1.6.0,1.6.1)',\ + org.openhab.core.addon.xml;version='[4.0.0,4.0.1)' diff --git a/itests/org.openhab.core.voice.tests/itest.bndrun b/itests/org.openhab.core.voice.tests/itest.bndrun index febdf64d8c6..d953bd89bd4 100644 --- a/itests/org.openhab.core.voice.tests/itest.bndrun +++ b/itests/org.openhab.core.voice.tests/itest.bndrun @@ -61,6 +61,8 @@ Fragment-Host: org.openhab.core.voice ch.qos.logback.classic;version='[1.2.11,1.2.12)',\ ch.qos.logback.core;version='[1.2.11,1.2.12)',\ biz.aQute.tester.junit-platform;version='[6.4.0,6.4.1)',\ + com.google.gson;version='[2.9.1,2.9.2)',\ + org.objectweb.asm;version='[9.4.0,9.4.1)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ org.openhab.core.audio;version='[4.0.0,4.0.1)',\ org.openhab.core;version='[4.0.0,4.0.1)',\ @@ -71,5 +73,5 @@ Fragment-Host: org.openhab.core.voice org.openhab.core.thing;version='[4.0.0,4.0.1)',\ org.openhab.core.voice;version='[4.0.0,4.0.1)',\ org.openhab.core.voice.tests;version='[4.0.0,4.0.1)',\ - com.google.gson;version='[2.9.1,2.9.2)',\ - org.objectweb.asm;version='[9.4.0,9.4.1)' + io.methvin.directory-watcher;version='[0.17.1,0.17.2)',\ + com.sun.jna;version='[5.12.1,5.12.2)' diff --git a/itests/pom.xml b/itests/pom.xml index e2f28f2dc70..4a20b096657 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -33,7 +33,6 @@ org.openhab.core.config.xml.tests org.openhab.core.ephemeris.tests org.openhab.core.io.rest.core.tests - org.openhab.core.model.core.tests org.openhab.core.model.item.tests org.openhab.core.model.rule.tests org.openhab.core.model.script.tests