mirror of
https://github.com/danieldemus/openhab-core.git
synced 2025-02-04 08:03:53 +01:00
Refactor AbstractScriptFileWatcher (#3255)
Signed-off-by: Jan N. Klug <github@klug.nrw>
This commit is contained in:
parent
a09ad3d37c
commit
ecbb854e03
@ -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>
|
||||||
|
@ -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;
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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();
|
||||||
|
}
|
@ -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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user