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
}
}
- if (file.canRead() && (ENTRY_CREATE.equals(kind) || ENTRY_MODIFY.equals(kind))) {
- addFiles(listFiles(file.toPath(), watchSubDirectories()));
+ if (file.canRead() && (kind == CREATE || kind == MODIFY)) {
+ addFiles(listFiles(file.toPath(), watchSubDirectories));
}
}
}
@@ -341,17 +342,17 @@ private boolean createAndLoad(ScriptFileReference ref) {
}
private void initialImport() {
- File directory = new File(pathToWatch);
+ File directory = watchPath.toFile();
if (!directory.exists()) {
if (!directory.mkdirs()) {
- logger.warn("Failed to create watched directory: {}", pathToWatch);
+ logger.warn("Failed to create watched directory: {}", watchPath);
}
} else if (directory.isFile()) {
- logger.warn("Trying to watch directory {}, however it is a file", pathToWatch);
+ logger.warn("Trying to watch directory {}, however it is a file", watchPath);
}
- addFiles(listFiles(directory.toPath(), watchSubDirectories())).thenRun(() -> 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