diff --git a/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java b/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java new file mode 100644 index 0000000000..11b61f64c4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/script/AbstractTbelInvokeTest.java @@ -0,0 +1,48 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.script; + +import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.ScriptType; +import org.thingsboard.script.api.tbel.TbelInvokeService; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.controller.AbstractControllerTest; + +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; + +public abstract class AbstractTbelInvokeTest extends AbstractControllerTest { + + @Autowired + protected TbelInvokeService invokeService; + + protected UUID evalScript(String script) throws ExecutionException, InterruptedException { + return invokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, script, "msg", "metadata", "msgType").get(); + } + + protected String invokeScriptResultString(UUID scriptId, String str) throws ExecutionException, InterruptedException { + var msg = JacksonUtil.fromString(str, Map.class); + return invokeScript(scriptId, str).toString(); + } + protected Object invokeScript(UUID scriptId, String str) throws ExecutionException, InterruptedException { + var msg = JacksonUtil.fromString(str, Map.class); + return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get(); + } +} diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java new file mode 100644 index 0000000000..188599a530 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeDocsIoTest.java @@ -0,0 +1,2151 @@ +/** + * Copyright © 2016-2024 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.script; + +import org.junit.jupiter.api.Test; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.script.api.tbel.TbDate; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DaoSqlTest +class TbelInvokeDocsIoTest extends AbstractTbelInvokeTest { + + private String decoderStr; + private String msgStr; + + private final String msgMapStr = """ + {"temperature": 42, "nested" : "508"}; + """; + private final LinkedHashMap expectedMap = new LinkedHashMap<>(Map.of("temperature", 42, "nested", "508")); + + // Simple Property Expression + @Test + void simplePropertyBooleanExpression_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"temperature": 15, "humidity" : 30} + """; + decoderStr = "return (msg.temperature > 10 && msg.temperature < 20) || (msg.humidity > 10 && msg.humidity < 60);"; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + boolean expected = true; + assertEquals(expected, actual); + } + + // Multiple statements + @Test + public void multipleStatements_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var a = 2; + var b = 2; + return a + b; + """; + Integer expected = 4; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // Maps + @Test + public void mapsChangeValueKey_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.temperature = 0; + return msg; + """; + LinkedHashMap expected = expectedMap; + expected.put("temperature", 0); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + + @Test + public void mapsCheckExistenceKey_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + if(msg.temperature != null){ + msg.temperature = 2; + } + return msg; + """; + LinkedHashMap expected = expectedMap; + expected.put("temperature", 2); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsNullSafeExpressionsUsing_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.existingKey = {}; + msg.existingKey.smth = 20; + if(msg.?nonExistingKey.smth > 10){ + msg.temperature = 2; + } + if(msg.?existingKey.smth > 10){ + msg.nested = "100"; + } + return msg; + """; + LinkedHashMap expected = expectedMap; + expected.put("nested", "100"); + LinkedHashMap existingKey = new LinkedHashMap<>(Map.of("smth", 20)); + expected.put("existingKey", existingKey); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsIterateThrough_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + foreach(element : msg.entrySet()){ + if(element.getKey() == null){ + return raiseError("Bad getKey"); + } + if(element.key == null){ + return raiseError("Bad key"); + } + if(element.getValue() == null){ + return raiseError("Bad getValue"); + } + if(element.value == null){ + return raiseError("Bad value"); + } + } + return msg; + """; + LinkedHashMap expected = expectedMap; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsGetInfoSize_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + var size = msg.size(); + return size; + """; + Integer expected = 2; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsGetInfoMemorySize_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + var memorySize = msg.memorySize(); + return memorySize; + """; + Long expected = 24L; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsGetInfoPut_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.humidity = 73; + msg.put("humidity", 74); + msg.putIfAbsent("temperature1", 73); + return msg; + """; + LinkedHashMap expected = expectedMap; + expected.put("humidity", 74); + expected.putIfAbsent("temperature1", 73); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsChangeValuePut_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.temperature = 73; + msg.putIfAbsent("temperature1", 73); + var put1 = msg.put("temperature", 74); + msg.putIfAbsent("humidity", 73); + var putIfAbsent1 = msg.putIfAbsent("humidity", 74); + return {map: msg, + put1: put1, + putIfAbsent1: putIfAbsent1 + }; + """; + LinkedHashMap expectedMapChangeValue = expectedMap; + expectedMapChangeValue.replace("temperature", 42, 74); + expectedMapChangeValue.put("humidity", 73); + expectedMapChangeValue.putIfAbsent("temperature1", 73); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("map", expectedMapChangeValue); + expected.put("put1", 73); + expected.put("putIfAbsent1", 73); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsChangeValueReplace_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.put("humidity", 73); + msg.putIfAbsent("temperature1", 73); + var replace = msg.replace("temperature", 56); + var replace1 = msg.replace("temperature", 56, 45); + var replace2 = msg.replace("temperature", 48, 56); + return {map: msg, + replace: replace, + replace1: replace1, + replace2: replace2 + } + """; + LinkedHashMap expectedMapReplaceValue = expectedMap; + expectedMapReplaceValue.replace("temperature", 42, 45); + expectedMapReplaceValue.put("humidity", 73); + expectedMapReplaceValue.putIfAbsent("temperature1", 73); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("map", expectedMapReplaceValue); + expected.put("replace", 42); + expected.put("replace1", true); + expected.put("replace2", false); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsRemoveEntryFromMapByKey_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.put("humidity", 73); + msg.putIfAbsent("temperature1", 73); + msg.remove("temperature"); + return msg; + """; + LinkedHashMap expected = expectedMap; + expected.put("humidity", 73); + expected.putIfAbsent("temperature1", 73); + expected.remove("temperature"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsGetKeysValues_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.put("humidity", 73); + msg.putIfAbsent("temperature1", 73); + msg.remove("temperature"); + var keys = msg.keys(); + var values = msg.values(); + return {keys: keys, + values: values + } + """; + LinkedHashMap expectedMapKeysValues = expectedMap; + expectedMapKeysValues.put("humidity", 73); + expectedMapKeysValues.putIfAbsent("temperature1", 73); + expectedMapKeysValues.remove("temperature"); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("keys", expectedMapKeysValues.keySet().stream().toList()); + expected.put("values", expectedMapKeysValues.values().stream().toList()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void mapsSort_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.put("humidity", 73); + msg.putIfAbsent("temperature1", 73); + msg.remove("temperature"); + msg.sortByKey(); + var mapSortByKey = msg.clone(); + var sortByValue = msg.sortByValue(); + return {mapSortByKey: mapSortByKey, + sortByValue: sortByValue, + mapSortByValue: msg + } + """; + LinkedHashMap expectedMapSortByKey = new LinkedHashMap<>(); + expectedMapSortByKey.put("humidity", 73); + expectedMapSortByKey.put("nested", "508"); + expectedMapSortByKey.put("temperature1", 73); + LinkedHashMap expectedMapSortByValue = new LinkedHashMap<>(); + expectedMapSortByValue.put("humidity", 73); + expectedMapSortByValue.put("temperature1", 73); + expectedMapSortByValue.put("nested", "508"); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("mapSortByKey", expectedMapSortByKey); + expected.put("mapSortByValue", expectedMapSortByValue); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + assertEquals(expected.get("mapSortByKey"), ((LinkedHashMap) actual).get("mapSortByKey")); + assertEquals(((LinkedHashMap) expected.get("mapSortByKey")).keySet().toArray()[0], ((LinkedHashMap) ((LinkedHashMap) actual).get("mapSortByKey")).keySet().toArray()[0]); + assertEquals(((LinkedHashMap) expected.get("mapSortByKey")).keySet().toArray()[1], ((LinkedHashMap) ((LinkedHashMap) actual).get("mapSortByKey")).keySet().toArray()[1]); + assertEquals(((LinkedHashMap) expected.get("mapSortByKey")).keySet().toArray()[2], ((LinkedHashMap) ((LinkedHashMap) actual).get("mapSortByKey")).keySet().toArray()[2]); + assertEquals(expected.get("mapSortByValue"), ((LinkedHashMap) actual).get("mapSortByValue")); + assertEquals(((LinkedHashMap) expected.get("mapSortByValue")).keySet().toArray()[0], ((LinkedHashMap) ((LinkedHashMap) actual).get("mapSortByValue")).keySet().toArray()[0]); + assertEquals(((LinkedHashMap) expected.get("mapSortByValue")).keySet().toArray()[1], ((LinkedHashMap) ((LinkedHashMap) actual).get("mapSortByValue")).keySet().toArray()[1]); + assertEquals(((LinkedHashMap) expected.get("mapSortByValue")).keySet().toArray()[2], ((LinkedHashMap) ((LinkedHashMap) actual).get("mapSortByValue")).keySet().toArray()[2]); + } + + @Test + public void mapsAddNewEntry_Test() throws ExecutionException, InterruptedException { + msgStr = msgMapStr; + decoderStr = """ + msg.put("humidity", 73); + msg.putIfAbsent("temperature1", 73); + msg.remove("temperature"); + var mapAdd = {"test": 12, "input" : {"http": 130}}; + msg.putAll(mapAdd); + return msg; + """; + LinkedHashMap expected = expectedMap; + expected.put("humidity", 73); + expected.putIfAbsent("temperature1", 73); + expected.remove("temperature"); + LinkedHashMap expectedMapAdd = new LinkedHashMap<>(); + expectedMapAdd.put("test", 12); + LinkedHashMap expectedInput = new LinkedHashMap<>(); + expectedInput.put("http", 130); + expectedMapAdd.put("input", expectedInput); + expected.putAll(expectedMapAdd); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // Lists + @Test + public void listsCreateNewList_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["A", "B", "C"]} + """; + decoderStr = """ + var list = msg.list; + var list0 = list[0]; + var listSize = list.size(); + var smthForeach = ""; + foreach (item : list) { + smthForeach += item; + } + var smthForLoop= ""; + for (var i =0; i < list.size; i++) { + smthForLoop += list[i]; + } + return {list: list, + list0: list0, + listSize: listSize, + smthForeach: smthForeach, + smthForLoop: smthForLoop + } + """; + List expectedList = new ArrayList<>(List.of("A", "B", "C")); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("list", expectedList); + expected.put("list0", expectedList.get(0)); + expected.put("listSize", expectedList.size()); + AtomicReference smth = new AtomicReference<>(""); + expectedList.forEach(s -> smth.updateAndGet(v -> v + s)); + expected.put("smthForeach", smth.get()); + expected.put("smthForLoop", smth.get()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + /** + * add/push + * delete + */ + @Test + public void listAddPushDelete_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["A", "B", "C", "B", "C", "hello", 34567]} + """; + decoderStr = """ + var list = msg.list; + var msgRez = {}; + // add/push + msgRez.put("list", list.clone()); + var listAdd = ["thigsboard", 4, 67]; + var addAll = list.addAll(listAdd); + msgRez.put("addAll", addAll); + msgRez.put("addAllList", list.clone()); + var addAllIndex = list.addAll(2, listAdd); + msgRez.put("addAllIndex", addAllIndex); + msgRez.put("addAllIndexList", list.clone()); + var addIndex = list.add(3, "thigsboard"); + if(addIndex != null) { + msgRez.put("addIndex", addIndex); + }; + msgRez.put("addIndexList", list.clone()); + var push = list.push("thigsboard"); + msgRez.put("push", push); + if(push != null) { + msgRez.put("push", push); + } + msgRez.put("pushList", list.clone()); + var unshift = list.unshift("r"); + msgRez.put("unshift", unshift); + msgRez.put("unshiftList", list.clone()); + var unshiftQ = ["Q", 4]; + msgRez.put("unshift", list.unshift(unshiftQ)); + msgRez.put("unshiftQunList", list.clone()); + // delete + msgRez.put("removeIndex2", list.remove(2)); + msgRez.put("removeIndex2List", list.clone()); + msgRez.put("removeValueC", list.remove("C")); + msgRez.put("removeValueCList", list.clone()); + msgRez.put("shift", list.shift()); + msgRez.put("shiftList", list.clone()); + msgRez.put("pop", list.pop()); + msgRez.put("popList", list.clone()); + msgRez.put("splice3", list.splice(3)); + msgRez.put("splice3List", list.clone()); + msgRez.put("splice2", list.splice(2, 2)); + msgRez.put("splice2List", list.clone()); + msgRez.put("splice1_4", list.splice(1, 4, "start", 5, "end")); + msgRez.put("splice1_4List", list.clone()); + return msgRez; + """; + ArrayList list = new ArrayList<>(List.of("A", "B", "C", "B", "C", "hello", 34567)); + LinkedHashMap expected = new LinkedHashMap<>(); + // add/push + expected.put("list", list.clone()); + List listAdd = new ArrayList<>(List.of("thigsboard", 4, 67)); + expected.put("addAll", list.addAll(listAdd)); + expected.put("addAllList", list.clone()); + expected.put("addAllIndex", list.addAll(2, listAdd)); + expected.put("addAllIndexList", list.clone()); + list.add(3, "thigsboard"); + expected.put("addIndexList", list.clone()); + expected.put("push", list.add("thigsboard")); + expected.put("pushList", list.clone()); + list.add(0, "r"); + expected.put("unshiftList", list.clone()); + ArrayList unshiftQ = new ArrayList<>(List.of("Q", 4)); + list.add(0, unshiftQ); + expected.put("unshiftQunList", list.clone()); + // delete + expected.put("removeIndex2", list.remove(2)); + expected.put("removeIndex2List", list.clone()); + expected.put("removeValueC", list.remove("C")); + expected.put("removeValueCList", list.clone()); + expected.put("shift", list.remove(0)); + expected.put("shiftList", list.clone()); + expected.put("pop", list.remove(list.size() - 1)); + expected.put("popList", list.clone()); + expected.put("splice3", splice(list, 3)); + expected.put("splice3List", list.clone()); + expected.put("splice2", splice(list, 2, 2)); + expected.put("splice2List", list.clone()); + expected.put("splice1_4", splice(list, 1, 4, "start", 5, "end")); + expected.put("splice1_4List", list.clone()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void listChange_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": ["r", "start", 5, "end"]} + """; + decoderStr = """ + var list = msg.list; + var msgRez = {}; + msgRez.put("list", list.clone()); + msgRez.put("set", list.set(3, "65")); + msgRez.put("setList", list.clone()); + list[1] = "98"; + msgRez.put("listChangeIndex1List", list.clone()); + list[0] = 2096; + msgRez.put("listChangeIndex0List", list.clone()); + return msgRez; + """; + ArrayList list = new ArrayList<>(List.of("r", "start", 5, "end")); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("list", list.clone()); + expected.put("set", list.set(3, "65")); + expected.put("setList", list.clone()); + list.set(1, "98"); + expected.put("listChangeIndex1List", list.clone()); + list.set(0, 2096); + expected.put("listChangeIndex0List", list.clone()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void listSort_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": [2096, "98", 5, "65"]} + """; + decoderStr = """ + var list = msg.list; + var msgRez = {}; + msgRez.put("list", list.clone()); + msgRez.put("sort", list.sort()); + msgRez.put("sortList", list.clone()); + msgRez.put("sortTrue", list.sort(true)); + msgRez.put("sortTrueList", list.clone()); + msgRez.put("sortFalse", list.sort(false)); + msgRez.put("sortFalseList", list.clone()); + msgRez.put("reverse", list.reverse()); + msgRez.put("reverseList", list.clone()); + msgRez.put("fill", list.fill(67)); + msgRez.put("fillList", list.clone()); + msgRez.put("fill4", list.fill(4, 1)); + msgRez.put("fill4List", list.clone()); + msgRez.put("fill4_6", list.fill(2, 1, 4)); + msgRez.put("fill4_6List", list.clone()); + return msgRez; + """; + ArrayList list = new ArrayList<>(List.of(2096, "98", 5, "65")); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("list", list.clone()); + list.sort(numericCompAsc); + expected.put("sortList", list.clone()); + expected.put("sortTrueList", list.clone()); + list.sort(numericCompDesc); + expected.put("sortFalseList", list.clone()); + Collections.reverse(list); + expected.put("reverseList", list.clone()); + expected.put("fill", fill(list, 67)); + expected.put("fillList", list.clone()); + expected.put("fill4", fill(list, 4, 1)); + expected.put("fill4List", list.clone()); + expected.put("fill4_6", fill(list, 2, 1, 4)); + expected.put("fill4_6List", list.clone()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void listNewListOrStringSort_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": [67, 2, 2, 2]} + """; + decoderStr = """ + var list = msg.list; + var listAdd = ["thigsboard", 4, 67]; + var msgRez = {}; + msgRez.put("list", list.clone()); + msgRez.put("toSorted", list.toSorted()); + msgRez.put("toSortedList", list.clone()); + msgRez.put("toSortedTrue", list.toSorted(true)); + msgRez.put("toSortedTrueList", list.clone()); + msgRez.put("toSortedFalse", list.toSorted(false)); + msgRez.put("toSortedFalseList", list.clone()); + msgRez.put("toReversed", list.toReversed()); + msgRez.put("toReversedList", list.clone()); + msgRez.put("slice", list.slice()); + msgRez.put("sliceList", list.clone()); + msgRez.put("slice4", list.slice(3)); + msgRez.put("slice4List", list.clone()); + msgRez.put("slice1_5", list.slice(0,2)); + msgRez.put("slice1_5List", list.clone()); + msgRez.put("with1", list.with(1, 69)); + msgRez.put("with1List", list.clone()); + msgRez.put("concat", list.concat(listAdd)); + msgRez.put("concatList", list.clone()); + msgRez.put("join", list.join()); + msgRez.put("joinList", list.clone()); + msgRez.put("toSpliced2", list.toSpliced(1, 0, "Feb")); + msgRez.put("toSpliced2List", list.clone()); + msgRez.put("toSpliced0_2", list.toSpliced(0, 2)); + msgRez.put("toSpliced0_2List", list.clone()); + msgRez.put("toSpliced2_2", list.toSpliced(2, 2)); + msgRez.put("toSpliced2_2List", list.clone()); + msgRez.put("toSpliced4_5", list.toSpliced(2, 4, "start", 5, "end")); + msgRez.put("toSpliced4_5List", list.clone()); + return msgRez; + """; + ArrayList list = new ArrayList<>(List.of(67, 2, 2, 2)); + ArrayList oldList = (ArrayList) list.clone(); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("list", oldList); + list.sort(numericCompAsc); + expected.put("toSorted", list); + expected.put("toSortedList", oldList); + expected.put("toSortedTrue", list); + expected.put("toSortedTrueList", oldList); + list = (ArrayList) oldList.clone(); + list.sort(numericCompDesc); + expected.put("toSortedFalse", list); + expected.put("toSortedFalseList", oldList); + list = (ArrayList) oldList.clone(); + Collections.reverse(list); + expected.put("toReversed", list); + expected.put("toReversedList", oldList); + list = (ArrayList) oldList.clone(); + expected.put("slice", list); + expected.put("sliceList", oldList); + expected.put("slice4", list.subList(3, 4)); + expected.put("slice4List", oldList); + expected.put("slice1_5", list.subList(0, 2)); + expected.put("slice1_5List", oldList); + list = (ArrayList) oldList.clone(); + list.set(1, 69); + list.add(2); + expected.put("with1", list); + expected.put("with1List", oldList); + list = (ArrayList) oldList.clone(); + List listAdd = new ArrayList<>(List.of("thigsboard", 4, 67)); + list.addAll(listAdd); + expected.put("concat", list); + expected.put("concatList", oldList); + list = (ArrayList) oldList.clone(); + String join = list.toString().substring(1, list.toString().length() - 1).replaceAll(" ", ""); + expected.put("join", join); + expected.put("joinList", oldList); + list.set(1, "Feb"); + list.add(2); + expected.put("toSpliced2", list); + expected.put("toSpliced2List", oldList); + list = (ArrayList) oldList.clone(); + list.remove(0); + list.remove(0); + expected.put("toSpliced0_2", list); + expected.put("toSpliced0_2List", oldList); + list = (ArrayList) oldList.clone(); + list.remove(2); + list.remove(2); + expected.put("toSpliced2_2", list); + expected.put("toSpliced2_2List", oldList); + list = (ArrayList) oldList.clone(); + list.remove(2); + list.remove(2); + list.add("start"); + list.add(5); + list.add("end"); + expected.put("toSpliced4_5", list); + expected.put("toSpliced4_5List", oldList); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void listGetInfo_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"list": [67, 2, 2, 2]} + """; + decoderStr = """ + var list = msg.list; + var listAdd = ["thigsboard", 4, 67]; + return { + list: list.clone(), + length: list.length(), + memorySize: list.memorySize(), + indOf1: list.indexOf("B", 1), + indOf2: list.indexOf(2, 2), + sStr: list.validateClazzInArrayIsOnlyString() + } + """; + ArrayList list = new ArrayList<>(List.of(67, 2, 2, 2)); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("list", list); + expected.put("length", list.size()); + expected.put("memorySize", 32L); + expected.put("indOf1", -1); + expected.put("indOf2", 2); + expected.put("sStr", false); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // Arrays + @Test + public void arrays_Test() throws ExecutionException, InterruptedException { + String str = "My String"; + msgStr = String.format(""" + {"str": "%s"} + """, str); + decoderStr = """ + // Create new array + var array = new int[3]; + array[0] = 1; + array[1] = 2; + array[2] = 3; + var str = msg.str; + var str0 = str[0]; + function sum(list){ + var result = 0; + for(var i = 0; i < list.length; i++){ + result += list[i]; + } + return result; + }; + var sum = sum(array); + return { + array: array, + str: str, + str0: str0, + sum: sum + } + """; + ArrayList list = new ArrayList<>(List.of(1, 2, 3)); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("array", list); + expected.put("str", str); + expected.put("str0", str.toCharArray()[0]); + expected.put("sum", 6); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void arraysWillCauseArrayIndexOutOfBoundsException_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {} + """; + decoderStr = """ + var array = new int[3]; + array[0] = 1; + array[1] = 2; + array[2] = 3; + array[3] = 4; + return array; + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Invalid statement: 4") + .hasMessageContaining("[Line: 5, Column: 12]"); + } + + // Using Java Classes + @Test + public void sqrt_Test() throws ExecutionException, InterruptedException { + int i = 4; + msgStr = String.format(""" + {"i": %d} + """, i); + decoderStr = """ + var foo = Math.sqrt(msg.i); + return{ + i: msg.i, + foo: foo + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("i", i); + expected.put("foo", Math.sqrt(i)); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // Convert Number to Hexadecimal/Octal/Binary String + @Test + public void byteIntegerToString_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var b16 = Integer.toString(0x1A, 16); + var b10 = Integer.toString(0x1A, 10); + var b8 = Integer.toString(0x1A, 8); + var b2 = Integer.toString(0x1A, 2); + return{ + b16: b16, + b10: b10, + b8: b8, + b2: b2 + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("b16", "1a"); + expected.put("b10", "26"); + expected.put("b8", "32"); + expected.put("b2", "11010"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void integerIntegerToString_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var i16 = Integer.toString(-255, 16); + var i10 = Integer.toString(-255, 10); + var i8 = Integer.toString(-255, 8); + var i2 = Integer.toString(-255, 2); + return{ + i16: i16, + i10: i10, + i8: i8, + i2: i2 + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("i16", "-ff"); + expected.put("i10", "-255"); + expected.put("i8", "-377"); + expected.put("i2", "-11111111"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void floatToString_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var f0 =7823764.8374; + var f16 = Float.toHexString(f0); + var f10 = f0.toString(); + return{ + f16: f16, + f10: f10 + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("f16", "0x1.dd8654p22"); + expected.put("f10", "7823764.8374"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void longToString_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var l16 = Long.toString(9223372036854775807, 16); + var l10 = Long.toString(9223372036854775807, 10); + var l8 = Long.toString(9223372036854775807, 8); + var l2 = Long.toString(9223372036854775807, 2); + return{ + l16: l16, + l10: l10, + l8: l8, + l2: l2 + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("l16", "7fffffffffffffff"); + expected.put("l10", "9223372036854775807"); + expected.put("l8", "777777777777777777777"); + expected.put("l2", "111111111111111111111111111111111111111111111111111111111111111"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void doubleToString_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var dd0 = 99993219.156013e-002; + var dd16 = Double.toHexString(dd0); + var dd10 = String.format("%.8f",dd0); + return{ + dd16: dd16, + dd10: dd10 + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + Double dd0 = 99993219.156013e-002d; + expected.put("dd16", "0x1.e83f862142b5bp19"); + expected.put("dd10", String.format("%.8f", dd0)); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected.toString(), actual.toString()); + } + + // Flow Control + @Test + public void ifTthenElseBlocks_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"temperature": 20} + """; + decoderStr = """ + if (msg.temperature > 0) { + return "Greater than zero!"; + } else if (msg.temperature == -1) { + return "Minus one!"; + } else { + return "Something else!"; + } + """; + String expected = "Greater than zero!"; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void ternarySstatements_Test() throws ExecutionException, InterruptedException { + msgStr = """ + {"temperature": 20} + """; + decoderStr = """ + return msg.temperature > 0 ? "Yes" : "No"; + """; + String expected = "Yes"; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void foreach_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var numbers = [1, 2, 3]; + var sum = 0; + foreach (n : numbers) { + sum+=n; + } + return sum; + """; + Integer expected = 6; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void foreachStringsIiterable_Test() throws ExecutionException, InterruptedException { + String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + msgStr = String.format(""" + {"str": "%s"} + """, str); + decoderStr = """ + int l = msg.str.length(); + var array = new int[l]; + var i = 0; + foreach (c : msg.str) { + array[i] = (int)c; + i++; + } + return array; + """; + List expected = new ArrayList<>(List.of(str.chars().boxed().toArray(Integer[]::new))); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void forLoop_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var sum = 0; + for (var i =0; i < 100; i++) {\s + sum += i; + } + return sum; + """; + Integer expected = 4950; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void doWhile_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var x = "doWhile"; + var a = null; + do { + x = a; + } + while (x != null); + return x; + """; + String expected = null; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void doUntil_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var x = "doWhile"; + var a = null; + do { + x = a; + } + until (x == null); + return x; + """; + String expected = null; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void standardWhile_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var x = "doWhile"; + var a = null; + while (x != null) { + x = a; + } + return x; + """; + String expected = null; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void standardUntil_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var x = "doWhile"; + var a = null; + until (x == null) { + x = a; + } + return x; + """; + String expected = null; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // Helper functions + @Test + public void atobBtoa_Test() throws ExecutionException, InterruptedException { + String str = "Hello, world"; + msgStr = String.format(""" + {"str": "%s"} + """, str); + decoderStr = """ + var encodedData = btoa(msg.str); + var decodedData = atob(encodedData); + return{ + "btoa": encodedData, + "atob": decodedData + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("btoa", Base64.getEncoder().encodeToString(str.getBytes())); + expected.put("atob", str); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void toFixed_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return{ + toFixed1: toFixed(0.345, 1), + toFixed2: toFixed(0.345, 2) + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("toFixed1", 0.3); + expected.put("toFixed2", 0.35); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void stringToBytesBinaryString_Test() throws ExecutionException, InterruptedException { + String base64Str = "eyJoZWxsbyI6ICJ3b3JsZCJ9"; + String inputStr = "hello, world"; + msgStr = String.format(""" + { + "base64Str": "%s", + "inputStr": "%s" + } + """, base64Str, inputStr); + decoderStr = """ + var bytesStr = atob(msg.base64Str); + var charsetStr = "UTF8"; + return{ + bytesStr: stringToBytes(bytesStr), + inputStr: stringToBytes(msg.inputStr), + inputStrUTF8: stringToBytes(msg.inputStr, charsetStr) + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("bytesStr", bytesToList(Base64.getDecoder().decode(base64Str))); + expected.put("inputStr", bytesToList(inputStr.getBytes())); + expected.put("inputStrUTF8", bytesToList(inputStr.getBytes(StandardCharsets.UTF_8))); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void stringToBytesObjectFromJsonAsString_Test() throws ExecutionException, InterruptedException { + String inputStr = "hello, world"; + msgStr = "{}"; + decoderStr = String.format(""" + var dataMap = {}; + dataMap.inputStr = "%s"; + var dataJsonStr = JSON.stringify(dataMap); + var dataJson = JSON.parse(dataJsonStr); + return stringToBytes(dataJson.inputStr); + """, inputStr); + List expected = bytesToList(inputStr.getBytes()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void bytesToString_Test() throws ExecutionException, InterruptedException { + byte[] inputBytes = new byte[]{0x48, 0x45, 0x4C, 0x4C, 0x4F}; + msgStr = "{}"; + decoderStr = """ + var bytes = [0x48,0x45,0x4C,0x4C,0x4F]; + return bytesToString(bytes); + """; + String expected = new String(inputBytes); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void decodeToJson_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var base64Str = "eyJoZWxsbyI6ICJ3b3JsZCJ9"; + var bytesStr = atob(base64Str); + var bytes = stringToBytes(bytesStr); + return decodeToJson(bytes); + """; + String expected = "{\"hello\":\"world\"}"; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, JacksonUtil.toString(actual)); + } + + @Test + public void isTypeInValue_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return{ + "binary": isBinary("1100110"), + "notBinary": isBinary("2100110"), + "octal": isOctal("4567734"), + "notOctal": isOctal("8100110"), + "decimal": isDecimal("4567039"), + "notDecimal": isDecimal("C100110"), + "hexadecimal": isHexadecimal("F5D7039"), + "notHexadecimal": isHexadecimal("K100110") + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("binary", 2); + expected.put("notBinary", -1); + expected.put("octal", 8); + expected.put("notOctal", -1); + expected.put("decimal", 10); + expected.put("notDecimal", -1); + expected.put("hexadecimal", 16); + expected.put("notHexadecimal", -1); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void encodeDecodeUri_Test() throws ExecutionException, InterruptedException { + String uriOriginal = "-_.!~*'();/?:@&=+$,#ht://example.ж д a/path with spaces/?param1=Київ 1¶m2=Україна2"; + String uriEncode = "-_.!~*'();/?:@&=+$,#ht://example.%D0%B6%20%D0%B4%20a/path%20with%20spaces/?param1=%D0%9A%D0%B8%D1%97%D0%B2%201¶m2=%D0%A3%D0%BA%D1%80%D0%B0%D1%97%D0%BD%D0%B02"; + msgStr = "{}"; + decoderStr = String.format(""" + var uriOriginal = "%s"; + var uriEncode = "%s"; + return{ + "encodeURI": encodeURI(uriOriginal), + "decodeURI": decodeURI(uriEncode) + } + """, uriOriginal, uriEncode); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("encodeURI", uriEncode); + expected.put("decodeURI", uriOriginal); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void raiseError_Test() throws ExecutionException, InterruptedException { + String message = "frequency_weighting_type must be 0, 1 or 2."; + msgStr = "{}"; + decoderStr = String.format(""" + var message = "%s"; + return raiseError(message); + """, message); + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining(message); + + Integer value = 4; + message = "frequency_weighting_type must be 0, 1 or 2. A value of " + value + " is invalid."; + decoderStr = String.format(""" + var message = "%s"; + return raiseError(message); + """, message); + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining(message); + } + + @Test + public void printUnsignedBytes_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var hexStrBe = "D8FF"; + var listBe = hexToBytes(hexStrBe); + return printUnsignedBytes(listBe); + """; + ArrayList expected = new ArrayList<>(List.of(216, 255)); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + decoderStr = """ + var hexStrLe = "FFD8"; + var listLe = hexToBytes(hexStrLe); + return printUnsignedBytes(listLe); + """; + expected = new ArrayList<>(List.of(255, 216)); + actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void pad_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + // padStart + decoderStr = """ + var str1 = "010011"; + var str2 ="1001010011"; + var fullNumber = "203439900FFCD5581"; + var last4Digits = fullNumber.substring(11); + return { + padStart1: padStart(str1, 8, '0'), + padStart2: padStart(str2, 8, '0'), + padStart16: padStart(str2, 16, '*'), + padStartFullNumber: padStart(last4Digits, fullNumber.length(), '*') + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("padStart1", "00010011"); + expected.put("padStart2", "1001010011"); + expected.put("padStart16", "******1001010011"); + expected.put("padStartFullNumber", "***********CD5581"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + // padEnd + decoderStr = """ + var str1 = "010011"; + var str2 ="1001010011"; + var fullNumber = "203439900FFCD5581"; + var last4Digits = fullNumber.substring(0, 11); + return { + padEnd1: padEnd(str1, 8, '0'), + padEnd2: padEnd(str2, 8, '0'), + padEnd16: padEnd(str2, 16, '*'), + padEndFullNumber: padEnd(last4Digits, fullNumber.length(), '*') + } + """; + expected = new LinkedHashMap<>(); + expected.put("padEnd1", "01001100"); + expected.put("padEnd2", "1001010011"); + expected.put("padEnd16", "1001010011******"); + expected.put("padEndFullNumber", "203439900FF******"); + actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void numberToRadixStringIntLongFloatDouble_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var i = 0x7FFFFFFF; + return { + "intToHex1": intToHex(i, true, true), + "intToHex2": intToHex(171, true, false), + "intToHex3": intToHex(0xABCDEF, false, true, 4), + "intToHex4": intToHex(0xABCD, false, false, 2), + "longToHex1": longToHex(9223372036854775807, true, true), + "longToHex2": longToHex(0x7A12BCD3, true, true, 4), + "longToHex3": longToHex(0x7A12BCD3, false, false, 4), + "floatToHex1": floatToHex(123456789.00), + "floatToHex2": floatToHex(123456789.00, false), + "doubleToHex1": doubleToHex(1729.1729d), + "doubleToHex2": doubleToHex(1729.1729d, false) + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("intToHex1", "0x7FFFFFFF"); + expected.put("intToHex2", "AB"); + expected.put("intToHex3", "0xCDAB"); + expected.put("intToHex4", "AB"); + expected.put("longToHex1", "0x7FFFFFFFFFFFFFFF"); + expected.put("longToHex2", "0xBCD3"); + expected.put("longToHex3", "127A"); + expected.put("floatToHex1", "0x4CEB79A3"); + expected.put("floatToHex2", "0xA379EB4C"); + expected.put("doubleToHex1", "0x409B04B10CB295EA"); + expected.put("doubleToHex2", "0xEA95B20CB1049B40"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void intLongToRadixString_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return { + "bin1": intLongToRadixString(58, 2), + "bin2": intLongToRadixString(9223372036854775807, 2), + "octal1": intLongToRadixString(13158, 8), + "octal2": intLongToRadixString(-13158, 8), + "decimal": intLongToRadixString(-13158, 10), + "hexDecimal1": intLongToRadixString(13158, 16), + "hexDecimal2": intLongToRadixString(-13158, 16), + "hexDecimal3": intLongToRadixString(9223372036854775807, 16), + "hexDecimal4": intLongToRadixString(-13158, 16, true, true) + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("bin1", "00111010"); + expected.put("bin2", "0111111111111111111111111111111111111111111111111111111111111111"); + expected.put("octal1", "31546"); + expected.put("octal2", "1777777777777777746232"); + expected.put("decimal", "-13158"); + expected.put("hexDecimal1", "3366"); + expected.put("hexDecimal2", "FFCC9A"); + expected.put("hexDecimal3", "7FFFFFFFFFFFFFFF"); + expected.put("hexDecimal4", "0xFFCC9A"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void parseHex_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return { + "hexToInt1": parseHexToInt("BBAA"), + "hexToInt2": parseHexToInt("BBAA", true), + "hexToInt3": parseHexToInt("AABB", false), + "hexToInt4": parseHexToInt("BBAA", false), + "hexToFloat1": parseHexToFloat("41EA62CC"), + "hexToFloat2": parseHexToFloat("41EA62CC", true), + "hexToFloat3": parseHexToFloat("41EA62CC", false), + "hexToFloat4": parseHexToFloat("CC62EA41", false), + "hexIntLongToFloat1": parseHexIntLongToFloat("0x0A", true), + "hexIntLongToFloat2": parseHexIntLongToFloat("0x0A", false), + "hexIntLongToFloat3": parseHexIntLongToFloat("0x00000A", true), + "hexIntLongToFloat4": parseHexIntLongToFloat("0x0A0000", false), + "hexIntLongToFloat5": parseHexIntLongToFloat("0x000A0A", true), + "hexIntLongToFloat6": parseHexIntLongToFloat("0x0A0A00", false), + "hexToDouble1": parseHexToDouble("409B04B10CB295EA"), + "hexToDouble2": parseHexToDouble("409B04B10CB295EA", false), + "hexToDouble3": parseHexToDouble("409B04B10CB295EA", true), + "hexToDouble4": parseHexToDouble("EA95B20CB1049B40", false) + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("hexToInt1", 48042); + expected.put("hexToInt2", 48042); + expected.put("hexToInt3", 48042); + expected.put("hexToInt4", 43707); + expected.put("hexToFloat1", 29.29824f); + expected.put("hexToFloat2", 29.29824f); + expected.put("hexToFloat3", -5.948442E7f); + expected.put("hexToFloat4", 29.29824f); + expected.put("hexIntLongToFloat1", 10.0f); + expected.put("hexIntLongToFloat2", 10.0f); + expected.put("hexIntLongToFloat3", 10.0f); + expected.put("hexIntLongToFloat4", 10.0f); + expected.put("hexIntLongToFloat5", 2570.0f); + expected.put("hexIntLongToFloat6", 2570.0f); + expected.put("hexToDouble1", 1729.1729); + expected.put("hexToDouble2", -2.7208640774822924E205); + expected.put("hexToDouble3", 1729.1729); + expected.put("hexToDouble4", 1729.1729); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void parseBytes_Test() throws ExecutionException, InterruptedException { + byte[] bytesExecutionArrayList = new byte[]{(byte) 0xAA, (byte) 0xBB, (byte) 0xCC, (byte) 0xDD}; + msgStr = "{}"; + decoderStr = """ + var bytes = [0xBB, 0xAA]; + var list = [-69, 83]; + var intValByte = [0xAA, 0xBB, 0xCC, 0xDD]; + var longValByte = [64, -101, 4, -79, 12, -78, -107, -22]; + var bytesFloat = [0x0A]; + var floatValByte = [0x41, 0xEA, 0x62, 0xCC]; + var floatValList = [65, -22, 98, -52]; + var intValByteFromInt = [0x00, 0x00, 0x00, 0x0A]; + var dataAT101 = "0x01756403671B01048836BF7701F000090722050000"; + var byteAT101 = hexToBytes(dataAT101); + var offsetLatInt = 9; + var coordinatesAsHex = "0x32D009423F23B300B0106E08D96B6C00"; + var coordinatesasBytes = hexToBytes(coordinatesAsHex); + var offsetLatLong = 0; + var factor = 1e15; + var bytesExecutionArrayList = [0xAA, 0xBB, 0xCC, 0xDD]; + return { + "bytesToHex1": bytesToHex(bytes), + "bytesToHex2": bytesToHex(list), + "bytesToInt1": parseBytesToInt(intValByte, 0, 3), + "bytesToInt2": parseBytesToInt(intValByte, 0, 3, true), + "bytesToInt3": parseBytesToInt(intValByte, 0, 3, false), + "bytesToLong1": parseBytesToLong(longValByte, 0, 8), + "bytesToLong2": parseBytesToLong(longValByte, 0, 8, false), + "bytesToFloat1": parseBytesToFloat(bytesFloat), + "bytesToFloat2": parseBytesToFloat(floatValByte, 0), + "bytesToFloat3": parseBytesToFloat(floatValByte, 0, 2, false), + "bytesToFloat4": parseBytesToFloat(floatValByte, 0, 2, true), + "bytesToFloat5": parseBytesToFloat(floatValByte, 0, 3, false), + "bytesToFloat6": parseBytesToFloat(floatValByte, 0, 3, true), + "bytesToFloat7": parseBytesToFloat(floatValByte, 0, 4, false), + "bytesToFloat8": parseBytesToFloat(floatValList, 0), + "bytesToFloat9": parseBytesToFloat(floatValList, 0, 4, false), + "bytesIntToFloat1": parseBytesIntToFloat(intValByteFromInt, 3, 1, true), + "bytesIntToFloat2": parseBytesIntToFloat(intValByteFromInt, 3, 1, false), + "bytesIntToFloat3": parseBytesIntToFloat(intValByteFromInt, 2, 2, true), + "bytesIntToFloat4": parseBytesIntToFloat(intValByteFromInt, 2, 2, false), + "bytesIntToFloat5": parseBytesIntToFloat(intValByteFromInt, 0, 4, true), + "bytesIntToFloat6": parseBytesIntToFloat(intValByteFromInt, 0, 4, false), + "bytesIntToFloat7": parseBytesIntToFloat(byteAT101, offsetLatInt, 4, false) / 1000000, + "bytesIntToFloat8": parseBytesIntToFloat(byteAT101, offsetLatInt + 4, 4, false) / 1000000, + "bytesLongToDouble1": parseBytesLongToDouble(coordinatesasBytes, offsetLatLong, 8, false) / factor, + "bytesLongToDouble2": parseBytesLongToDouble(coordinatesasBytes, offsetLatLong + 8, 8, false) / factor, + "bytesLongToExecutionArrayList": bytesToExecutionArrayList(bytesExecutionArrayList) + + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("bytesToHex1", "BBAA"); + expected.put("bytesToHex2", "BB53"); + expected.put("bytesToInt1", 11189196); + expected.put("bytesToInt2", 11189196); + expected.put("bytesToInt3", 13417386); + expected.put("bytesToLong1", 4655319798286292458L); + expected.put("bytesToLong2", -1543131529725306048L); + expected.put("bytesToFloat1", 1.4E-44f); + expected.put("bytesToFloat2", 29.29824f); + expected.put("bytesToFloat3", 8.4034E-41f); + expected.put("bytesToFloat4", 2.3646E-41f); + expected.put("bytesToFloat5", 9.083913E-39f); + expected.put("bytesToFloat6", 6.053388E-39f); + expected.put("bytesToFloat7", -5.948442E7f); + expected.put("bytesToFloat8", 29.29824f); + expected.put("bytesToFloat9", -5.948442E7f); + expected.put("bytesIntToFloat1", 10.0f); + expected.put("bytesIntToFloat2", 10.0f); + expected.put("bytesIntToFloat3", 10.0f); + expected.put("bytesIntToFloat4", 2560.0f); + expected.put("bytesIntToFloat5", 10.0f); + expected.put("bytesIntToFloat6", 1.6777216E8f); + expected.put("bytesIntToFloat7", 24.62495f); + expected.put("bytesIntToFloat8", 118.030576f); + expected.put("bytesLongToDouble1", 50.422775429058610d); + expected.put("bytesLongToDouble2", 30.517877378257072d); + expected.put("bytesLongToExecutionArrayList", bytesToList(bytesExecutionArrayList)); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // parseBinaryArray + @Test + public void parseBinaryArray_Test() throws ExecutionException, InterruptedException { + byte[] value = new byte[]{1, 0, 0, 1, 1, 1}; + msgStr = "{}"; + decoderStr = """ + // parseByteToBinaryArray + var byteVal = 0x39; + var value = parseByteToBinaryArray(byteVal, 6, false); + var actualLowCurrent1Alarm = value[0]; + var actualHighCurrent1Alarm = value[1]; + var actualLowCurrent2Alarm = value[2]; + var actualHighCurrent2Alarm = value[3]; + var actualLowCurrent3Alarm = value[4]; + var actualHighCurrent3Alarm = value[5]; + // parseBytesToBinaryArray + var bytesVal = [0xCE, 0xB2]; + // parseLongToBinaryArray + var longValue = 52914L; + return { + // parseByteToBinaryArray + "parseByteToBinaryArray_All": parseByteToBinaryArray(byteVal), + "parseByteToBinaryArray_1": parseByteToBinaryArray(byteVal, 3), + "parseByteToBinaryArray_2": parseByteToBinaryArray(byteVal, 8, false), + "parseByteToBinaryArray_3": parseByteToBinaryArray(byteVal, 5, false), + "parseByteToBinaryArray_4": parseByteToBinaryArray(byteVal, 4, false), + "parseByteToBinaryArray_5": parseByteToBinaryArray(byteVal, 3, false), + "parseByteToBinaryArray_value": parseByteToBinaryArray(byteVal, 6, false), + "parseByteToBinaryArray_value0": value[0], + "parseByteToBinaryArray_value1": value[1], + "parseByteToBinaryArray_value2": value[2], + "parseByteToBinaryArray_value3": value[3], + "parseByteToBinaryArray_value4": value[4], + "parseByteToBinaryArray_value5": value[5], + // parseBytesToBinaryArray + "parseBytesToBinaryArray_All": parseBytesToBinaryArray(bytesVal), + "parseBytesToBinaryArray_1": parseBytesToBinaryArray(bytesVal, 15), + "parseBytesToBinaryArray_2": parseBytesToBinaryArray(bytesVal, 14), + "parseBytesToBinaryArray_3": parseBytesToBinaryArray(bytesVal, 2), + // parseLongToBinaryArray + "parseLongToBinaryArray_1": parseLongToBinaryArray(longValue), + "parseLongToBinaryArray_2": parseLongToBinaryArray(longValue, 16), + // parseBinaryArrayToInt + "parseBinaryArrayToIn_1": parseBinaryArrayToInt([1, 0, 0, 1, 1, 1, 1, 1]), + "parseBinaryArrayToIn_2": parseBinaryArrayToInt([1, 0, 0, 1, 1, 1, 1, 1], 1, 7) + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + // parseByteToBinaryArray + expected.put("parseByteToBinaryArray_All", bytesToList(new byte[]{0, 0, 1, 1, 1, 0, 0, 1})); + expected.put("parseByteToBinaryArray_1", bytesToList(new byte[]{0, 0, 1})); + expected.put("parseByteToBinaryArray_2", bytesToList(new byte[]{1, 0, 0, 1, 1, 1, 0, 0})); + expected.put("parseByteToBinaryArray_3", bytesToList(new byte[]{1, 0, 0, 1, 1})); + expected.put("parseByteToBinaryArray_4", bytesToList(new byte[]{1, 0, 0, 1})); + expected.put("parseByteToBinaryArray_5", bytesToList(new byte[]{1, 0, 0})); + expected.put("parseByteToBinaryArray_value", bytesToList(value)); + expected.put("parseByteToBinaryArray_value0", value[0]); + expected.put("parseByteToBinaryArray_value1", value[1]); + expected.put("parseByteToBinaryArray_value2", value[2]); + expected.put("parseByteToBinaryArray_value3", value[3]); + expected.put("parseByteToBinaryArray_value4", value[4]); + expected.put("parseByteToBinaryArray_value5", value[5]); + // parseBytesToBinaryArray + expected.put("parseBytesToBinaryArray_All", bytesToList(new byte[]{1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0})); + expected.put("parseBytesToBinaryArray_1", bytesToList(new byte[]{1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0})); + expected.put("parseBytesToBinaryArray_2", bytesToList(new byte[]{0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0})); + expected.put("parseBytesToBinaryArray_3", bytesToList(new byte[]{1, 0})); + // parseLongToBinaryArray + expected.put("parseLongToBinaryArray_1", bytesToList(new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0})); + expected.put("parseLongToBinaryArray_2", bytesToList(new byte[]{1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0})); + // parseBinaryArrayToInt + expected.put("parseBinaryArrayToIn_1", -97); + expected.put("parseBinaryArrayToIn_2", 31); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // parseNumber + @Test + public void parseInt_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return { + "parseInt_01": parseInt("0"), + "parseInt_02": parseInt("473"), + "parseInt_03": parseInt("+42"), + "parseInt_04": parseInt("-0", 10), + "parseInt_05": parseInt("-0xFF"), + "parseInt_06": parseInt("-FF", 16), + "parseInt_07": parseInt("1100110", 2), + "parseInt_08": parseInt("2147483647", 10), + "parseInt_09": parseInt("-2147483648", 10), + "parseInt_10": parseInt("Kona", 27) + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("parseInt_01", 0); + expected.put("parseInt_02", 473); + expected.put("parseInt_03", 42); + expected.put("parseInt_04", 0); + expected.put("parseInt_05", -255); + expected.put("parseInt_06", -255); + expected.put("parseInt_07", 102); + expected.put("parseInt_08", 2147483647); + expected.put("parseInt_09", -2147483648); + expected.put("parseInt_10", 411787); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + + decoderStr = """ + return parseInt("2147483648", 10); + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: parseInt(\"2147483648\", 10): For input string: \"2147483648\"") + .hasMessageContaining("[Line: 1, Column: 12]"); + + decoderStr = """ + return parseInt("99", 8); + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: parseInt(\"99\", 8): Failed radix [8] for value: \"99\"") + .hasMessageContaining("[Line: 1, Column: 12]"); + + decoderStr = """ + return parseInt("Kona", 10); + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: parseInt(\"Kona\", 10): Failed radix [10] for value: \"Kona\"") + .hasMessageContaining("[Line: 1, Column: 12]"); + } + + @Test + public void parseLong_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return { + "parseLong_01": parseLong("0"), + "parseLong_02": parseLong("473"), + "parseLong_03": parseLong("+42"), + "parseLong_04": parseLong("-0", 10), + "parseLong_05": parseLong("-0xFFFF"), + "parseLong_06": parseLong("-FFFF", 16), + "parseLong_07": parseLong("11001101100110", 2), + "parseLong_08": parseLong("777777777777777777777", 8), + "parseLong_09": parseLong("KonaLong", 27), + "parseLong_10": parseLong("9223372036854775807", 10), + "parseLong_11": parseLong("-9223372036854775808", 10), + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("parseLong_01", 0L); + expected.put("parseLong_02", 473L); + expected.put("parseLong_03", 42L); + expected.put("parseLong_04", 0L); + expected.put("parseLong_05", -65535L); + expected.put("parseLong_06", -65535L); + expected.put("parseLong_07", 13158L); + expected.put("parseLong_08", 9223372036854775807L); + expected.put("parseLong_09", 218840926543L); + expected.put("parseLong_10", 9223372036854775807L); + expected.put("parseLong_11", -9223372036854775808L); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + + decoderStr = """ + return parseLong("9223372036854775808", 10); + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: parseLong(\"9223372036854775808\", 10): For input string: \"9223372036854775808\"") + .hasMessageContaining("[Line: 1, Column: 12]"); + + decoderStr = """ + return parseLong("0xFGFFFFFF", 16); + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: parseLong(\"0xFGFFFFFF\", 16): Failed radix [16] for value: \"0xFGFFFFFF\"") + .hasMessageContaining("[Line: 1, Column: 12]"); + + decoderStr = """ + return parseLong("FFFFFFFFF", 16); + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: parseLong(\"FFFFFFFFF\", 16): The hexadecimal value: \"FFFFFFFFF\"") + .hasMessageContaining("[Line: 1, Column: 12]"); + + decoderStr = """ + return parseLong("1787", 8); + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: parseLong(\"1787\", 8): Failed radix [8] for value: \"1787\"") + .hasMessageContaining("[Line: 1, Column: 12]"); + + decoderStr = """ + return parseLong("KonaLong", 10); + """; + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("Error: parseLong(\"KonaLong\", 10): Failed radix [10] for value: \"KonaLong\"") + .hasMessageContaining("[Line: 1, Column: 12]"); + } + + @Test + public void parseFloat_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return parseFloat("4.2"); + """; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(4.2f, actual); + } + + @Test + public void parseDouble_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return parseDouble("4.2"); + """; + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(4.2d, actual); + } + + // base64 + @Test + public void base64_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + return { + "base64ToHex": base64ToHex("Kkk="), + "bytesToBase64": bytesToBase64([42, 73]), + "base64ToBytes": base64ToBytes("Kkk=") + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("base64ToHex", "2A49"); + expected.put("bytesToBase64", "Kkk="); + expected.put("base64ToBytes", bytesToList(new byte[]{42, 73})); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // toFlatMap + @Test + public void toFlatMap_Test() throws ExecutionException, InterruptedException { + msgStr = """ + { + "key1": "value1", + "key2": 12, + "key3": { + "key4": "value4", + "key5": 34, + "key6": [{ + "key7": "value7" + }, + { + "key8": "value8" + }, + "just_string_value_in_array", + 56 + ], + "key9": { + "key10": ["value10_1", "value10_2", "value10_3"] + } + }, + "key_to_overwrite": "root_value", + "key11": { + "key_to_overwrite": "second_level_value" + } + }; + """; + decoderStr = """ + return { + "toFlatMap": toFlatMap(msg), + "toFlatMapFalse": toFlatMap(msg, false), + "toFlatMapExcludeList": toFlatMap(msg, ["key1", "key3"]), + "toFlatMapExcludeListFalse": toFlatMap(msg, ["key2", "key4"], false) + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + LinkedHashMap expectedJson = new LinkedHashMap<>(); + expectedJson.put("key1", "value1"); + expectedJson.put("key2", 12); + expectedJson.put("key3.key4", "value4"); + expectedJson.put("key3.key5", 34); + expectedJson.put("key3.key6.0.key7", "value7"); + expectedJson.put("key3.key6.1.key8", "value8"); + expectedJson.put("key3.key6.2", "just_string_value_in_array"); + expectedJson.put("key3.key6.3", 56); + expectedJson.put("key3.key9.key10.0", "value10_1"); + expectedJson.put("key3.key9.key10.1", "value10_2"); + expectedJson.put("key3.key9.key10.2", "value10_3"); + expectedJson.put("key_to_overwrite", "root_value"); + expectedJson.put("key11.key_to_overwrite", "second_level_value"); + expected.put("toFlatMap", expectedJson); + LinkedHashMap expectedJsonFalse = new LinkedHashMap<>(); + expectedJsonFalse.put("key1", "value1"); + expectedJsonFalse.put("key2", 12); + expectedJsonFalse.put("key4", "value4"); + expectedJsonFalse.put("key5", 34); + expectedJsonFalse.put("key7", "value7"); + expectedJsonFalse.put("key8", "value8"); + expectedJsonFalse.put("key6.2", "just_string_value_in_array"); + expectedJsonFalse.put("key6.3", 56); + expectedJsonFalse.put("key10.0", "value10_1"); + expectedJsonFalse.put("key10.1", "value10_2"); + expectedJsonFalse.put("key10.2", "value10_3"); + expectedJsonFalse.put("key_to_overwrite", "second_level_value"); + expected.put("toFlatMapFalse", expectedJsonFalse); + LinkedHashMap expectedJsonExcludeList = new LinkedHashMap<>(); + expectedJsonExcludeList.put("key2", 12); + expectedJsonExcludeList.put("key_to_overwrite", "root_value"); + expectedJsonExcludeList.put("key11.key_to_overwrite", "second_level_value"); + expected.put("toFlatMapExcludeList", expectedJsonExcludeList); + LinkedHashMap expectedJsonExcludeListFalse = new LinkedHashMap<>(); + expectedJsonExcludeListFalse.put("key1", "value1"); + expectedJsonExcludeListFalse.put("key5", 34); + expectedJsonExcludeListFalse.put("key7", "value7"); + expectedJsonExcludeListFalse.put("key8", "value8"); + expectedJsonExcludeListFalse.put("key6.2", "just_string_value_in_array"); + expectedJsonExcludeListFalse.put("key6.3", 56); + expectedJsonExcludeListFalse.put("key10.0", "value10_1"); + expectedJsonExcludeListFalse.put("key10.1", "value10_2"); + expectedJsonExcludeListFalse.put("key10.2", "value10_3"); + expectedJsonExcludeListFalse.put("key_to_overwrite", "second_level_value"); + expected.put("toFlatMapExcludeListFalse", expectedJsonExcludeListFalse); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + // tbDate + @Test + public void tbDateStringTZ_Test() throws ExecutionException, InterruptedException { + String s2 = "2023-08-06T12:04:05.123"; + msgStr = "{}"; + decoderStr = String.format(""" + var d1 = new Date("2023-08-06T04:04:05.123Z"); // TZ => "UTC" + var d2 = new Date("%s"); // TZ = ZoneId.systemDefault() instant + var d3 = new Date("2023-08-06T04:04:05.00-04"); // TZ => "-04" + var d4 = new Date("2023-08-06T04:04:05.00+02:00"); // TZ => "+02:00" + var d5 = new Date("2023-08-06T04:04:05.00+03:00:00"); // TZ => "+03:00:00" + var d6 = new Date("Tue, 3 Jun 2008 11:05:30 GMT"); // TZ => "GMT" + var d7 = new Date("Tue, 3 Jun 2008 11:05:30 +043056"); // TZ => "+043056" + return { + "dIso_1": d1.toISOString(), + "dIso_2": d2.toISOString(), + "dIso_3": d3.toISOString(), + "dIso_4": d4.toISOString(), + "dIso_5": d5.toISOString(), + "dIso_6": d6.toISOString(), + "dIso_7": d7.toISOString() + } + """, s2); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("dIso_1", "2023-08-06T04:04:05.123Z"); + TbDate d2 = new TbDate(s2); + expected.put("dIso_2", d2.toISOString()); + expected.put("dIso_3", "2023-08-06T08:04:05Z"); + expected.put("dIso_4", "2023-08-06T02:04:05Z"); + expected.put("dIso_5", "2023-08-06T01:04:05Z"); + expected.put("dIso_6", "2008-06-03T11:05:30Z"); + expected.put("dIso_7", "2008-06-03T06:34:34Z"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void tbDateStringPattern_Test() throws ExecutionException, InterruptedException { + String s3 = "2023-08-06 12:04:05.000"; + String pattern3 = "yyyy-MM-dd HH:mm:ss.SSS"; + msgStr = "{}"; + decoderStr = String.format(""" + var pattern = "yyyy-MM-dd HH:mm:ss.SSSXXX"; + var d1 = new Date("2023-08-06 04:04:05.000Z", pattern); // Pattern without TZ => ZoneId "UTC" + var d2 = new Date("2023-08-06 04:04:05.000-04:00", pattern); // Pattern with TZ => "-04:00" + var d3 = new Date("%s","%s"); // Pattern without TZ, TZ = ZoneId.systemDefault() instant + return { + "dIso_1": d1.toISOString(), + "dIso_2": d2.toISOString(), + "dIso_3": d3.toISOString() + } + """, s3, pattern3); + LinkedHashMap expected = new LinkedHashMap<>(); + expected.put("dIso_1", "2023-08-06T04:04:05Z"); + expected.put("dIso_2", "2023-08-06T08:04:05Z"); + TbDate d3 = new TbDate(s3, pattern3); + expected.put("dIso_3", d3.toISOString()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void tbDateStringPatternLocale_Test() throws ExecutionException, InterruptedException { + String sDe = "12:15:30 PM, So. 10/09/2022"; + String sUs = "12:15:30 PM, Sun 10/09/2022"; + var pattern = "hh:mm:ss a, EEE M/d/uuuu"; + msgStr = "{}"; + decoderStr = String.format(""" + var pattern = "%s"; + var d1 = new Date("%s", pattern, "de"); // Pattern without TZ, TZ = ZoneId.systemDefault() instant + var d2 = new Date("%s", pattern, "en-US"); // Pattern without TZ, TZ = ZoneId.systemDefault() instant + return { + "dIso_1": d1.toISOString(), + "dLocal_1": d1.toLocaleString("de"), + "dIso_2": d2.toISOString(), + "dLocal_2": d2.toLocaleString("en-US") + } + """, pattern, sDe, sUs); + LinkedHashMap expected = new LinkedHashMap<>(); + TbDate d1 = new TbDate(sDe, pattern, "de"); + expected.put("dIso_1", d1.toISOString()); + expected.put("dLocal_1", d1.toLocaleString("de")); + TbDate d2 = new TbDate(sUs, pattern, "en-US"); + expected.put("dIso_2", d2.toISOString()); + expected.put("dLocal_2", d2.toLocaleString("en-US")); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + + decoderStr = String.format(""" + var pattern = "%s"; + var d = new Date("02:15:30 PM, Sun 10/09/2022", pattern, "de"); + return d; + """, pattern); + assertThatThrownBy(() -> { + invokeScript(evalScript(decoderStr), msgStr); + }).hasMessageContaining("[Error: could not create constructor") + .hasMessageContaining("[Line: 2, Column: 16]"); + } + + + @Test + public void tbDateStringPatternLocaleTZ_Test() throws ExecutionException, InterruptedException { + String s = "2023-08-06 04:04:05.000+02:00"; + var pattern = "yyyy-MM-dd HH:mm:ss.SSSXXX"; + msgStr = "{}"; + decoderStr = String.format(""" + var d = new Date("%s", "%s", "de", "America/New_York"); // Pattern with TZ => "+02:00" but `Time Zone` as parameter = "America/New_York" = "-04:00" + return { + "dIso": d.toISOString(), + "dLocal_de": d.toLocaleString("de"), + "dLocal_us": d.toLocaleString("en-US") + } + """, s, pattern); + LinkedHashMap expected = new LinkedHashMap<>(); + TbDate d = new TbDate(s, pattern, "de", "America/New_York"); + expected.put("dIso", d.toISOString()); + expected.put("dLocal_de", d.toLocaleString("de")); + expected.put("dLocal_us", d.toLocaleString("en-US")); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void tbDateInstantYearMonthDateHrsMinSecond_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var d1 = new Date(2023, 8, 6, 4, 4, 5, "America/New_York"); + var d2 = new Date(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); + return { + "dLocal_1": d1.toLocaleString(), + "dIso_1": d1.toISOString(), + "date_1": d1.toString(), + "dLocal_2": d2.toLocaleString(), + "dLocal_2_us": d2.toLocaleString("en-us", "America/New_York"), + "dIso_2": d2.toISOString(), + "date_2": d2.toString() + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + TbDate d1 = new TbDate(2023, 8, 6, 4, 4, 5, "America/New_York"); + expected.put("dLocal_1", d1.toLocaleString()); + expected.put("dIso_1", d1.toISOString()); + expected.put("date_1", d1.toString()); + TbDate d2 = new TbDate(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); + expected.put("dLocal_2", d2.toLocaleString()); + expected.put("dLocal_2_us", "8/5/23, 10:04:05 PM"); + expected.put("dIso_2", d2.toISOString()); + expected.put("date_2", d2.toString()); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void tbDateLocale_Test() throws ExecutionException, InterruptedException { + String s2 = "2023-08-06T04:04:05.000"; + msgStr = "{}"; + decoderStr = String.format(""" + // Input date Without TZ (TZ Default = ZoneId.systemDefault()) + var d1 = new Date(2023, 8, 6, 4, 4, 5); // Parameters (int year, int month, int dayOfMonth, int hours, int minutes, int seconds) => TZ Default = ZoneId.systemDefault() + var d2 = new Date("%s"); // Parameter (String 'yyyy-MM-ddThh:mm:ss.ms') => TZ Default = ZoneId.systemDefault() + // Input date With TZ (TZ = parameter TZ or 'Z' equals 'UTC') + var d3 = new Date(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); // Parameters (int year, int month, int dayOfMonth, int hours, int minutes, int seconds, TZ) => TZ "Europe/Berlin" + return { + "dLocal_1_us": d1.toLocaleString("en-US"), + "dIso_1": d1.toISOString(), + "d1": d1.toString(), + "dIso_2": d2.toISOString(), + "dLocal_2_de": d2.toLocaleString("de"), + "dLocal_2_utc": d2.toLocaleString("UTC"), + "dIso_3": d3.toISOString(), + "dLocal_3_utc": d3.toLocaleString("UTC"), + "dLocal_3_us": d3.toLocaleString("en-us"), + "dLocal_3_de": d3.toLocaleString("de") + } + """, s2); + LinkedHashMap expected = new LinkedHashMap<>(); + TbDate d1 = new TbDate(2023, 8, 6, 4, 4, 5); + expected.put("dLocal_1_us", "8/6/23, 4:04:05 AM"); + expected.put("dIso_1", d1.toISOString()); + expected.put("d1", d1.toString()); + TbDate d2 = new TbDate(s2); + expected.put("dIso_2", d2.toISOString()); + expected.put("dLocal_2_de", "06.08.23, 04:04:05"); + expected.put("dLocal_2_utc", "2023-08-06 04:04:05"); + TbDate d3 = new TbDate(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); + expected.put("dIso_3", d3.toISOString()); + expected.put("dLocal_3_utc", d3.toLocaleString("UTC")); + expected.put("dLocal_3_us", d3.toLocaleString("en-us")); + expected.put("dLocal_3_de", d3.toLocaleString("de")); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void tbDateLocaleTZ_Test() throws ExecutionException, InterruptedException { + msgStr = "{}"; + decoderStr = """ + var d = new Date(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); // Parameters (int year, int month, int dayOfMonth, int hours, int minutes, int seconds, TZ) => TZ "Europe/Berlin"; + return { + "dIso": d.toISOString(), + "dLocal_utc": d.toLocaleString("UTC"), + "dLocal_us": d.toLocaleString("en-us", "America/New_York"), + "dLocal_de": d.toLocaleString("de", "Europe/Berlin") + } + """; + LinkedHashMap expected = new LinkedHashMap<>(); + TbDate d = new TbDate(2023, 8, 6, 4, 4, 5, "Europe/Berlin"); + expected.put("dIso", d.toISOString()); + expected.put("dLocal_utc", d.toLocaleString("UTC")); + expected.put("dLocal_us", "8/5/23, 10:04:05 PM"); + expected.put("dLocal_de", "06.08.23, 04:04:05"); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + @Test + public void tbDateLocalPatternMap_Test() throws ExecutionException, InterruptedException { + String s1 = "2023-08-06T04:04:05.00Z"; + String s2 = "2023-08-06T04:04:05.000"; + String options1_2Str = "{\"timeZone\":\"Europe/Berlin\"}"; + msgStr = "{}"; + decoderStr = String.format(""" + var d1 = new Date("%s"); // TZ => "UTC" + var d2 = new Date("%s"); // TZ => Default = ZoneId.systemDefault + var options1_2 = %s; // TZ = "+02:00" + var options1_2Str = JSON.stringify(options1_2); + var d3 = new Date(2023, 8, 6, 4, 4, 5); // TZ => Default = ZoneId.systemDefault + var options3 = {"timeZone":"Europe/Berlin", "pattern": "M-d/yyyy, h:mm=ss a"}; + var options3Str = JSON.stringify(options3); + var d4 = new Date(2023, 8, 6, 4, 4, 5, "UTC"); // TZ => "UTC" + var options4 = {"timeZone":"Europe/Berlin","dateStyle":"full","timeStyle":"full"}; + var options4Str = JSON.stringify(options4); + return { + "dIso_1": d1.toISOString(), + "dLocal_1": d1.toLocaleString(), + "dLocal_1_options": d1.toLocaleString("en-US", options1_2Str), + "dIso_2": d2.toISOString(), + "dLocal_2_options": d2.toLocaleString("en-US", options1_2Str);, + "dIso_3": d3.toISOString(), + "dLocal_3_options": d3.toLocaleString("en-US", options3Str), + "dIso_4": d4.toISOString(), + "dLocal_4_options_ua": d4.toLocaleString("uk-UA", options4Str), + "dLocal_4_options_us": d4.toLocaleString("en-US", options4Str), + "dLocal_4_options_de": d4.toLocaleString("de", options4Str) + } + """, s1, s2, options1_2Str); + LinkedHashMap expected = new LinkedHashMap<>(); + TbDate d1 = new TbDate(s1); + expected.put("dIso_1", "2023-08-06T04:04:05Z"); + expected.put("dLocal_1", d1.toLocaleString()); + expected.put("dLocal_1_options", d1.toLocaleString("en-US", options1_2Str)); + TbDate d2 = new TbDate(s2); + expected.put("dIso_2", d2.toISOString()); + expected.put("dLocal_2_options", d2.toLocaleString("en-US", options1_2Str)); + TbDate d3 = new TbDate(2023, 8, 6, 4, 4, 5); + String options3Str = "{\"timeZone\":\"Europe/Berlin\", \"pattern\": \"M-d/yyyy, h:mm=ss a\"}"; + expected.put("dIso_3", d3.toISOString()); + expected.put("dLocal_3_options", d3.toLocaleString("en-US", options3Str)); + TbDate d4 = new TbDate(2023, 8, 6, 4, 4, 5, "UTC"); + String options4Str = "{\"timeZone\":\"Europe/Berlin\",\"dateStyle\":\"full\",\"timeStyle\":\"full\"}"; + expected.put("dIso_4", "2023-08-06T04:04:05Z"); + expected.put("dLocal_4_options_ua", d4.toLocaleString("uk-UA", options4Str)); + expected.put("dLocal_4_options_us", d4.toLocaleString("en-US", options4Str)); + expected.put("dLocal_4_options_de", d4.toLocaleString("de", options4Str)); + Object actual = invokeScript(evalScript(decoderStr), msgStr); + assertEquals(expected, actual); + } + + + private List splice(List oldList, int start, int deleteCount, Object... values) { + start = initStartIndex(oldList, start); + deleteCount = deleteCount < 0 ? 0 : Math.min(deleteCount, (oldList.size() - start)); + List removed = new ArrayList<>(); + while (deleteCount > 0) { + removed.add(oldList.remove(start)); + deleteCount--; + } + int insertIdx = start; + for (Object e : values) { + oldList.add(insertIdx++, e); + } + return removed; + } + + private List splice(List oldList, int start) { + return this.splice(oldList, start, oldList.size() - start); + } + + private List fill(List oldList, Object value) { + return fill(oldList, value, 0); + } + + private List fill(List oldList, Object value, int start) { + return fill(oldList, value, start, oldList.size()); + } + + private List fill(List oldList, Object value, int start, int end) { + start = initStartIndex(oldList, start); + end = initEndIndex(oldList, end); + + if (start < oldList.size() && end > start) { + for (int i = start; i < end; ++i) { + oldList.set(i, value); + } + } + return oldList; + } + + private int initStartIndex(List oldList, int start) { + return start < -oldList.size() ? 0 : + start < 0 ? start + oldList.size() : + start; + } + + private int initEndIndex(List oldList, int end) { + return end < -oldList.size() ? 0 : + end < 0 ? end + oldList.size() : + Math.min(end, oldList.size()); + } + + private static final Comparator numericCompAsc = new Comparator() { + public int compare(Object o1, Object o2) { + Double first = Double.parseDouble(String.valueOf(o1)); + Double second = Double.parseDouble(String.valueOf(o2)); + return first.compareTo(second); + } + }; + + private static final Comparator numericCompDesc = new Comparator() { + public int compare(Object o1, Object o2) { + Double first = Double.parseDouble(String.valueOf(o1)); + Double second = Double.parseDouble(String.valueOf(o2)); + return second.compareTo(first); + } + }; + + private static List bytesToList(byte[] bytes) { + List list = new ArrayList<>(); + for (byte aByte : bytes) { + list.add(aByte); + } + return list; + } +} + diff --git a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java index 9e3b6221b1..071fdf77fc 100644 --- a/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/script/TbelInvokeServiceTest.java @@ -20,15 +20,10 @@ import com.github.benmanes.caffeine.cache.Cache; import org.junit.Assert; import org.junit.Ignore; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.test.context.TestPropertySource; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.script.api.ScriptType; -import org.thingsboard.script.api.tbel.TbelInvokeService; import org.thingsboard.script.api.tbel.TbelScript; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.controller.AbstractControllerTest; import org.thingsboard.server.dao.service.DaoSqlTest; import java.io.Serializable; @@ -42,7 +37,6 @@ import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_REQUEST; @DaoSqlTest @TestPropertySource(properties = { @@ -52,10 +46,7 @@ import static org.thingsboard.server.common.data.msg.TbMsgType.POST_TELEMETRY_RE "tbel.max_errors=2", "tbel.compiled_scripts_cache_size=100" }) -class TbelInvokeServiceTest extends AbstractControllerTest { - - @Autowired - private TbelInvokeService invokeService; +class TbelInvokeServiceTest extends AbstractTbelInvokeTest { @Value("${tbel.max_errors}") private int maxJsErrors; @@ -69,14 +60,14 @@ class TbelInvokeServiceTest extends AbstractControllerTest { for (int i = 0; i < 100; i++) { msg.put("temperature", i); boolean expected = i > 20; - boolean result = Boolean.parseBoolean(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScriptResultString(scriptId, JacksonUtil.toString(msg))); Assert.assertEquals(expected, result); } long startTs = System.currentTimeMillis(); for (int i = 0; i < iterations; i++) { msg.put("temperature", i); boolean expected = i > 20; - boolean result = Boolean.parseBoolean(invokeScript(scriptId, JacksonUtil.toString(msg))); + boolean result = Boolean.parseBoolean(invokeScriptResultString(scriptId, JacksonUtil.toString(msg))); Assert.assertEquals(expected, result); } long duration = System.currentTimeMillis() - startTs; @@ -101,7 +92,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest { for (int i = 0; i < maxJsErrors; i++) { assertThatThrownBy(() -> { - invokeScript(scriptId, hugeMsg); + invokeScriptResultString(scriptId, hugeMsg); }).hasMessageContaining("input arguments exceed maximum"); } assertThatScriptIsBlocked(scriptId); @@ -114,7 +105,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest { for (int i = 0; i < maxJsErrors; i++) { assertThatThrownBy(() -> { - invokeScript(scriptId, "{}"); + invokeScriptResultString(scriptId, "{}"); }).hasMessageContaining("result exceeds maximum allowed size"); } assertThatScriptIsBlocked(scriptId); @@ -182,7 +173,7 @@ class TbelInvokeServiceTest extends AbstractControllerTest { scriptsIds.add(scriptId); for (int j = 0; j < i; j++) { - invokeScript(scriptId, "{ \"temperature\": 12 }"); // so that scriptsIds is ordered by number of invocations + invokeScriptResultString(scriptId, "{ \"temperature\": 12 }"); // so that scriptsIds is ordered by number of invocations } } @@ -201,23 +192,13 @@ class TbelInvokeServiceTest extends AbstractControllerTest { UUID scriptRemovedFromCache = scriptsIds.get(0); assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNull(); - invokeScript(scriptRemovedFromCache, "{ \"temperature\": 12 }"); + invokeScriptResultString(scriptRemovedFromCache, "{ \"temperature\": 12 }"); assertThat(compiledScriptsCache.getIfPresent(scriptIdToHash.get(scriptRemovedFromCache))).isNotNull(); } private void assertThatScriptIsBlocked(UUID scriptId) { assertThatThrownBy(() -> { - invokeScript(scriptId, "{}"); + invokeScriptResultString(scriptId, "{}"); }).hasMessageContaining("invocation is blocked due to maximum error"); } - - private UUID evalScript(String script) throws ExecutionException, InterruptedException { - return invokeService.eval(TenantId.SYS_TENANT_ID, ScriptType.RULE_NODE_SCRIPT, script, "msg", "metadata", "msgType").get(); - } - - private String invokeScript(UUID scriptId, String str) throws ExecutionException, InterruptedException { - var msg = JacksonUtil.fromString(str, Map.class); - return invokeService.invokeScript(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", POST_TELEMETRY_REQUEST.name()).get().toString(); - } - }