diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/pom.xml b/bundles/org.openhab.core.automation.module.script.rulesupport/pom.xml
index b5d0000f4..888d75f68 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/pom.xml
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/pom.xml
@@ -25,6 +25,12 @@
org.openhab.core.automation.module.script
${project.version}
+
+ org.openhab.core.bundles
+ org.openhab.core.test
+ ${project.version}
+ test
+
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/collection/BidiSetBag.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/BidiSetBag.java
similarity index 99%
rename from bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/collection/BidiSetBag.java
rename to bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/BidiSetBag.java
index 5321ecba7..8e4578a9f 100644
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/collection/BidiSetBag.java
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/BidiSetBag.java
@@ -10,7 +10,7 @@
*
* SPDX-License-Identifier: EPL-2.0
*/
-package org.openhab.core.automation.module.script.rulesupport.internal.loader.collection;
+package org.openhab.core.automation.module.script.rulesupport.internal.loader;
import java.util.Collections;
import java.util.HashMap;
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 2b8367459..4d516b2de 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
@@ -18,6 +18,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.automation.module.script.ScriptEngineManager;
import org.openhab.core.automation.module.script.rulesupport.loader.AbstractScriptFileWatcher;
import org.openhab.core.service.ReadyService;
+import org.openhab.core.service.StartLevelService;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate;
@@ -37,8 +38,8 @@ public class DefaultScriptFileWatcher extends AbstractScriptFileWatcher {
@Activate
public DefaultScriptFileWatcher(final @Reference ScriptEngineManager manager,
- final @Reference ReadyService readyService) {
- super(manager, readyService, FILE_DIRECTORY);
+ final @Reference ReadyService readyService, final @Reference StartLevelService startLevelService) {
+ super(manager, readyService, startLevelService, FILE_DIRECTORY);
}
@Activate
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileReference.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileReference.java
new file mode 100644
index 000000000..acb886e2b
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileReference.java
@@ -0,0 +1,119 @@
+/**
+ * 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.automation.module.script.rulesupport.internal.loader;
+
+import java.nio.file.Path;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Script File wrapper offering various methods to inspect the script
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+@NonNullByDefault
+public class ScriptFileReference implements Comparable {
+ private static final Logger LOGGER = LoggerFactory.getLogger(ScriptFileReference.class);
+
+ private final AtomicBoolean queued = new AtomicBoolean();
+ private final AtomicBoolean loaded = new AtomicBoolean();
+
+ private final Path scriptFilePath;
+ private final String scriptType;
+ private final int startLevel;
+
+ /**
+ * A {@link ScriptFileReference} with a given scriptType
+ *
+ * @param scriptFilePath the {@link Path} of the file
+ * @param scriptType the script type
+ * @param startLevel the system start level required for this script
+ */
+ public ScriptFileReference(Path scriptFilePath, String scriptType, int startLevel) {
+ this.scriptFilePath = scriptFilePath;
+ this.scriptType = scriptType;
+ this.startLevel = startLevel;
+ }
+
+ public String getScriptIdentifier() {
+ return getScriptIdentifier(scriptFilePath);
+ }
+
+ public Path getScriptFilePath() {
+ return scriptFilePath;
+ }
+
+ public String getScriptType() {
+ return scriptType;
+ }
+
+ public int getStartLevel() {
+ return startLevel;
+ }
+
+ public AtomicBoolean getLoadedStatus() {
+ return loaded;
+ }
+
+ public AtomicBoolean getQueueStatus() {
+ return queued;
+ }
+
+ @Override
+ public int compareTo(ScriptFileReference other) {
+ int startLevelCompare = Integer.compare(startLevel, other.startLevel);
+ if (startLevelCompare != 0) {
+ return startLevelCompare;
+ }
+
+ String name1 = scriptFilePath.getFileName().toString();
+ LOGGER.trace("o1 [{}], name1 [{}]", scriptFilePath, name1);
+
+ String name2 = other.scriptFilePath.getFileName().toString();
+ LOGGER.trace("o2 [{}], name2 [{}]", other.scriptFilePath, name2);
+
+ int nameCompare = name1.compareToIgnoreCase(name2);
+ if (nameCompare != 0) {
+ return nameCompare;
+ } else {
+ return scriptFilePath.getParent().toString()
+ .compareToIgnoreCase(other.scriptFilePath.getParent().toString());
+ }
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ScriptFileReference that = (ScriptFileReference) o;
+ return scriptFilePath.equals(that.scriptFilePath);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(scriptFilePath, scriptType, startLevel);
+ }
+
+ public static String getScriptIdentifier(Path scriptFilePath) {
+ return scriptFilePath.toString();
+ }
+}
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 46af2a84b..b1e8b23b8 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
@@ -27,7 +27,7 @@ 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.collection.BidiSetBag;
+import org.openhab.core.automation.module.script.rulesupport.internal.loader.BidiSetBag;
import org.openhab.core.service.AbstractWatchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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 d2005f857..6f1b2dae0 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
@@ -14,27 +14,29 @@ package org.openhab.core.automation.module.script.rulesupport.loader;
import static java.nio.file.StandardWatchEventKinds.*;
-import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
-import java.net.MalformedURLException;
-import java.net.URL;
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;
import java.util.Optional;
import java.util.Set;
-import java.util.TreeSet;
+import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.TimeUnit;
-import java.util.function.Supplier;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
import javax.script.ScriptEngine;
@@ -44,6 +46,7 @@ import org.openhab.core.OpenHAB;
import org.openhab.core.automation.module.script.ScriptDependencyTracker;
import org.openhab.core.automation.module.script.ScriptEngineContainer;
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;
@@ -55,137 +58,145 @@ import org.slf4j.LoggerFactory;
/**
* The {@link AbstractScriptFileWatcher} is default implementation for watching a directory for files. If a new/modified
- * file is detected, the script
- * is read and passed to the {@link ScriptEngineManager}. It needs to be sub-classed for actual use.
+ * file is detected, the script is read and passed to the {@link ScriptEngineManager}. It needs to be sub-classed for
+ * actual use.
*
* @author Simon Merschjohann - Initial contribution
* @author Kai Kreuzer - improved logging and removed thread pool
* @author Jonathan Gilbert - added dependency tracking & per-script start levels; made reusable
- * @author Jan N. Klug - Refactored dependy tracking to script engine factories
+ * @author Jan N. Klug - Refactored dependency tracking to script engine factories
*/
@NonNullByDefault
public abstract class AbstractScriptFileWatcher extends AbstractWatchService implements ReadyService.ReadyTracker,
- ScriptDependencyTracker.Listener, ScriptEngineManager.FactoryChangeListener {
+ ScriptDependencyTracker.Listener, ScriptEngineManager.FactoryChangeListener, ScriptFileWatcher {
- private static final long RECHECK_INTERVAL = 20;
+ private static final Set EXCLUDED_FILE_EXTENSIONS = Set.of("txt", "old", "example", "backup", "md", "swp",
+ "tmp", "bak");
+
+ private static final List START_LEVEL_PATTERNS = List.of( //
+ Pattern.compile(".*/sl(\\d{2})/[^/]+"), // script in immediate slXX directory
+ Pattern.compile(".*/[^/]+\\.sl(\\d{2})\\.[^/.]+") // script named .slXX.
+ );
private final Logger logger = LoggerFactory.getLogger(AbstractScriptFileWatcher.class);
private final ScriptEngineManager manager;
private final ReadyService readyService;
- private @Nullable ScheduledExecutorService scheduler;
- private Supplier executorFactory;
+ protected ScheduledExecutorService scheduler;
- private final Set pending = ConcurrentHashMap.newKeySet();
- private final Set loaded = ConcurrentHashMap.newKeySet();
+ private final Map scriptMap = new ConcurrentHashMap<>();
+ private final Map scriptLockMap = new ConcurrentHashMap<>();
+ private final CompletableFuture<@Nullable Void> initialized = new CompletableFuture<>();
private volatile int currentStartLevel = 0;
public AbstractScriptFileWatcher(final ScriptEngineManager manager, final ReadyService readyService,
- final String fileDirectory) {
+ final StartLevelService startLevelService, final String fileDirectory) {
super(OpenHAB.getConfigFolder() + File.separator + fileDirectory);
this.manager = manager;
this.readyService = readyService;
- this.executorFactory = () -> Executors
- .newSingleThreadScheduledExecutor(new NamedThreadFactory("scriptwatcher"));
manager.addFactoryChangeListener(this);
readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE));
+
+ this.scheduler = getScheduler();
+
+ currentStartLevel = startLevelService.getStartLevel();
+ if (currentStartLevel > StartLevelService.STARTLEVEL_MODEL) {
+ initialImport();
+ }
+ }
+
+ @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)
+ *
+ * @return a {@link ScheduledExecutorService}
+ */
+ protected ScheduledExecutorService getScheduler() {
+ return Executors.newSingleThreadScheduledExecutor(new NamedThreadFactory("scriptwatcher"));
}
@Override
public void deactivate() {
manager.removeFactoryChangeListener(this);
readyService.unregisterTracker(this);
-
- ScheduledExecutorService localScheduler = scheduler;
- if (localScheduler != null) {
- localScheduler.shutdownNow();
- scheduler = null;
- }
-
super.deactivate();
+
+ CompletableFuture.allOf(
+ Set.copyOf(scriptMap.keySet()).stream().map(this::removeFile).toArray(CompletableFuture>[]::new))
+ .thenRun(scheduler::shutdownNow);
}
@Override
- public void activate() {
- File directory = new File(pathToWatch);
- if (!directory.exists()) {
- if (!directory.mkdirs()) {
- logger.warn("Failed to create watched directory: {}", pathToWatch);
- }
- } else if (directory.isFile()) {
- logger.warn("Trying to watch directory {}, however it is a file", pathToWatch);
- }
-
- super.activate();
+ public CompletableFuture<@Nullable Void> ifInitialized() {
+ return initialized;
}
/**
- * Override the executor service. Can be used for testing.
+ * Get the scriptType (file-extension or MIME-type) for a given file
+ *
+ * The scriptType is determined by the file extension. The extensions in {@link #EXCLUDED_FILE_EXTENSIONS} are
+ * ignored. Implementations should override this
+ * method if they provide a MIME-type instead of the file extension.
*
- * @param executorFactory supplier of ScheduledExecutorService
+ * @param scriptFilePath the {@link Path} to the script
+ * @return an {@link Optional} containing the script type
*/
- void setExecutorFactory(Supplier executorFactory) {
- this.executorFactory = executorFactory;
+ protected Optional getScriptType(Path scriptFilePath) {
+ String fileName = scriptFilePath.toString();
+ int index = fileName.lastIndexOf(".");
+ if (index == -1) {
+ return Optional.empty();
+ }
+ String fileExtension = fileName.substring(index + 1);
+
+ // ignore known file extensions for "temp" files
+ if (EXCLUDED_FILE_EXTENSIONS.contains(fileExtension) || fileExtension.endsWith("~")) {
+ return Optional.empty();
+ }
+ return Optional.of(fileExtension);
}
/**
- * Processes initial import of resources.
+ * Gets the individual start level for a given file
+ *
+ * The start level is derived from the name and location of
+ * the file by {@link #START_LEVEL_PATTERNS}. If no match is found, {@link StartLevelService#STARTLEVEL_RULEENGINE}
+ * is used.
*
- * @param rootDirectory the root directory to import initial resources from
+ * @param scriptFilePath the {@link Path} to the script
+ * @return the start level for this script
*/
- private void importInitialResources(File rootDirectory) {
- if (rootDirectory.exists()) {
- File[] files = rootDirectory.listFiles();
- if (files != null) {
- Collection resources = new TreeSet<>();
- for (File f : files) {
- if (!f.isHidden()) {
- resources.addAll(collectResources(f));
- }
- }
- resources.forEach(this::importFileWhenReady);
- }
- }
- }
-
- /**
- * Collects all resources from the specified file or directory,
- * possibly including subdirectories.
- *
- * The results will be sorted.
- *
- * @param file the file or directory to import resources from
- */
- private Collection collectResources(File file) {
- Collection resources = new TreeSet<>();
- if (!file.exists()) {
- return resources;
- }
-
- if (file.isDirectory()) {
- if (watchSubDirectories()) {
- File[] files = file.listFiles();
- if (files != null) {
- for (File f : files) {
- if (!f.isHidden()) {
- resources.addAll(collectResources(f));
- }
- }
+ protected int getStartLevel(Path scriptFilePath) {
+ for (Pattern p : START_LEVEL_PATTERNS) {
+ Matcher m = p.matcher(scriptFilePath.toString());
+ if (m.find() && m.groupCount() > 0) {
+ try {
+ return Integer.parseInt(m.group(1));
+ } catch (NumberFormatException nfe) {
+ logger.warn("Extracted start level {} from {}, but it's not an integer. Ignoring.", m.group(1),
+ scriptFilePath);
}
}
- } else {
- try {
- URL url = file.toURI().toURL();
- resources.add(new ScriptFileReference(url));
- } catch (MalformedURLException e) {
- // can't happen for the 'file' protocol handler with a correctly formatted URI
- logger.debug("Can't create a URL", e);
- }
}
- return resources;
+
+ return StartLevelService.STARTLEVEL_RULEENGINE;
+ }
+
+ private List listFiles(Path path, boolean includeSubDirectory) {
+ try (Stream stream = Files.walk(path, includeSubDirectory ? Integer.MAX_VALUE : 1)) {
+ return stream.filter(file -> !Files.isDirectory(file)).toList();
+ } catch (IOException ignored) {
+ }
+ return List.of();
}
@Override
@@ -202,180 +213,194 @@ public abstract class AbstractScriptFileWatcher extends AbstractWatchService imp
protected void processWatchEvent(@Nullable WatchEvent> event, @Nullable Kind> kind, @Nullable Path path) {
File file = path.toFile();
if (!file.isHidden()) {
- try {
- if (ENTRY_DELETE.equals(kind)) {
- if (file.isDirectory()) {
- if (watchSubDirectories()) {
- synchronized (this) {
- String prefix = file.toURI().toURL().getPath();
- Set toRemove = loaded.stream()
- .filter(f -> f.getScriptFileURL().getFile().startsWith(prefix))
- .collect(Collectors.toSet());
- toRemove.forEach(this::removeFile);
- }
+ if (ENTRY_DELETE.equals(kind)) {
+ if (file.isDirectory()) {
+ if (watchSubDirectories()) {
+ synchronized (this) {
+ String prefix = path.getParent().toString();
+ Set toRemove = scriptMap.keySet().stream().filter(ref -> ref.startsWith(prefix))
+ .collect(Collectors.toSet());
+ toRemove.forEach(this::removeFile);
}
- } else {
- removeFile(new ScriptFileReference(file.toURI().toURL()));
}
+ } else {
+ removeFile(ScriptFileReference.getScriptIdentifier(file.toPath()));
}
+ }
- if (file.canRead() && (ENTRY_CREATE.equals(kind) || ENTRY_MODIFY.equals(kind))) {
- collectResources(file).forEach(this::importFileWhenReady);
- }
- } catch (MalformedURLException e) {
- logger.error("malformed", e);
+ if (file.canRead() && (ENTRY_CREATE.equals(kind) || ENTRY_MODIFY.equals(kind))) {
+ addFiles(listFiles(file.toPath(), watchSubDirectories()));
}
}
}
- private void removeFile(ScriptFileReference ref) {
- dequeueUrl(ref);
+ private CompletableFuture addFiles(Collection files) {
+ return CompletableFuture.allOf(files.stream().map(this::getScriptFileReference).filter(Optional::isPresent)
+ .map(Optional::get).sorted().map(this::addScriptFileReference).toArray(CompletableFuture>[]::new));
+ }
+
+ private CompletableFuture addScriptFileReference(ScriptFileReference newRef) {
+ ScriptFileReference ref = scriptMap.computeIfAbsent(newRef.getScriptIdentifier(), k -> newRef);
+ // check if we are ready to load the script, otherwise we don't need to queue it
+ if (currentStartLevel >= ref.getStartLevel() && !ref.getQueueStatus().getAndSet(true)) {
+ return importFileWhenReady(ref.getScriptIdentifier());
+ }
+ return CompletableFuture.completedFuture(null);
+ }
+
+ private Optional getScriptFileReference(Path path) {
+ return getScriptType(path).map(scriptType -> new ScriptFileReference(path, scriptType, getStartLevel(path)));
+ }
+
+ private CompletableFuture removeFile(String scriptIdentifier) {
+ return CompletableFuture.runAsync(() -> {
+ Lock lock = getLockForScript(scriptIdentifier);
+ try {
+ ScriptFileReference ref = scriptMap.remove(scriptIdentifier);
+
+ if (ref == null) {
+ logger.warn("Failed to unload script '{}': script reference not found.", scriptIdentifier);
+ return;
+ }
+
+ if (ref.getLoadedStatus().get()) {
+ manager.removeEngine(scriptIdentifier);
+ logger.debug("Unloaded script '{}'", scriptIdentifier);
+ } else {
+ logger.debug("Dequeued script '{}'", scriptIdentifier);
+ }
+ } finally {
+ scriptLockMap.remove(scriptIdentifier);
+ lock.unlock();
+ }
+ }, scheduler);
+ }
+
+ private synchronized Lock getLockForScript(String scriptIdentifier) {
+ Lock lock = scriptLockMap.computeIfAbsent(scriptIdentifier, k -> new ReentrantLock());
+ lock.lock();
+
+ return lock;
+ }
+
+ private CompletableFuture importFileWhenReady(String scriptIdentifier) {
+ return CompletableFuture.runAsync(() -> {
+ ScriptFileReference ref = scriptMap.get(scriptIdentifier);
+ if (ref == null) {
+ logger.warn("Failed to import script '{}': script reference not found", scriptIdentifier);
+ return;
+ }
+
+ ref.getQueueStatus().set(false);
+
+ Lock lock = getLockForScript(scriptIdentifier);
+ try {
+ if (ref.getLoadedStatus().get()) {
+ manager.removeEngine(scriptIdentifier);
+ logger.debug("Unloaded script '{}'", scriptIdentifier);
+ }
+
+ if (manager.isSupported(ref.getScriptType()) && ref.getStartLevel() <= currentStartLevel) {
+ logger.info("(Re-)Loading script '{}'", scriptIdentifier);
+ if (createAndLoad(ref)) {
+ ref.getLoadedStatus().set(true);
+ } else {
+ // make sure script engine is successfully closed and the loading is re-tried
+ manager.removeEngine(ref.getScriptIdentifier());
+ ref.getLoadedStatus().set(false);
+ }
+ } else {
+ ref.getLoadedStatus().set(false);
+ logger.debug("Enqueued script '{}'", ref.getScriptIdentifier());
+ }
+ } finally {
+ lock.unlock();
+ }
+ }, scheduler);
+ }
+
+ private boolean createAndLoad(ScriptFileReference ref) {
String scriptIdentifier = ref.getScriptIdentifier();
- manager.removeEngine(scriptIdentifier);
- loaded.remove(ref);
- }
-
- private synchronized void importFileWhenReady(ScriptFileReference ref) {
- if (loaded.contains(ref)) {
- this.removeFile(ref); // if already loaded, remove first
- }
-
- Optional scriptType = ref.getScriptType();
-
- scriptType.ifPresent(type -> {
- if (ref.getStartLevel() <= currentStartLevel && manager.isSupported(type)) {
- importFile(ref);
- } else {
- enqueue(ref);
- }
- });
- }
-
- protected void importFile(ScriptFileReference ref) {
- if (createAndLoad(ref)) {
- loaded.add(ref);
- }
- }
-
- protected boolean createAndLoad(ScriptFileReference ref) {
- String fileName = ref.getScriptFileURL().getFile();
- Optional scriptType = ref.getScriptType();
- assert scriptType.isPresent();
-
- try (InputStreamReader reader = new InputStreamReader(
- new BufferedInputStream(ref.getScriptFileURL().openStream()), StandardCharsets.UTF_8)) {
- logger.info("Loading script '{}'", fileName);
-
- String scriptIdentifier = ref.getScriptIdentifier();
-
- ScriptEngineContainer container = manager.createScriptEngine(scriptType.get(), scriptIdentifier);
-
+ try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(ref.getScriptFilePath()),
+ StandardCharsets.UTF_8)) {
+ ScriptEngineContainer container = manager.createScriptEngine(ref.getScriptType(),
+ ref.getScriptIdentifier());
if (container != null) {
- container.getScriptEngine().put(ScriptEngine.FILENAME, fileName);
- manager.loadScript(container.getIdentifier(), reader);
-
- logger.debug("Script loaded: {}", fileName);
- return true;
- } else {
- logger.error("Script loading error, ignoring file: {}", fileName);
+ container.getScriptEngine().put(ScriptEngine.FILENAME, scriptIdentifier);
+ if (manager.loadScript(container.getIdentifier(), reader)) {
+ return true;
+ }
}
+ logger.warn("Script loading error, ignoring file '{}'", scriptIdentifier);
} catch (IOException e) {
- logger.error("Failed to load file '{}': {}", ref.getScriptFileURL().getFile(), e.getMessage());
+ logger.warn("Failed to load file '{}': {}", ref.getScriptFilePath(), e.getMessage());
}
return false;
}
- private void enqueue(ScriptFileReference ref) {
- synchronized (pending) {
- pending.add(ref);
- }
+ private void initialImport() {
+ File directory = new File(pathToWatch);
- logger.debug("Enqueued {}", ref.getScriptIdentifier());
- }
-
- private void dequeueUrl(ScriptFileReference ref) {
- synchronized (pending) {
- pending.remove(ref);
- }
-
- logger.debug("Dequeued {}", ref.getScriptIdentifier());
- }
-
- private void checkFiles(int forLevel) {
- List newlySupported;
-
- synchronized (pending) {
- newlySupported = pending.stream()
- .filter(ref -> manager.isSupported(ref.getScriptType().get()) && forLevel >= ref.getStartLevel())
- .sorted().collect(Collectors.toList());
- pending.removeAll(newlySupported);
- }
-
- newlySupported.forEach(this::importFileWhenReady);
- }
-
- private synchronized void onStartLevelChanged(int newLevel) {
- int previousLevel = currentStartLevel;
- currentStartLevel = newLevel;
-
- if (previousLevel < StartLevelService.STARTLEVEL_MODEL) { // not yet started
- if (newLevel >= StartLevelService.STARTLEVEL_MODEL) { // start
- ScheduledExecutorService localScheduler = executorFactory.get();
- scheduler = localScheduler;
- localScheduler.submit(() -> importInitialResources(new File(pathToWatch)));
- localScheduler.scheduleWithFixedDelay(() -> checkFiles(currentStartLevel), 0, RECHECK_INTERVAL,
- TimeUnit.SECONDS);
- }
- } else { // already started
- assert scheduler != null;
- if (newLevel < StartLevelService.STARTLEVEL_MODEL) { // stop
- scheduler.shutdown();
- scheduler = null;
- } else if (newLevel > previousLevel) {
- scheduler.submit(() -> checkFiles(newLevel));
+ if (!directory.exists()) {
+ if (!directory.mkdirs()) {
+ logger.warn("Failed to create watched directory: {}", pathToWatch);
}
+ } else if (directory.isFile()) {
+ logger.warn("Trying to watch directory {}, however it is a file", pathToWatch);
}
+
+ addFiles(listFiles(directory.toPath(), watchSubDirectories())).thenRun(() -> initialized.complete(null));
}
@Override
- public void onDependencyChange(@Nullable String scriptId) {
- logger.debug("Reimporting {}...", scriptId);
- try {
- ScriptFileReference scriptFileReference = new ScriptFileReference(new URL(scriptId));
- if (loaded.contains(scriptFileReference)) {
- importFileWhenReady(scriptFileReference);
- }
- } catch (MalformedURLException ignored) {
+ public void onDependencyChange(String scriptIdentifier) {
+ logger.debug("Reimporting {}...", scriptIdentifier);
+ ScriptFileReference ref = scriptMap.get(scriptIdentifier);
+ if (ref != null && !ref.getQueueStatus().getAndSet(true)) {
+ importFileWhenReady(scriptIdentifier);
}
- logger.debug("Ignoring dependency change for {} as it is no file or not loaded by this file watcher", scriptId);
}
@Override
public synchronized void onReadyMarkerAdded(ReadyMarker readyMarker) {
- int newLevel = Integer.parseInt(readyMarker.getIdentifier());
+ int previousLevel = currentStartLevel;
+ currentStartLevel = Integer.parseInt(readyMarker.getIdentifier());
- if (newLevel > currentStartLevel) {
- onStartLevelChanged(newLevel);
+ if (currentStartLevel < StartLevelService.STARTLEVEL_STATES) {
+ // ignore start level less than 30
+ return;
+ }
+
+ if (currentStartLevel == StartLevelService.STARTLEVEL_STATES) {
+ initialImport();
+ } else {
+ scriptMap.values().stream().sorted()
+ .filter(ref -> needsStartLevelProcessing(ref, previousLevel, currentStartLevel))
+ .forEach(ref -> importFileWhenReady(ref.getScriptIdentifier()));
}
}
- @Override
- @SuppressWarnings("PMD.EmptyWhileStmt")
- public synchronized void onReadyMarkerRemoved(ReadyMarker readyMarker) {
- int newLevel = Integer.parseInt(readyMarker.getIdentifier());
+ private boolean needsStartLevelProcessing(ScriptFileReference ref, int previousLevel, int newLevel) {
+ int refStartLevel = ref.getStartLevel();
+ return !ref.getLoadedStatus().get() && newLevel >= refStartLevel && previousLevel < refStartLevel
+ && !ref.getQueueStatus().getAndSet(true);
+ }
- if (currentStartLevel > newLevel) {
- while (newLevel-- > 0 && !readyService
- .isReady(new ReadyMarker(StartLevelService.STARTLEVEL_MARKER_TYPE, Integer.toString(newLevel)))) {
- }
- onStartLevelChanged(newLevel);
- }
+ @Override
+ public void onReadyMarkerRemoved(ReadyMarker readyMarker) {
+ // we don't need to process this, as openHAB only reduces its start level when the system is shutdown
+ // in this case the service is de-activated anyway and al scripts are removed by {@link #deactivate}
}
@Override
public void factoryAdded(@Nullable String scriptType) {
+ scriptMap.forEach((scriptIdentifier, ref) -> {
+ if (ref.getScriptType().equals(scriptType) && !ref.getQueueStatus().getAndSet(true)) {
+ importFileWhenReady(scriptIdentifier);
+ }
+ });
}
@Override
@@ -383,6 +408,9 @@ public abstract class AbstractScriptFileWatcher extends AbstractWatchService imp
if (scriptType == null) {
return;
}
- loaded.stream().filter(ref -> scriptType.equals(ref.getScriptType().get())).forEach(this::importFileWhenReady);
+ Set toRemove = scriptMap.values().stream()
+ .filter(ref -> ref.getLoadedStatus().get() && scriptType.equals(ref.getScriptType()))
+ .map(ScriptFileReference::getScriptIdentifier).collect(Collectors.toSet());
+ toRemove.forEach(this::removeFile);
}
}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/ScriptFileReference.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/ScriptFileReference.java
deleted file mode 100644
index 8a6ba4df5..000000000
--- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/ScriptFileReference.java
+++ /dev/null
@@ -1,140 +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.automation.module.script.rulesupport.loader;
-
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Objects;
-import java.util.Optional;
-import java.util.Set;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-import org.openhab.core.service.StartLevelService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Script File wrapper offering various methods to inspect the script
- *
- * @author Jonathan Gilbert - initial contribution
- */
-@NonNullByDefault
-public class ScriptFileReference implements Comparable {
-
- private static final Set EXCLUDED_FILE_EXTENSIONS = Set.of("txt", "old", "example", "backup", "md", "swp",
- "tmp", "bak");
-
- private static final Pattern[] START_LEVEL_PATTERNS = new Pattern[] { Pattern.compile(".*/sl(\\d{2})/[^/]+"), // script
- // in
- // immediate
- // slXX
- // directory
- Pattern.compile(".*/[^/]+\\.sl(\\d{2})\\.[^/.]+") // script named .slXX.
- };
-
- private static final Logger LOGGER = LoggerFactory.getLogger(ScriptFileReference.class);
-
- private final URL scriptFileURL;
-
- public ScriptFileReference(URL scriptFileURL) {
- this.scriptFileURL = scriptFileURL;
- }
-
- public URL getScriptFileURL() {
- return scriptFileURL;
- }
-
- public int getStartLevel() {
- for (Pattern p : START_LEVEL_PATTERNS) {
- Matcher m = p.matcher(scriptFileURL.getPath());
- if (m.find() && m.groupCount() > 0) {
- try {
- return Integer.parseInt(m.group(1));
- } catch (NumberFormatException nfe) {
- LOGGER.warn("Extracted start level {} from {}, but it's not an integer. Ignoring.", m.group(1),
- scriptFileURL.getPath());
- }
- }
- }
-
- return StartLevelService.STARTLEVEL_RULEENGINE;
- }
-
- public Optional getScriptType() {
- String fileName = scriptFileURL.getPath();
- int index = fileName.lastIndexOf(".");
- if (index == -1) {
- return Optional.empty();
- }
- String fileExtension = fileName.substring(index + 1);
-
- // ignore known file extensions for "temp" files
- if (EXCLUDED_FILE_EXTENSIONS.contains(fileExtension) || fileExtension.endsWith("~")) {
- return Optional.empty();
- }
- return Optional.of(fileExtension);
- }
-
- public String getScriptIdentifier() {
- return scriptFileURL.toString();
- }
-
- @Override
- public int compareTo(ScriptFileReference other) {
- try {
- Path path1 = Paths.get(scriptFileURL.toURI());
- String name1 = path1.getFileName().toString();
- LOGGER.trace("o1 [{}], path1 [{}], name1 [{}]", scriptFileURL, path1, name1);
-
- Path path2 = Paths.get(other.scriptFileURL.toURI());
- String name2 = path2.getFileName().toString();
- LOGGER.trace("o2 [{}], path2 [{}], name2 [{}]", other.scriptFileURL, path2, name2);
-
- int startLevelCompare = Integer.compare(getStartLevel(), other.getStartLevel());
- if (startLevelCompare != 0) {
- return startLevelCompare;
- }
- int nameCompare = name1.compareToIgnoreCase(name2);
- if (nameCompare != 0) {
- return nameCompare;
- } else {
- return path1.getParent().toString().compareToIgnoreCase(path2.getParent().toString());
- }
- } catch (URISyntaxException e) {
- LOGGER.error("URI syntax exception", e);
- return 0;
- }
- }
-
- @Override
- public boolean equals(@Nullable Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- ScriptFileReference that = (ScriptFileReference) o;
- return scriptFileURL.equals(that.scriptFileURL);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(scriptFileURL);
- }
-}
diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/ScriptFileWatcher.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/ScriptFileWatcher.java
new file mode 100644
index 000000000..e0fa918ac
--- /dev/null
+++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/loader/ScriptFileWatcher.java
@@ -0,0 +1,37 @@
+/**
+ * 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.automation.module.script.rulesupport.loader;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.core.service.ReadyMarker;
+
+/**
+ * The {@link ScriptFileWatcher} interface needs to be implemented by script file watchers. Services that implement this
+ * interface can be tracked to set a {@link ReadyMarker} once all services have completed their initial loading.
+ *
+ * @author Jan N. Klug - Initial contribution
+ */
+@NonNullByDefault
+public interface ScriptFileWatcher {
+
+ /**
+ * Returns a {@link CompletableFuture} that completes when the {@link ScriptFileWatcher} has completed it's
+ * initial loading of files.
+ *
+ * @return the {@link CompletableFuture}
+ */
+ CompletableFuture<@Nullable Void> ifInitialized();
+}
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 0555376e4..54cfa0b55 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
@@ -13,15 +13,21 @@
package org.openhab.core.automation.module.script.rulesupport.loader;
import static java.nio.file.StandardWatchEventKinds.*;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
+import static org.openhab.core.OpenHAB.CONFIG_DIR_PROG_ARGUMENT;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
-import java.util.concurrent.Executors;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.atomic.AtomicInteger;
import javax.script.ScriptEngine;
@@ -31,7 +37,6 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir;
-import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -41,36 +46,49 @@ import org.openhab.core.automation.module.script.ScriptDependencyTracker;
import org.openhab.core.automation.module.script.ScriptEngineContainer;
import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.openhab.core.automation.module.script.ScriptEngineManager;
-import org.openhab.core.automation.module.script.rulesupport.internal.loader.DelegatingScheduledExecutorService;
+import org.openhab.core.automation.module.script.rulesupport.internal.loader.ScriptFileReference;
import org.openhab.core.service.ReadyMarker;
import org.openhab.core.service.ReadyService;
import org.openhab.core.service.StartLevelService;
+import org.openhab.core.test.java.JavaTest;
import org.opentest4j.AssertionFailedError;
/**
- * Test for Script File Watcher, covering differing start levels and dependency tracking
+ * Test for {@link AbstractScriptFileWatcher}, covering differing start levels and dependency tracking
*
- * @author Jonathan Gilbert - initial contribution
+ * @author Jonathan Gilbert - Initial contribution
+ * @author Jan N. Klug - Refactoring and improvements
*/
@NonNullByDefault
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
-class AbstractScriptFileWatcherTest {
+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({}) ReadyService readyService;
+ private @Mock @NonNullByDefault({}) StartLevelService startLevelServiceMock;
+ private @Mock @NonNullByDefault({}) ReadyService readyServiceMock;
protected @NonNullByDefault({}) @TempDir Path tempScriptDir;
+ private final AtomicInteger atomicInteger = new AtomicInteger();
+
+ private int currentStartLevel = 0;
+
@BeforeEach
public void setUp() {
- scriptFileWatcher = new AbstractScriptFileWatcher(scriptEngineManagerMock, readyService,
- "automation" + File.separator + "jsr223") {
- };
- scriptFileWatcher.activate();
+ System.setProperty(CONFIG_DIR_PROG_ARGUMENT, tempScriptDir.toString());
+
+ atomicInteger.set(0);
+ currentStartLevel = 0;
+
+ // ensure initialize is not called on initialization
+ when(startLevelServiceMock.getStartLevel()).thenAnswer(invocation -> currentStartLevel);
+
+ scriptFileWatcher = createScriptFileWatcher();
}
@AfterEach
@@ -78,7 +96,502 @@ class AbstractScriptFileWatcherTest {
scriptFileWatcher.deactivate();
}
- protected Path getFile(String name) {
+ @Test
+ public void testLoadOneDefaultFileAlreadyStarted() {
+ 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);
+ updateStartLevel(100);
+
+ Path p = getFile("script.js");
+
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p.toString());
+ }
+
+ @Test
+ public void testSubDirectoryIncludedInInitialImport() {
+ 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);
+ Path p0 = getFile("script.js");
+ Path p1 = getFile("dir/script.js");
+
+ updateStartLevel(100);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js",
+ ScriptFileReference.getScriptIdentifier(p0));
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js",
+ ScriptFileReference.getScriptIdentifier(p1));
+ }
+
+ @Test
+ public void testSubDirectoryIgnoredInInitialImport() {
+ 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");
+
+ updateStartLevel(100);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js",
+ ScriptFileReference.getScriptIdentifier(p0));
+ verify(scriptEngineManagerMock, never()).createScriptEngine("js", ScriptFileReference.getScriptIdentifier(p1));
+ }
+
+ @Test
+ public void testLoadOneDefaultFileWaitUntilStarted() {
+ 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);
+ updateStartLevel(20);
+
+ Path p = getFile("script.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+
+ awaitEmptyQueue();
+
+ // verify not yet called
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
+
+ // verify is called when the start level increases
+ updateStartLevel(100);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p.toString());
+ }
+
+ @Test
+ public void testLoadOneCustomFileWaitUntilStarted() {
+ 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);
+
+ updateStartLevel(50);
+
+ Path p = getFile("script.sl60.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+
+ awaitEmptyQueue();
+
+ // verify not yet called
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
+
+ // verify is called when the start level increases
+ updateStartLevel(100);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p.toString());
+ }
+
+ @Test
+ public void testLoadTwoCustomFilesDifferentStartLevels() {
+ 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);
+ updateStartLevel(20);
+
+ Path p1 = getFile("script.sl70.js");
+ Path p2 = getFile("script.sl50.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p1);
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p2);
+
+ awaitEmptyQueue();
+
+ // verify not yet called
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
+
+ updateStartLevel(40);
+
+ awaitEmptyQueue();
+
+ // verify not yet called
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
+
+ updateStartLevel(60);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p2.toString());
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), eq(p1.toString()));
+
+ updateStartLevel(80);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p1.toString());
+ }
+
+ @Test
+ public void testLoadTwoCustomFilesAlternativePatternDifferentStartLevels() {
+ 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);
+
+ Path p1 = getFile("sl70/script.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p1);
+ Path p2 = getFile("sl50/script.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p2);
+
+ // verify not yet called
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
+
+ updateStartLevel(40);
+
+ // verify not yet called
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
+
+ updateStartLevel(60);
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p2.toString());
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), eq(p1.toString()));
+
+ updateStartLevel(80);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p1.toString());
+ }
+
+ @Test
+ public void testLoadOneDefaultFileDelayedSupport() {
+ when(scriptEngineManagerMock.isSupported("js")).thenReturn(false);
+ 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);
+ updateStartLevel(100);
+
+ Path p = getFile("script.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+
+ // verify not yet called but checked
+ waitForAssert(() -> verify(scriptEngineManagerMock).isSupported(anyString()));
+ verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
+
+ // add support is added for .js files
+ when(scriptEngineManagerMock.isSupported("js")).thenReturn(true);
+ scriptFileWatcher.factoryAdded("js");
+
+ awaitEmptyQueue();
+
+ // verify script has now been processed
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p.toString());
+ }
+
+ @Test
+ public void testOrderingWithinSingleStartLevel() {
+ 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);
+ updateStartLevel(50);
+
+ Path p64 = getFile("script.sl64.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p64);
+ Path p66 = getFile("script.sl66.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p66);
+ Path p65 = getFile("script.sl65.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p65);
+
+ updateStartLevel(70);
+
+ awaitEmptyQueue();
+
+ InOrder inOrder = inOrder(scriptEngineManagerMock);
+
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p64.toString());
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p65.toString());
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p66.toString());
+ }
+
+ @Test
+ public void testOrderingStartlevelFolders() {
+ 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);
+
+ Path p50 = getFile("a_script.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p50);
+ Path p40 = getFile("sl40/b_script.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p40);
+ Path p30 = getFile("sl30/script.js");
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p30);
+
+ awaitEmptyQueue();
+
+ updateStartLevel(70);
+
+ awaitEmptyQueue();
+
+ InOrder inOrder = inOrder(scriptEngineManagerMock);
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p30.toString());
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p40.toString());
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p50.toString());
+ }
+
+ @Test
+ public void testReloadActiveWhenDependencyChanged() {
+ ScriptEngineFactory scriptEngineFactoryMock = mock(ScriptEngineFactory.class);
+ when(scriptEngineFactoryMock.getDependencyTracker()).thenReturn(scriptDependencyTrackerMock);
+ when(scriptEngineManagerMock.isSupported("js")).thenReturn(true);
+ ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class);
+ when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class));
+ when(scriptEngineContainer.getFactory()).thenReturn(scriptEngineFactoryMock);
+ when(scriptEngineManagerMock.createScriptEngine(anyString(), anyString())).thenReturn(scriptEngineContainer);
+ when(scriptEngineManagerMock.loadScript(any(), any())).thenReturn(true);
+
+ updateStartLevel(100);
+
+ Path p = getFile("script.js");
+
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js", p.toString());
+
+ scriptFileWatcher.onDependencyChange(p.toString());
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000).times(2)).createScriptEngine("js", p.toString());
+ }
+
+ @Test
+ public void testNotReloadInactiveWhenDependencyChanged() {
+ 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);
+ updateStartLevel(100);
+
+ Path p = getFile("script.js");
+
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+
+ awaitEmptyQueue();
+
+ scriptFileWatcher.onDependencyChange(p.toString());
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, never()).createScriptEngine("js", p.toString());
+ }
+
+ @Test
+ public void testRemoveBeforeReAdd() {
+ 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);
+ updateStartLevel(100);
+
+ Path p = getFile("script.js");
+
+ InOrder inOrder = inOrder(scriptEngineManagerMock);
+ String scriptIdentifier = ScriptFileReference.getScriptIdentifier(p);
+
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+
+ awaitEmptyQueue();
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", scriptIdentifier);
+
+ scriptFileWatcher.processWatchEvent(null, ENTRY_MODIFY, p);
+
+ awaitEmptyQueue();
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).removeEngine(scriptIdentifier);
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", scriptIdentifier);
+ }
+
+ @Test
+ public void testDirectoryAdded() {
+ 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);
+ updateStartLevel(100);
+
+ Path p1 = getFile("dir/script.js");
+ Path p2 = getFile("dir/script2.js");
+ Path d = p1.getParent();
+
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, d);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p1.toString());
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p2.toString());
+ }
+
+ @Test
+ public void testDirectoryAddedSubDirIncluded() {
+ 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);
+ 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);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js",
+ ScriptFileReference.getScriptIdentifier(p1));
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js",
+ ScriptFileReference.getScriptIdentifier(p2));
+ }
+
+ @Test
+ public void testDirectoryAddedSubDirIgnored() {
+ 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);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js",
+ ScriptFileReference.getScriptIdentifier(p1));
+ verify(scriptEngineManagerMock, never()).createScriptEngine("js", ScriptFileReference.getScriptIdentifier(p2));
+ }
+
+ @Test
+ public void testSortsAllFilesInNewDirectory() {
+ 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);
+ updateStartLevel(100);
+
+ Path p20 = getFile("dir/script.sl20.js");
+ Path p10 = getFile("dir/script2.sl10.js");
+ Path d = p10.getParent();
+
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, d);
+
+ awaitEmptyQueue();
+
+ InOrder inOrder = inOrder(scriptEngineManagerMock);
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p10.toString());
+ inOrder.verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p20.toString());
+ }
+
+ @Test
+ public void testDirectoryRemoved() {
+ 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);
+ updateStartLevel(100);
+
+ Path p1 = getFile("dir/script.js");
+ 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);
+
+ awaitEmptyQueue();
+
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p1.toString());
+ verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p2.toString());
+ verify(scriptEngineManagerMock, timeout(10000)).removeEngine(p1.toString());
+ verify(scriptEngineManagerMock, timeout(10000)).removeEngine(p2.toString());
+ }
+
+ @Test
+ public void testScriptEngineRemovedOnFailedLoad() {
+ 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(false);
+ updateStartLevel(100);
+
+ Path p = getFile("script.js");
+
+ when(scriptEngineContainer.getIdentifier()).thenReturn(ScriptFileReference.getScriptIdentifier(p));
+
+ scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+
+ awaitEmptyQueue();
+
+ InOrder inOrder = inOrder(scriptEngineManagerMock);
+ inOrder.verify(scriptEngineManagerMock, timeout(1000)).createScriptEngine("js",
+ ScriptFileReference.getScriptIdentifier(p));
+ inOrder.verify(scriptEngineManagerMock, timeout(1000))
+ .loadScript(eq(ScriptFileReference.getScriptIdentifier(p)), any());
+ inOrder.verify(scriptEngineManagerMock, timeout(1000)).removeEngine(ScriptFileReference.getScriptIdentifier(p));
+ }
+
+ @Test
+ public void testIfInitializedForEarlyInitialization() {
+ CompletableFuture> initialized = scriptFileWatcher.ifInitialized();
+
+ assertThat(initialized.isDone(), is(false));
+
+ updateStartLevel(StartLevelService.STARTLEVEL_STATES);
+ waitForAssert(() -> assertThat(initialized.isDone(), is(true)));
+ }
+
+ @Test
+ public void testIfInitializedForLateInitialization() {
+ when(startLevelServiceMock.getStartLevel()).thenReturn(StartLevelService.STARTLEVEL_RULEENGINE);
+ AbstractScriptFileWatcher watcher = createScriptFileWatcher();
+
+ waitForAssert(() -> assertThat(watcher.ifInitialized().isDone(), is(true)));
+
+ watcher.deactivate();
+ }
+
+ private Path getFile(String name) {
Path tempFile = tempScriptDir.resolve(name);
try {
File parent = tempFile.getParent().toFile();
@@ -96,336 +609,56 @@ class AbstractScriptFileWatcherTest {
return Path.of(tempFile.toUri());
}
- void updateStartLevel(int level) {
- scriptFileWatcher
- .onReadyMarkerAdded(new ReadyMarker(StartLevelService.STARTLEVEL_MARKER_TYPE, Integer.toString(level)));
+ /**
+ * Increase the start level in steps of 10
+ *
+ * @param level the target start-level
+ */
+ private void updateStartLevel(int level) {
+ while (currentStartLevel < level) {
+ currentStartLevel += 10;
+ scriptFileWatcher.onReadyMarkerAdded(
+ new ReadyMarker(StartLevelService.STARTLEVEL_MARKER_TYPE, Integer.toString(currentStartLevel)));
+ }
}
- @Test
- public void testLoadOneDefaultFileAlreadyStarted() {
- 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);
+ private AbstractScriptFileWatcher createScriptFileWatcher() {
+ return new AbstractScriptFileWatcher(scriptEngineManagerMock, readyServiceMock, startLevelServiceMock, "") {
- updateStartLevel(100);
+ @Override
+ protected ScheduledExecutorService getScheduler() {
+ return new CountingScheduledExecutor(atomicInteger);
+ }
- Path p = getFile("script.js");
-
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
-
- verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p.toFile().toURI().toString());
+ @Override
+ protected boolean watchSubDirectories() {
+ return watchSubDirectories;
+ }
+ };
}
- @Test
- public void testLoadOneDefaultFileWaitUntilStarted() {
- 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);
-
- Path p = getFile("script.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
-
- // verify not yet called
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
-
- // verify is called when the start level increases
- updateStartLevel(100);
- verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p.toFile().toURI().toString());
+ private void awaitEmptyQueue() {
+ waitForAssert(() -> assertThat(atomicInteger.get(), is(0)));
}
- @Test
- public void testLoadOneCustomFileWaitUntilStarted() {
- 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);
+ private static class CountingScheduledExecutor extends ScheduledThreadPoolExecutor {
- updateStartLevel(50);
+ private final AtomicInteger counter;
- Path p = getFile("script.sl60.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
+ public CountingScheduledExecutor(AtomicInteger counter) {
+ super(1);
- // verify not yet called
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
+ this.counter = counter;
+ }
- // verify is called when the start level increases
- updateStartLevel(100);
- verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p.toFile().toURI().toString());
- }
-
- @Test
- public void testLoadTwoCustomFilesDifferentStartLevels() {
- 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);
-
- Path p1 = getFile("script.sl70.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p1);
- Path p2 = getFile("script.sl50.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p2);
-
- // verify not yet called
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
-
- updateStartLevel(40);
-
- // verify not yet called
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
-
- updateStartLevel(60);
-
- verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p2.toFile().toURI().toString());
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), eq(p1.toFile().toURI().toString()));
-
- updateStartLevel(80);
-
- verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p1.toFile().toURI().toString());
- }
-
- @Test
- public void testLoadTwoCustomFilesAlternativePatternDifferentStartLevels() {
- 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);
-
- Path p1 = getFile("sl70/script.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p1);
- Path p2 = getFile("sl50/script.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p2);
-
- // verify not yet called
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
-
- updateStartLevel(40);
-
- // verify not yet called
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
-
- updateStartLevel(60);
-
- verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p2.toFile().toURI().toString());
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), eq(p1.toFile().toURI().toString()));
-
- updateStartLevel(80);
-
- verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p1.toFile().toURI().toString());
- }
-
- @Test
- public void testLoadOneDefaultFileDelayedSupport() {
- // set an executor which captures the scheduled task
- ScheduledExecutorService scheduledExecutorService = spy(
- new DelegatingScheduledExecutorService(Executors.newSingleThreadScheduledExecutor()));
- ArgumentCaptor scheduledTask = ArgumentCaptor.forClass(Runnable.class);
- scriptFileWatcher.setExecutorFactory(() -> scheduledExecutorService);
-
- when(scriptEngineManagerMock.isSupported("js")).thenReturn(false);
- ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class);
- when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class));
- when(scriptEngineManagerMock.createScriptEngine(anyString(), anyString())).thenReturn(scriptEngineContainer);
- updateStartLevel(100);
-
- verify(scheduledExecutorService).scheduleWithFixedDelay(scheduledTask.capture(), anyLong(), anyLong(), any());
-
- Path p = getFile("script.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
-
- // verify not yet called
- verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString());
-
- // add support is added for .js files
- when(scriptEngineManagerMock.isSupported("js")).thenReturn(true);
- // update (in current thread)
- scheduledTask.getValue().run();
-
- // verify script has now been processed
- verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p.toFile().toURI().toString());
- }
-
- @Test
- public void testOrderingWithinSingleStartLevel() {
- 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);
-
- Path p64 = getFile("script.sl64.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p64);
- Path p66 = getFile("script.sl66.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p66);
- Path p65 = getFile("script.sl65.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p65);
-
- updateStartLevel(70);
-
- InOrder inOrder = inOrder(scriptEngineManagerMock);
-
- inOrder.verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p64.toFile().toURI().toString());
- inOrder.verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p65.toFile().toURI().toString());
- inOrder.verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p66.toFile().toURI().toString());
- }
-
- @Test
- public void testOrderingStartlevelFolders() {
- 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);
-
- Path p50 = getFile("a_script.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p50);
- Path p40 = getFile("sl40/b_script.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p40);
- Path p30 = getFile("sl30/script.js");
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p30);
-
- updateStartLevel(70);
-
- InOrder inOrder = inOrder(scriptEngineManagerMock);
-
- inOrder.verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p30.toFile().toURI().toString());
- inOrder.verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p40.toFile().toURI().toString());
- inOrder.verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p50.toFile().toURI().toString());
- }
-
- @Test
- public void testReloadActiveWhenDependencyChanged() {
- ScriptEngineFactory scriptEngineFactoryMock = mock(ScriptEngineFactory.class);
- when(scriptEngineFactoryMock.getDependencyTracker()).thenReturn(scriptDependencyTrackerMock);
- when(scriptEngineManagerMock.isSupported("js")).thenReturn(true);
- ScriptEngineContainer scriptEngineContainer = mock(ScriptEngineContainer.class);
- when(scriptEngineContainer.getScriptEngine()).thenReturn(mock(ScriptEngine.class));
- when(scriptEngineContainer.getFactory()).thenReturn(scriptEngineFactoryMock);
- when(scriptEngineManagerMock.createScriptEngine(anyString(), anyString())).thenReturn(scriptEngineContainer);
-
- updateStartLevel(100);
-
- Path p = getFile("script.js");
-
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
-
- scriptFileWatcher.onDependencyChange(p.toFile().toURI().toString());
-
- verify(scriptEngineManagerMock, timeout(10000).times(2)).createScriptEngine("js",
- p.toFile().toURI().toString());
- }
-
- @Test
- public void testNotReloadInactiveWhenDependencyChanged() {
- 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);
-
- Path p = getFile("script.js");
-
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
-
- scriptFileWatcher.onDependencyChange(p.toFile().toURI().toString());
-
- verify(scriptEngineManagerMock, never()).createScriptEngine("js", p.toFile().toURI().toString());
- }
-
- @Test
- public void testRemoveBeforeReAdd() {
- 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);
-
- updateStartLevel(100);
-
- Path p = getFile("script.js");
-
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p);
- scriptFileWatcher.processWatchEvent(null, ENTRY_MODIFY, p);
-
- verify(scriptEngineManagerMock).removeEngine(p.toFile().toURI().toString());
- verify(scriptEngineManagerMock, timeout(10000).times(2)).createScriptEngine("js",
- p.toFile().toURI().toString());
- }
-
- @Test
- public void testDirectoryAdded() {
- 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);
-
- updateStartLevel(100);
-
- Path p1 = getFile("dir/script.js");
- Path p2 = getFile("dir/script2.js");
- Path d = p1.getParent();
-
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, d);
-
- verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p1.toFile().toURI().toString());
- verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p2.toFile().toURI().toString());
- }
-
- @Test
- public void testSortsAllFilesInNewDirectory() {
- 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);
-
- updateStartLevel(100);
-
- Path p20 = getFile("dir/script.sl20.js");
- Path p10 = getFile("dir/script2.sl10.js");
- Path d = p10.getParent();
-
- scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, d);
-
- InOrder inOrder = inOrder(scriptEngineManagerMock);
-
- inOrder.verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p10.toFile().toURI().toString());
- inOrder.verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js",
- p20.toFile().toURI().toString());
- }
-
- @Test
- public void testDirectoryRemoved() {
- 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);
-
- updateStartLevel(100);
-
- Path p1 = getFile("dir/script.js");
- 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);
-
- verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p1.toFile().toURI().toString());
- verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p2.toFile().toURI().toString());
- verify(scriptEngineManagerMock, timeout(10000)).removeEngine(p1.toFile().toURI().toString());
- verify(scriptEngineManagerMock, timeout(10000)).removeEngine(p2.toFile().toURI().toString());
+ @Override
+ public Future> submit(@NonNullByDefault({}) Runnable runnable) {
+ counter.getAndIncrement();
+ Runnable wrappedRunnable = () -> {
+ runnable.run();
+ counter.getAndDecrement();
+ };
+ return super.submit(wrappedRunnable);
+ }
}
}
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java
index 29e356019..e6fb4e9bf 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java
@@ -42,8 +42,9 @@ public interface ScriptEngineManager {
*
* @param engineIdentifier the unique identifier for the ScriptEngine (script file path or UUID)
* @param scriptData the content of the script
+ * @return true
if the script was successfully loaded, false
otherwise
*/
- void loadScript(String engineIdentifier, InputStreamReader scriptData);
+ boolean loadScript(String engineIdentifier, InputStreamReader scriptData);
/**
* Unloads the ScriptEngine loaded with the engineIdentifier
diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java
index 2fdeaf5b6..9d8d7e801 100644
--- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java
+++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java
@@ -174,13 +174,12 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
}
@Override
- public void loadScript(String engineIdentifier, InputStreamReader scriptData) {
+ public boolean loadScript(String engineIdentifier, InputStreamReader scriptData) {
ScriptEngineContainer container = loadedScriptEngineInstances.get(engineIdentifier);
if (container == null) {
logger.error("Could not load script, as no ScriptEngine has been created");
} else {
ScriptEngine engine = container.getScriptEngine();
-
try {
engine.eval(scriptData);
if (engine instanceof Invocable) {
@@ -193,11 +192,13 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
} else {
logger.trace("ScriptEngine does not support Invocable interface");
}
+ return true;
} catch (Exception ex) {
logger.error("Error during evaluation of script '{}': {}", engineIdentifier, ex.getMessage());
logger.debug("", ex);
}
}
+ return false;
}
@Override