[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>
|
||||
<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>
|
||||
|
@ -44,6 +45,62 @@
|
|||
</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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue