# 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
<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>
--- /dev/null
+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
--- /dev/null
+# 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!
+```
--- /dev/null
+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
--- /dev/null
+<?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>
--- /dev/null
+<?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>
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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;
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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();
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+com.oracle.truffle.regex.RegexLanguageProvider
+com.oracle.truffle.js.lang.JavaScriptLanguageProvider
<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>