]> git.basschouten.com Git - openhab-addons.git/blob
7ba654eec7cfbab43945446bea73cb14973f3394
[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                         .fileSystem(new DelegatingFileSystem(FileSystems.getDefault().provider()) {
74                             @Override
75                             public SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options,
76                                     FileAttribute<?>... attrs) throws IOException {
77                                 if (scriptDependencyListener != null) {
78                                     scriptDependencyListener.accept(path.toString());
79                                 }
80
81                                 if (path.toString().endsWith(".js")) {
82                                     return new PrefixedSeekableByteChannel(
83                                             ("require=" + REQUIRE_WRAPPER_NAME + "(require);").getBytes(),
84                                             super.newByteChannel(path, options, attrs));
85                                 } else {
86                                     return super.newByteChannel(path, options, attrs);
87                                 }
88                             }
89                         }));
90     }
91
92     @Override
93     protected void beforeInvocation() {
94         if (initialized) {
95             return;
96         }
97
98         ScriptContext ctx = delegate.getContext();
99
100         // these are added post-construction, so we need to fetch them late
101         this.engineIdentifier = (String) ctx.getAttribute(CONTEXT_KEY_ENGINE_IDENTIFIER);
102         if (this.engineIdentifier == null) {
103             throw new IllegalStateException("Failed to retrieve engine identifier from engine bindings");
104         }
105
106         ScriptExtensionAccessor scriptExtensionAccessor = (ScriptExtensionAccessor) ctx
107                 .getAttribute(CONTEXT_KEY_EXTENSION_ACCESSOR);
108         if (scriptExtensionAccessor == null) {
109             throw new IllegalStateException("Failed to retrieve script extension accessor from engine bindings");
110         }
111
112         scriptDependencyListener = (Consumer<String>) ctx
113                 .getAttribute("oh.dependency-listener"/* CONTEXT_KEY_DEPENDENCY_LISTENER */);
114         if (scriptDependencyListener == null) {
115             LOGGER.warn(
116                     "Failed to retrieve script script dependency listener from engine bindings. Script dependency tracking will be disabled.");
117         }
118
119         ScriptExtensionModuleProvider scriptExtensionModuleProvider = new ScriptExtensionModuleProvider(
120                 scriptExtensionAccessor);
121
122         Function<Function<Object[], Object>, Function<String, Object>> wrapRequireFn = originalRequireFn -> moduleName -> scriptExtensionModuleProvider
123                 .locatorFor(delegate.getPolyglotContext(), engineIdentifier).locateModule(moduleName)
124                 .map(m -> (Object) m).orElseGet(() -> originalRequireFn.apply(new Object[] { moduleName }));
125
126         delegate.getBindings(ScriptContext.ENGINE_SCOPE).put(REQUIRE_WRAPPER_NAME, wrapRequireFn);
127         delegate.put("require", wrapRequireFn.apply((Function<Object[], Object>) delegate.get("require")));
128
129         initialized = true;
130     }
131 }