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> <artifactId>org.openhab.core.automation.module.script</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.openhab.core.bundles</groupId>
<artifactId>org.openhab.core.test</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -10,7 +10,7 @@
* *
* SPDX-License-Identifier: EPL-2.0 * 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.Collections;
import java.util.HashMap; 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.ScriptEngineManager;
import org.openhab.core.automation.module.script.rulesupport.loader.AbstractScriptFileWatcher; import org.openhab.core.automation.module.script.rulesupport.loader.AbstractScriptFileWatcher;
import org.openhab.core.service.ReadyService; 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.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Deactivate; import org.osgi.service.component.annotations.Deactivate;
@ -37,8 +38,8 @@ public class DefaultScriptFileWatcher extends AbstractScriptFileWatcher {
@Activate @Activate
public DefaultScriptFileWatcher(final @Reference ScriptEngineManager manager, public DefaultScriptFileWatcher(final @Reference ScriptEngineManager manager,
final @Reference ReadyService readyService) { final @Reference ReadyService readyService, final @Reference StartLevelService startLevelService) {
super(manager, readyService, FILE_DIRECTORY); super(manager, readyService, startLevelService, FILE_DIRECTORY);
} }
@Activate @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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.automation.module.script.ScriptDependencyTracker; 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.openhab.core.service.AbstractWatchService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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 static java.nio.file.StandardWatchEventKinds.*;
import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.WatchEvent; import java.nio.file.WatchEvent;
import java.nio.file.WatchEvent.Kind; import java.nio.file.WatchEvent.Kind;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock;
import java.util.function.Supplier; 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.Collectors;
import java.util.stream.Stream;
import javax.script.ScriptEngine; 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.ScriptDependencyTracker;
import org.openhab.core.automation.module.script.ScriptEngineContainer; import org.openhab.core.automation.module.script.ScriptEngineContainer;
import org.openhab.core.automation.module.script.ScriptEngineManager; 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.common.NamedThreadFactory;
import org.openhab.core.service.AbstractWatchService; import org.openhab.core.service.AbstractWatchService;
import org.openhab.core.service.ReadyMarker; 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 * The {@link AbstractScriptFileWatcher} is default implementation for watching a directory for files. If a new/modified
* file is detected, the script * file is detected, the script is read and passed to the {@link ScriptEngineManager}. It needs to be sub-classed for
* is read and passed to the {@link ScriptEngineManager}. It needs to be sub-classed for actual use. * actual use.
* *
* @author Simon Merschjohann - Initial contribution * @author Simon Merschjohann - Initial contribution
* @author Kai Kreuzer - improved logging and removed thread pool * @author Kai Kreuzer - improved logging and removed thread pool
* @author Jonathan Gilbert - added dependency tracking & per-script start levels; made reusable * @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 @NonNullByDefault
public abstract class AbstractScriptFileWatcher extends AbstractWatchService implements ReadyService.ReadyTracker, 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 Logger logger = LoggerFactory.getLogger(AbstractScriptFileWatcher.class);
private final ScriptEngineManager manager; private final ScriptEngineManager manager;
private final ReadyService readyService; private final ReadyService readyService;
private @Nullable ScheduledExecutorService scheduler; protected ScheduledExecutorService scheduler;
private Supplier<ScheduledExecutorService> executorFactory;
private final Set<ScriptFileReference> pending = ConcurrentHashMap.newKeySet(); private final Map<String, ScriptFileReference> scriptMap = new ConcurrentHashMap<>();
private final Set<ScriptFileReference> loaded = ConcurrentHashMap.newKeySet(); private final Map<String, Lock> scriptLockMap = new ConcurrentHashMap<>();
private final CompletableFuture<@Nullable Void> initialized = new CompletableFuture<>();
private volatile int currentStartLevel = 0; private volatile int currentStartLevel = 0;
public AbstractScriptFileWatcher(final ScriptEngineManager manager, final ReadyService readyService, public AbstractScriptFileWatcher(final ScriptEngineManager manager, final ReadyService readyService,
final String fileDirectory) { final StartLevelService startLevelService, final String fileDirectory) {
super(OpenHAB.getConfigFolder() + File.separator + fileDirectory); super(OpenHAB.getConfigFolder() + File.separator + fileDirectory);
this.manager = manager; this.manager = manager;
this.readyService = readyService; this.readyService = readyService;
this.executorFactory = () -> Executors
.newSingleThreadScheduledExecutor(new NamedThreadFactory("scriptwatcher"));
manager.addFactoryChangeListener(this); manager.addFactoryChangeListener(this);
readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE)); 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 @Override
public void deactivate() { public void deactivate() {
manager.removeFactoryChangeListener(this); manager.removeFactoryChangeListener(this);
readyService.unregisterTracker(this); readyService.unregisterTracker(this);
ScheduledExecutorService localScheduler = scheduler;
if (localScheduler != null) {
localScheduler.shutdownNow();
scheduler = null;
}
super.deactivate(); super.deactivate();
CompletableFuture.allOf(
Set.copyOf(scriptMap.keySet()).stream().map(this::removeFile).toArray(CompletableFuture<?>[]::new))
.thenRun(scheduler::shutdownNow);
} }
@Override @Override
public void activate() { public CompletableFuture<@Nullable Void> ifInitialized() {
File directory = new File(pathToWatch); return initialized;
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();
} }
/** /**
* 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) { protected Optional<String> getScriptType(Path scriptFilePath) {
this.executorFactory = executorFactory; 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) { protected int getStartLevel(Path scriptFilePath) {
if (rootDirectory.exists()) { for (Pattern p : START_LEVEL_PATTERNS) {
File[] files = rootDirectory.listFiles(); Matcher m = p.matcher(scriptFilePath.toString());
if (files != null) { if (m.find() && m.groupCount() > 0) {
Collection<ScriptFileReference> resources = new TreeSet<>(); try {
for (File f : files) { return Integer.parseInt(m.group(1));
if (!f.isHidden()) { } catch (NumberFormatException nfe) {
resources.addAll(collectResources(f)); logger.warn("Extracted start level {} from {}, but it's not an integer. Ignoring.", m.group(1),
} scriptFilePath);
}
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));
}
}
} }
} }
} 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 @Override
@ -202,180 +213,194 @@ public abstract class AbstractScriptFileWatcher extends AbstractWatchService imp
protected void processWatchEvent(@Nullable WatchEvent<?> event, @Nullable Kind<?> kind, @Nullable Path path) { protected void processWatchEvent(@Nullable WatchEvent<?> event, @Nullable Kind<?> kind, @Nullable Path path) {
File file = path.toFile(); File file = path.toFile();
if (!file.isHidden()) { if (!file.isHidden()) {
try { if (ENTRY_DELETE.equals(kind)) {
if (ENTRY_DELETE.equals(kind)) { if (file.isDirectory()) {
if (file.isDirectory()) { if (watchSubDirectories()) {
if (watchSubDirectories()) { synchronized (this) {
synchronized (this) { String prefix = path.getParent().toString();
String prefix = file.toURI().toURL().getPath(); Set<String> toRemove = scriptMap.keySet().stream().filter(ref -> ref.startsWith(prefix))
Set<ScriptFileReference> toRemove = loaded.stream() .collect(Collectors.toSet());
.filter(f -> f.getScriptFileURL().getFile().startsWith(prefix)) toRemove.forEach(this::removeFile);
.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))) { if (file.canRead() && (ENTRY_CREATE.equals(kind) || ENTRY_MODIFY.equals(kind))) {
collectResources(file).forEach(this::importFileWhenReady); addFiles(listFiles(file.toPath(), watchSubDirectories()));
}
} catch (MalformedURLException e) {
logger.error("malformed", e);
} }
} }
} }
private void removeFile(ScriptFileReference ref) { private CompletableFuture<Void> addFiles(Collection<Path> files) {
dequeueUrl(ref); 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(); String scriptIdentifier = ref.getScriptIdentifier();
manager.removeEngine(scriptIdentifier); try (InputStreamReader reader = new InputStreamReader(Files.newInputStream(ref.getScriptFilePath()),
loaded.remove(ref); StandardCharsets.UTF_8)) {
} ScriptEngineContainer container = manager.createScriptEngine(ref.getScriptType(),
ref.getScriptIdentifier());
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);
if (container != null) { if (container != null) {
container.getScriptEngine().put(ScriptEngine.FILENAME, fileName); container.getScriptEngine().put(ScriptEngine.FILENAME, scriptIdentifier);
manager.loadScript(container.getIdentifier(), reader); if (manager.loadScript(container.getIdentifier(), reader)) {
return true;
logger.debug("Script loaded: {}", fileName); }
return true;
} else {
logger.error("Script loading error, ignoring file: {}", fileName);
} }
logger.warn("Script loading error, ignoring file '{}'", scriptIdentifier);
} catch (IOException e) { } 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; return false;
} }
private void enqueue(ScriptFileReference ref) { private void initialImport() {
synchronized (pending) { File directory = new File(pathToWatch);
pending.add(ref);
}
logger.debug("Enqueued {}", ref.getScriptIdentifier()); if (!directory.exists()) {
} if (!directory.mkdirs()) {
logger.warn("Failed to create watched directory: {}", pathToWatch);
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));
} }
} 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 @Override
public void onDependencyChange(@Nullable String scriptId) { public void onDependencyChange(String scriptIdentifier) {
logger.debug("Reimporting {}...", scriptId); logger.debug("Reimporting {}...", scriptIdentifier);
try { ScriptFileReference ref = scriptMap.get(scriptIdentifier);
ScriptFileReference scriptFileReference = new ScriptFileReference(new URL(scriptId)); if (ref != null && !ref.getQueueStatus().getAndSet(true)) {
if (loaded.contains(scriptFileReference)) { importFileWhenReady(scriptIdentifier);
importFileWhenReady(scriptFileReference);
}
} catch (MalformedURLException ignored) {
} }
logger.debug("Ignoring dependency change for {} as it is no file or not loaded by this file watcher", scriptId);
} }
@Override @Override
public synchronized void onReadyMarkerAdded(ReadyMarker readyMarker) { public synchronized void onReadyMarkerAdded(ReadyMarker readyMarker) {
int newLevel = Integer.parseInt(readyMarker.getIdentifier()); int previousLevel = currentStartLevel;
currentStartLevel = Integer.parseInt(readyMarker.getIdentifier());
if (newLevel > currentStartLevel) { if (currentStartLevel < StartLevelService.STARTLEVEL_STATES) {
onStartLevelChanged(newLevel); // 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 private boolean needsStartLevelProcessing(ScriptFileReference ref, int previousLevel, int newLevel) {
@SuppressWarnings("PMD.EmptyWhileStmt") int refStartLevel = ref.getStartLevel();
public synchronized void onReadyMarkerRemoved(ReadyMarker readyMarker) { return !ref.getLoadedStatus().get() && newLevel >= refStartLevel && previousLevel < refStartLevel
int newLevel = Integer.parseInt(readyMarker.getIdentifier()); && !ref.getQueueStatus().getAndSet(true);
}
if (currentStartLevel > newLevel) { @Override
while (newLevel-- > 0 && !readyService public void onReadyMarkerRemoved(ReadyMarker readyMarker) {
.isReady(new ReadyMarker(StartLevelService.STARTLEVEL_MARKER_TYPE, Integer.toString(newLevel)))) { // 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}
onStartLevelChanged(newLevel);
}
} }
@Override @Override
public void factoryAdded(@Nullable String scriptType) { public void factoryAdded(@Nullable String scriptType) {
scriptMap.forEach((scriptIdentifier, ref) -> {
if (ref.getScriptType().equals(scriptType) && !ref.getQueueStatus().getAndSet(true)) {
importFileWhenReady(scriptIdentifier);
}
});
} }
@Override @Override
@ -383,6 +408,9 @@ public abstract class AbstractScriptFileWatcher extends AbstractWatchService imp
if (scriptType == null) { if (scriptType == null) {
return; 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; package org.openhab.core.automation.module.script.rulesupport.loader;
import static java.nio.file.StandardWatchEventKinds.*; 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.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import static org.openhab.core.OpenHAB.CONFIG_DIR_PROG_ARGUMENT;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.nio.file.Path; 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.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
import javax.script.ScriptEngine; 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.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.api.io.TempDir;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder; import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension; 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.ScriptEngineContainer;
import org.openhab.core.automation.module.script.ScriptEngineFactory; import org.openhab.core.automation.module.script.ScriptEngineFactory;
import org.openhab.core.automation.module.script.ScriptEngineManager; 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.ReadyMarker;
import org.openhab.core.service.ReadyService; import org.openhab.core.service.ReadyService;
import org.openhab.core.service.StartLevelService; import org.openhab.core.service.StartLevelService;
import org.openhab.core.test.java.JavaTest;
import org.opentest4j.AssertionFailedError; 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 @NonNullByDefault
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT) @MockitoSettings(strictness = Strictness.LENIENT)
class AbstractScriptFileWatcherTest { class AbstractScriptFileWatcherTest extends JavaTest {
private boolean watchSubDirectories = true;
private @NonNullByDefault({}) AbstractScriptFileWatcher scriptFileWatcher; private @NonNullByDefault({}) AbstractScriptFileWatcher scriptFileWatcher;
private @Mock @NonNullByDefault({}) ScriptEngineManager scriptEngineManagerMock; private @Mock @NonNullByDefault({}) ScriptEngineManager scriptEngineManagerMock;
private @Mock @NonNullByDefault({}) ScriptDependencyTracker scriptDependencyTrackerMock; 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; protected @NonNullByDefault({}) @TempDir Path tempScriptDir;
private final AtomicInteger atomicInteger = new AtomicInteger();
private int currentStartLevel = 0;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
scriptFileWatcher = new AbstractScriptFileWatcher(scriptEngineManagerMock, readyService, System.setProperty(CONFIG_DIR_PROG_ARGUMENT, tempScriptDir.toString());
"automation" + File.separator + "jsr223") {
}; atomicInteger.set(0);
scriptFileWatcher.activate(); currentStartLevel = 0;
// ensure initialize is not called on initialization
when(startLevelServiceMock.getStartLevel()).thenAnswer(invocation -> currentStartLevel);
scriptFileWatcher = createScriptFileWatcher();
} }
@AfterEach @AfterEach
@ -78,7 +96,502 @@ class AbstractScriptFileWatcherTest {
scriptFileWatcher.deactivate(); 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); Path tempFile = tempScriptDir.resolve(name);
try { try {
File parent = tempFile.getParent().toFile(); File parent = tempFile.getParent().toFile();
@ -96,336 +609,56 @@ class AbstractScriptFileWatcherTest {
return Path.of(tempFile.toUri()); return Path.of(tempFile.toUri());
} }
void updateStartLevel(int level) { /**
scriptFileWatcher * Increase the start level in steps of 10
.onReadyMarkerAdded(new ReadyMarker(StartLevelService.STARTLEVEL_MARKER_TYPE, Integer.toString(level))); *
* @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 private AbstractScriptFileWatcher createScriptFileWatcher() {
public void testLoadOneDefaultFileAlreadyStarted() { return new AbstractScriptFileWatcher(scriptEngineManagerMock, readyServiceMock, startLevelServiceMock, "") {
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); @Override
protected ScheduledExecutorService getScheduler() {
return new CountingScheduledExecutor(atomicInteger);
}
Path p = getFile("script.js"); @Override
protected boolean watchSubDirectories() {
scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); return watchSubDirectories;
}
verify(scriptEngineManagerMock, timeout(10000)).createScriptEngine("js", p.toFile().toURI().toString()); };
} }
@Test private void awaitEmptyQueue() {
public void testLoadOneDefaultFileWaitUntilStarted() { waitForAssert(() -> assertThat(atomicInteger.get(), is(0)));
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());
} }
@Test private static class CountingScheduledExecutor extends ScheduledThreadPoolExecutor {
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);
updateStartLevel(50); private final AtomicInteger counter;
Path p = getFile("script.sl60.js"); public CountingScheduledExecutor(AtomicInteger counter) {
scriptFileWatcher.processWatchEvent(null, ENTRY_CREATE, p); super(1);
// verify not yet called this.counter = counter;
verify(scriptEngineManagerMock, never()).createScriptEngine(anyString(), anyString()); }
// verify is called when the start level increases @Override
updateStartLevel(100); public Future<?> submit(@NonNullByDefault({}) Runnable runnable) {
verify(scriptEngineManagerMock, timeout(10000).times(1)).createScriptEngine("js", counter.getAndIncrement();
p.toFile().toURI().toString()); Runnable wrappedRunnable = () -> {
} runnable.run();
counter.getAndDecrement();
@Test };
public void testLoadTwoCustomFilesDifferentStartLevels() { return super.submit(wrappedRunnable);
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());
} }
} }

View File

@ -42,8 +42,9 @@ public interface ScriptEngineManager {
* *
* @param engineIdentifier the unique identifier for the ScriptEngine (script file path or UUID) * @param engineIdentifier the unique identifier for the ScriptEngine (script file path or UUID)
* @param scriptData the content of the script * @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 * Unloads the ScriptEngine loaded with the engineIdentifier

View File

@ -174,13 +174,12 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
} }
@Override @Override
public void loadScript(String engineIdentifier, InputStreamReader scriptData) { public boolean loadScript(String engineIdentifier, InputStreamReader scriptData) {
ScriptEngineContainer container = loadedScriptEngineInstances.get(engineIdentifier); ScriptEngineContainer container = loadedScriptEngineInstances.get(engineIdentifier);
if (container == null) { if (container == null) {
logger.error("Could not load script, as no ScriptEngine has been created"); logger.error("Could not load script, as no ScriptEngine has been created");
} else { } else {
ScriptEngine engine = container.getScriptEngine(); ScriptEngine engine = container.getScriptEngine();
try { try {
engine.eval(scriptData); engine.eval(scriptData);
if (engine instanceof Invocable) { if (engine instanceof Invocable) {
@ -193,11 +192,13 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager {
} else { } else {
logger.trace("ScriptEngine does not support Invocable interface"); logger.trace("ScriptEngine does not support Invocable interface");
} }
return true;
} catch (Exception ex) { } catch (Exception ex) {
logger.error("Error during evaluation of script '{}': {}", engineIdentifier, ex.getMessage()); logger.error("Error during evaluation of script '{}': {}", engineIdentifier, ex.getMessage());
logger.debug("", ex); logger.debug("", ex);
} }
} }
return false;
} }
@Override @Override