diff --git a/bundles/org.openhab.automation.jsscripting/pom.xml b/bundles/org.openhab.automation.jsscripting/pom.xml index 5a5ff68b054cc..dde3319bf46d7 100644 --- a/bundles/org.openhab.automation.jsscripting/pom.xml +++ b/bundles/org.openhab.automation.jsscripting/pom.xml @@ -25,6 +25,7 @@ 21.3.0 6.2.1 ${project.version} + openhab@0.0.1-beta.3 @@ -44,6 +45,62 @@ + + com.github.eirslett + frontend-maven-plugin + 1.12.0 + + v12.16.1 + target/js + + + + Install node and npm + + install-node-and-npm + + generate-sources + + + npm install + + npm + + + install ${ohjs.version} webpack webpack-cli + + + + npx webpack + + npx + + + webpack -c ./node_modules/openhab/webpack.config.js --entry ./node_modules/openhab/ -o ./dist + + + + + + org.codehaus.mojo + build-helper-maven-plugin + + + + add-resource + + generate-sources + + + + target/js/dist + node_modules + + + + + + 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 index d6b98efeb7382..4fa8ac8d98003 100644 --- 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 @@ -20,15 +20,26 @@ 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 void scopeValues(ScriptEngine scriptEngine, Map scopeValu @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 config) { + modified(config); + } + + @Modified + protected void modified(Map config) { + Object injectionEnabled = config.get(CFG_INJECTION_ENABLED); + this.injectionEnabled = injectionEnabled == null || (Boolean) injectionEnabled; } } 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 index 6621d5d1a1578..f4939baa0f08b 100644 --- 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 @@ -15,22 +15,32 @@ 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 @@ * GraalJS Script Engine implementation * * @author Jonathan Gilbert - Initial contribution + * @author Dan Cunningham - Script injections */ public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable { 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 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 SeekableByteChannel newByteChannel(Path path, Set o 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 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 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 @@ protected void beforeInvocation() { delegate.put("require", wrapRequireFn.apply((Function) 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 index 0000000000000..f150632f6a66c --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/fs/ReadOnlySeekableByteArrayChannel.java @@ -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 index 0000000000000..0592076ff8f65 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/AbstractScriptExtensionProvider.java @@ -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> types; + private Map> idToTypes = new ConcurrentHashMap<>(); + + protected abstract String getPresetName(); + + protected abstract void initializeTypes(final BundleContext context); + + protected void addType(String name, Function value) { + types.put(name, value); + } + + @Activate + public void activate(final BundleContext context) { + types = new HashMap<>(); + initializeTypes(context); + } + + @Override + public Collection getDefaultPresets() { + return Collections.emptyList(); + } + + @Override + public Collection getPresets() { + return Collections.singleton(getPresetName()); + } + + @Override + public Collection getTypes() { + return types.keySet(); + } + + @Override + public Object get(String scriptIdentifier, String type) throws IllegalArgumentException { + + Map forScript = idToTypes.computeIfAbsent(scriptIdentifier, k -> new HashMap<>()); + return forScript.computeIfAbsent(type, k -> types.get(k).apply(scriptIdentifier)); + } + + @Override + public Map importPreset(String scriptIdentifier, String preset) { + if (getPresetName().equals(preset)) { + Map 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 index 0000000000000..ee67607c894d1 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ClassExtender.java @@ -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 index 0000000000000..859029543832a --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/Lifecycle.java @@ -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 listeners = new ArrayList<>(); + + public void addDisposeHook(Consumer listener, int priority) { + addListener(listener, priority); + } + + public void addDisposeHook(Consumer listener) { + addDisposeHook(listener, DEFAULT_PRIORITY); + } + + private void addListener(Consumer 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 fn) { + this.priority = priority; + this.fn = fn; + } + + int priority; + Consumer 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 index 0000000000000..5ec867ba70fd6 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/OSGiScriptExtensionProvider.java @@ -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 index 0000000000000..6c18c598cc0ba --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptDisposalAware.java @@ -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 index 0000000000000..29ac13a93f977 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/java/org/openhab/automation/jsscripting/internal/scope/ScriptDisposalAwareScriptExtensionProvider.java @@ -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> types; + private Map> idToTypes = new ConcurrentHashMap<>(); + + protected abstract String getPresetName(); + + protected abstract void initializeTypes(final BundleContext context); + + protected void addType(String name, Function value) { + types.put(name, value); + } + + @Activate + public void activate(final BundleContext context) { + types = new HashMap<>(); + initializeTypes(context); + } + + @Override + public Collection getDefaultPresets() { + return Collections.emptyList(); + } + + @Override + public Collection getPresets() { + return Collections.singleton(getPresetName()); + } + + @Override + public Collection getTypes() { + return types.keySet(); + } + + @Override + public Object get(String scriptIdentifier, String type) throws IllegalArgumentException { + + Map forScript = idToTypes.computeIfAbsent(scriptIdentifier, k -> new HashMap<>()); + return forScript.computeIfAbsent(type, k -> types.get(k).apply(scriptIdentifier)); + } + + @Override + public Map importPreset(String scriptIdentifier, String preset) { + if (getPresetName().equals(preset)) { + Map 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 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 index 0000000000000..3348680be3321 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/resources/OH-INF/config/config.xml @@ -0,0 +1,20 @@ + + + + + + + If disabled, the OH scripting library can be imported manually using "require('openhab')" + ]]> + + + + + true + + + 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 index 0000000000000..d60a905174028 --- /dev/null +++ b/bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js @@ -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 + ':', ''])); + } + } + } + }; + + 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);