[jsscripting] openhab-js integration (#11656)
Fixes #11222 Signed-off-by: Dan Cunningham <dan@digitaldan.com>
This commit is contained in:
parent
306c30eda1
commit
4481ecff61
@ -25,6 +25,7 @@
|
|||||||
<graal.version>21.3.0</graal.version>
|
<graal.version>21.3.0</graal.version>
|
||||||
<asm.version>6.2.1</asm.version>
|
<asm.version>6.2.1</asm.version>
|
||||||
<oh.version>${project.version}</oh.version>
|
<oh.version>${project.version}</oh.version>
|
||||||
|
<ohjs.version>openhab@0.0.1-beta.3</ohjs.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
@ -44,6 +45,62 @@
|
|||||||
</execution>
|
</execution>
|
||||||
</executions>
|
</executions>
|
||||||
</plugin>
|
</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>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
|
|||||||
@ -20,15 +20,26 @@ import java.util.Map;
|
|||||||
import javax.script.ScriptEngine;
|
import javax.script.ScriptEngine;
|
||||||
|
|
||||||
import org.openhab.core.automation.module.script.ScriptEngineFactory;
|
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.Component;
|
||||||
|
import org.osgi.service.component.annotations.Modified;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
|
* An implementation of {@link ScriptEngineFactory} with customizations for GraalJS ScriptEngines.
|
||||||
*
|
*
|
||||||
* @author Jonathan Gilbert - Initial contribution
|
* @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 {
|
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";
|
public static final String MIME_TYPE = "application/javascript;version=ECMAScript-2021";
|
||||||
|
|
||||||
@ -59,7 +70,18 @@ public final class GraalJSScriptEngineFactory implements ScriptEngineFactory {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public ScriptEngine createScriptEngine(String scriptType) {
|
public ScriptEngine createScriptEngine(String scriptType) {
|
||||||
OpenhabGraalJSScriptEngine engine = new OpenhabGraalJSScriptEngine();
|
return new DebuggingGraalScriptEngine<>(
|
||||||
return new DebuggingGraalScriptEngine<>(engine);
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,22 +15,32 @@ package org.openhab.automation.jsscripting.internal;
|
|||||||
import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
|
import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.nio.channels.SeekableByteChannel;
|
import java.nio.channels.SeekableByteChannel;
|
||||||
|
import java.nio.file.AccessMode;
|
||||||
import java.nio.file.FileSystems;
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.LinkOption;
|
||||||
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.OpenOption;
|
import java.nio.file.OpenOption;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.attribute.FileAttribute;
|
import java.nio.file.attribute.FileAttribute;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
import javax.script.ScriptContext;
|
import javax.script.ScriptContext;
|
||||||
|
import javax.script.ScriptException;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.graalvm.polyglot.Context;
|
import org.graalvm.polyglot.Context;
|
||||||
import org.graalvm.polyglot.Engine;
|
import org.graalvm.polyglot.Engine;
|
||||||
import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
|
import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
|
||||||
import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
|
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.fs.watch.JSDependencyTracker;
|
||||||
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
|
import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
|
||||||
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
|
import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
|
||||||
@ -43,32 +53,36 @@ import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
|
|||||||
* GraalJS Script Engine implementation
|
* GraalJS Script Engine implementation
|
||||||
*
|
*
|
||||||
* @author Jonathan Gilbert - Initial contribution
|
* @author Jonathan Gilbert - Initial contribution
|
||||||
|
* @author Dan Cunningham - Script injections
|
||||||
*/
|
*/
|
||||||
public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {
|
public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {
|
||||||
|
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
|
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__";
|
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
|
// these fields start as null because they are populated on first use
|
||||||
private @NonNullByDefault({}) String engineIdentifier;
|
private @NonNullByDefault({}) String engineIdentifier;
|
||||||
private @NonNullByDefault({}) Consumer<String> scriptDependencyListener;
|
private @NonNullByDefault({}) Consumer<String> scriptDependencyListener;
|
||||||
|
|
||||||
private boolean initialized = false;
|
private boolean initialized = false;
|
||||||
|
private String globalScript;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
|
* Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
|
||||||
* lifecycle and provides hooks for scripts to do so too.
|
* 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
|
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(
|
delegate = GraalJSScriptEngine.create(
|
||||||
Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false")
|
Engine.newBuilder().allowExperimentalOptions(true).option("engine.WarnInterpreterOnly", "false")
|
||||||
.build(),
|
.build(),
|
||||||
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
|
Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
|
||||||
.option("js.commonjs-require-cwd", JSDependencyTracker.LIB_PATH)
|
.option("js.commonjs-require-cwd", JSDependencyTracker.LIB_PATH)
|
||||||
.option("js.nashorn-compat", "true") // to ease
|
.option("js.nashorn-compat", "true") // to ease migration
|
||||||
// migration
|
|
||||||
.option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
|
.option("js.ecmascript-version", "2021") // nashorn compat will enforce es5 compatibility, we
|
||||||
// want ecma2021
|
// want ecma2021
|
||||||
.option("js.commonjs-require", "true") // enable CommonJS module support
|
.option("js.commonjs-require", "true") // enable CommonJS module support
|
||||||
@ -80,15 +94,52 @@ public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngi
|
|||||||
if (scriptDependencyListener != null) {
|
if (scriptDependencyListener != null) {
|
||||||
scriptDependencyListener.accept(path.toString());
|
scriptDependencyListener.accept(path.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path.toString().endsWith(".js")) {
|
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(
|
return new PrefixedSeekableByteChannel(
|
||||||
("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
|
("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(), sbc);
|
||||||
super.newByteChannel(path, options, attrs));
|
|
||||||
} else {
|
} else {
|
||||||
return super.newByteChannel(path, options, attrs);
|
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")));
|
delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
|
||||||
|
|
||||||
initialized = true;
|
initialized = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
eval(globalScript);
|
||||||
|
} catch (ScriptException e) {
|
||||||
|
LOGGER.error("Could not inject global script", e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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();
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
204
bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js
generated
vendored
Normal file
204
bundles/org.openhab.automation.jsscripting/src/main/resources/node_modules/@jsscripting-globals.js
generated
vendored
Normal file
@ -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);
|
||||||
Loading…
x
Reference in New Issue
Block a user