]> git.basschouten.com Git - openhab-addons.git/blob
aa62c1308592e4107c35079c6272c3ad19e59517
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13
14 package org.openhab.automation.jsscripting.internal;
15
16 import static org.openhab.core.automation.module.script.ScriptEngineFactory.*;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.nio.channels.SeekableByteChannel;
21 import java.nio.file.FileSystems;
22 import java.nio.file.OpenOption;
23 import java.nio.file.Path;
24 import java.nio.file.attribute.FileAttribute;
25 import java.util.Set;
26 import java.util.function.Consumer;
27 import java.util.function.Function;
28
29 import javax.script.ScriptContext;
30
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.graalvm.polyglot.Context;
33 import org.openhab.automation.jsscripting.internal.fs.DelegatingFileSystem;
34 import org.openhab.automation.jsscripting.internal.fs.PrefixedSeekableByteChannel;
35 import org.openhab.automation.jsscripting.internal.scriptengine.InvocationInterceptingScriptEngineWithInvocable;
36 import org.openhab.core.OpenHAB;
37 import org.openhab.core.automation.module.script.ScriptExtensionAccessor;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine;
42
43 /**
44  * GraalJS Script Engine implementation
45  *
46  * @author Jonathan Gilbert - Initial contribution
47  */
48 public class OpenhabGraalJSScriptEngine extends InvocationInterceptingScriptEngineWithInvocable<GraalJSScriptEngine> {
49
50     private static final Logger LOGGER = LoggerFactory.getLogger(OpenhabGraalJSScriptEngine.class);
51
52     private static final String REQUIRE_WRAPPER_NAME = "__wraprequire__";
53     private static final String MODULE_DIR = String.join(File.separator, OpenHAB.getConfigFolder(), "automation", "lib",
54             "javascript", "personal");
55
56     // these fields start as null because they are populated on first use
57     private @NonNullByDefault({}) String engineIdentifier;
58     private @NonNullByDefault({}) Consumer<String> scriptDependencyListener;
59
60     private boolean initialized = false;
61
62     /**
63      * Creates an implementation of ScriptEngine (& Invocable), wrapping the contained engine, that tracks the script
64      * lifecycle and provides hooks for scripts to do so too.
65      */
66     public OpenhabGraalJSScriptEngine() {
67         super(null); // delegate depends on fields not yet initialised, so we cannot set it immediately
68         delegate = GraalJSScriptEngine.create(null,
69                 Context.newBuilder("js").allowExperimentalOptions(true).allowAllAccess(true)
70                         .option("js.commonjs-require-cwd", MODULE_DIR).option("js.nashorn-compat", "true") // to ease
71                                                                                                            // migration
72                         .option("js.commonjs-require", "true") // enable CommonJS module support
73                         .hostClassLoader(getClass().getClassLoader())
74                         .fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
75                             @Override
76                             public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
77                                     FileAttribute<?>... attrs) throws IOException {
78                                 if (scriptDependencyListener != null) {
79                                     scriptDependencyListener.accept(path.toString());
80                                 }
81
82                                 if (path.toString().endsWith(".js")) {
83                                     return new PrefixedSeekableByteChannel(
84                                             ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
85                                             super.newByteChannel(path, options, attrs));
86                                 } else {
87                                     return super.newByteChannel(path, options, attrs);
88                                 }
89                             }
90                         }));
91     }
92
93     @Override
94     protected void beforeInvocation() {
95         if (initialized) {
96             return;
97         }
98
99         ScriptContext ctx = delegate.getContext();
100
101         // these are added post-construction, so we need to fetch them late
102         this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER);
103         if (this.engineIdentifier == null) {
104             throw new IllegalStateException("Failed to retrieve engine identifier from engine bindings");
105         }
106
107         ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx
108                 .getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR);
109         if (scriptExtensionAccessor == null) {
110             throw new IllegalStateException("Failed to retrieve script extension accessor from engine bindings");
111         }
112
113         scriptDependencyListener = (Consumer<String>) ctx
114                 .getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */);
115         if (scriptDependencyListener == null) {
116             LOGGER.warn(
117                     "Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
118         }
119
120         ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
121                 scriptExtensionAccessor);
122
123         Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
124                 .locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
125                 .map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
126
127         delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
128         delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
129
130         initialized = true;
131     }
132 }