]> git.basschouten.com Git - openhab-addons.git/commitdiff
[jsscripting] openhab-js integration (#11656)
authorDan Cunningham <dan@digitaldan.com>
Mon, 13 Dec 2021 07:13:13 +0000 (23:13 -0800)
committerGitHub <noreply@github.com>
Mon, 13 Dec 2021 07:13:13 +0000 (08:13 +0100)
Fixes #11222

Signed-off-by: Dan Cunningham <dan@digitaldan.com>
12 files changed:
bundles/org.openhab.automation.jsscripting/pom.xml
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/GraalJSScriptEngineFactory.java
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/OpenhabGraalJSScriptEngine.java
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/ReadOnlySeekableByteArrayChannel.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/AbstractScriptExtensionProvider.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ClassExtender.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/Lifecycle.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/OSGiScriptExtensionProvider.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptDisposalAware.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptDisposalAwareScriptExtensionProvider.java [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml [new file with mode: 0644]
bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js [new file with mode: 0644]

index 5a5ff68b054ccc60779caf0186748e90f92ef6d4..dde3319bf46d785cccd0c663ebf08521988adb58 100644 (file)
@@ -25,6 +25,7 @@
     <graal.version>21.3.0</graal.version>
     <asm.version>6.2.1</asm.version>
     <oh.version>${project.version}</oh.version>
+    <ohjs.version>openhab@0.0.1-beta.3</ohjs.version>
   </properties>
 
   <build>
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <groupId>com.github.eirslett</groupId>
+        <artifactId>frontend-maven-plugin</artifactId>
+        <version>1.12.0</version>
+        <configuration>
+          <nodeVersion>v12.16.1</nodeVersion>
+          <workingDirectory>target/js</workingDirectory>
+        </configuration>
+        <executions>
+          <execution>
+            <id>Install node and npm</id>
+            <goals>
+              <goal>install-node-and-npm</goal>
+            </goals>
+            <phase>generate-sources</phase>
+          </execution>
+          <execution>
+            <id>npm install</id>
+            <goals>
+              <goal>npm</goal>
+            </goals>
+            <configuration>
+              <arguments>install ${ohjs.version} webpack webpack-cli</arguments>
+            </configuration>
+          </execution>
+          <execution>
+            <id>npx webpack</id>
+            <goals>
+              <goal>npx</goal>
+            </goals>
+            <configuration>
+              <arguments>webpack -c ./node_modules/openhab/webpack.config.js --entry ./node_modules/openhab/ -o ./dist</arguments>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>add-resource</goal>
+            </goals>
+            <phase>generate-sources</phase>
+            <configuration>
+              <resources>
+                <resource>
+                  <directory>target/js/dist</directory>
+                  <targetPath>node_modules</targetPath>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>
 
index d6b98efeb73829ec98612241ee46e90af87569c2..4fa8ac8d980032f51ef0a310c8a690a8e29b1035 100644 (file)
@@ -20,15 +20,26 @@ import java.util.Map;
 import javax.script.ScriptEngine;
 
 import org.openhab.core.automation.module.script.ScriptEngineFactory;
+import org.openhab.core.config.core.ConfigurableService;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
+import org.osgi.service.component.annotations.Modified;
 
 /**
  * An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
  *
  * @author Jonathan Gilbert - Initial contribution
+ * @author Dan Cunningham - Script injections
  */
-@Component(service = ScriptEngineFactory.class)
+@Component(service = ScriptEngineFactory.class, configurationPid = "org.openhab.automation.jsscripting", property = Constants.SERVICE_PID
+        + "=org.openhab.automation.jsscripting")
+@ConfigurableService(category = "automation", label = "JS Scripting", description_uri = "automation:jsscripting")
 public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
+    private static final String CFG_INJECTION_ENABLED = "injectionEnabled";
+    private static final String INJECTION_CODE = "Object.assign(this, require('openhab'));";
+    private boolean injectionEnabled;
 
     public static final String MIME_TYPE = "application/javascript;version=ECMAScript-2021";
 
@@ -59,7 +70,18 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
 
     @Override
     public ScriptEngine createScriptEngine(String scriptType) {
-        OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine();
-        return new DebuggingGraalScriptEngine<>(engine);
+        return new DebuggingGraalScriptEngine<>(
+                new OpenhabGraalJSScriptEngine(injectionEnabled ? INJECTION_CODE : null));
+    }
+
+    @Activate
+    protected void activate(BundleContext context, Map<String, ?> config) {
+        modified(config);
+    }
+
+    @Modified
+    protected void modified(Map<String, ?> config) {
+        Object injectionEnabled = config.get(CFG_INJECTION_ENABLED);
+        this.injectionEnabled = injectionEnabled == null || (Boolean) injectionEnabled;
     }
 }
index 6621d5d1a157833287d13d1522530374022aaeff..f4939baa0f08b6aa93ac588d50a5b765f59b094c 100644 (file)
@@ -15,22 +15,32 @@ package org.openhab.automation.jsscripting.internal;
 import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.nio.channels.SeekableByteChannel;
+import java.nio.file.AccessMode;
 import java.nio.file.FileSystems;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.nio.file.attribute.FileAttribute;
+import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
 import javax.script.ScriptContext;
+import javax.script.ScriptException;
 
 import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.graalvm.polyglot.Context;
 import org.graalvm.polyglot.Engine;
 import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
 import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
+import org.openhab.automation.jsscripting.internal.fs.ReadOnlySeekableByteArrayChannel;
 import org.openhab.automation.jsscripting.internal.fs.watch.JSDependencyTracker;
 import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
 import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
@@ -43,32 +53,36 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
  * GraalJS Script Engine implementation
  *
  * @author Jonathan Gilbert - Initial contribution
+ * @author Dan Cunningham - Script injections
  */
 public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
-
+    private static final String GLOBAL_REQUIRE = "require(\"@jsscripting-globals\");";
     private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
+    // final CommonJS search path for our library
+    private static final Path LOCAL_NODE_PATH = Paths.get("/node_modules");
 
     // these fields start as null because they are populated on first use
     private @NonNullByDefault({}) String engineIdentifier;
     private @NonNullByDefault({}) Consumer<String> scriptDependencyListener;
 
     private boolean initialized = false;
+    private String globalScript;
 
     /**
      * 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() {
+    public OpenhabGraalJSScriptEngine(@Nullable String injectionCode) {
         super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
+        this.globalScript = GLOBAL_REQUIRE + (injectionCode != null ? injectionCode : "");
         delegate = GraalJSScriptEngine.create(
                 Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false")
                         .build(),
                 Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
                         .option("js.commonjs-require-cwd", JSDependencyTracker.LIB_PATH)
-                        .option("js.nashorn-compat", "true") // to ease
-                        // migration
+                        .option("js.nashorn-compat", "true") // to ease migration
                         .option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
                                                                  // want ecma2021
                         .option("js.commonjs-require", "true") // enable CommonJS module support
@@ -80,15 +94,52 @@ public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngi
                                 if (scriptDependencyListener != null) {
                                     scriptDependencyListener.accept(path.toString());
                                 }
-
                                 if (path.toString().endsWith(".js")) {
+                                    SeekableByteChannel sbc = null;
+                                    if (path.startsWith(LOCAL_NODE_PATH)) {
+                                        InputStream is = getClass().getResourceAsStream(path.toString());
+                                        if (is == null) {
+                                            throw new IOException("Could not read " + path.toString());
+                                        }
+                                        sbc = new ReadOnlySeekableByteArrayChannel(is.readAllBytes());
+                                    } else {
+                                        sbc = super.newByteChannel(path, options, attrs);
+                                    }
                                     return new PrefixedSeekableByteChannel(
-                                            ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
-                                            super.newByteChannel(path, options, attrs));
+                                            ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(), sbc);
                                 } else {
                                     return super.newByteChannel(path, options, attrs);
                                 }
                             }
+
+                            @Override
+                            public void checkAccess(Path path, Set<? extends AccessMode> modes,
+                                    LinkOption... linkOptions) throws IOException {
+                                if (path.startsWith(LOCAL_NODE_PATH)) {
+                                    if (getClass().getResource(path.toString()) == null) {
+                                        throw new NoSuchFileException(path.toString());
+                                    }
+                                } else {
+                                    super.checkAccess(path, modes, linkOptions);
+                                }
+                            }
+
+                            @Override
+                            public Map<String, Object> readAttributes(Path path, String attributes,
+                                    LinkOption... options) throws IOException {
+                                if (path.startsWith(LOCAL_NODE_PATH)) {
+                                    return Collections.singletonMap("isRegularFile", true);
+                                }
+                                return super.readAttributes(path, attributes, options);
+                            }
+
+                            @Override
+                            public Path toRealPath(Path path, LinkOption... linkOptions) throws IOException {
+                                if (path.startsWith(LOCAL_NODE_PATH)) {
+                                    return path;
+                                }
+                                return super.toRealPath(path, linkOptions);
+                            }
                         }));
     }
 
@@ -130,5 +181,11 @@ public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngi
         delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
 
         initialized = true;
+
+        try {
+            eval(globalScript);
+        } catch (ScriptException e) {
+            LOGGER.error("Could not inject global script", e);
+        }
     }
 }
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/ReadOnlySeekableByteArrayChannel.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/ReadOnlySeekableByteArrayChannel.java
new file mode 100644 (file)
index 0000000..f150632
--- /dev/null
@@ -0,0 +1,93 @@
+/**
+ * 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.ClosedChannelException;
+import java.nio.channels.SeekableByteChannel;
+
+/**
+ * Simple wrapper around a byte array to provide a SeekableByteChannel for consumption
+ *
+ * @author Dan Cunningham - Initial contribution
+ */
+public class ReadOnlySeekableByteArrayChannel implements SeekableByteChannel {
+    private byte[] data;
+    private int position;
+    private boolean closed;
+
+    public ReadOnlySeekableByteArrayChannel(byte[] data) {
+        this.data = data;
+    }
+
+    @Override
+    public long position() {
+        return position;
+    }
+
+    @Override
+    public SeekableByteChannel position(long newPosition) throws IOException {
+        ensureOpen();
+        position = (int) Math.max(0, Math.min(newPosition, size()));
+        return this;
+    }
+
+    @Override
+    public long size() {
+        return data.length;
+    }
+
+    @Override
+    public int read(ByteBuffer buf) throws IOException {
+        ensureOpen();
+        int remaining = (int) size() - position;
+        if (remaining <= 0) {
+            return -1;
+        }
+        int readBytes = buf.remaining();
+        if (readBytes > remaining) {
+            readBytes = remaining;
+        }
+        buf.put(data, position, readBytes);
+        position += readBytes;
+        return readBytes;
+    }
+
+    @Override
+    public void close() {
+        closed = true;
+    }
+
+    @Override
+    public boolean isOpen() {
+        return !closed;
+    }
+
+    @Override
+    public int write(ByteBuffer b) throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public SeekableByteChannel truncate(long newSize) {
+        throw new UnsupportedOperationException();
+    }
+
+    private void ensureOpen() throws ClosedChannelException {
+        if (!isOpen()) {
+            throw new ClosedChannelException();
+        }
+    }
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/AbstractScriptExtensionProvider.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/AbstractScriptExtensionProvider.java
new file mode 100644 (file)
index 0000000..0592076
--- /dev/null
@@ -0,0 +1,89 @@
+/**
+ * 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.scope;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import org.openhab.core.automation.module.script.ScriptExtensionProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+
+/**
+ * Base class to offer support for script extension providers
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+public abstract class AbstractScriptExtensionProvider implements ScriptExtensionProvider {
+    private Map<String, Function<String, Object>> types;
+    private Map<String, Map<String, Object>> idToTypes = new ConcurrentHashMap<>();
+
+    protected abstract String getPresetName();
+
+    protected abstract void initializeTypes(final BundleContext context);
+
+    protected void addType(String name, Function<String, Object> value) {
+        types.put(name, value);
+    }
+
+    @Activate
+    public void activate(final BundleContext context) {
+        types = new HashMap<>();
+        initializeTypes(context);
+    }
+
+    @Override
+    public Collection<String> getDefaultPresets() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public Collection<String> getPresets() {
+        return Collections.singleton(getPresetName());
+    }
+
+    @Override
+    public Collection<String> getTypes() {
+        return types.keySet();
+    }
+
+    @Override
+    public Object get(String scriptIdentifier, String type) throws IllegalArgumentException {
+
+        Map<String, Object> forScript = idToTypes.computeIfAbsent(scriptIdentifier, k -> new HashMap<>());
+        return forScript.computeIfAbsent(type, k -> types.get(k).apply(scriptIdentifier));
+    }
+
+    @Override
+    public Map<String, Object> importPreset(String scriptIdentifier, String preset) {
+        if (getPresetName().equals(preset)) {
+            Map<String, Object> results = new HashMap<>(types.size());
+            for (String type : types.keySet()) {
+                results.put(type, get(scriptIdentifier, type));
+            }
+            return results;
+        }
+
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public void unload(String scriptIdentifier) {
+        // ignore by default
+    }
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ClassExtender.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ClassExtender.java
new file mode 100644 (file)
index 0000000..ee67607
--- /dev/null
@@ -0,0 +1,25 @@
+/**
+ * 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.scope;
+
+//import com.oracle.truffle.js.runtime.java.adapter.JavaAdapterFactory;
+
+/**
+ * Class utility to allow creation of 'extendable' classes with a classloader of the current bundle, rather than the
+ * classloader of the file being extended.
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+public class ClassExtender {
+    private ClassLoader classLoader = getClass().getClassLoader();
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/Lifecycle.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/Lifecycle.java
new file mode 100644 (file)
index 0000000..8590295
--- /dev/null
@@ -0,0 +1,64 @@
+/**
+ * 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.scope;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Consumer;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Allows scripts to register for lifecycle events
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+public class Lifecycle implements ScriptDisposalAware {
+    private static final Logger logger = LoggerFactory.getLogger(Lifecycle.class);
+    public static final int DEFAULT_PRIORITY = 50;
+    private List<Hook> listeners = new ArrayList<>();
+
+    public void addDisposeHook(Consumer<Object> listener, int priority) {
+        addListener(listener, priority);
+    }
+
+    public void addDisposeHook(Consumer<Object> listener) {
+        addDisposeHook(listener, DEFAULT_PRIORITY);
+    }
+
+    private void addListener(Consumer<Object> listener, int priority) {
+        listeners.add(new Hook(priority, listener));
+    }
+
+    @Override
+    public void unload(String scriptIdentifier) {
+        try {
+            listeners.stream().sorted(Comparator.comparingInt(h -> h.priority))
+                    .forEach(h -> h.fn.accept(scriptIdentifier));
+        } catch (RuntimeException ex) {
+            logger.warn("Script unloading halted due to exception in disposal: {}: {}", ex.getClass(), ex.getMessage());
+        }
+    }
+
+    private static class Hook {
+        public Hook(int priority, Consumer<Object> fn) {
+            this.priority = priority;
+            this.fn = fn;
+        }
+
+        int priority;
+        Consumer<Object> fn;
+    }
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/OSGiScriptExtensionProvider.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/OSGiScriptExtensionProvider.java
new file mode 100644 (file)
index 0000000..5ec867b
--- /dev/null
@@ -0,0 +1,40 @@
+/**
+ * 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.scope;
+
+import org.openhab.core.automation.module.script.ScriptExtensionProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Component;
+
+/**
+ * ScriptExtensionProvider which provides various functions to help scripts to work with OSGi
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+@Component(immediate = true, service = ScriptExtensionProvider.class)
+public class OSGiScriptExtensionProvider extends ScriptDisposalAwareScriptExtensionProvider {
+
+    @Override
+    protected String getPresetName() {
+        return "osgi";
+    }
+
+    @Override
+    protected void initializeTypes(final BundleContext context) {
+        ClassExtender classExtender = new ClassExtender();
+
+        addType("bundleContext", k -> context);
+        addType("lifecycle", k -> new Lifecycle());
+        addType("classutil", k -> classExtender);
+    }
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptDisposalAware.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptDisposalAware.java
new file mode 100644 (file)
index 0000000..6c18c59
--- /dev/null
@@ -0,0 +1,32 @@
+/**
+ * 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.scope;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * Specifies that an object is aware of script disposal events
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+@NonNullByDefault
+public interface ScriptDisposalAware {
+
+    /**
+     * Indicates that the script has been disposed
+     *
+     * @param scriptIdentifier the identifier for the script
+     */
+    void unload(String scriptIdentifier);
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptDisposalAwareScriptExtensionProvider.java b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptDisposalAwareScriptExtensionProvider.java
new file mode 100644 (file)
index 0000000..29ac13a
--- /dev/null
@@ -0,0 +1,98 @@
+/**
+ * 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.scope;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import org.openhab.core.automation.module.script.ScriptExtensionProvider;
+import org.osgi.framework.BundleContext;
+import org.osgi.service.component.annotations.Activate;
+
+/**
+ * Base class to offer support for script extension providers
+ *
+ * @author Jonathan Gilbert - Initial contribution
+ */
+public abstract class ScriptDisposalAwareScriptExtensionProvider
+        implements ScriptExtensionProvider, ScriptDisposalAware {
+    private Map<String, Function<String, Object>> types;
+    private Map<String, Map<String, Object>> idToTypes = new ConcurrentHashMap<>();
+
+    protected abstract String getPresetName();
+
+    protected abstract void initializeTypes(final BundleContext context);
+
+    protected void addType(String name, Function<String, Object> value) {
+        types.put(name, value);
+    }
+
+    @Activate
+    public void activate(final BundleContext context) {
+        types = new HashMap<>();
+        initializeTypes(context);
+    }
+
+    @Override
+    public Collection<String> getDefaultPresets() {
+        return Collections.emptyList();
+    }
+
+    @Override
+    public Collection<String> getPresets() {
+        return Collections.singleton(getPresetName());
+    }
+
+    @Override
+    public Collection<String> getTypes() {
+        return types.keySet();
+    }
+
+    @Override
+    public Object get(String scriptIdentifier, String type) throws IllegalArgumentException {
+
+        Map<String, Object> forScript = idToTypes.computeIfAbsent(scriptIdentifier, k -> new HashMap<>());
+        return forScript.computeIfAbsent(type, k -> types.get(k).apply(scriptIdentifier));
+    }
+
+    @Override
+    public Map<String, Object> importPreset(String scriptIdentifier, String preset) {
+        if (getPresetName().equals(preset)) {
+            Map<String, Object> results = new HashMap<>(types.size());
+            for (String type : types.keySet()) {
+                results.put(type, get(scriptIdentifier, type));
+            }
+            return results;
+        }
+
+        return Collections.emptyMap();
+    }
+
+    @Override
+    public void unload(String scriptIdentifier) {
+        Map<String, Object> forScript = idToTypes.remove(scriptIdentifier);
+
+        if (forScript != null) {
+            for (Object o : forScript.values()) {
+                if (o instanceof ScriptDisposalAware) {
+                    ((ScriptDisposalAware) o).unload(scriptIdentifier);
+                }
+            }
+        }
+    }
+}
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml b/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml
new file mode 100644 (file)
index 0000000..3348680
--- /dev/null
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<config-description:config-descriptions
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
+       xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
+               https://openhab.org/schemas/config-description-1.0.0.xsd">
+       <config-description uri="automation:jsscripting">
+               <parameter name="injectionEnabled" type="boolean" required="true">
+                       <label>Use Built-in Global Variables</label>
+                       <description><![CDATA[ Import all variables from the OH scripting library into all rules for common services like items, things, actions, log, etc... <br>
+                       If disabled, the OH scripting library can be imported manually using "<i>require('openhab')</i>"
+                       ]]></description>
+                       <options>
+                               <option value="true">Use Built-in Variables</option>
+                               <option value="false">Do Not Use Built-in Variables</option>
+                       </options>
+                       <default>true</default>
+               </parameter>
+       </config-description>
+</config-description:config-descriptions>
diff --git a/bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js b/bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js
new file mode 100644 (file)
index 0000000..d60a905
--- /dev/null
@@ -0,0 +1,204 @@
+
+(function (global) {
+    'use strict';
+
+    const System = Java.type('java.lang.System');
+    const log = Java.type("org.slf4j.LoggerFactory").getLogger("org.openhab.automation.script");
+    const ScriptExecution = Java.type('org.openhab.core.model.script.actions.ScriptExecution');
+    const ZonedDateTime = Java.type('java.time.ZonedDateTime');
+
+    const formatRegExp = /%[sdj%]/g;
+
+    function stringify(value) {
+        try {
+            if (Java.isJavaObject(value)) {
+                return value.toString();
+            } else {
+                // special cases
+                if (value === undefined) {
+                    return "undefined"
+                }
+                if (typeof value === 'function') {
+                    return "[Function]"
+                }
+                if (value instanceof RegExp) {
+                    return value.toString();
+                }
+                // fallback to JSON
+                return JSON.stringify(value, null, 2);
+            }
+        } catch (e) {
+            return '[Circular: ' + e + ']';
+        }
+    }
+
+    function format(f) {
+        if (typeof f !== 'string') {
+            var objects = [];
+            for (var index = 0; index < arguments.length; index++) {
+                objects.push(stringify(arguments[index]));
+            }
+            return objects.join(' ');
+        }
+
+        if (arguments.length === 1) return f;
+
+        var i = 1;
+        var args = arguments;
+        var len = args.length;
+        var str = String(f).replace(formatRegExp, function (x) {
+            if (x === '%%') return '%';
+            if (i >= len) return x;
+            switch (x) {
+                case '%s': return String(args[i++]);
+                case '%d': return Number(args[i++]);
+                case '%j':
+                    try {
+                        return stringify(args[i++]);
+                    } catch (_) {
+                        return '[Circular]';
+                    }
+                // falls through
+                default:
+                    return x;
+            }
+        });
+        for (var x = args[i]; i < len; x = args[++i]) {
+            if (x === null || (typeof x !== 'object' && typeof x !== 'symbol')) {
+                str += ' ' + x;
+            } else {
+                str += ' ' + stringify(x);
+            }
+        }
+        return str;
+    }
+
+    const counters = {};
+    const timers = {};
+
+    const console = {
+        'assert': function (expression, message) {
+            if (!expression) {
+                log.error(message);
+            }
+        },
+
+        count: function (label) {
+            let counter;
+
+            if (label) {
+                if (counters.hasOwnProperty(label)) {
+                    counter = counters[label];
+                } else {
+                    counter = 0;
+                }
+
+                // update
+                counters[label] = ++counter;
+                log.debug(format.apply(null, [label + ':', counter]));
+            }
+        },
+
+        debug: function () {
+            log.debug(format.apply(null, arguments));
+        },
+
+        info: function () {
+            log.info(format.apply(null, arguments));
+        },
+
+        log: function () {
+            log.info(format.apply(null, arguments));
+        },
+
+        warn: function () {
+            log.warn(format.apply(null, arguments));
+        },
+
+        error: function () {
+            log.error(format.apply(null, arguments));
+        },
+
+        trace: function (e) {
+            if (Java.isJavaObject(e)) {
+                log.trace(e.getLocalizedMessage(), e);
+            } else {
+                if (e.stack) {
+                    log.trace(e.stack);
+                } else {
+                    if (e.message) {
+                        log.trace(format.apply(null, [(e.name || 'Error') + ':', e.message]));
+                    } else {
+                        log.trace((e.name || 'Error'));
+                    }
+                }
+            }
+        },
+
+        time: function (label) {
+            if (label) {
+                timers[label] = System.currentTimeMillis();
+            }
+        },
+        timeEnd: function (label) {
+            if (label) {
+                const now = System.currentTimeMillis();
+                if (timers.hasOwnProperty(label)) {
+                    log.info(format.apply(null, [label + ':', (now - timers[label]) + 'ms']));
+                    delete timers[label];
+                } else {
+                    log.info(format.apply(null, [label + ':', '<no timer>']));
+                }
+            }
+        }
+    };
+
+    function setTimeout(cb, delay) {
+        const args = Array.prototype.slice.call(arguments, 2);
+        return ScriptExecution.createTimerWithArgument(
+            ZonedDateTime.now().plusNanos(delay * 1000000),
+            args,
+            function (args) {
+                cb.apply(global, args);
+            }
+        );
+    }
+
+    function clearTimeout(timer) {
+        if (timer !== undefined && timer.isActive()) {
+            timer.cancel();
+        }
+    }
+
+    function setInterval(cb, delay) {
+        const args = Array.prototype.slice.call(arguments, 2);
+        const delayNanos = delay * 1000000
+        let timer = ScriptExecution.createTimerWithArgument(
+            ZonedDateTime.now().plusNanos(delayNanos),
+            args,
+            function (args) {
+                cb.apply(global, args);
+                if (!timer.isCancelled()) {
+                    timer.reschedule(ZonedDateTime.now().plusNanos(delayNanos));
+                }
+            }
+        );
+        return timer;
+    }
+
+    function clearInterval(timer) {
+        clearTimeout(timer);
+    }
+
+    //Polyfil common functions onto the global object
+    globalThis.console = console;
+    globalThis.setTimeout = setTimeout;
+    globalThis.clearTimeout = clearTimeout;
+    globalThis.setInterval = setInterval;
+    globalThis.clearInterval = clearInterval;
+
+    //Support legacy NodeJS libraries 
+    globalThis.global = globalThis;
+    globalThis.process = { env: { NODE_ENV: '' } };
+
+})(this);