]> git.basschouten.com Git - openhab-addons.git/commitdiff
[js-transform] inline java script support (#11473)
authorpali <pauli.anttila@gmail.com>
Sun, 12 Dec 2021 21:09:36 +0000 (23:09 +0200)
committerGitHub <noreply@github.com>
Sun, 12 Dec 2021 21:09:36 +0000 (22:09 +0100)
* [js-transform] inline java script support

Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>
bundles/org.openhab.transform.javascript/README.md
bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptEngineManager.java
bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptTransformationService.java
bundles/org.openhab.transform.javascript/src/main/resources/OH-INF/config/javascriptProfile.xml
bundles/org.openhab.transform.javascript/src/test/java/org/openhab/transform/javascript/internal/JavaScriptTransformationServiceTest.java

index a0d7d0ce847269a5b4714713a9206be643120882..1ca1c5c9624e4282c5d63501cab3f2d31200f1fb 100644 (file)
@@ -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
 
index 97ec03dad3ab461dbeedee4ba4cf20486deacbc0..d7dd93d657157849f9e06d83dd49796ad116d2ee 100644 (file)
@@ -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<String, CompiledScript> compiledScriptMap = new ConcurrentHashMap<>(4, 0.5f, 2);
+    private final ExpiringCacheMap<String, CompiledScript> 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);
+    }
 }
index 7a712568c2449f757b1be980b515b3ba6e50ca4f..2294d11bf1b835bf875bad48ef917442d871ab46 100644 (file)
@@ -71,47 +71,55 @@ public class JavaScriptTransformationService implements TransformationService, C
     }
 
     /**
-     * Transforms the input <code>source</code> by Java Script. It expects the
+     * Transforms the input <code>source</code> 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<String, String> vars = Collections.emptyMap();
-        String fn = filename;
+        String result = "";
 
-        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");
+        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");
+                }
             }
-        }
 
-        String result = "";
+            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));
index 9bae0c235c660dff2308696ded83b8b94e031e01..ad4f046ce11dc2f6c3a236c503d4974be784555b 100644 (file)
@@ -6,9 +6,9 @@
 
        <config-description uri="profile:transform:JS">
                <parameter name="function" type="text" required="true">
-                       <label>JavaScript Filename</label>
-                       <description>Filename of the JavaScript in the transform folder. The state will be available in the variable
-                               \"input\".</description>
+                       <label>JavaScript Filename or Inline Script</label>
+                       <description>Filename of the JavaScript in the transform folder or inline script starting with "|" character. The
+                               state will be available in the variable "input".</description>
                        <limitToOptions>false</limitToOptions>
                </parameter>
                <parameter name="sourceFormat" type="text">
index f379b7e6c4dd299cba92b291089214c11eedf8b7..33751571c666b1245ca5f6ce35eac053a241567d 100644 (file)
@@ -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";