diff --git a/CODEOWNERS b/CODEOWNERS index 3a9dcf65fe6..0b3707515fb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,6 +6,7 @@ # Add-on maintainers: /bundles/org.openhab.automation.groovyscripting/ @wborn +/bundles/org.openhab.automation.jsscripting/ @jpg0 /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers /bundles/org.openhab.automation.pidcontroller/ @fwolter /bundles/org.openhab.binding.adorne/ @theiding diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 82397f5e9eb..3485b385073 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -31,6 +31,11 @@ org.openhab.automation.pidcontroller ${project.version} + + org.openhab.addons.bundles + org.openhab.automation.jsscripting + ${project.version} + org.openhab.addons.bundles org.openhab.binding.adorne diff --git a/bundles/org.openhab.automation.jsscripting/NOTICE b/bundles/org.openhab.automation.jsscripting/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.automation.jsscripting/README.md b/bundles/org.openhab.automation.jsscripting/README.md new file mode 100644 index 00000000000..23b1dcd9bd1 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/README.md @@ -0,0 +1,43 @@ +# JavaScript Scripting + +This add-on provides support for JavaScript (ECMAScript 2021+) that can be used as a scripting language within automation rules. + +## Creating JavaScript Scripts + +When this add-on is installed, JavaScript script actions will be run by this add-on and allow ECMAScript 2021+ features. + +Alternatively, you can create scripts in the `automation/jsr223` configuration directory. +If you create an empty file called `test.js`, you will see a log line with information similar to: + +```text + ... [INFO ] [.a.m.s.r.i.l.ScriptFileWatcher:150 ] - Loading script 'test.js' +``` + +To enable debug logging, use the [console logging]({{base}}/administration/logging.html) commands to enable debug logging for the automation functionality: + +```text +log:set DEBUG org.openhab.core.automation +``` + +For more information on the available APIs in scripts see the [JSR223 Scripting]({{base}}/configuration/jsr223.html) documentation. + +## Script Examples + +JavaScript scripts provide access to almost all the functionality in an openHAB runtime environment. +As a simple example, the following script logs "Hello, World!". +Note that `console.log` will usually not work since the output has no terminal to display the text. +The openHAB server uses the [SLF4J](https://www.slf4j.org/) library for logging. + +```js +const LoggerFactory = Java.type('org.slf4j.LoggerFactory'); + +LoggerFactory.getLogger("org.openhab.core.automation.examples").info("Hello world!"); +``` + +Depending on the openHAB logging configuration, you may need to prefix logger names with `org.openhab.core.automation` for them to show up in the log file (or you modify the logging configuration). + +The script uses the [LoggerFactory](https://www.slf4j.org/apidocs/org/slf4j/Logger.html) to obtain a named logger and then logs a message like: + +```text + ... [INFO ] [.openhab.core.automation.examples:-2 ] - Hello world! +``` diff --git a/bundles/org.openhab.automation.jsscripting/bnd.bnd b/bundles/org.openhab.automation.jsscripting/bnd.bnd new file mode 100644 index 00000000000..47c2bad3c8d --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/bnd.bnd @@ -0,0 +1,13 @@ +Bundle-SymbolicName: ${project.artifactId} +DynamicImport-Package: * +Import-Package: org.openhab.core.automation.module.script,javax.management,javax.script,javax.xml.datatype,javax.xml.stream;version="[1.0,2)",org.osgi.framework;version="[1.8,2)",org.slf4j;version="[1.7,2)" +Require-Capability: osgi.extender; + filter:="(osgi.extender=osgi.serviceloader.processor)", + osgi.serviceloader; + filter:="(osgi.serviceloader=org.graalvm.polyglot.impl.AbstractPolyglotImpl)"; + cardinality:=multiple + +SPI-Provider: * +SPI-Consumer: * + +-fixupmessages "Classes found in the wrong directory"; restrict:=error; is:=warning diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml new file mode 100644 index 00000000000..ab549b2354b --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/pom.xml @@ -0,0 +1,115 @@ + + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.1.0-SNAPSHOT + + + org.openhab.automation.jsscripting + + openHAB Add-ons :: Bundles :: Automation :: JSScripting + + + + !sun.misc.*, + !sun.reflect.*, + !com.sun.management.*, + !jdk.internal.reflect.*, + !jdk.vm.ci.services + + 20.1.0 + 6.2.1 + ${project.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + embed-dependencies + + unpack-dependencies + + + META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider + + + + + + + + + + org.graalvm.truffle + truffle-api + ${graal.version} + + + org.graalvm.js + js-scriptengine + ${graal.version} + + + org.graalvm.js + js-launcher + ${graal.version} + + + org.graalvm.sdk + graal-sdk + ${graal.version} + + + org.graalvm.regex + regex + ${graal.version} + + + org.graalvm.js + js + ${graal.version} + + + com.ibm.icu + icu4j + 62.1 + + + + + org.ow2.asm + asm + ${asm.version} + + + org.ow2.asm + asm-commons + ${asm.version} + + + org.ow2.asm + asm-tree + ${asm.version} + + + org.ow2.asm + asm-util + ${asm.version} + + + org.ow2.asm + asm-analysis + ${asm.version} + + + diff --git a/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml b/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml new file mode 100644 index 00000000000..31760e97aac --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml @@ -0,0 +1,10 @@ + + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.automation.jsscripting/${project.version} + + diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java new file mode 100644 index 00000000000..d3a1623df7d --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting; + +import com.oracle.truffle.js.runtime.java.adapter.JavaAdapterFactory; + +/** + * Class utility to allow creation of 'extendable' classes with a classloader of the GraalJS bundle, rather than the + * classloader of the file being extended. + * + * @author Jonathan Gilbert - Initial contribution + */ +public class ClassExtender { + private static ClassLoader classLoader = ClassExtender.class.getClassLoader(); + + public static Object extend(String className) { + try { + return extend(Class.forName(className)); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Cannot find class " + className, e); + } + } + + public static Object extend(Class clazz) { + return JavaAdapterFactory.getAdapterClassFor(clazz, null, classLoader); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java new file mode 100644 index 00000000000..0f21f455d16 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.graalvm.polyglot.PolyglotException; +import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps ScriptEngines provided by Graal to provide error messages and stack traces for scripts. + * + * @author Jonathan Gilbert - Initial contribution + */ +@NonNullByDefault +class DebuggingGraalScriptEngine + extends InvocationInterceptingScriptEngineWithInvocable { + + private static final Logger stackLogger = LoggerFactory.getLogger("org.openhab.automation.script.javascript.stack"); + + public DebuggingGraalScriptEngine(T delegate) { + super(delegate); + } + + @Override + public ScriptException afterThrowsInvocation(ScriptException se) { + Throwable cause = se.getCause(); + if (cause instanceof PolyglotException) { + stackLogger.error("Failed to execute script:", cause); + } + return se; + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java new file mode 100644 index 00000000000..14e9f783e83 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import javax.script.ScriptEngine; + +import org.openhab.core.automation.module.script.ScriptEngineFactory; +import org.osgi.service.component.annotations.Component; + +import com.oracle.truffle.js.scriptengine.GraalJSEngineFactory; + +/** + * An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines. + * + * @author Jonathan Gilbert - Initial contribution + */ +@Component(service = ScriptEngineFactory.class) +public final class GraalJSScriptEngineFactory implements ScriptEngineFactory { + + @Override + public List getScriptTypes() { + List scriptTypes = new ArrayList<>(); + GraalJSEngineFactory graalJSEngineFactory = new GraalJSEngineFactory(); + + scriptTypes.addAll(graalJSEngineFactory.getMimeTypes()); + scriptTypes.addAll(graalJSEngineFactory.getExtensions()); + + return Collections.unmodifiableList(scriptTypes); + } + + @Override + public void scopeValues(ScriptEngine scriptEngine, Map scopeValues) { + // noop; the are retrieved via modules, not injected + } + + @Override + public ScriptEngine createScriptEngine(String scriptType) { + OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine(); + return new DebuggingGraalScriptEngine<>(engine); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java new file mode 100644 index 00000000000..13e817a525b --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal; + +import java.util.Optional; + +import org.graalvm.polyglot.Value; + +/** + * Locates modules from a module name + * + * @author Jonathan Gilbert - Initial contribution + */ +public interface ModuleLocator { + Optional locateModule(String name); +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java new file mode 100644 index 00000000000..8541eb7db85 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal; + +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; + +import java.io.File; +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.FileSystems; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.attribute.FileAttribute; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; + +import javax.script.ScriptContext; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.graalvm.polyglot.Context; +import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem; +import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel; +import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable; +import org.openhab.core.OpenHAB; +import org.openhab.core.automation.module.script.ScriptExtensionAccessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; + +/** + * GraalJS Script Engine implementation + * + * @author Jonathan Gilbert - Initial contribution + */ +public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable { + + private static final Logger logger = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class); + + private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__"; + private static final String MODULE_DIR = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib", + "javascript", "personal"); + + // these fields start as null because they are populated on first use + @NonNullByDefault({}) + private String engineIdentifier; + @NonNullByDefault({}) + private Consumer scriptDependencyListener; + + private boolean initialized = false; + + /** + * Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script + * lifecycle and provides hooks for scripts to do so too. + */ + public OpenhabGraalJSScriptEngine() { + super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately + delegate = GraalJSScriptEngine.create(null, + Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true) + .option("js.commonjs-require-cwd", MODULE_DIR).option("js.nashorn-compat", "true") // to ease + // migration + .option("js.commonjs-require", "true") // enable CommonJS module support + .fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) { + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, + FileAttribute... attrs) throws IOException { + if (scriptDependencyListener != null) { + scriptDependencyListener.accept(path.toString()); + } + + if (path.toString().endsWith(".js")) { + return new PrefixedSeekableByteChannel( + ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(), + super.newByteChannel(path, options, attrs)); + } else { + return super.newByteChannel(path, options, attrs); + } + } + })); + } + + @Override + protected void beforeInvocation() { + if (initialized) { + return; + } + + ScriptContext ctx = delegate.getContext(); + + // these are added post-construction, so we need to fetch them late + this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER); + if (this.engineIdentifier == null) { + throw new IllegalStateException("Failed to retrieve engine identifier from engine bindings"); + } + + ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx + .getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR); + if (scriptExtensionAccessor == null) { + throw new IllegalStateException("Failed to retrieve script extension accessor from engine bindings"); + } + + scriptDependencyListener = (Consumer) ctx + .getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */); + if (scriptDependencyListener == null) { + logger.warn( + "Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled."); + } + + ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider( + scriptExtensionAccessor); + + Function, Function> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider + .locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName) + .map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName })); + + delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn); + delegate.put("require", wrapRequireFn.apply((Function) delegate.get("require"))); + + initialized = true; + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java new file mode 100644 index 00000000000..d2d14011426 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; +import org.openhab.automation.jsscripting.internal.threading.ThreadsafeWrappingScriptedAutomationManagerDelegate; +import org.openhab.core.automation.module.script.ScriptExtensionAccessor; +import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager; + +/** + * Class providing script extensions via CommonJS modules. + * + * @author Jonathan Gilbert - Initial contribution + */ + +@NonNullByDefault +public class ScriptExtensionModuleProvider { + + private static final String RUNTIME_MODULE_PREFIX = "@runtime"; + private static final String DEFAULT_MODULE_NAME = "Defaults"; + + private final ScriptExtensionAccessor scriptExtensionAccessor; + + public ScriptExtensionModuleProvider(ScriptExtensionAccessor scriptExtensionAccessor) { + this.scriptExtensionAccessor = scriptExtensionAccessor; + } + + public ModuleLocator locatorFor(Context ctx, String engineIdentifier) { + return name -> { + String[] segments = name.split("/"); + if (segments[0].equals(RUNTIME_MODULE_PREFIX)) { + if (segments.length == 1) { + return runtimeModule(DEFAULT_MODULE_NAME, engineIdentifier, ctx); + } else { + return runtimeModule(segments[1], engineIdentifier, ctx); + } + } + + return Optional.empty(); + }; + } + + private Optional runtimeModule(String name, String scriptIdentifier, Context ctx) { + + Map symbols; + + if (DEFAULT_MODULE_NAME.equals(name)) { + symbols = scriptExtensionAccessor.findDefaultPresets(scriptIdentifier); + } else { + symbols = scriptExtensionAccessor.findPreset(name, scriptIdentifier); + } + + return Optional.of(symbols).map(this::processValues).map(v -> toValue(ctx, v)); + } + + private Value toValue(Context ctx, Map map) { + try { + return ctx.eval(Source.newBuilder( // convert to Map to JS Object + "js", + "(function (mapOfValues) {\n" + "let rv = {};\n" + "for (var key in mapOfValues) {\n" + + " rv[key] = mapOfValues.get(key);\n" + "}\n" + "return rv;\n" + "})", + "").build()).execute(map); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to generate exports", e); + } + } + + /** + * Some specific objects need wrapping when exposed to a GraalJS environment. This method does this. + * + * @param values the map of names to values of things to process + * @return a map of the processed keys and values + */ + private Map processValues(Map values) { + Map rv = new HashMap<>(values); + + for (Map.Entry entry : rv.entrySet()) { + if (entry.getValue() instanceof ScriptedAutomationManager) { + entry.setValue(new ThreadsafeWrappingScriptedAutomationManagerDelegate( + (ScriptedAutomationManager) entry.getValue())); + } + } + + return rv; + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/DelegatingFileSystem.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/DelegatingFileSystem.java new file mode 100644 index 00000000000..7d1c05f3639 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/DelegatingFileSystem.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal.fs; + +import java.io.IOException; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.AccessMode; +import java.nio.file.DirectoryStream; +import java.nio.file.LinkOption; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.spi.FileSystemProvider; +import java.util.Map; +import java.util.Set; + +import org.graalvm.polyglot.io.FileSystem; + +/** + * Delegate wrapping a {@link FileSystem} + * + * @author Jonathan Gilbert - Initial contribution + */ +public class DelegatingFileSystem implements FileSystem { + private FileSystemProvider delegate; + + public DelegatingFileSystem(FileSystemProvider delegate) { + this.delegate = delegate; + } + + @Override + public Path parsePath(URI uri) { + return Paths.get(uri); + } + + @Override + public Path parsePath(String path) { + return Paths.get(path); + } + + @Override + public void checkAccess(Path path, Set modes, LinkOption... linkOptions) throws IOException { + delegate.checkAccess(path, modes.toArray(new AccessMode[0])); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + delegate.createDirectory(dir, attrs); + } + + @Override + public void delete(Path path) throws IOException { + delegate.delete(path); + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) + throws IOException { + return delegate.newByteChannel(path, options, attrs); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) + throws IOException { + return delegate.newDirectoryStream(dir, filter); + } + + @Override + public Path toAbsolutePath(Path path) { + return path.toAbsolutePath(); + } + + @Override + public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException { + return path.toRealPath(linkOptions); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + return delegate.readAttributes(path, attributes, options); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/PrefixedSeekableByteChannel.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/PrefixedSeekableByteChannel.java new file mode 100644 index 00000000000..2bd98ca8580 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/PrefixedSeekableByteChannel.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal.fs; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.util.Arrays; + +/** + * Wrapper for a {@link SeekableByteChannel} allowing prefixing the stream with a fixed array of bytes + * + * @author Jonathan Gilbert - Initial contribution + */ +public class PrefixedSeekableByteChannel implements SeekableByteChannel { + + private byte[] prefix; + private SeekableByteChannel source; + private long position; + + public PrefixedSeekableByteChannel(byte[] prefix, SeekableByteChannel source) { + this.prefix = prefix; + this.source = source; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + + int read = 0; + + if (position < prefix.length) { + dst.put(Arrays.copyOfRange(prefix, (int) position, prefix.length)); + read += prefix.length - position; + } + + read += source.read(dst); + + position += read; + + return read; + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw new IOException("Read only!"); + } + + @Override + public long position() throws IOException { + return position; + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + + this.position = newPosition; + + if (newPosition > prefix.length) { + source.position(newPosition - prefix.length); + } + + return this; + } + + @Override + public long size() throws IOException { + return source.size() + prefix.length; + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + throw new IOException("Read only!"); + } + + @Override + public boolean isOpen() { + return source.isOpen(); + } + + @Override + public void close() throws IOException { + source.close(); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/DelegatingScriptEngineWithInvocable.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/DelegatingScriptEngineWithInvocable.java new file mode 100644 index 00000000000..a9a9be39285 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/DelegatingScriptEngineWithInvocable.java @@ -0,0 +1,128 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal.scriptengine; + +import java.io.Reader; + +import javax.script.Bindings; +import javax.script.Invocable; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; + +/** + * {@link ScriptEngine} implementation that delegates to a supplied ScriptEngine instance. Allows overriding specific + * methods. + * + * @author Jonathan Gilbert - Initial contribution + */ +public abstract class DelegatingScriptEngineWithInvocable + implements ScriptEngine, Invocable { + protected T delegate; + + public DelegatingScriptEngineWithInvocable(T delegate) { + this.delegate = delegate; + } + + @Override + public Object eval(String s, ScriptContext scriptContext) throws ScriptException { + return delegate.eval(s, scriptContext); + } + + @Override + public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException { + return delegate.eval(reader, scriptContext); + } + + @Override + public Object eval(String s) throws ScriptException { + return delegate.eval(s); + } + + @Override + public Object eval(Reader reader) throws ScriptException { + return delegate.eval(reader); + } + + @Override + public Object eval(String s, Bindings bindings) throws ScriptException { + return delegate.eval(s, bindings); + } + + @Override + public Object eval(Reader reader, Bindings bindings) throws ScriptException { + return delegate.eval(reader, bindings); + } + + @Override + public void put(String s, Object o) { + delegate.put(s, o); + } + + @Override + public Object get(String s) { + return delegate.get(s); + } + + @Override + public Bindings getBindings(int i) { + return delegate.getBindings(i); + } + + @Override + public void setBindings(Bindings bindings, int i) { + delegate.setBindings(bindings, i); + } + + @Override + public Bindings createBindings() { + return delegate.createBindings(); + } + + @Override + public ScriptContext getContext() { + return delegate.getContext(); + } + + @Override + public void setContext(ScriptContext scriptContext) { + delegate.setContext(scriptContext); + } + + @Override + public ScriptEngineFactory getFactory() { + return delegate.getFactory(); + } + + @Override + public Object invokeMethod(Object o, String s, Object... objects) throws ScriptException, NoSuchMethodException { + return delegate.invokeMethod(o, s, objects); + } + + @Override + public Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException { + return delegate.invokeFunction(s, objects); + } + + @Override + public T getInterface(Class aClass) { + return delegate.getInterface(aClass); + } + + @Override + public T getInterface(Object o, Class aClass) { + return delegate.getInterface(o, aClass); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/InvocationInterceptingScriptEngineWithInvocable.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/InvocationInterceptingScriptEngineWithInvocable.java new file mode 100644 index 00000000000..082a98dde15 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/InvocationInterceptingScriptEngineWithInvocable.java @@ -0,0 +1,127 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal.scriptengine; + +import java.io.Reader; + +import javax.script.Bindings; +import javax.script.Invocable; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptException; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Delegate allowing AOP-style interception of calls, either before Invocation, or upon a {@link ScriptException}. + * being thrown. + * + * @param The delegate class + * @author Jonathan Gilbert - Initial contribution + */ +@NonNullByDefault +public abstract class InvocationInterceptingScriptEngineWithInvocable + extends DelegatingScriptEngineWithInvocable { + + public InvocationInterceptingScriptEngineWithInvocable(T delegate) { + super(delegate); + } + + protected void beforeInvocation() { + } + + protected ScriptException afterThrowsInvocation(ScriptException se) { + return se; + } + + @Override + public Object eval(String s, ScriptContext scriptContext) throws ScriptException { + try { + beforeInvocation(); + return super.eval(s, scriptContext); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException { + try { + beforeInvocation(); + return super.eval(reader, scriptContext); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(String s) throws ScriptException { + try { + beforeInvocation(); + return super.eval(s); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(Reader reader) throws ScriptException { + try { + beforeInvocation(); + return super.eval(reader); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(String s, Bindings bindings) throws ScriptException { + try { + beforeInvocation(); + return super.eval(s, bindings); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(Reader reader, Bindings bindings) throws ScriptException { + try { + beforeInvocation(); + return super.eval(reader, bindings); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object invokeMethod(Object o, String s, Object... objects) throws ScriptException, NoSuchMethodException { + try { + beforeInvocation(); + return super.invokeMethod(o, s, objects); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException { + try { + beforeInvocation(); + return super.invokeFunction(s, objects); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeSimpleRuleDelegate.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeSimpleRuleDelegate.java new file mode 100644 index 00000000000..e7f4e0b3bc1 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeSimpleRuleDelegate.java @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal.threading; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRuleActionHandler; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.Configuration; + +/** + * An version of {@link SimpleRule} which controls multithreaded execution access to this specific rule. This is useful + * for rules which wrap GraalJS Contexts, which are not multithreaded. + * + * @author Jonathan Gilbert - Initial contribution + */ +@NonNullByDefault +class ThreadsafeSimpleRuleDelegate implements Rule, SimpleRuleActionHandler { + + private final Object lock; + private final SimpleRule delegate; + + /** + * Constructor requires a lock object and delegate to forward invocations to. + * + * @param lock rule executions will synchronize on this object + * @param delegate the delegate to forward invocations to + */ + ThreadsafeSimpleRuleDelegate(Object lock, SimpleRule delegate) { + this.lock = lock; + this.delegate = delegate; + } + + @Override + @NonNullByDefault({}) + public Object execute(Action module, Map inputs) { + synchronized (lock) { + return delegate.execute(module, inputs); + } + } + + @Override + public String getUID() { + return delegate.getUID(); + } + + @Override + @Nullable + public String getTemplateUID() { + return delegate.getTemplateUID(); + } + + public void setTemplateUID(@Nullable String templateUID) { + delegate.setTemplateUID(templateUID); + } + + @Override + @Nullable + public String getName() { + return delegate.getName(); + } + + public void setName(@Nullable String ruleName) { + delegate.setName(ruleName); + } + + @Override + public Set getTags() { + return delegate.getTags(); + } + + public void setTags(@Nullable Set ruleTags) { + delegate.setTags(ruleTags); + } + + @Override + @Nullable + public String getDescription() { + return delegate.getDescription(); + } + + public void setDescription(@Nullable String ruleDescription) { + delegate.setDescription(ruleDescription); + } + + @Override + public Visibility getVisibility() { + return delegate.getVisibility(); + } + + public void setVisibility(@Nullable Visibility visibility) { + delegate.setVisibility(visibility); + } + + @Override + public Configuration getConfiguration() { + return delegate.getConfiguration(); + } + + public void setConfiguration(@Nullable Configuration ruleConfiguration) { + delegate.setConfiguration(ruleConfiguration); + } + + @Override + public List getConfigurationDescriptions() { + return delegate.getConfigurationDescriptions(); + } + + public void setConfigurationDescriptions(@Nullable List configDescriptions) { + delegate.setConfigurationDescriptions(configDescriptions); + } + + @Override + public List getConditions() { + return delegate.getConditions(); + } + + public void setConditions(@Nullable List conditions) { + delegate.setConditions(conditions); + } + + @Override + public List getActions() { + return delegate.getActions(); + } + + @Override + public List getTriggers() { + return delegate.getTriggers(); + } + + public void setActions(@Nullable List actions) { + delegate.setActions(actions); + } + + public void setTriggers(@Nullable List triggers) { + delegate.setTriggers(triggers); + } + + @Override + public List getModules() { + return delegate.getModules(); + } + + public List getModules(@Nullable Class moduleClazz) { + return delegate.getModules(moduleClazz); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + return delegate.equals(obj); + } + + @Override + @Nullable + public Module getModule(String moduleId) { + return delegate.getModule(moduleId); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeWrappingScriptedAutomationManagerDelegate.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeWrappingScriptedAutomationManagerDelegate.java new file mode 100644 index 00000000000..a5ea8dc46cb --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeWrappingScriptedAutomationManagerDelegate.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2021 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.automation.jsscripting.internal.threading; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager; +import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleConditionHandler; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandler; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.TriggerType; + +/** + * A replacement for {@link ScriptedAutomationManager} which wraps all rule registrations in a + * {@link ThreadsafeSimpleRuleDelegate}. This means that all rules registered via this class with be run in serial per + * instance of this class that they are registered with. + * + * @author Jonathan Gilbert - Initial contribution + */ +@NonNullByDefault +public class ThreadsafeWrappingScriptedAutomationManagerDelegate { + + private ScriptedAutomationManager delegate; + private Object lock = new Object(); + + public ThreadsafeWrappingScriptedAutomationManagerDelegate(ScriptedAutomationManager delegate) { + this.delegate = delegate; + } + + public void removeModuleType(String UID) { + delegate.removeModuleType(UID); + } + + public void removeHandler(String typeUID) { + delegate.removeHandler(typeUID); + } + + public void removePrivateHandler(String privId) { + delegate.removePrivateHandler(privId); + } + + public void removeAll() { + delegate.removeAll(); + } + + public Rule addRule(Rule element) { + // wrap in a threadsafe version, safe per context + if (element instanceof SimpleRule) { + element = new ThreadsafeSimpleRuleDelegate(lock, (SimpleRule) element); + } + + return delegate.addRule(element); + } + + public void addConditionType(ConditionType condititonType) { + delegate.addConditionType(condititonType); + } + + public void addConditionHandler(String uid, ScriptedHandler conditionHandler) { + delegate.addConditionHandler(uid, conditionHandler); + } + + public String addPrivateConditionHandler(SimpleConditionHandler conditionHandler) { + return delegate.addPrivateConditionHandler(conditionHandler); + } + + public void addActionType(ActionType actionType) { + delegate.addActionType(actionType); + } + + public void addActionHandler(String uid, ScriptedHandler actionHandler) { + delegate.addActionHandler(uid, actionHandler); + } + + public String addPrivateActionHandler(SimpleActionHandler actionHandler) { + return delegate.addPrivateActionHandler(actionHandler); + } + + public void addTriggerType(TriggerType triggerType) { + delegate.addTriggerType(triggerType); + } + + public void addTriggerHandler(String uid, ScriptedHandler triggerHandler) { + delegate.addTriggerHandler(uid, triggerHandler); + } + + public String addPrivateTriggerHandler(SimpleTriggerHandler triggerHandler) { + return delegate.addPrivateTriggerHandler(triggerHandler); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/resources/META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider b/bundles/org.openhab.automation.jsscripting/src/main/resources/META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider new file mode 100755 index 00000000000..1fb86e4d04b --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/resources/META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider @@ -0,0 +1,2 @@ +com.oracle.truffle.regex.RegexLanguageProvider +com.oracle.truffle.js.lang.JavaScriptLanguageProvider diff --git a/bundles/pom.xml b/bundles/pom.xml index f1bf230403a..29fe4b9adc9 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -21,6 +21,7 @@ org.openhab.automation.groovyscripting org.openhab.automation.jythonscripting org.openhab.automation.pidcontroller + org.openhab.automation.jsscripting org.openhab.io.homekit org.openhab.io.hueemulation