diff --git a/CODEOWNERS b/CODEOWNERS index 96f873cd0c5c0..666bd08184ced 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -7,6 +7,7 @@ # Add-on maintainers: /bundles/org.openhab.automation.groovyscripting/ @wborn /bundles/org.openhab.automation.jythonscripting/ @openhab/add-ons-maintainers +/bundles/org.openhab.automation.jsscripting/ @jpg0 /bundles/org.openhab.binding.adorne/ @theiding /bundles/org.openhab.binding.airquality/ @kubawolanin /bundles/org.openhab.binding.airvisualnode/ @3cky diff --git a/bundles/org.openhab.automation.jsscripting/.classpath b/bundles/org.openhab.automation.jsscripting/.classpath new file mode 100644 index 0000000000000..a5d95095ccaaf --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bundles/org.openhab.automation.jsscripting/.project b/bundles/org.openhab.automation.jsscripting/.project new file mode 100644 index 0000000000000..277326b68bb9c --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/.project @@ -0,0 +1,23 @@ + + + org.openhab.automation.jsscripting + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/bundles/org.openhab.automation.jsscripting/NOTICE b/bundles/org.openhab.automation.jsscripting/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/NOTICE @@ -0,0 +1,13 @@ +This content is produced and maintained by the openHAB project. + +* Project home: https://www.openhab.org + +== Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Eclipse Public License 2.0 which is available at +https://www.eclipse.org/legal/epl-2.0/. + +== Source Code + +https://github.com/openhab/openhab-addons diff --git a/bundles/org.openhab.automation.jsscripting/README.md b/bundles/org.openhab.automation.jsscripting/README.md new file mode 100644 index 0000000000000..51b2f237c80a0 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/README.md @@ -0,0 +1,3 @@ +# JSScripting + +TODO \ No newline at end of file diff --git a/bundles/org.openhab.automation.jsscripting/bnd.bnd b/bundles/org.openhab.automation.jsscripting/bnd.bnd new file mode 100644 index 0000000000000..47c2bad3c8d92 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/bnd.bnd @@ -0,0 +1,13 @@ +Bundle-SymbolicName: ${project.artifactId} +DynamicImport-Package: * +Import-Package: org.openhab.core.automation.module.script,javax.management,javax.script,javax.xml.datatype,javax.xml.stream;version="[1.0,2)",org.osgi.framework;version="[1.8,2)",org.slf4j;version="[1.7,2)" +Require-Capability: osgi.extender; + filter:="(osgi.extender=osgi.serviceloader.processor)", + osgi.serviceloader; + filter:="(osgi.serviceloader=org.graalvm.polyglot.impl.AbstractPolyglotImpl)"; + cardinality:=multiple + +SPI-Provider: * +SPI-Consumer: * + +-fixupmessages "Classes found in the wrong directory"; restrict:=error; is:=warning diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml new file mode 100644 index 0000000000000..61b1912a72a1b --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/pom.xml @@ -0,0 +1,140 @@ + + + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.automation.jsscripting + + openHAB Add-ons :: Bundles :: Automation :: JSScripting + + + + !sun.misc.*, + !sun.reflect.*, + !com.sun.management.*, + !jdk.internal.reflect.*, + !jdk.vm.ci.services + + 20.1.0 + 6.2.1 + ${project.version} + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + embed-dependencies + + unpack-dependencies + + + META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider + + + + + + + + + + org.graalvm.truffle + truffle-api + ${graal.version} + + + org.graalvm.js + js-scriptengine + ${graal.version} + + + org.graalvm.js + js-launcher + ${graal.version} + + + org.graalvm.sdk + graal-sdk + ${graal.version} + + + org.graalvm.regex + regex + ${graal.version} + + + org.graalvm.js + js + ${graal.version} + + + com.ibm.icu + icu4j + 62.1 + + + + + org.ow2.asm + asm + ${asm.version} + + + org.ow2.asm + asm-commons + ${asm.version} + + + org.ow2.asm + asm-tree + ${asm.version} + + + org.ow2.asm + asm-util + ${asm.version} + + + org.ow2.asm + asm-analysis + ${asm.version} + + + + org.openhab.core.bom + org.openhab.core.bom.compile + pom + provided + + + org.openhab.core.bom + org.openhab.core.bom.openhab-core + pom + provided + + + diff --git a/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml b/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml new file mode 100644 index 0000000000000..1c046da270965 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/feature/feature.xml @@ -0,0 +1,22 @@ + + + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.automation.jsscripting/${project.version} + + diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java new file mode 100644 index 0000000000000..836b8b931b405 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/ClassExtender.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jsscripting; + +import com.oracle.truffle.js.runtime.java.adapter.JavaAdapterFactory; + +/** + * Class utility to allow creation of 'extendable' classes with a classloader of the GraalJS bundle, rather than the + * classloader of the file being extended. + * + * @author Jonathan Gilbert - Initial contribution + */ +public class ClassExtender { + private static ClassLoader classLoader = ClassExtender.class.getClassLoader(); + + public static Object extend(String className) { + try { + return extend(Class.forName(className)); + } catch (ClassNotFoundException e) { + throw new RuntimeException("Cannot find class " + className, e); + } + } + + public static Object extend(Class clazz) { + return JavaAdapterFactory.getAdapterClassFor(clazz, null, classLoader); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java new file mode 100644 index 0000000000000..7e094d7348a94 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/DebuggingGraalScriptEngine.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.automation.jsscripting.internal; + +import javax.script.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.graalvm.polyglot.PolyglotException; +import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps ScriptEngines provided by Graal to provide error messages and stack traces for scripts. + * + * @author Jonathan Gilbert - Initial contribution + */ +@NonNullByDefault +class DebuggingGraalScriptEngine + extends InvocationInterceptingScriptEngineWithInvocable { + + private static final Logger stackLogger = LoggerFactory.getLogger("org.openhab.automation.script.javascript.stack"); + + public DebuggingGraalScriptEngine(T delegate) { + super(delegate); + } + + @Override + public ScriptException afterThrowsInvocation(ScriptException se) { + Throwable cause = se.getCause(); + if (cause instanceof PolyglotException) { + stackLogger.error("Failed to execute script:", cause); + } + return se; + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java new file mode 100644 index 0000000000000..2a5682636a94d --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jsscripting.internal; + +import java.util.*; + +import javax.script.ScriptEngine; + +import org.openhab.core.automation.module.script.ScriptEngineFactory; +import org.osgi.service.component.annotations.Component; + +import com.oracle.truffle.js.scriptengine.GraalJSEngineFactory; + +/** + * An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines. + * + * @author Jonathan Gilbert - Initial contribution + */ +@Component(service = ScriptEngineFactory.class) +public final class GraalJSScriptEngineFactory implements ScriptEngineFactory { + + @Override + public List getScriptTypes() { + List scriptTypes = new ArrayList<>(); + GraalJSEngineFactory graalJSEngineFactory = new GraalJSEngineFactory(); + + scriptTypes.addAll(graalJSEngineFactory.getMimeTypes()); + scriptTypes.addAll(graalJSEngineFactory.getExtensions()); + + return Collections.unmodifiableList(scriptTypes); + } + + @Override + public void scopeValues(ScriptEngine scriptEngine, Map scopeValues) { + // noop; the are retrieved via modules, not injected + } + + @Override + public ScriptEngine createScriptEngine(String scriptType) { + OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine(); + return new DebuggingGraalScriptEngine<>(engine); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java new file mode 100644 index 0000000000000..ec54afb7df085 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ModuleLocator.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.automation.jsscripting.internal; + +import java.util.Optional; + +import org.graalvm.polyglot.Value; + +/** + * Locates modules from a module name + * + * @author Jonathan Gilbert - Initial contribution + */ +public interface ModuleLocator { + Optional locateModule(String name); +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java new file mode 100644 index 0000000000000..39623c50a8c75 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java @@ -0,0 +1,132 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.automation.jsscripting.internal; + +import static org.openhab.core.automation.module.script.ScriptEngineFactory.*; + +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.*; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.graalvm.polyglot.Context; +import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem; +import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel; +import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable; +import org.openhab.core.OpenHAB; +import org.openhab.core.automation.module.script.ScriptExtensionAccessor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; + +/** + * GraalJS Script Engine implementation + * + * @author Jonathan Gilbert - Initial contribution + */ +public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable { + + private static final Logger logger = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class); + + private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__"; + private static final String MODULE_DIR = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib", + "javascript", "personal"); + + // these fields start as null because they are populated on first use + @NonNullByDefault({}) + private String engineIdentifier; + @NonNullByDefault({}) + private Consumer scriptDependencyListener; + + private boolean initialized = false; + + /** + * Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script + * lifecycle and provides hooks for scripts to do so too. + */ + public OpenhabGraalJSScriptEngine() { + super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately + delegate = GraalJSScriptEngine.create(null, + Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true) + .option("js.commonjs-require-cwd", MODULE_DIR).option("js.nashorn-compat", "true") // to ease + // migration + .option("js.commonjs-require", "true") // enable CommonJS module support + .fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) { + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, + FileAttribute... attrs) throws IOException { + if (scriptDependencyListener != null) { + scriptDependencyListener.accept(path.toString()); + } + + if (path.toString().endsWith(".js")) { + return new PrefixedSeekableByteChannel( + ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(), + super.newByteChannel(path, options, attrs)); + } else { + return super.newByteChannel(path, options, attrs); + } + } + })); + } + + @Override + protected void beforeInvocation() { + if (initialized) + return; + + ScriptContext ctx = delegate.getContext(); + + // these are added post-construction, so we need to fetch them late + this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER); + if (this.engineIdentifier == null) { + throw new IllegalStateException("Failed to retrieve engine identifier from engine bindings"); + } + + ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx + .getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR); + if (scriptExtensionAccessor == null) { + throw new IllegalStateException("Failed to retrieve script extension accessor from engine bindings"); + } + + scriptDependencyListener = (Consumer) ctx + .getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */); + if (scriptDependencyListener == null) { + logger.warn( + "Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled."); + } + + ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider( + scriptExtensionAccessor); + + Function, Function> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider + .locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName) + .map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName })); + + delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn); + delegate.put("require", wrapRequireFn.apply((Function) delegate.get("require"))); + + initialized = true; + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java new file mode 100644 index 0000000000000..8e968338340f5 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/ScriptExtensionModuleProvider.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jsscripting.internal; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; +import org.openhab.automation.jsscripting.internal.threading.ThreadsafeWrappingScriptedAutomationManagerDelegate; +import org.openhab.core.automation.module.script.ScriptExtensionAccessor; +import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager; + +/** + * Class providing script extensions via CommonJS modules. + * + * @author Jonathan Gilbert - Initial contribution + */ + +@NonNullByDefault +public class ScriptExtensionModuleProvider { + + private static final String RUNTIME_MODULE_PREFIX = "@runtime"; + private static final String DEFAULT_MODULE_NAME = "Defaults"; + + private final ScriptExtensionAccessor scriptExtensionAccessor; + + public ScriptExtensionModuleProvider(ScriptExtensionAccessor scriptExtensionAccessor) { + this.scriptExtensionAccessor = scriptExtensionAccessor; + } + + public ModuleLocator locatorFor(Context ctx, String engineIdentifier) { + return name -> { + String[] segments = name.split("/"); + if (segments[0].equals(RUNTIME_MODULE_PREFIX)) { + if (segments.length == 1) { + return runtimeModule(DEFAULT_MODULE_NAME, engineIdentifier, ctx); + } else { + return runtimeModule(segments[1], engineIdentifier, ctx); + } + } + + return Optional.empty(); + }; + } + + private Optional runtimeModule(String name, String scriptIdentifier, Context ctx) { + + Map symbols; + + if (DEFAULT_MODULE_NAME.equals(name)) { + symbols = scriptExtensionAccessor.findDefaultPresets(scriptIdentifier); + } else { + symbols = scriptExtensionAccessor.findPreset(name, scriptIdentifier); + } + + return Optional.of(symbols).map(this::processValues).map(v -> toValue(ctx, v)); + } + + private Value toValue(Context ctx, Map map) { + try { + return ctx.eval(Source.newBuilder( // convert to Map to JS Object + "js", + "(function (mapOfValues) {\n" + "let rv = {};\n" + "for (var key in mapOfValues) {\n" + + " rv[key] = mapOfValues.get(key);\n" + "}\n" + "return rv;\n" + "})", + "").build()).execute(map); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to generate exports", e); + } + } + + /** + * Some specific objects need wrapping when exposed to a GraalJS environment. This method does this. + * + * @param values the map of names to values of things to process + * @return a map of the processed keys and values + */ + private Map processValues(Map values) { + Map rv = new HashMap<>(values); + + for (Map.Entry entry : rv.entrySet()) { + if (entry.getValue() instanceof ScriptedAutomationManager) { + entry.setValue(new ThreadsafeWrappingScriptedAutomationManagerDelegate( + (ScriptedAutomationManager) entry.getValue())); + } + } + + return rv; + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/DelegatingFileSystem.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/DelegatingFileSystem.java new file mode 100644 index 0000000000000..1a7c8fc669d26 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/DelegatingFileSystem.java @@ -0,0 +1,90 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.automation.jsscripting.internal.fs; + +import java.io.IOException; +import java.net.URI; +import java.nio.channels.SeekableByteChannel; +import java.nio.file.*; +import java.nio.file.attribute.FileAttribute; +import java.nio.file.spi.FileSystemProvider; +import java.util.Map; +import java.util.Set; + +import org.graalvm.polyglot.io.FileSystem; + +/** + * Delegate wrapping a {@link FileSystem} + * + * @author Jonathan Gilbert - Initial contribution + */ +public class DelegatingFileSystem implements FileSystem { + private FileSystemProvider delegate; + + public DelegatingFileSystem(FileSystemProvider delegate) { + this.delegate = delegate; + } + + @Override + public Path parsePath(URI uri) { + return Paths.get(uri); + } + + @Override + public Path parsePath(String path) { + return Paths.get(path); + } + + @Override + public void checkAccess(Path path, Set modes, LinkOption... linkOptions) throws IOException { + delegate.checkAccess(path, modes.toArray(new AccessMode[0])); + } + + @Override + public void createDirectory(Path dir, FileAttribute... attrs) throws IOException { + delegate.createDirectory(dir, attrs); + } + + @Override + public void delete(Path path) throws IOException { + delegate.delete(path); + } + + @Override + public SeekableByteChannel newByteChannel(Path path, Set options, FileAttribute... attrs) + throws IOException { + return delegate.newByteChannel(path, options, attrs); + } + + @Override + public DirectoryStream newDirectoryStream(Path dir, DirectoryStream.Filter filter) + throws IOException { + return delegate.newDirectoryStream(dir, filter); + } + + @Override + public Path toAbsolutePath(Path path) { + return path.toAbsolutePath(); + } + + @Override + public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException { + return path.toRealPath(linkOptions); + } + + @Override + public Map readAttributes(Path path, String attributes, LinkOption... options) throws IOException { + return delegate.readAttributes(path, attributes, options); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/PrefixedSeekableByteChannel.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/PrefixedSeekableByteChannel.java new file mode 100644 index 0000000000000..ca9638b5807d6 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/PrefixedSeekableByteChannel.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.automation.jsscripting.internal.fs; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.SeekableByteChannel; +import java.util.Arrays; + +/** + * Wrapper for a {@link SeekableByteChannel} allowing prefixing the stream with a fixed array of bytes + * + * @author Jonathan Gilbert - Initial contribution + */ +public class PrefixedSeekableByteChannel implements SeekableByteChannel { + + private byte[] prefix; + private SeekableByteChannel source; + private long position; + + public PrefixedSeekableByteChannel(byte[] prefix, SeekableByteChannel source) { + this.prefix = prefix; + this.source = source; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + + int read = 0; + + if (position < prefix.length) { + dst.put(Arrays.copyOfRange(prefix, (int) position, prefix.length)); + read += prefix.length - position; + } + + read += source.read(dst); + + position += read; + + return read; + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw new IOException("Read only!"); + } + + @Override + public long position() throws IOException { + return position; + } + + @Override + public SeekableByteChannel position(long newPosition) throws IOException { + + this.position = newPosition; + + if (newPosition > prefix.length) { + source.position(newPosition - prefix.length); + } + + return this; + } + + @Override + public long size() throws IOException { + return source.size() + prefix.length; + } + + @Override + public SeekableByteChannel truncate(long size) throws IOException { + throw new IOException("Read only!"); + } + + @Override + public boolean isOpen() { + return source.isOpen(); + } + + @Override + public void close() throws IOException { + source.close(); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/DelegatingScriptEngineWithInvocable.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/DelegatingScriptEngineWithInvocable.java new file mode 100644 index 0000000000000..c2e7fb6a14637 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/DelegatingScriptEngineWithInvocable.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.automation.jsscripting.internal.scriptengine; + +import java.io.Reader; + +import javax.script.*; + +/** + * {@link ScriptEngine} implementation that delegates to a supplied ScriptEngine instance. Allows overriding specific + * methods. + * + * @author Jonathan Gilbert - Initial contribution + */ +public abstract class DelegatingScriptEngineWithInvocable + implements ScriptEngine, Invocable { + protected T delegate; + + public DelegatingScriptEngineWithInvocable(T delegate) { + this.delegate = delegate; + } + + @Override + public Object eval(String s, ScriptContext scriptContext) throws ScriptException { + return delegate.eval(s, scriptContext); + } + + @Override + public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException { + return delegate.eval(reader, scriptContext); + } + + @Override + public Object eval(String s) throws ScriptException { + return delegate.eval(s); + } + + @Override + public Object eval(Reader reader) throws ScriptException { + return delegate.eval(reader); + } + + @Override + public Object eval(String s, Bindings bindings) throws ScriptException { + return delegate.eval(s, bindings); + } + + @Override + public Object eval(Reader reader, Bindings bindings) throws ScriptException { + return delegate.eval(reader, bindings); + } + + @Override + public void put(String s, Object o) { + delegate.put(s, o); + } + + @Override + public Object get(String s) { + return delegate.get(s); + } + + @Override + public Bindings getBindings(int i) { + return delegate.getBindings(i); + } + + @Override + public void setBindings(Bindings bindings, int i) { + delegate.setBindings(bindings, i); + } + + @Override + public Bindings createBindings() { + return delegate.createBindings(); + } + + @Override + public ScriptContext getContext() { + return delegate.getContext(); + } + + @Override + public void setContext(ScriptContext scriptContext) { + delegate.setContext(scriptContext); + } + + @Override + public ScriptEngineFactory getFactory() { + return delegate.getFactory(); + } + + @Override + public Object invokeMethod(Object o, String s, Object... objects) throws ScriptException, NoSuchMethodException { + return delegate.invokeMethod(o, s, objects); + } + + @Override + public Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException { + return delegate.invokeFunction(s, objects); + } + + @Override + public T getInterface(Class aClass) { + return delegate.getInterface(aClass); + } + + @Override + public T getInterface(Object o, Class aClass) { + return delegate.getInterface(o, aClass); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/InvocationInterceptingScriptEngineWithInvocable.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/InvocationInterceptingScriptEngineWithInvocable.java new file mode 100644 index 0000000000000..76e0ffe32bc45 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scriptengine/InvocationInterceptingScriptEngineWithInvocable.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.automation.jsscripting.internal.scriptengine; + +import java.io.Reader; + +import javax.script.*; + +import org.eclipse.jdt.annotation.NonNull; + +/** + * Delegate allowing AOP-style interception of calls, either before Invocation, or upon a {@link ScriptException}. + * being thrown. + * + * @author Jonathan Gilbert - Initial contribution + * + * @param The delegate class + */ +public abstract class InvocationInterceptingScriptEngineWithInvocable + extends DelegatingScriptEngineWithInvocable { + + public InvocationInterceptingScriptEngineWithInvocable(T delegate) { + super(delegate); + } + + protected void beforeInvocation() { + } + + protected @NonNull ScriptException afterThrowsInvocation(@NonNull ScriptException se) { + return se; + } + + @Override + public Object eval(String s, ScriptContext scriptContext) throws ScriptException { + try { + beforeInvocation(); + return super.eval(s, scriptContext); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(Reader reader, ScriptContext scriptContext) throws ScriptException { + try { + beforeInvocation(); + return super.eval(reader, scriptContext); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(String s) throws ScriptException { + try { + beforeInvocation(); + return super.eval(s); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(Reader reader) throws ScriptException { + try { + beforeInvocation(); + return super.eval(reader); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(String s, Bindings bindings) throws ScriptException { + try { + beforeInvocation(); + return super.eval(s, bindings); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object eval(Reader reader, Bindings bindings) throws ScriptException { + try { + beforeInvocation(); + return super.eval(reader, bindings); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object invokeMethod(Object o, String s, Object... objects) throws ScriptException, NoSuchMethodException { + try { + beforeInvocation(); + return super.invokeMethod(o, s, objects); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } + + @Override + public Object invokeFunction(String s, Object... objects) throws ScriptException, NoSuchMethodException { + try { + beforeInvocation(); + return super.invokeFunction(s, objects); + } catch (ScriptException se) { + throw afterThrowsInvocation(se); + } + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeSimpleRuleDelegate.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeSimpleRuleDelegate.java new file mode 100644 index 0000000000000..2a1a3d39da34e --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeSimpleRuleDelegate.java @@ -0,0 +1,185 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.automation.jsscripting.internal.threading; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.automation.Action; +import org.openhab.core.automation.Condition; +import org.openhab.core.automation.Module; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.Trigger; +import org.openhab.core.automation.Visibility; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRuleActionHandler; +import org.openhab.core.config.core.ConfigDescriptionParameter; +import org.openhab.core.config.core.Configuration; + +/** + * An version of {@link SimpleRule} which controls multithreaded execution access to this specific rule. This is useful + * for rules which wrap GraalJS Contexts, which are not multithreaded. + * + * @author Jonathan Gilbert - Initial contribution + */ +@NonNullByDefault +class ThreadsafeSimpleRuleDelegate implements Rule, SimpleRuleActionHandler { + + private final Object lock; + private final SimpleRule delegate; + + /** + * Constructor requires a lock object and delegate to forward invocations to. + * + * @param lock rule executions will synchronize on this object + * @param delegate the delegate to forward invocations to + */ + ThreadsafeSimpleRuleDelegate(Object lock, SimpleRule delegate) { + this.lock = lock; + this.delegate = delegate; + } + + @Override + @NonNullByDefault({}) + public Object execute(Action module, Map inputs) { + synchronized (lock) { + return delegate.execute(module, inputs); + } + } + + @Override + public String getUID() { + return delegate.getUID(); + } + + @Override + @Nullable + public String getTemplateUID() { + return delegate.getTemplateUID(); + } + + public void setTemplateUID(@Nullable String templateUID) { + delegate.setTemplateUID(templateUID); + } + + @Override + @Nullable + public String getName() { + return delegate.getName(); + } + + public void setName(@Nullable String ruleName) { + delegate.setName(ruleName); + } + + @Override + public Set getTags() { + return delegate.getTags(); + } + + public void setTags(@Nullable Set ruleTags) { + delegate.setTags(ruleTags); + } + + @Override + @Nullable + public String getDescription() { + return delegate.getDescription(); + } + + public void setDescription(@Nullable String ruleDescription) { + delegate.setDescription(ruleDescription); + } + + @Override + public Visibility getVisibility() { + return delegate.getVisibility(); + } + + public void setVisibility(@Nullable Visibility visibility) { + delegate.setVisibility(visibility); + } + + @Override + public Configuration getConfiguration() { + return delegate.getConfiguration(); + } + + public void setConfiguration(@Nullable Configuration ruleConfiguration) { + delegate.setConfiguration(ruleConfiguration); + } + + @Override + public List getConfigurationDescriptions() { + return delegate.getConfigurationDescriptions(); + } + + public void setConfigurationDescriptions(@Nullable List configDescriptions) { + delegate.setConfigurationDescriptions(configDescriptions); + } + + @Override + public List getConditions() { + return delegate.getConditions(); + } + + public void setConditions(@Nullable List conditions) { + delegate.setConditions(conditions); + } + + @Override + public List getActions() { + return delegate.getActions(); + } + + @Override + public List getTriggers() { + return delegate.getTriggers(); + } + + public void setActions(@Nullable List actions) { + delegate.setActions(actions); + } + + public void setTriggers(@Nullable List triggers) { + delegate.setTriggers(triggers); + } + + @Override + public List getModules() { + return delegate.getModules(); + } + + public List getModules(@Nullable Class moduleClazz) { + return delegate.getModules(moduleClazz); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + return delegate.equals(obj); + } + + @Override + @Nullable + public Module getModule(String moduleId) { + return delegate.getModule(moduleId); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeWrappingScriptedAutomationManagerDelegate.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeWrappingScriptedAutomationManagerDelegate.java new file mode 100644 index 0000000000000..aa1339998a214 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/threading/ThreadsafeWrappingScriptedAutomationManagerDelegate.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010-2020 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ + +package org.openhab.automation.jsscripting.internal.threading; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.automation.Rule; +import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedAutomationManager; +import org.openhab.core.automation.module.script.rulesupport.shared.ScriptedHandler; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleActionHandler; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleConditionHandler; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleRule; +import org.openhab.core.automation.module.script.rulesupport.shared.simple.SimpleTriggerHandler; +import org.openhab.core.automation.type.ActionType; +import org.openhab.core.automation.type.ConditionType; +import org.openhab.core.automation.type.TriggerType; + +/** + * A replacement for {@link ScriptedAutomationManager} which wraps all rule registrations in a + * {@link ThreadsafeSimpleRuleDelegate}. This means that all rules registered via this class with be run in serial per + * instance of this class that they are registered with. + * + * @author Jonathan Gilbert - Initial contribution + */ +@NonNullByDefault +public class ThreadsafeWrappingScriptedAutomationManagerDelegate { + + private ScriptedAutomationManager delegate; + private Object lock = new Object(); + + public ThreadsafeWrappingScriptedAutomationManagerDelegate(ScriptedAutomationManager delegate) { + this.delegate = delegate; + } + + public void removeModuleType(String UID) { + delegate.removeModuleType(UID); + } + + public void removeHandler(String typeUID) { + delegate.removeHandler(typeUID); + } + + public void removePrivateHandler(String privId) { + delegate.removePrivateHandler(privId); + } + + public void removeAll() { + delegate.removeAll(); + } + + public Rule addRule(Rule element) { + // wrap in a threadsafe version, safe per context + if (element instanceof SimpleRule) { + element = new ThreadsafeSimpleRuleDelegate(lock, (SimpleRule) element); + } + + return delegate.addRule(element); + } + + public void addConditionType(ConditionType condititonType) { + delegate.addConditionType(condititonType); + } + + public void addConditionHandler(String uid, ScriptedHandler conditionHandler) { + delegate.addConditionHandler(uid, conditionHandler); + } + + public String addPrivateConditionHandler(SimpleConditionHandler conditionHandler) { + return delegate.addPrivateConditionHandler(conditionHandler); + } + + public void addActionType(ActionType actionType) { + delegate.addActionType(actionType); + } + + public void addActionHandler(String uid, ScriptedHandler actionHandler) { + delegate.addActionHandler(uid, actionHandler); + } + + public String addPrivateActionHandler(SimpleActionHandler actionHandler) { + return delegate.addPrivateActionHandler(actionHandler); + } + + public void addTriggerType(TriggerType triggerType) { + delegate.addTriggerType(triggerType); + } + + public void addTriggerHandler(String uid, ScriptedHandler triggerHandler) { + delegate.addTriggerHandler(uid, triggerHandler); + } + + public String addPrivateTriggerHandler(SimpleTriggerHandler triggerHandler) { + return delegate.addPrivateTriggerHandler(triggerHandler); + } +} diff --git a/bundles/org.openhab.automation.jsscripting/src/main/resources/META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider b/bundles/org.openhab.automation.jsscripting/src/main/resources/META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider new file mode 100755 index 0000000000000..1fb86e4d04bc2 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/resources/META-INF/services/com.oracle.truffle.api.TruffleLanguage$Provider @@ -0,0 +1,2 @@ +com.oracle.truffle.regex.RegexLanguageProvider +com.oracle.truffle.js.lang.JavaScriptLanguageProvider diff --git a/bundles/pom.xml b/bundles/pom.xml index 1d69ef8c099f1..4c8ad081aa3e5 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -20,6 +20,7 @@ org.openhab.automation.groovyscripting org.openhab.automation.jythonscripting + org.openhab.automation.jsscripting org.openhab.io.homekit org.openhab.io.hueemulation