[js-transform] Introduced support for additional parameters (#10901)

* [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>
This commit is contained in:
pali
2021-07-31 13:42:50 +03:00
committed by GitHub
parent 02c2513e28
commit 4b57ea28c8
8 changed files with 249 additions and 9 deletions

View 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) {

View File

@@ -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);
}
}