]> git.basschouten.com Git - openhab-addons.git/commitdiff
[jsscripting] ES6+ Support (#8516)
authorJonathan Gilbert <github.10.jgilbert@xoxy.net>
Thu, 6 May 2021 07:08:47 +0000 (17:08 +1000)
committerGitHub <noreply@github.com>
Thu, 6 May 2021 07:08:47 +0000 (09:08 +0200)
Signed-off-by: Jonathan Gilbert <jpg@trillica.com>
21 files changed:
CODEOWNERS
bom/openhab-addons/pom.xml
bundles/org.openhab.automation.jsscripting/NOTICE [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/README.md [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/bnd.bnd [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/pom.xml [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/DelegatingFileSystem.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/PrefixedSeekableByteChannel.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/DelegatingScriptEngineWithInvocable.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/InvocationInterceptingScriptEngineWithInvocable.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeSimpleRuleDelegate.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeWrappingScriptedAutomationManagerDelegate.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/resources/META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider [new file with mode: 0755]
bundles/pom.xml

index 3a9dcf65fe61dcd4038022c9b695d031a860615e..0b3707515fbf9a1ea4ff13d2b07d97d4d49b99a5 100644 (file)
@@ -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
index 82397f5e9ebae07d9712e8741d87b1dfc3275de8..3485b385073aa9769ab151608003c19d715ead9a 100644 (file)
       <artifactId>org.openhab.automation.pidcontroller</artifactId>
       <version>${project.version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.openhab.addons.bundles</groupId>
+      <artifactId>org.openhab.automation.jsscripting</artifactId>
+      <version>${project.version}</version>
+    </dependency>
     <dependency>
       <groupId>org.openhab.addons.bundles</groupId>
       <artifactId>org.openhab.binding.adorne</artifactId>
diff --git a/bundles/org.openhab.automation.jsscripting/NOTICE b/bundles/org.openhab.automation.jsscripting/NOTICE
new file mode 100644 (file)
index 0000000..38d625e
--- /dev/null
@@ -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 (file)
index 0000000..23b1dcd
--- /dev/null
@@ -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 (file)
index 0000000..47c2bad
--- /dev/null
@@ -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 (file)
index 0000000..ab549b2
--- /dev/null
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.openhab.addons.bundles</groupId>
+    <artifactId>org.openhab.addons.reactor.bundles</artifactId>
+    <version>3.1.0-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>org.openhab.automation.jsscripting</artifactId>
+
+  <name>openHAB Add-ons :: Bundles :: Automation :: JSScripting</name>
+
+  <properties>
+    <bnd.importpackage>
+      !sun.misc.*,
+      !sun.reflect.*,
+      !com.sun.management.*,
+      !jdk.internal.reflect.*,
+      !jdk.vm.ci.services
+    </bnd.importpackage>
+    <graal.version>20.1.0</graal.version>
+    <asm.version>6.2.1</asm.version>
+    <oh.version>${project.version}</oh.version>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>embed-dependencies</id>
+            <goals>
+              <goal>unpack-dependencies</goal>
+            </goals>
+            <configuration>
+              <excludes>META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider</excludes> <!-- we'll provide this -->
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.graalvm.truffle</groupId>
+      <artifactId>truffle-api</artifactId>
+      <version>${graal.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.graalvm.js</groupId>
+      <artifactId>js-scriptengine</artifactId>
+      <version>${graal.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.graalvm.js</groupId>
+      <artifactId>js-launcher</artifactId>
+      <version>${graal.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.graalvm.sdk</groupId>
+      <artifactId>graal-sdk</artifactId>
+      <version>${graal.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.graalvm.regex</groupId>
+      <artifactId>regex</artifactId>
+      <version>${graal.version}</version>
+    </dependency>
+    <dependency> <!-- this must come AFTER the regex lib -->
+      <groupId>org.graalvm.js</groupId>
+      <artifactId>js</artifactId>
+      <version>${graal.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>com.ibm.icu</groupId>
+      <artifactId>icu4j</artifactId>
+      <version>62.1</version>
+    </dependency>
+
+    <!-- include as version required is older than OH provides -->
+    <dependency>
+      <groupId>org.ow2.asm</groupId>
+      <artifactId>asm</artifactId>
+      <version>${asm.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.ow2.asm</groupId>
+      <artifactId>asm-commons</artifactId>
+      <version>${asm.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.ow2.asm</groupId>
+      <artifactId>asm-tree</artifactId>
+      <version>${asm.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.ow2.asm</groupId>
+      <artifactId>asm-util</artifactId>
+      <version>${asm.version}</version>
+    </dependency>
+    <dependency>
+      <groupId>org.ow2.asm</groupId>
+      <artifactId>asm-analysis</artifactId>
+      <version>${asm.version}</version>
+    </dependency>
+  </dependencies>
+</project>
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 (file)
index 0000000..31760e9
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<features name="org.openhab.automation.jsscripting-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
+       <repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
+
+       <feature name="openhab-automation-jsscripting" description="JSScripting" version="${project.version}">
+               <feature>openhab-runtime-base</feature>
+               <bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.automation.jsscripting/${project.version}</bundle>
+       </feature>
+</features>
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 (file)
index 0000000..d3a1623
--- /dev/null
@@ -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 (file)
index 0000000..0f21f45
--- /dev/null
@@ -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<T extends ScriptEngine & Invocable>
+        extends InvocationInterceptingScriptEngineWithInvocable<T> {
+
+    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 (file)
index 0000000..14e9f78
--- /dev/null
@@ -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<String> getScriptTypes() {
+        List<String> 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<String, Object> 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 (file)
index 0000000..13e817a
--- /dev/null
@@ -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<Value> 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 (file)
index 0000000..8541eb7
--- /dev/null
@@ -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<GraalJSScriptEngine> {
+
+    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<String> 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<? extends OpenOption> 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<String>) 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<Object[], Object>, Function<String, Object>> 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<Object[], Object>) 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 (file)
index 0000000..d2d1401
--- /dev/null
@@ -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<Value> runtimeModule(String name, String scriptIdentifier, Context ctx) {
+
+        Map<String, Object> 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<String, Object> 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" + "})",
+                    "<generated>").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<String, Object> processValues(Map<String, Object> values) {
+        Map<String, Object> rv = new HashMap<>(values);
+
+        for (Map.Entry<String, Object> 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 (file)
index 0000000..7d1c05f
--- /dev/null
@@ -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<? extends AccessMode> 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<? extends OpenOption> options, FileAttribute<?>... attrs)
+            throws IOException {
+        return delegate.newByteChannel(path, options, attrs);
+    }
+
+    @Override
+    public DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> 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<String, Object> 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 (file)
index 0000000..2bd98ca
--- /dev/null
@@ -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 (file)
index 0000000..a9a9be3
--- /dev/null
@@ -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<T extends ScriptEngine & Invocable>
+        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> T getInterface(Class<T> aClass) {
+        return delegate.getInterface(aClass);
+    }
+
+    @Override
+    public <T> T getInterface(Object o, Class<T> 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 (file)
index 0000000..082a98d
--- /dev/null
@@ -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 <T> The delegate class
+ * @author Jonathan Gilbert - Initial contribution
+ */
+@NonNullByDefault
+public abstract class InvocationInterceptingScriptEngineWithInvocable<T extends ScriptEngine & Invocable>
+        extends DelegatingScriptEngineWithInvocable<T> {
+
+    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 (file)
index 0000000..e7f4e0b
--- /dev/null
@@ -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<String, ?> 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<String> getTags() {
+        return delegate.getTags();
+    }
+
+    public void setTags(@Nullable Set<String> 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<ConfigDescriptionParameter> getConfigurationDescriptions() {
+        return delegate.getConfigurationDescriptions();
+    }
+
+    public void setConfigurationDescriptions(@Nullable List<ConfigDescriptionParameter> configDescriptions) {
+        delegate.setConfigurationDescriptions(configDescriptions);
+    }
+
+    @Override
+    public List<Condition> getConditions() {
+        return delegate.getConditions();
+    }
+
+    public void setConditions(@Nullable List<Condition> conditions) {
+        delegate.setConditions(conditions);
+    }
+
+    @Override
+    public List<Action> getActions() {
+        return delegate.getActions();
+    }
+
+    @Override
+    public List<Trigger> getTriggers() {
+        return delegate.getTriggers();
+    }
+
+    public void setActions(@Nullable List<Action> actions) {
+        delegate.setActions(actions);
+    }
+
+    public void setTriggers(@Nullable List<Trigger> triggers) {
+        delegate.setTriggers(triggers);
+    }
+
+    @Override
+    public List<Module> getModules() {
+        return delegate.getModules();
+    }
+
+    public <T extends Module> List<T> getModules(@Nullable Class<T> 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 (file)
index 0000000..a5ea8dc
--- /dev/null
@@ -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 (executable)
index 0000000..1fb86e4
--- /dev/null
@@ -0,0 +1,2 @@
+com.oracle.truffle.regex.RegexLanguageProvider
+com.oracle.truffle.js.lang.JavaScriptLanguageProvider
index f1bf230403ac0f96c8281eb6a5563b4c825f32a1..29fe4b9adc98fd9d4e1d7f629ebbfad29ec7d467 100644 (file)
@@ -21,6 +21,7 @@
     <module>org.openhab.automation.groovyscripting</module>
     <module>org.openhab.automation.jythonscripting</module>
     <module>org.openhab.automation.pidcontroller</module>
+    <module>org.openhab.automation.jsscripting</module>
     <!-- io -->
     <module>org.openhab.io.homekit</module>
     <module>org.openhab.io.hueemulation</module>