diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/DependencyTracker.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/DependencyTracker.java new file mode 100644 index 0000000000..e6cfe990c0 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/DependencyTracker.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2010-2020 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.util.HashSet; +import java.util.Set; + +import org.openhab.core.automation.module.script.rulesupport.internal.loader.collection.BidiSetBag; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Tracks dependencies between scripts and reloads dependees + * + * @author Jonathan Gilbert + */ +public abstract class DependencyTracker { + + private final Logger logger = LoggerFactory.getLogger(DependencyTracker.class); + + private final BidiSetBag scriptToLibs = new BidiSetBag<>(); + private final ScriptLibraryWatcher scriptLibraryWatcher = new ScriptLibraryWatcher() { + @Override + void updateFile(String libraryPath) { + Set scripts; + synchronized (scriptToLibs) { + scripts = new HashSet<>(scriptToLibs.getKeys(libraryPath)); // take a copy as it will change as we + // reimport + } + DependencyTracker.this.logger.debug("Library {} changed; reimporting {} scripts...", libraryPath, + scripts.size()); + for (String scriptUrl : scripts) { + reimportScript(scriptUrl); + } + } + }; + + public void activate() { + scriptLibraryWatcher.activate(); + } + + public void deactivate() { + scriptLibraryWatcher.deactivate(); + } + + public abstract void reimportScript(String scriptPath); + + public void addLibForScript(String scriptPath, String libPath) { + synchronized (scriptToLibs) { + scriptToLibs.put(scriptPath, libPath); + } + } + + public void removeScript(String scriptPath) { + synchronized (scriptToLibs) { + scriptToLibs.removeKey(scriptPath); + } + } +} diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileWatcher.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileWatcher.java index dd7df10138..e2cbd5ddad 100644 --- a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileWatcher.java +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptFileWatcher.java @@ -75,6 +75,7 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac private final ScriptEngineManager manager; private final ReadyService readyService; + private @Nullable DependencyTracker dependencyTracker; private @Nullable ScheduledExecutorService scheduler; private final Map> urlsByScriptExtension = new ConcurrentHashMap<>(); @@ -93,6 +94,18 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac super.activate(); readyService.registerTracker(this, new ReadyMarkerFilter().withType(StartLevelService.STARTLEVEL_MARKER_TYPE) .withIdentifier(Integer.toString(StartLevelService.STARTLEVEL_MODEL))); + dependencyTracker = new DependencyTracker() { + @Override + public void reimportScript(String scriptPath) { + logger.debug("Reimporting {}...", scriptPath); + try { + importFile(new URL(scriptPath)); + } catch (MalformedURLException e) { + logger.warn("Failed to reimport {} as it cannot be parsed as a URL", scriptPath); + } + } + }; + dependencyTracker.activate(); } @Deactivate @@ -104,6 +117,12 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac localScheduler.shutdownNow(); scheduler = null; } + + if (dependencyTracker != null) { + dependencyTracker.deactivate(); + dependencyTracker = null; + } + super.deactivate(); } @@ -164,7 +183,9 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac private void removeFile(URL url) { dequeueUrl(url); - manager.removeEngine(getScriptIdentifier(url)); + String scriptIdentifier = getScriptIdentifier(url); + dependencyTracker.removeScript(scriptIdentifier); + manager.removeEngine(scriptIdentifier); loaded.remove(url); } @@ -184,11 +205,12 @@ public class ScriptFileWatcher extends AbstractWatchService implements ReadyTrac StandardCharsets.UTF_8)) { logger.info("Loading script '{}'", fileName); - ScriptEngineContainer container = manager.createScriptEngine(scriptType, - getScriptIdentifier(url)); + String scriptIdentifier = getScriptIdentifier(url); + ScriptEngineContainer container = manager.createScriptEngine(scriptType, scriptIdentifier); if (container != null) { - manager.loadScript(container.getIdentifier(), reader); + manager.loadScript(container.getIdentifier(), reader, + dependency -> dependencyTracker.addLibForScript(scriptIdentifier, dependency)); loaded.add(url); logger.debug("Script loaded: {}", fileName); } else { diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptLibraryWatcher.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptLibraryWatcher.java new file mode 100644 index 0000000000..66558e34de --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/ScriptLibraryWatcher.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2010-2020 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 static java.nio.file.StandardWatchEventKinds.ENTRY_CREATE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_DELETE; +import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.WatchEvent; + +import org.openhab.core.OpenHAB; +import org.openhab.core.service.AbstractWatchService; + +/** + * Listens for changes to script libraries + * + * @author Jonathan Gilbert + */ +abstract class ScriptLibraryWatcher extends AbstractWatchService { + + public static final String LIB_PATH = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib", + "javascript"); + + ScriptLibraryWatcher() { + super(LIB_PATH); + } + + @Override + protected boolean watchSubDirectories() { + return true; + } + + @Override + protected WatchEvent.Kind[] getWatchEventKinds(Path path) { + return new WatchEvent.Kind[] { ENTRY_CREATE, ENTRY_DELETE, ENTRY_MODIFY }; + } + + @Override + protected void processWatchEvent(WatchEvent watchEvent, WatchEvent.Kind kind, Path path) { + File file = path.toFile(); + if (!file.isHidden()) { + if (kind.equals(ENTRY_DELETE)) { + this.updateFile(file.getPath()); + } + + if (file.canRead() && (kind.equals(ENTRY_CREATE) || kind.equals(ENTRY_MODIFY))) { + this.updateFile(file.getPath()); + } + } + } + + abstract void updateFile(String filePath); +} diff --git a/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/collection/BidiSetBag.java b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/collection/BidiSetBag.java new file mode 100644 index 0000000000..8c65cf5f82 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script.rulesupport/src/main/java/org/openhab/core/automation/module/script/rulesupport/internal/loader/collection/BidiSetBag.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2010-2020 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.collection; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Bidirectional bag of unique elements. A map allowing multiple, unique values to be stored against a single key. + * Provides optimized lookup of values for a key, as well as keys referencing a value. + * + * @author Jonathan Gilbert - Initial contribution + * @param Type of Key + * @param Type of Value + */ +public class BidiSetBag { + private Map> keyToValues = new HashMap<>(); + private Map> valueToKeys = new HashMap<>(); + + public void put(K key, V value) { + addElement(keyToValues, key, value); + addElement(valueToKeys, value, key); + } + + public Set getValues(K key) { + Set existing = keyToValues.get(key); + return existing == null ? Collections.emptySet() : Collections.unmodifiableSet(existing); + } + + public Set getKeys(V value) { + Set existing = valueToKeys.get(value); + return existing == null ? Collections.emptySet() : Collections.unmodifiableSet(existing); + } + + public Set removeKey(K key) { + Set values = keyToValues.remove(key); + if (values != null) { + for (V value : values) { + valueToKeys.computeIfPresent(value, (k, v) -> { + v.remove(key); + return v; + }); + } + return values; + } else { + return Collections.emptySet(); + } + } + + public Set removeValue(V value) { + Set keys = valueToKeys.remove(value); + if (keys != null) { + for (K key : keys) { + keyToValues.computeIfPresent(key, (k, v) -> { + v.remove(value); + return v; + }); + } + return keys; + } else { + return Collections.emptySet(); + } + } + + private static void addElement(Map> map, T key, U value) { + Set elements = map.compute(key, (k, l) -> l == null ? new HashSet<>() : l); + elements.add(value); + } +} diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptDependencyListener.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptDependencyListener.java new file mode 100644 index 0000000000..d151953272 --- /dev/null +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptDependencyListener.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2020 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; + +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Interface that allows listener to be notified of script dependencies (libraries) + * + * @author Jonathan Gilbert - Initial contribution + */ +@NonNullByDefault +@FunctionalInterface +public interface ScriptDependencyListener extends Consumer { + void accept(String dependency); +} diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java index 353090a526..065a166251 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineFactory.java @@ -45,6 +45,11 @@ public interface ScriptEngineFactory { */ String CONTEXT_KEY_EXTENSION_ACCESSOR = "oh.extension-accessor"; + /** + * Key to access Dependency Listener {@link ScriptDependencyListener} + */ + String CONTEXT_KEY_DEPENDENCY_LISTENER = "oh.dependency-listener"; + /** * This method returns a list of file extensions and MimeTypes that are supported by the ScriptEngine, e.g. py, * application/python, js, application/javascript, etc. diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java index bfc3f514d6..46278e50d2 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/ScriptEngineManager.java @@ -45,6 +45,16 @@ public interface ScriptEngineManager { */ void loadScript(String engineIdentifier, InputStreamReader scriptData); + /** + * Loads a script and initializes its scope variables + * + * @param engineIdentifier the unique identifier for the ScriptEngine (script file path or UUID) + * @param scriptData the content of the script + * @param scriptDependencyListener listener to be notified of script dependencies + */ + void loadScript(String engineIdentifier, InputStreamReader scriptData, + ScriptDependencyListener scriptDependencyListener); + /** * Unloads the ScriptEngine loaded with the engineIdentifier * diff --git a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java index 0f9fc2137b..813f41a66c 100644 --- a/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java +++ b/bundles/org.openhab.core.automation.module.script/src/main/java/org/openhab/core/automation/module/script/internal/ScriptEngineManagerImpl.java @@ -12,6 +12,7 @@ */ package org.openhab.core.automation.module.script.internal; +import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_DEPENDENCY_LISTENER; import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_ENGINE_IDENTIFIER; import static org.openhab.core.automation.module.script.ScriptEngineFactory.CONTEXT_KEY_EXTENSION_ACCESSOR; @@ -28,6 +29,7 @@ import javax.script.SimpleScriptContext; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.module.script.ScriptDependencyListener; import org.openhab.core.automation.module.script.ScriptEngineContainer; import org.openhab.core.automation.module.script.ScriptEngineFactory; import org.openhab.core.automation.module.script.ScriptEngineManager; @@ -136,17 +138,9 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager { logger.debug("Added ScriptEngine for language '{}' with identifier: {}", scriptType, engineIdentifier); - ScriptContext scriptContext = engine.getContext(); + addAttributeToScriptContext(engine, CONTEXT_KEY_ENGINE_IDENTIFIER, engineIdentifier); + addAttributeToScriptContext(engine, CONTEXT_KEY_EXTENSION_ACCESSOR, scriptExtensionManager); - if (scriptContext == null) { - scriptContext = new SimpleScriptContext(); - engine.setContext(scriptContext); - } - - scriptContext.setAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER, engineIdentifier, - ScriptContext.ENGINE_SCOPE); - scriptContext.setAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR, scriptExtensionManager, - ScriptContext.ENGINE_SCOPE); } else { logger.error("ScriptEngine for language '{}' could not be created for identifier: {}", scriptType, engineIdentifier); @@ -161,11 +155,22 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager { @Override public void loadScript(String engineIdentifier, InputStreamReader scriptData) { + loadScript(engineIdentifier, scriptData, null); + } + + @Override + public void loadScript(String engineIdentifier, InputStreamReader scriptData, + @Nullable ScriptDependencyListener dependencyListener) { ScriptEngineContainer container = loadedScriptEngineInstances.get(engineIdentifier); if (container == null) { logger.error("Could not load script, as no ScriptEngine has been created"); } else { ScriptEngine engine = container.getScriptEngine(); + + if (dependencyListener != null) { + addAttributeToScriptContext(engine, CONTEXT_KEY_DEPENDENCY_LISTENER, dependencyListener); + } + try { engine.eval(scriptData); if (engine instanceof Invocable) { @@ -235,4 +240,15 @@ public class ScriptEngineManagerImpl implements ScriptEngineManager { public boolean isSupported(String scriptType) { return findEngineFactory(scriptType) != null; } + + private void addAttributeToScriptContext(ScriptEngine engine, String name, Object value) { + ScriptContext scriptContext = engine.getContext(); + + if (scriptContext == null) { + scriptContext = new SimpleScriptContext(); + engine.setContext(scriptContext); + } + + scriptContext.setAttribute(name, value, ScriptContext.ENGINE_SCOPE); + } }