Refactor AbstractScriptFileWatcher (#3255)

Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
J-N-K 2023-01-12 19:37:42 +01:00 committed by GitHub
parent a09ad3d37c
commit ecbb854e03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 990 additions and 704 deletions

View File

@ -25,6 +25,12 @@
<artifactId>org.openhab.core.automation.module.script</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@ -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;

View File

@ -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

View File

@ -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<ScriptFileReference> {
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();
}
}

View File

@ -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;

View File

@ -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<String> EXCLUDED_FILE_EXTENSIONS = Set.of("txt", "old", "example", "backup", "md", "swp",
"tmp", "bak");
private static final List<Pattern> START_LEVEL_PATTERNS = List.of( //
Pattern.compile(".*/sl(\\d{2})/[^/]+"), // script in immediate slXX directory
Pattern.compile(".*/[^/]+\\.sl(\\d{2})\\.[^/.]+") // script named <name>.slXX.<ext>
);
private final Logger logger = LoggerFactory.getLogger(AbstractScriptFileWatcher.class);
private final ScriptEngineManager manager;
private final ReadyService readyService;
private @Nullable ScheduledExecutorService scheduler;
private Supplier<ScheduledExecutorService> executorFactory;
protected ScheduledExecutorService scheduler;
private final Set<ScriptFileReference> pending = ConcurrentHashMap.newKeySet();
private final Set<ScriptFileReference> loaded = ConcurrentHashMap.newKeySet();
private final Map<String, ScriptFileReference> scriptMap = new ConcurrentHashMap<>();
private final Map<String, Lock> 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
* <p />
* 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<String>} containing the script type
*/
void setExecutorFactory(Supplier<ScheduledExecutorService> executorFactory) {
this.executorFactory = executorFactory;
protected Optional<String> 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
* <p />
* 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<ScriptFileReference> 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<ScriptFileReference> collectResources(File file) {
Collection<ScriptFileReference> 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<Path> listFiles(Path path, boolean includeSubDirectory) {
try (Stream<Path> 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<ScriptFileReference> 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<String> 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<Void> addFiles(Collection<Path> files) {
return CompletableFuture.allOf(files.stream().map(this::getScriptFileReference).filter(Optional::isPresent)
.map(Optional::get).sorted().map(this::addScriptFileReference).toArray(CompletableFuture<?>[]::new));
}
private CompletableFuture<Void> 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<ScriptFileReference> getScriptFileReference(Path path) {
return getScriptType(path).map(scriptType -> new ScriptFileReference(path, scriptType, getStartLevel(path)));
}
private CompletableFuture<Void> 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<Void> 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<String> 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<String> 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<ScriptFileReference> 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<String> toRemove = scriptMap.values().stream()
.filter(ref -> ref.getLoadedStatus().get() && scriptType.equals(ref.getScriptType()))
.map(ScriptFileReference::getScriptIdentifier).collect(Collectors.toSet());
toRemove.forEach(this::removeFile);
}
}

View File

@ -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<ScriptFileReference> {
private static final Set<String> 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 <name>.slXX.<ext>
};
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<String> 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);
}
}

View File

@ -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<Void>} that completes when the {@link ScriptFileWatcher} has completed it's
* initial loading of files.
*
* @return the {@link CompletableFuture}
*/
CompletableFuture<@Nullable Void> ifInitialized();
}

View File

@ -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<Runnable> 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);
}
}
}

View File

@ -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 <code>true</code> if the script was successfully loaded, <code>false</code> otherwise
*/
void loadScript(String engineIdentifier, InputStreamReader scriptData);
boolean loadScript(String engineIdentifier, InputStreamReader scriptData);
/**
* Unloads the ScriptEngine loaded with the engineIdentifier

View File

@ -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