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`).
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
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;
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;
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
* @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) {
}
}
+ /**
+ * 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.
*
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);
+ }
}
}
/**
- * 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));
<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">
});
}
+ @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";