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