diff --git a/bundles/org.openhab.transform.javascript/README.md b/bundles/org.openhab.transform.javascript/README.md index a0d7d0ce8..1ca1c5c96 100644 --- a/bundles/org.openhab.transform.javascript/README.md +++ b/bundles/org.openhab.transform.javascript/README.md @@ -5,6 +5,10 @@ Transform an input to an output using JavaScript. It expects the transformation rule to be read from a file which is stored under the `transform` folder. To organize the various transformations, one should use subfolders. +Simple transformation rules can also be given as a inline script. +Inline script should be start by `|` character following the JavaScript. +Beware that complex inline script could cause issues to e.g. item file parsing. + ## Examples Let's assume we have received a string containing `foo bar baz` and we're looking for a length of the last word (`baz`). @@ -37,6 +41,10 @@ transform/scale.js: Following example will return value `23.54` when `input` data is `214`. +### Inline script example: + +Normally JavaScript transformation is given by filename, e.g. `JS(transform/getValue.js)`. +Inline script can be given by `|` character following the JavaScript, e.g. `JS(| input / 10)`. ## Test JavaScript diff --git a/bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptEngineManager.java b/bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptEngineManager.java index 97ec03dad..d7dd93d65 100644 --- a/bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptEngineManager.java +++ b/bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptEngineManager.java @@ -17,6 +17,11 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.Duration; +import java.util.Base64; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -27,6 +32,7 @@ import javax.script.ScriptEngineManager; import javax.script.ScriptException; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.cache.ExpiringCacheMap; import org.openhab.core.transform.TransformationException; import org.osgi.service.component.annotations.Component; import org.slf4j.Logger; @@ -46,6 +52,8 @@ public class JavaScriptEngineManager { private final ScriptEngineManager manager = new ScriptEngineManager(); /* keep memory foot print low. max 2 concurrent threads are estimated */ private final Map compiledScriptMap = new ConcurrentHashMap<>(4, 0.5f, 2); + private final ExpiringCacheMap cacheForInlineScripts = new ExpiringCacheMap<>( + Duration.ofDays(1)); /** * Get a pre compiled script {@link CompiledScript} from cache. If it is not in the cache, then load it from @@ -55,7 +63,7 @@ public class JavaScriptEngineManager { * @return a pre compiled script {@link CompiledScript} * @throws TransformationException if compile of JavaScript failed */ - protected CompiledScript getScript(final String filename) throws TransformationException { + protected CompiledScript getCompiledScriptByFilename(final String filename) throws TransformationException { synchronized (compiledScriptMap) { CompiledScript compiledScript = compiledScriptMap.get(filename); if (compiledScript != null) { @@ -78,6 +86,35 @@ public class JavaScriptEngineManager { } } + /** + * Get a pre compiled script {@link CompiledScript} from cache. If it is not in the cache, then compile + * it and put a pre compiled version into the cache. + * + * @param script JavaScript which should be returned as a pre compiled + * @return a pre compiled script {@link CompiledScript} + * @throws TransformationException if compile of JavaScript failed + */ + protected CompiledScript getCompiledScriptByInlineScript(final String script) throws TransformationException { + synchronized (cacheForInlineScripts) { + try { + final String hash = calcHash(script); + final CompiledScript compiledScript = cacheForInlineScripts.get(hash); + if (compiledScript != null) { + logger.debug("Loading JavaScript from cache."); + return compiledScript; + } else { + logger.debug("Compiling script {}", script); + final ScriptEngine engine = manager.getEngineByName("javascript"); + final CompiledScript cScript = ((Compilable) engine).compile(script); + cacheForInlineScripts.put(hash, () -> cScript); + return cScript; + } + } catch (ScriptException | NoSuchAlgorithmException e) { + throw new TransformationException("An error occurred while compiling JavaScript. " + e.getMessage(), e); + } + } + } + /** * remove a pre compiled script from cache. * @@ -87,4 +124,10 @@ public class JavaScriptEngineManager { logger.debug("Removing JavaScript {} from cache.", fileName); compiledScriptMap.remove(fileName); } + + private String calcHash(final String script) throws NoSuchAlgorithmException { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hash = digest.digest(script.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(hash); + } } diff --git a/bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptTransformationService.java b/bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptTransformationService.java index 7a712568c..2294d11bf 100644 --- a/bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptTransformationService.java +++ b/bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptTransformationService.java @@ -71,47 +71,55 @@ public class JavaScriptTransformationService implements TransformationService, C } /** - * Transforms the input source by Java Script. It expects the + * Transforms the input source by Java Script. If script is a filename, it expects the * transformation rule to be read from a file which is stored under the * 'configurations/transform' folder. To organize the various * transformations one should use subfolders. * - * @param filename the name of the file which contains the Java script + * @param filenameOrInlineScript parameter can be 1) the name of the file which contains the Java script * transformation rule. Filename can also include additional * variables in URI query variable format which will be injected - * to script engine. Transformation service inject input (source) - * to 'input' variable. + * to script engine. 2) inline script when starting with '|' character. + * Transformation service inject input (source) to 'input' variable. * @param source the input to transform */ @Override - public @Nullable String transform(String filename, String source) throws TransformationException { + public @Nullable String transform(String filenameOrInlineScript, String source) throws TransformationException { final long startTime = System.currentTimeMillis(); - logger.debug("about to transform '{}' by the JavaScript '{}'", source, filename); + logger.debug("about to transform '{}' by the JavaScript '{}'", source, filenameOrInlineScript); Map vars = Collections.emptyMap(); - String fn = filename; - - if (filename.contains("?")) { - String[] parts = filename.split("\\?"); - if (parts.length > 2) { - throw new TransformationException("Questionmark should be defined only once in the filename"); - } - fn = parts[0]; - try { - vars = splitQuery(parts[1]); - } catch (UnsupportedEncodingException e) { - throw new TransformationException("Illegal filename syntax"); - } - if (isReservedWordUsed(vars)) { - throw new TransformationException( - "'" + SCRIPT_DATA_WORD + "' word is reserved and can't be used in additional parameters"); - } - } - String result = ""; + CompiledScript cScript; + + if (filenameOrInlineScript.startsWith("|")) { + // inline java script + cScript = manager.getCompiledScriptByInlineScript(filenameOrInlineScript.substring(1)); + } else { + String filename = filenameOrInlineScript; + + if (filename.contains("?")) { + String[] parts = filename.split("\\?"); + if (parts.length > 2) { + throw new TransformationException("Questionmark should be defined only once in the filename"); + } + filename = parts[0]; + try { + vars = splitQuery(parts[1]); + } catch (UnsupportedEncodingException e) { + throw new TransformationException("Illegal filename syntax"); + } + if (isReservedWordUsed(vars)) { + throw new TransformationException( + "'" + SCRIPT_DATA_WORD + "' word is reserved and can't be used in additional parameters"); + } + } + + cScript = manager.getCompiledScriptByFilename(filename); + } + try { - final CompiledScript cScript = manager.getScript(fn); final Bindings bindings = cScript.getEngine().createBindings(); bindings.put(SCRIPT_DATA_WORD, source); vars.forEach((k, v) -> bindings.put(k, v)); diff --git a/bundles/org.openhab.transform.javascript/src/main/resources/OH-INF/config/javascriptProfile.xml b/bundles/org.openhab.transform.javascript/src/main/resources/OH-INF/config/javascriptProfile.xml index 9bae0c235..ad4f046ce 100644 --- a/bundles/org.openhab.transform.javascript/src/main/resources/OH-INF/config/javascriptProfile.xml +++ b/bundles/org.openhab.transform.javascript/src/main/resources/OH-INF/config/javascriptProfile.xml @@ -6,9 +6,9 @@ - - Filename of the JavaScript in the transform folder. The state will be available in the variable - \"input\". + + Filename of the JavaScript in the transform folder or inline script starting with "|" character. The + state will be available in the variable "input". false diff --git a/bundles/org.openhab.transform.javascript/src/test/java/org/openhab/transform/javascript/internal/JavaScriptTransformationServiceTest.java b/bundles/org.openhab.transform.javascript/src/test/java/org/openhab/transform/javascript/internal/JavaScriptTransformationServiceTest.java index f379b7e6c..33751571c 100644 --- a/bundles/org.openhab.transform.javascript/src/test/java/org/openhab/transform/javascript/internal/JavaScriptTransformationServiceTest.java +++ b/bundles/org.openhab.transform.javascript/src/test/java/org/openhab/transform/javascript/internal/JavaScriptTransformationServiceTest.java @@ -78,6 +78,24 @@ public class JavaScriptTransformationServiceTest { }); } + @Test + public void testInlineScript() throws Exception { + final String DATA = "100"; + final String SCRIPT = "| input / 10"; + + String transformedResponse = processor.transform(SCRIPT, DATA); + assertEquals("10.0", transformedResponse); + } + + @Test + public void testInlineScriptIncludingPipe() throws Exception { + final String DATA = "1"; + final String SCRIPT = "| false || (input == '1')"; + + String transformedResponse = processor.transform(SCRIPT, DATA); + assertEquals("true", transformedResponse); + } + @Test public void testReadmeExampleWithoutSubFolder() throws Exception { final String DATA = "foo bar baz";