2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.transform.javascript.internal;
16 import java.io.FileInputStream;
17 import java.io.IOException;
18 import java.io.InputStreamReader;
19 import java.io.Reader;
20 import java.nio.charset.StandardCharsets;
21 import java.security.MessageDigest;
22 import java.security.NoSuchAlgorithmException;
23 import java.time.Duration;
24 import java.util.Base64;
26 import java.util.concurrent.ConcurrentHashMap;
28 import javax.script.Compilable;
29 import javax.script.CompiledScript;
30 import javax.script.ScriptEngine;
31 import javax.script.ScriptEngineManager;
32 import javax.script.ScriptException;
34 import org.eclipse.jdt.annotation.NonNullByDefault;
35 import org.openhab.core.cache.ExpiringCacheMap;
36 import org.openhab.core.transform.TransformationException;
37 import org.osgi.service.component.annotations.Component;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * Simple cache for compiled JavaScript files.
44 * @author Thomas Kordelle - Initial contribution
45 * @author Thomas Kordelle - pre compiled scripts
48 @Component(service = JavaScriptEngineManager.class)
49 public class JavaScriptEngineManager {
51 private final Logger logger = LoggerFactory.getLogger(JavaScriptEngineManager.class);
52 private final ScriptEngineManager manager = new ScriptEngineManager();
53 /* keep memory foot print low. max 2 concurrent threads are estimated */
54 private final Map<String, CompiledScript> compiledScriptMap = new ConcurrentHashMap<>(4, 0.5f, 2);
55 private final ExpiringCacheMap<String, CompiledScript> cacheForInlineScripts = new ExpiringCacheMap<>(
59 * Get a pre compiled script {@link CompiledScript} from cache. If it is not in the cache, then load it from
60 * storage and put a pre compiled version into the cache.
62 * @param filename name of the JavaScript file to load
63 * @return a pre compiled script {@link CompiledScript}
64 * @throws TransformationException if compile of JavaScript failed
66 protected CompiledScript getCompiledScriptByFilename(final String filename) throws TransformationException {
67 synchronized (compiledScriptMap) {
68 CompiledScript compiledScript = compiledScriptMap.get(filename);
69 if (compiledScript != null) {
70 logger.debug("Loading JavaScript {} from cache.", filename);
71 return compiledScript;
73 final String path = TransformationScriptWatcher.TRANSFORM_FOLDER + File.separator + filename;
74 logger.debug("Loading script {} from storage ", path);
75 try (final Reader reader = new InputStreamReader(new FileInputStream(path))) {
76 final ScriptEngine engine = manager.getEngineByName("javascript");
77 final CompiledScript cScript = ((Compilable) engine).compile(reader);
78 logger.debug("Putting compiled JavaScript {} to cache.", cScript);
79 compiledScriptMap.put(filename, cScript);
81 } catch (IOException | ScriptException e) {
82 throw new TransformationException("An error occurred while loading JavaScript. " + e.getMessage(),
90 * Get a pre compiled script {@link CompiledScript} from cache. If it is not in the cache, then compile
91 * it and put a pre compiled version into the cache.
93 * @param script JavaScript which should be returned as a pre compiled
94 * @return a pre compiled script {@link CompiledScript}
95 * @throws TransformationException if compile of JavaScript failed
97 protected CompiledScript getCompiledScriptByInlineScript(final String script) throws TransformationException {
98 synchronized (cacheForInlineScripts) {
100 final String hash = calcHash(script);
101 final CompiledScript compiledScript = cacheForInlineScripts.get(hash);
102 if (compiledScript != null) {
103 logger.debug("Loading JavaScript from cache.");
104 return compiledScript;
106 logger.debug("Compiling script {}", script);
107 final ScriptEngine engine = manager.getEngineByName("javascript");
108 final CompiledScript cScript = ((Compilable) engine).compile(script);
109 cacheForInlineScripts.put(hash, () -> cScript);
112 } catch (ScriptException | NoSuchAlgorithmException e) {
113 throw new TransformationException("An error occurred while compiling JavaScript. " + e.getMessage(), e);
119 * remove a pre compiled script from cache.
121 * @param fileName name of the script file to remove
123 protected void removeFromCache(String fileName) {
124 logger.debug("Removing JavaScript {} from cache.", fileName);
125 compiledScriptMap.remove(fileName);
128 private String calcHash(final String script) throws NoSuchAlgorithmException {
129 MessageDigest digest = MessageDigest.getInstance("SHA-256");
130 byte[] hash = digest.digest(script.getBytes(StandardCharsets.UTF_8));
131 return Base64.getEncoder().encodeToString(hash);