diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 1f21435bd3..f80af0789e 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -229,6 +229,10 @@ public class JacksonUtil { } } + public static void addKvEntry(ObjectNode entityNode, KvEntry kvEntry) { + addKvEntry(entityNode, kvEntry, kvEntry.getKey()); + } + public static void addKvEntry(ObjectNode entityNode, KvEntry kvEntry, String key) { addKvEntry(entityNode, kvEntry, key, OBJECT_MAPPER); } @@ -249,7 +253,4 @@ public class JacksonUtil { } } - public static void addKvEntry(ObjectNode entityNode, KvEntry kvEntry) { - addKvEntry(entityNode, kvEntry, kvEntry.getKey()); - } } diff --git a/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java b/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java new file mode 100644 index 0000000000..22cd1b3ff6 --- /dev/null +++ b/common/util/src/test/java/org/thingsboard/common/util/JacksonUtilTest.java @@ -0,0 +1,35 @@ +/** + * Copyright © 2016-2022 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.common.util; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Test; + +public class JacksonUtilTest { + + @Test + public void allow_unquoted_field_mapper_test() { + String data = "{data: 123}"; + JsonNode actualResult = JacksonUtil.toJsonNode(data, JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER); // should be: {"data": 123} + ObjectNode expectedResult = JacksonUtil.newObjectNode(); + expectedResult.put("data", 123); // {"data": 123} + Assert.assertEquals(expectedResult, actualResult); + Assert.assertThrows(IllegalArgumentException.class, () -> JacksonUtil.toJsonNode(data)); // syntax exception due to missing quotes in the field name! + } + +} \ No newline at end of file diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java index 1b94bbfd2b..32f2d16484 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; @@ -91,16 +92,19 @@ public abstract class TbAbstractGetAttributesNode> failuresMap = new ConcurrentHashMap<>(); ListenableFuture>>> allFutures = Futures.allAsList( - getLatestTelemetry(ctx, entityId, msg, LATEST_TS, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresMap), + getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresMap), getAttrAsync(ctx, entityId, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg), failuresMap), getAttrAsync(ctx, entityId, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), msg), failuresMap), getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresMap) @@ -114,10 +118,11 @@ public abstract class TbAbstractGetAttributesNode { String prefix = getPrefix(keyScope); kvEntryList.forEach(kvEntry -> { + String key = prefix + kvEntry.getKey(); if (fetchToData) { - JacksonUtil.addKvEntry((ObjectNode) msgDataNode, kvEntry, prefix + kvEntry.getKey(), JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER); + JacksonUtil.addKvEntry((ObjectNode) msgDataNode, kvEntry, key); } else { - msgMetaData.putValue(prefix + kvEntry.getKey(), kvEntry.getValueAsString()); + msgMetaData.putValue(key, kvEntry.getValueAsString()); } }); }); @@ -145,7 +150,7 @@ public abstract class TbAbstractGetAttributesNode>> getLatestTelemetry(TbContext ctx, EntityId entityId, TbMsg msg, String scope, List keys, ConcurrentHashMap> failuresMap) { + private ListenableFuture>> getLatestTelemetry(TbContext ctx, EntityId entityId, List keys, ConcurrentHashMap> failuresMap) { if (CollectionUtils.isEmpty(keys)) { return Futures.immediateFuture(null); } @@ -155,7 +160,7 @@ public abstract class TbAbstractGetAttributesNode { if (tsKvEntry.getValue() == null) { if (isTellFailureIfAbsent) { - computeFailuresMap(scope, failuresMap, tsKvEntry.getKey()); + computeFailuresMap(LATEST_TS, failuresMap, tsKvEntry.getKey()); } } else if (getLatestValueWithTs) { listTsKvEntry.add(getValueWithTs(tsKvEntry)); @@ -164,7 +169,7 @@ public abstract class TbAbstractGetAttributesNode> mapTsKvEntry = new HashMap<>(); - mapTsKvEntry.put(scope, listTsKvEntry); + mapTsKvEntry.put(LATEST_TS, listTsKvEntry); return mapTsKvEntry; }, MoreExecutors.directExecutor()); } @@ -172,7 +177,8 @@ public abstract class TbAbstractGetAttributesNode serverAttributes; private List sharedAttributes; private List tsKeys; + private long ts; @Before public void before() throws TbNodeException { @@ -104,20 +106,21 @@ public class TbAbstractGetAttributesNodeTest { serverAttributes = getAttributeNames("server"); sharedAttributes = getAttributeNames("shared"); tsKeys = List.of("temperature", "humidity", "unknown"); + ts = System.currentTimeMillis(); Mockito.when(attributesService.find(tenantId, originator, DataConstants.CLIENT_SCOPE, clientAttributes)) - .thenReturn(Futures.immediateFuture(getListAttributeKvEntry(clientAttributes))); + .thenReturn(Futures.immediateFuture(getListAttributeKvEntry(clientAttributes, ts))); Mockito.when(attributesService.find(tenantId, originator, DataConstants.SERVER_SCOPE, serverAttributes)) - .thenReturn(Futures.immediateFuture(getListAttributeKvEntry(serverAttributes))); + .thenReturn(Futures.immediateFuture(getListAttributeKvEntry(serverAttributes, ts))); Mockito.when(attributesService.find(tenantId, originator, DataConstants.SHARED_SCOPE, sharedAttributes)) - .thenReturn(Futures.immediateFuture(getListAttributeKvEntry(sharedAttributes))); + .thenReturn(Futures.immediateFuture(getListAttributeKvEntry(sharedAttributes, ts))); Mockito.when(tsService.findLatest(tenantId, originator, tsKeys)) - .thenReturn(Futures.immediateFuture(getListTelemetryKvEntry(tsKeys))); + .thenReturn(Futures.immediateFuture(getListTsKvEntry(tsKeys, ts))); } @After @@ -143,8 +146,44 @@ public class TbAbstractGetAttributesNodeTest { checkTs(tsKeys, false, false, msgMetaData, null); } + @Test + public void fetchToMetadata_latestWithTs_whenOnMsg_then_success() throws Exception { + TbGetAttributesNode node = initNode(false, true, false); + TbMsg msg = getTbMsg(originator); + node.onMsg(ctx, msg); + + TbMsg resultMsg = checkMsg(); + TbMsgMetaData msgMetaData = resultMsg.getMetaData(); + + //check attributes + checkAttributes(clientAttributes, "cs_", false, msgMetaData, null); + checkAttributes(serverAttributes, "ss_", false, msgMetaData, null); + checkAttributes(sharedAttributes, "shared_", false, msgMetaData, null); + + //check timeseries with ts + checkTs(tsKeys, false, true, msgMetaData, null); + } + @Test public void fetchToData_whenOnMsg_then_success() throws Exception { + TbGetAttributesNode node = initNode(true, false, false); + TbMsg msg = getTbMsg(originator); + node.onMsg(ctx, msg); + + TbMsg resultMsg = checkMsg(); + JsonNode msgData = JacksonUtil.toJsonNode(resultMsg.getData()); + + //check attributes + checkAttributes(clientAttributes, "cs_", true, null, msgData); + checkAttributes(serverAttributes, "ss_", true, null, msgData); + checkAttributes(sharedAttributes, "shared_", true, null, msgData); + + //check timeseries + checkTs(tsKeys, true, false, null, msgData); + } + + @Test + public void fetchToData_latestWithTs_whenOnMsg_then_success() throws Exception { TbGetAttributesNode node = initNode(true, true, false); TbMsg msg = getTbMsg(originator); node.onMsg(ctx, msg); @@ -218,28 +257,26 @@ public class TbAbstractGetAttributesNodeTest { } private void checkTs(List tsKeys, boolean fetchToData, boolean getLatestValueWithTs, TbMsgMetaData msgMetaData, JsonNode msgData) { - long tsValue = 1L; + long value = 1L; for (String key : tsKeys) { if (key.equals("unknown")) { continue; } - String result; - if (fetchToData) { - if (getLatestValueWithTs) { - JsonNode resultTs = msgData.get(key); - Assert.assertNotNull(resultTs); - Assert.assertNotNull(resultTs.get("value")); - Assert.assertNotNull(resultTs.get("ts")); - result = resultTs.get("value").asText(); - } else { - result = msgData.get(key).asText(); - } + String actualValue; + String expectedValue; + if (getLatestValueWithTs) { + expectedValue = "{\"ts\":" + ts + ",\"value\":{\"data\":" + value + "}}"; } else { - result = msgMetaData.getValue(key); + expectedValue = "{\"data\":" + value + "}"; } - Assert.assertNotNull(result); - Assert.assertEquals(String.valueOf(tsValue), result); - tsValue++; + if (fetchToData) { + actualValue = JacksonUtil.toString(msgData.get(key)); + } else { + actualValue = msgMetaData.getValue(key); + } + Assert.assertNotNull(actualValue); + Assert.assertEquals(expectedValue, actualValue); + value++; } } @@ -273,20 +310,26 @@ public class TbAbstractGetAttributesNodeTest { return List.of(prefix + "_attr_1", prefix + "_attr_2", prefix + "_attr_3", "unknown"); } - private List getListAttributeKvEntry(List attributes) { - List attributeKvEntries = new ArrayList<>(); - attributes.stream().filter(attribute -> !attribute.equals("unknown")).forEach(attribute -> attributeKvEntries.add(new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry(attribute, attribute + "_value")))); - return attributeKvEntries; + private List getListAttributeKvEntry(List attributes, long ts) { + return attributes.stream() + .filter(attribute -> !attribute.equals("unknown")) + .map(attribute -> toAttributeKvEntry(ts, attribute)) + .collect(Collectors.toList()); } - private List getListTelemetryKvEntry(List keys) { + private BaseAttributeKvEntry toAttributeKvEntry(long ts, String attribute) { + return new BaseAttributeKvEntry(ts, new StringDataEntry(attribute, attribute + "_value")); + } + + private List getListTsKvEntry(List keys, long ts) { long value = 1L; List kvEntries = new ArrayList<>(); for (String key : keys) { if (key.equals("unknown")) { continue; } - kvEntries.add(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry(key, value))); + String dataValue = "{\"data\":" + value + "}"; + kvEntries.add(new BasicTsKvEntry(ts, new JsonDataEntry(key, dataValue))); value++; } return kvEntries;