diff --git a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java index fa75709d83..6c50561363 100644 --- a/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java +++ b/common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java @@ -18,7 +18,9 @@ package org.thingsboard.script.api.tbel; import org.mvel2.ExecutionContext; import org.mvel2.ParserConfiguration; import org.mvel2.execution.ExecutionArrayList; +import org.mvel2.execution.ExecutionHashMap; import org.mvel2.util.MethodStub; +import org.thingsboard.server.common.data.StringUtils; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -27,8 +29,12 @@ import java.math.RoundingMode; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.charset.StandardCharsets; +import java.util.ArrayList; import java.util.Base64; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Set; public class TbUtils { @@ -89,6 +95,14 @@ public class TbUtils { byte[].class))); parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex", ExecutionArrayList.class))); + parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", + ExecutionContext.class, Map.class))); + parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", + ExecutionContext.class, Map.class, boolean.class))); + parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", + ExecutionContext.class, Map.class, List.class))); + parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap", + ExecutionContext.class, Map.class, List.class, boolean.class))); } public static String btoa(String input) { @@ -225,7 +239,7 @@ public class TbUtils { } ExecutionArrayList data = new ExecutionArrayList<>(ctx); for (int i = 0; i < len; i += 2) { - data.add((byte)((Character.digit(hex.charAt(i), 16) << 4) + data.add((byte) ((Character.digit(hex.charAt(i), 16) << 4) + Character.digit(hex.charAt(i + 1), 16))); } return data; @@ -316,4 +330,62 @@ public class TbUtils { } return value; } + + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json) { + return toFlatMap(ctx, json, new ArrayList<>(), true); + } + + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json, boolean pathInKey) { + return toFlatMap(ctx, json, new ArrayList<>(), pathInKey); + } + + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json, List excludeList) { + return toFlatMap(ctx, json, excludeList, true); + } + + public static ExecutionHashMap toFlatMap(ExecutionContext ctx, Map json, List excludeList, boolean pathInKey) { + ExecutionHashMap map = new ExecutionHashMap<>(16, ctx); + parseRecursive(json, map, excludeList, "", pathInKey); + return map; + } + + private static void parseRecursive(Object json, Map map, List excludeList, String path, boolean pathInKey) { + if (json instanceof Map.Entry) { + Map.Entry entry = (Map.Entry) json; + if (StringUtils.isNotBlank(path)) { + path += "."; + } + if (excludeList.contains(entry.getKey())) { + return; + } + path += entry.getKey(); + json = entry.getValue(); + } + if (json instanceof Set || json instanceof List) { + String arrayPath = path + "."; + Object[] collection = ((Collection) json).toArray(); + for (int index = 0; index < collection.length; index++) { + parseRecursive(collection[index], map, excludeList, arrayPath + index, pathInKey); + } + } else if (json instanceof Map) { + Map node = (Map) json; + for (Map.Entry entry : node.entrySet()) { + parseRecursive(entry, map, excludeList, path, pathInKey); + } + } else { + if (pathInKey) { + map.put(path, json); + } else { + String key = path.substring(path.lastIndexOf('.') + 1); + if (StringUtils.isNumeric(key)) { + int pos = path.length(); + for (int i = 0; i < 2; i++) { + pos = path.lastIndexOf('.', pos - 1); + } + key = path.substring(pos + 1); + } + map.put(key, json); + } + } + } } diff --git a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java index f4cd610b72..82cd74ca30 100644 --- a/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java +++ b/common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java @@ -15,15 +15,50 @@ */ package org.thingsboard.script.api.tbel; +import lombok.extern.slf4j.Slf4j; +import org.junit.After; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.mvel2.ExecutionContext; +import org.mvel2.ParserContext; +import org.mvel2.SandboxedParserConfiguration; +import org.mvel2.execution.ExecutionArrayList; +import org.mvel2.execution.ExecutionHashMap; import java.nio.ByteBuffer; import java.util.ArrayList; +import java.util.Calendar; import java.util.List; +import java.util.Random; + +@Slf4j public class TbUtilsTest { + private ExecutionContext ctx; + + @Before + public void before() { + SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode(); + parserConfig.addImport("JSON", TbJson.class); + parserConfig.registerDataType("Date", TbDate.class, date -> 8L); + parserConfig.registerDataType("Random", Random.class, date -> 8L); + parserConfig.registerDataType("Calendar", Calendar.class, date -> 8L); + try { + TbUtils.register(parserConfig); + } catch (Exception e) { + log.error("Cannot register functions", e); + } + ctx = new ExecutionContext(parserConfig); + Assert.assertNotNull(ctx); + } + + @After + public void after() { + ctx.stop(); + } + @Test public void parseHexToInt() { Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB")); @@ -87,6 +122,71 @@ public class TbUtilsTest { Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, false)); } + @Test + public void toFlatMap() { + ExecutionHashMap inputMap = new ExecutionHashMap<>(16, ctx); + inputMap.put("name", "Alice"); + inputMap.put("age", 30); + inputMap.put("devices", new ExecutionArrayList<>(List.of( + new ExecutionHashMap<>(16, ctx) {{ + put("id", "dev001"); + put("type", "sensor"); + }}, + new ExecutionHashMap<>(16, ctx) {{ + put("id", "dev002"); + put("type", "actuator"); + }} + ), ctx)); + inputMap.put("settings", new ExecutionHashMap<>(16, ctx) {{ + put("notifications", true); + put("timezone", "UTC-5"); + put("params", new ExecutionHashMap<>(16, ctx) {{ + put("param1", "value1"); + put("param2", "value2"); + put("param3", new ExecutionHashMap<>(16, ctx) {{ + put("subParam1", "value1"); + put("subParam2", "value2"); + }}); + }}); + }}); + ExecutionArrayList excludeList = new ExecutionArrayList<>(ctx); + excludeList.addAll(List.of("age", "id", "param1", "subParam2")); + + ExecutionHashMap expectedMapWithPath = new ExecutionHashMap<>(16, ctx); + expectedMapWithPath.put("name", "Alice"); + expectedMapWithPath.put("devices.0.type", "sensor"); + expectedMapWithPath.put("devices.1.type", "actuator"); + expectedMapWithPath.put("settings.notifications", true); + expectedMapWithPath.put("settings.timezone", "UTC-5"); + expectedMapWithPath.put("settings.params.param2", "value2"); + expectedMapWithPath.put("settings.params.param3.subParam1", "value1"); + + ExecutionHashMap actualMapWithPaths = TbUtils.toFlatMap(ctx, inputMap, excludeList, true); + + Assert.assertEquals(expectedMapWithPath, actualMapWithPaths); + + ExecutionHashMap expectedMapWithoutPaths = new ExecutionHashMap<>(16, ctx); + expectedMapWithoutPaths.put("timezone", "UTC-5"); + expectedMapWithoutPaths.put("name", "Alice"); + expectedMapWithoutPaths.put("id", "dev002"); + expectedMapWithoutPaths.put("subParam2", "value2"); + expectedMapWithoutPaths.put("type", "actuator"); + expectedMapWithoutPaths.put("subParam1", "value1"); + expectedMapWithoutPaths.put("param1", "value1"); + expectedMapWithoutPaths.put("notifications", true); + expectedMapWithoutPaths.put("age", 30); + expectedMapWithoutPaths.put("param2", "value2"); + + ExecutionHashMap actualMapWithoutPaths = TbUtils.toFlatMap(ctx, inputMap, false); + + Assert.assertEquals(expectedMapWithoutPaths, actualMapWithoutPaths); + } + + + private static String keyToValue(String key, String extraSymbol) { + return key + "Value" + (extraSymbol == null ? "" : extraSymbol); + } + private static List toList(byte[] data) { List result = new ArrayList<>(data.length); for (Byte b : data) {