[js-transform] inline java script support (#11473)
* [js-transform] inline java script support Signed-off-by: Pauli Anttila <pauli.anttila@gmail.com>
This commit is contained in:
parent
26729956bc
commit
45729890b1
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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));
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in New Issue