]> git.basschouten.com Git - openhab-addons.git/commitdiff
[js-transform] Introduced support for additional parameters (#10901)
authorpali <pauli.anttila@gmail.com>
Sat, 31 Jul 2021 10:42:50 +0000 (13:42 +0300)
committerGitHub <noreply@github.com>
Sat, 31 Jul 2021 10:42:50 +0000 (12:42 +0200)
* [js-transform] Introduced support for additional parameters

Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>
* Added junit tests and updated readme

Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>
* Typo fixes

Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>
* Typo fix

Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>
* Fixed junit test

Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>
bundles/org.openhab.transform.javascript/README.md
bundles/org.openhab.transform.javascript/conf/transform/js/readme/readme.js [new file with mode: 0644]
bundles/org.openhab.transform.javascript/conf/transform/readme.js [new file with mode: 0644]
bundles/org.openhab.transform.javascript/conf/transform/returntest.js [new file with mode: 0644]
bundles/org.openhab.transform.javascript/conf/transform/scale.js [new file with mode: 0644]
bundles/org.openhab.transform.javascript/conf/transform/sum.js [new file with mode: 0644]
bundles/org.openhab.transform.javascript/src/main/java/org/openhab/transform/javascript/internal/JavaScriptTransformationService.java
bundles/org.openhab.transform.javascript/src/test/java/org/openhab/transform/javascript/internal/JavaScriptTransformationServiceTest.java [new file with mode: 0644]

index bd6a09018fc9c1da6cce3f977b2367b2b96ba647..a0d7d0ce847269a5b4714713a9206be643120882 100644 (file)
@@ -5,7 +5,7 @@ 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.
 
-## Example
+## 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`).
 
@@ -18,6 +18,26 @@ transform/getValue.js:
 })(input)
 ```
 
+JavaScript transformation syntax also support additional parameters which can be passed to the script. 
+This can prevent redundancy when transformation is needed for several use cases, but with small adaptations.
+additional parameters can be passed to the script via [URI](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) query syntax.
+
+As `input` name is reserved for transformed data, it can't be used in query parameters. 
+Also `?` and `&` characters are reserved, but if they need to passed as additional data, they can be escaped according to URI syntax.
+
+
+transform/scale.js:
+```
+(function(data, cf, d) {
+    return parseFloat(data) * parseFloat(cf) / parseFloat(d);
+})(input, correctionFactor, divider)
+```
+
+`transform/scale.js?correctionFactor=1.1&divider=10`
+
+Following example will return value `23.54` when `input` data is `214`.
+
+   
 ## Test JavaScript
 
 You can use online JavaScript testers to validate your script.
diff --git a/bundles/org.openhab.transform.javascript/conf/transform/js/readme/readme.js b/bundles/org.openhab.transform.javascript/conf/transform/js/readme/readme.js
new file mode 100644 (file)
index 0000000..28537e5
--- /dev/null
@@ -0,0 +1,4 @@
+(function(i) {
+    var array = i.split(" ");
+    return array[array.length - 1].length;
+})(input)
\ No newline at end of file
diff --git a/bundles/org.openhab.transform.javascript/conf/transform/readme.js b/bundles/org.openhab.transform.javascript/conf/transform/readme.js
new file mode 100644 (file)
index 0000000..28537e5
--- /dev/null
@@ -0,0 +1,4 @@
+(function(i) {
+    var array = i.split(" ");
+    return array[array.length - 1].length;
+})(input)
\ No newline at end of file
diff --git a/bundles/org.openhab.transform.javascript/conf/transform/returntest.js b/bundles/org.openhab.transform.javascript/conf/transform/returntest.js
new file mode 100644 (file)
index 0000000..874477a
--- /dev/null
@@ -0,0 +1,3 @@
+(function(i, a, b) {
+    return b;
+})(input, a, test)
\ No newline at end of file
diff --git a/bundles/org.openhab.transform.javascript/conf/transform/scale.js b/bundles/org.openhab.transform.javascript/conf/transform/scale.js
new file mode 100644 (file)
index 0000000..329ef51
--- /dev/null
@@ -0,0 +1,3 @@
+(function(data, cf, d) {
+    return parseFloat(data) * parseFloat(cf) / parseFloat(d);
+})(input, correctionFactor, divider)
diff --git a/bundles/org.openhab.transform.javascript/conf/transform/sum.js b/bundles/org.openhab.transform.javascript/conf/transform/sum.js
new file mode 100644 (file)
index 0000000..2bf0e79
--- /dev/null
@@ -0,0 +1,3 @@
+(function(i, a, b) {
+    return parseInt(i) + parseInt(a) + parseInt(b);
+})(input, a, b)
\ No newline at end of file
index 660bd632efd7cf9ea1641c23136a93ded157985b..7a712568c2449f757b1be980b515b3ba6e50ca4f 100644 (file)
@@ -14,11 +14,16 @@ package org.openhab.transform.javascript.internal;
 
 import java.io.File;
 import java.io.FilenameFilter;
+import java.io.UnsupportedEncodingException;
 import java.net.URI;
+import java.net.URLDecoder;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 import javax.script.Bindings;
@@ -56,6 +61,8 @@ public class JavaScriptTransformationService implements TransformationService, C
     private static final String CONFIG_PARAM_FUNCTION = "function";
     private static final String[] FILE_NAME_EXTENSIONS = { "js" };
 
+    private static final String SCRIPT_DATA_WORD = "input";
+
     private final JavaScriptEngineManager manager;
 
     @Activate
@@ -70,25 +77,44 @@ public class JavaScriptTransformationService implements TransformationService, C
      * transformations one should use subfolders.
      *
      * @param filename the name of the file which contains the Java script
-     *            transformation rule. Transformation service inject input
-     *            (source) to 'input' variable.
+     *            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.
      * @param source the input to transform
      */
     @Override
     public @Nullable String transform(String filename, String source) throws TransformationException {
-        if (filename == null || source == null) {
-            throw new TransformationException("the given parameters 'filename' and 'source' must not be null");
-        }
-
         final long startTime = System.currentTimeMillis();
         logger.debug("about to transform '{}' by the JavaScript '{}'", source, filename);
 
+        Map<String, String> 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 = "";
 
         try {
-            final CompiledScript cScript = manager.getScript(filename);
+            final CompiledScript cScript = manager.getScript(fn);
             final Bindings bindings = cScript.getEngine().createBindings();
-            bindings.put("input", source);
+            bindings.put(SCRIPT_DATA_WORD, source);
+            vars.forEach((k, v) -> bindings.put(k, v));
             result = String.valueOf(cScript.eval(bindings));
             return result;
         } catch (ScriptException e) {
@@ -99,6 +125,31 @@ public class JavaScriptTransformationService implements TransformationService, C
         }
     }
 
+    private boolean isReservedWordUsed(Map<String, String> map) {
+        for (String key : map.keySet()) {
+            if (SCRIPT_DATA_WORD.equals(key)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private Map<String, String> splitQuery(@Nullable String query) throws UnsupportedEncodingException {
+        Map<String, String> result = new LinkedHashMap<>();
+        if (query != null) {
+            String[] pairs = query.split("&");
+            for (String pair : pairs) {
+                String[] keyval = pair.split("=");
+                if (keyval.length != 2) {
+                    throw new UnsupportedEncodingException();
+                } else {
+                    result.put(URLDecoder.decode(keyval[0], "UTF-8"), URLDecoder.decode(keyval[1], "UTF-8"));
+                }
+            }
+        }
+        return result;
+    }
+
     @Override
     public @Nullable Collection<ParameterOption> getParameterOptions(URI uri, String param, @Nullable String context,
             @Nullable Locale locale) {
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
new file mode 100644 (file)
index 0000000..f379b7e
--- /dev/null
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.transform.javascript.internal;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Comparator;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.openhab.core.transform.TransformationException;
+import org.osgi.framework.BundleContext;
+
+/**
+ * @author Pauli Anttila - Initial contribution
+ */
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.WARN)
+public class JavaScriptTransformationServiceTest {
+
+    private static final String BASE_FOLDER = "target";
+    private static final String SRC_FOLDER = "conf";
+    private static final String CONFIG_FOLDER = BASE_FOLDER + File.separator + SRC_FOLDER;
+
+    private @Mock BundleContext bundleContext;
+
+    private TestableJavaScriptTransformationService processor;
+
+    private class TestableJavaScriptTransformationService extends JavaScriptTransformationService {
+        public TestableJavaScriptTransformationService(JavaScriptEngineManager manager) {
+            super(manager);
+        }
+    };
+
+    @BeforeEach
+    public void setUp() throws IOException {
+        JavaScriptEngineManager manager = new JavaScriptEngineManager();
+        processor = new TestableJavaScriptTransformationService(manager);
+        copyDirectory(SRC_FOLDER, CONFIG_FOLDER);
+    }
+
+    @AfterEach
+    public void tearDown() throws IOException {
+        try (Stream<Path> walk = Files.walk(Path.of(CONFIG_FOLDER))) {
+            walk.sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
+        }
+    }
+
+    private void copyDirectory(String from, String to) throws IOException {
+        Files.walk(Paths.get(from)).forEach(fromPath -> {
+            Path toPath = Paths.get(to, fromPath.toString().substring(from.length()));
+            try {
+                Files.copy(fromPath, toPath);
+            } catch (IOException e) {
+            }
+        });
+    }
+
+    @Test
+    public void testReadmeExampleWithoutSubFolder() throws Exception {
+        final String DATA = "foo bar baz";
+        final String SCRIPT = "readme.js";
+
+        String transformedResponse = processor.transform(SCRIPT, DATA);
+        assertEquals("3", transformedResponse);
+    }
+
+    @Test
+    public void testReadmeExampleWithSubFolders() throws Exception {
+        final String DATA = "foo bar baz";
+        final String SCRIPT = "js/readme/readme.js";
+
+        String transformedResponse = processor.transform(SCRIPT, DATA);
+        assertEquals("3", transformedResponse);
+    }
+
+    @Test
+    public void testReadmeScaleExample() throws Exception {
+        final String DATA = "214";
+        final String SCRIPT = "scale.js?correctionFactor=1.1&divider=10.js";
+
+        String transformedResponse = processor.transform(SCRIPT, DATA);
+        assertEquals("23.54", transformedResponse);
+    }
+
+    @Test
+    public void testAdditionalVariables() throws Exception {
+        final String DATA = "100";
+        final String SCRIPT = "sum.js?a=10&b=1";
+
+        String transformedResponse = processor.transform(SCRIPT, DATA);
+        assertEquals("111", transformedResponse);
+    }
+
+    @Test
+    public void testIllegalVariableName() throws Exception {
+        final String DATA = "100";
+        final String SCRIPT = "sum.js?a=10&input=fail&b=1";
+
+        Exception exception = assertThrows(TransformationException.class, () -> processor.transform(SCRIPT, DATA));
+        assertEquals("'input' word is reserved and can't be used in additional parameters", exception.getMessage());
+    }
+
+    @Test
+    public void testIllegalQuestionmarkSequence() throws Exception {
+        final String DATA = "100";
+        final String SCRIPT = "sum.js?a=1&test=ab?d&b=2";
+
+        Exception exception = assertThrows(TransformationException.class, () -> processor.transform(SCRIPT, DATA));
+        assertEquals("Questionmark should be defined only once in the filename", exception.getMessage());
+    }
+
+    @Test
+    public void testIllegalAmbersandSequence() throws Exception {
+        final String DATA = "foo";
+        final String SCRIPT = "returntest.js?a=1&test=ab&d&b=2";
+
+        Exception exception = assertThrows(TransformationException.class, () -> processor.transform(SCRIPT, DATA));
+        assertEquals("Illegal filename syntax", exception.getMessage());
+    }
+
+    @Test
+    public void testEncodedSpecialCharacters() throws Exception {
+        final String DATA = "100";
+        final String SCRIPT = "returntest.js?a=1&test=ab%3Fd%26f&b=2";
+
+        String transformedResponse = processor.transform(SCRIPT, DATA);
+        assertEquals("ab?d&f", transformedResponse);
+    }
+}