diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java index cb7b7d118b..6869268517 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java @@ -40,14 +40,7 @@ import java.util.Map; name = "to email", configClazz = TbMsgToEmailNodeConfiguration.class, nodeDescription = "Transforms message to email message", - nodeDetails = "Transforms message to email message. All email fields support templatization.

" + - "Additionally, supports adding inline base64 images to the mail body from message metadata " + - "by extracting them from images metadata key-value.
" + - "images key-value should represent a map of entries: cid --> encodedImage.
" + - "E.g., cid is a tb.example.png and
" + - "encodedImage is a data:image/png;base64,..., " + - "then you can use img HTML tag with src: cid:tb.example.png.

" + - "Set SEND_EMAIL output message type if transformation completed successfully.

" + + nodeDetails = "Transforms message to email message. If transformation completed successfully output message type will be set to SEND_EMAIL.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeToEmailConfig", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java index fc12a08294..22187b1524 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java @@ -22,10 +22,15 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.util.TbPair; +import java.util.List; +import java.util.regex.Pattern; + public abstract class TbAbstractTransformNodeWithTbMsgSource implements TbNode { private static final String FROM_METADATA_PROPERTY = "fromMetadata"; + protected List compiledKeyPatterns; + protected abstract String getKeyToUpgradeFromVersionZero(); @Override diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java index a88a87b400..e1ee2f4196 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java @@ -47,10 +47,10 @@ import java.util.NoSuchElementException; nodeDetails = "Configuration: " + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java index 05ae88b15f..02f031412b 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java @@ -30,23 +30,21 @@ import org.thingsboard.server.common.msg.TbMsg; import java.util.Arrays; import java.util.Iterator; -import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.regex.Pattern; import java.util.stream.Collectors; +// todo: check UI for copy key-value pairs @Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, - name = "copy key-values", + name = "copy key-value pairs", version = 1, configClazz = TbCopyKeysNodeConfiguration.class, - nodeDescription = "Copies key-values from message to message metadata or vice-versa.", - nodeDetails = "Fetches key-values from message or message metadata based on the keys list specified in the configuration " + - "and copies them into message metadata or into message itself in accordance with the fetch source. " + - "Keys that are absent in the fetch source will be ignored. " + - "Use regular expression(s) as a key(s) to copy keys by pattern.

" + + nodeDescription = "Copies key-value pairs from message to message metadata or vice-versa.", + nodeDetails = "Copies key-value pairs from the message body to metadata, or vice-versa, according to the configured direction and keys. " + + "Regular expressions can be used to define which keys-value pairs to copy. Any configured key not found in the source will be ignored.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeCopyKeysConfig", @@ -55,7 +53,6 @@ import java.util.stream.Collectors; public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource { private TbCopyKeysNodeConfiguration config; - private List patternKeys; private TbMsgSource copyFrom; @Override @@ -65,7 +62,7 @@ public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource { if (copyFrom == null) { throw new TbNodeException("CopyFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values())); } - this.patternKeys = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); + this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); } @Override @@ -80,10 +77,11 @@ public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource { ObjectNode msgDataNode = (ObjectNode) dataNode; Map metaDataMap = metaDataCopy.getData(); for (Map.Entry entry : metaDataMap.entrySet()) { - String keyData = entry.getKey(); - if (matches(keyData)) { + String mdKey = entry.getKey(); + String mdValue = entry.getValue(); + if (matches(mdKey)) { msgChanged = true; - msgDataNode.put(keyData, entry.getValue()); + msgDataNode.put(mdKey, mdValue); } } msgData = JacksonUtil.toString(msgDataNode); @@ -92,18 +90,18 @@ public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource { Iterator> iteratorNode = dataNode.fields(); while (iteratorNode.hasNext()) { Map.Entry entry = iteratorNode.next(); - String keyData = entry.getKey(); - if (matches(keyData)) { + String msgKey = entry.getKey(); + JsonNode msgValue = entry.getValue(); + if (matches(msgKey)) { msgChanged = true; - String value = entry.getValue().isTextual() ? - entry.getValue().asText() : JacksonUtil.toString(entry.getValue()); - metaDataCopy.putValue(keyData, value); + String value = msgValue.isTextual() ? + msgValue.asText() : JacksonUtil.toString(msgValue); + metaDataCopy.putValue(msgKey, value); } } break; default: log.debug("Unexpected CopyFrom value: {}. Allowed values: {}", copyFrom, TbMsgSource.values()); - break; } } ctx.tellSuccess(msgChanged ? TbMsg.transformMsg(msg, metaDataCopy, msgData) : msg); @@ -115,7 +113,7 @@ public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource { } boolean matches(String key) { - return patternKeys.stream().anyMatch(pattern -> pattern.matcher(key).matches()); + return compiledKeyPatterns.stream().anyMatch(pattern -> pattern.matcher(key).matches()); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java index 30e182a254..6899ac3fad 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java @@ -31,8 +31,6 @@ import org.thingsboard.server.common.msg.TbMsgMetaData; import java.util.ArrayList; import java.util.Arrays; -import java.util.List; -import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -40,12 +38,12 @@ import java.util.stream.Collectors; @Slf4j @RuleNode( type = ComponentType.TRANSFORMATION, - name = "delete key-values", + name = "delete key-value pairs", version = 1, configClazz = TbDeleteKeysNodeConfiguration.class, - nodeDescription = "Removes key-values from message or message metadata.", - nodeDetails = "Removes key-values from message or message metadata based on the keys list specified in the configuration. " + - "Use regular expression(s) as a key(s) to remove keys by pattern.

" + + nodeDescription = "Deletes key-value pairs from message or message metadata.", + nodeDetails = "Deletes key-value pairs from the message body or metadata according to the configured " + + "keys and/or regular expressions.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeDeleteKeysConfig", @@ -54,7 +52,6 @@ import java.util.stream.Collectors; public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource { private TbDeleteKeysNodeConfiguration config; - private List patternKeys; private TbMsgSource deleteFrom; @Override @@ -64,44 +61,44 @@ public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource { if (deleteFrom == null) { throw new TbNodeException("DeleteFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values())); } - this.patternKeys = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); + this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList()); } @Override public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { - TbMsgMetaData metaDataCopy = msg.getMetaData().copy(); - String msgData = msg.getData(); - List keysToDelete = new ArrayList<>(); + var metaDataCopy = msg.getMetaData().copy(); + var msgDataStr = msg.getData(); + boolean hasNoChanges = false; switch (deleteFrom) { case METADATA: - Map metaDataMap = metaDataCopy.getData(); - metaDataMap.forEach((keyMetaData, valueMetaData) -> { - if (matches(keyMetaData)) { - keysToDelete.add(keyMetaData); - } - }); - keysToDelete.forEach(metaDataMap::remove); + var metaDataMap = metaDataCopy.getData(); + var mdKeysToDelete = metaDataMap.keySet() + .stream() + .filter(this::matches) + .collect(Collectors.toList()); + mdKeysToDelete.forEach(metaDataMap::remove); metaDataCopy = new TbMsgMetaData(metaDataMap); + hasNoChanges = mdKeysToDelete.isEmpty(); break; case DATA: - JsonNode dataNode = JacksonUtil.toJsonNode(msgData); + JsonNode dataNode = JacksonUtil.toJsonNode(msgDataStr); if (dataNode.isObject()) { - ObjectNode msgDataObject = (ObjectNode) dataNode; - dataNode.fields().forEachRemaining(entry -> { - String keyData = entry.getKey(); - if (matches(keyData)) { - keysToDelete.add(keyData); + var msgDataObject = (ObjectNode) dataNode; + var msgKeysToDelete = new ArrayList(); + dataNode.fieldNames().forEachRemaining(key -> { + if (matches(key)) { + msgKeysToDelete.add(key); } }); - msgDataObject.remove(keysToDelete); - msgData = JacksonUtil.toString(msgDataObject); + msgDataObject.remove(msgKeysToDelete); + msgDataStr = JacksonUtil.toString(msgDataObject); + hasNoChanges = msgKeysToDelete.isEmpty(); } break; default: log.debug("Unexpected DeleteFrom value: {}. Allowed values: {}", deleteFrom, TbMsgSource.values()); - break; } - ctx.tellSuccess(keysToDelete.isEmpty() ? msg : TbMsg.transformMsg(msg, metaDataCopy, msgData)); + ctx.tellSuccess(hasNoChanges ? msg : TbMsg.transformMsg(msg, metaDataCopy, msgDataStr)); } @Override @@ -110,7 +107,7 @@ public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource { } boolean matches(String key) { - return patternKeys.stream().anyMatch(pattern -> pattern.matcher(key).matches()); + return compiledKeyPatterns.stream().anyMatch(pattern -> pattern.matcher(key).matches()); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java index 6e00b98e3e..b22ce20dcb 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java @@ -38,8 +38,7 @@ import java.util.concurrent.ExecutionException; name = "json path", configClazz = TbJsonPathNodeConfiguration.class, nodeDescription = "Transforms incoming message body using JSONPath expression.", - nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure. " + - "$ represents the root object or array.

" + + nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, icon = "functions", diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java index 7305c3ee78..9170016d06 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java @@ -39,9 +39,9 @@ import java.util.concurrent.ExecutionException; name = "rename keys", version = 1, configClazz = TbRenameKeysNodeConfiguration.class, - nodeDescription = "Renames message or message metadata key names.", - nodeDetails = "Renames key names in the message or message metadata based on the provided key names mapping. " + - "If key to rename doesn't exists in the specified source(message or message metadata) it will be ignored.

" + + nodeDescription = "Renames message or message metadata keys.", + nodeDetails = "Renames keys in the message or message metadata according to the provided mapping. " + + "If key to rename doesn't exists in the specified source (message or message metadata) it will be ignored.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, configDirective = "tbTransformationNodeRenameKeysConfig", @@ -75,11 +75,13 @@ public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource { case METADATA: Map metaDataMap = metaDataCopy.getData(); for (Map.Entry entry : renameKeysMapping.entrySet()) { - String nameKey = entry.getKey(); - if (metaDataMap.containsKey(nameKey)) { + String currentKeyName = entry.getKey(); + String newKeyName = entry.getValue(); + if (metaDataMap.containsKey(currentKeyName)) { msgChanged = true; - metaDataMap.put(entry.getValue(), metaDataMap.get(nameKey)); - metaDataMap.remove(nameKey); + String value = metaDataMap.get(currentKeyName); + metaDataMap.put(newKeyName, value); + metaDataMap.remove(currentKeyName); } } metaDataCopy = new TbMsgMetaData(metaDataMap); @@ -89,11 +91,13 @@ public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource { if (dataNode.isObject()) { ObjectNode msgData = (ObjectNode) dataNode; for (Map.Entry entry : renameKeysMapping.entrySet()) { - String nameKey = entry.getKey(); - if (msgData.has(nameKey)) { + String currentKeyName = entry.getKey(); + String newKeyName = entry.getValue(); + if (msgData.has(currentKeyName)) { msgChanged = true; - msgData.set(entry.getValue(), msgData.get(nameKey)); - msgData.remove(nameKey); + JsonNode value = msgData.get(currentKeyName); + msgData.set(newKeyName, value); + msgData.remove(currentKeyName); } } data = JacksonUtil.toString(msgData); @@ -101,7 +105,6 @@ public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource { break; default: log.debug("Unexpected RenameIn value: {}. Allowed values: {}", renameIn, TbMsgSource.values()); - break; } ctx.tellSuccess(msgChanged ? TbMsg.transformMsg(msg, metaDataCopy, data) : msg); } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java index 7a9c812ebc..40ddccbea7 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java @@ -40,8 +40,8 @@ import java.util.concurrent.ExecutionException; name = "split array msg", configClazz = EmptyNodeConfiguration.class, nodeDescription = "Split array message into several messages", - nodeDetails = "Split array message and returns inner objects as separate messages. " + - "Message metadata and type for each output message will be taken from original message.

" + + nodeDetails = "Splits an array message into individual elements, with each element sent as a separate message. " + + "All outbound messages will have the same type and metadata as the original array message.

" + "Output connections: Success, Failure.", uiResources = {"static/rulenode/rulenode-core-config.js"}, icon = "content_copy", diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java index a351022341..9d732301f8 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java @@ -42,7 +42,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class TbMsgToEmailNodeTest { @@ -115,7 +114,7 @@ public class TbMsgToEmailNodeTest { var originatorCaptor = ArgumentCaptor.forClass(EntityId.class); var metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class); var dataCaptor = ArgumentCaptor.forClass(String.class); - verify(ctxMock, times(1)).transformMsg( + verify(ctxMock).transformMsg( msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java index 1b3c199d12..bf88aa81a2 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; @@ -29,7 +32,6 @@ import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -38,11 +40,12 @@ import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -161,18 +164,28 @@ public class TbCopyKeysNodeTest { assertThat(newMsg).isSameAs(msg); } - @Test - void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig() { + return Stream.of( + Arguments.of(0, "{\"fromMetadata\":false,\"keys\":[\"temperature\"]}", true, "{\"copyFrom\":\"DATA\",\"keys\":[\"temperature\"]}") + ); + } + + @ParameterizedTest + @MethodSource + void givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig(int givenVersion, String givenConfigStr, + boolean hasChanges, String expectedConfigStr) throws Exception { // GIVEN - var config = new TbCopyKeysNodeConfiguration().defaultConfiguration(); - var oldConfigJson = (ObjectNode) JacksonUtil.valueToTree(config); - oldConfigJson.remove("copyFrom"); - oldConfigJson.put("fromMetadata", "false"); + willCallRealMethod().given(node).upgrade(anyInt(), any()); + JsonNode givenConfig = JacksonUtil.toJsonNode(givenConfigStr); + JsonNode expectedConfig = JacksonUtil.toJsonNode(expectedConfigStr); + // WHEN - TbPair upgrade = node.upgrade(0, oldConfigJson); + var upgradeResult = node.upgrade(givenVersion, givenConfig); + // THEN - assertTrue(upgrade.getFirst()); - assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass())); + assertThat(upgradeResult.getFirst()).isEqualTo(hasChanges); + ObjectNode upgradedConfig = (ObjectNode) upgradeResult.getSecond(); + assertThat(upgradedConfig).isEqualTo(expectedConfig); } private TbMsg getTbMsg(EntityId entityId, String data) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java index 042babb1b9..eaa3c11e7e 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; @@ -29,7 +32,6 @@ import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; @@ -38,15 +40,15 @@ import java.util.Collections; import java.util.Map; import java.util.Set; import java.util.UUID; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; public class TbDeleteKeysNodeTest { @@ -83,11 +85,11 @@ public class TbDeleteKeysNodeTest { } @Test - void givenMsgFromMetadata_whenOnMsg_thenVerifyOutput() throws Exception { + void givenDeleteFromMetadata_whenOnMsg_thenVerifyOutput() throws Exception { node.onMsg(ctx, getTbMsg(deviceId, TbMsg.EMPTY_JSON_OBJECT)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture()); + verify(ctx).tellSuccess(newMsgCaptor.capture()); verify(ctx, never()).tellFailure(any(), any()); TbMsg newMsg = newMsgCaptor.getValue(); @@ -99,7 +101,7 @@ public class TbDeleteKeysNodeTest { } @Test - void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception { + void givenDeleteFromMsgConfig_whenOnMsg_thenVerifyOutput() throws Exception { config.setDeleteFrom(TbMsgSource.DATA); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); node.init(ctx, nodeConfiguration); @@ -108,7 +110,7 @@ public class TbDeleteKeysNodeTest { node.onMsg(ctx, getTbMsg(deviceId, data)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture()); + verify(ctx).tellSuccess(newMsgCaptor.capture()); verify(ctx, never()).tellFailure(any(), any()); TbMsg newMsg = newMsgCaptor.getValue(); @@ -129,7 +131,7 @@ public class TbDeleteKeysNodeTest { node.onMsg(ctx, getTbMsg(deviceId, data)); ArgumentCaptor newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class); - verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture()); + verify(ctx).tellSuccess(newMsgCaptor.capture()); verify(ctx, never()).tellFailure(any(), any()); TbMsg newMsg = newMsgCaptor.getValue(); @@ -138,18 +140,28 @@ public class TbDeleteKeysNodeTest { assertThat(newMsg.getData()).isEqualTo(data); } - @Test - void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig() { + return Stream.of( + Arguments.of(0, "{\"fromMetadata\":false,\"keys\":[\"temperature\"]}", true, "{\"deleteFrom\":\"DATA\",\"keys\":[\"temperature\"]}") + ); + } + + @ParameterizedTest + @MethodSource + void givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig(int givenVersion, String givenConfigStr, + boolean hasChanges, String expectedConfigStr) throws Exception { // GIVEN - var config = new TbDeleteKeysNodeConfiguration().defaultConfiguration(); - var oldConfigJson = (ObjectNode) JacksonUtil.valueToTree(config); - oldConfigJson.remove("deleteFrom"); - oldConfigJson.put("fromMetadata", "false"); + willCallRealMethod().given(node).upgrade(anyInt(), any()); + JsonNode givenConfig = JacksonUtil.toJsonNode(givenConfigStr); + JsonNode expectedConfig = JacksonUtil.toJsonNode(expectedConfigStr); + // WHEN - TbPair upgrade = node.upgrade(0, oldConfigJson); + var upgradeResult = node.upgrade(givenVersion, givenConfig); + // THEN - assertTrue(upgrade.getFirst()); - assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass())); + assertThat(upgradeResult.getFirst()).isEqualTo(hasChanges); + ObjectNode upgradedConfig = (ObjectNode) upgradeResult.getSecond(); + assertThat(upgradedConfig).isEqualTo(expectedConfig); } private TbMsg getTbMsg(EntityId entityId, String data) { diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java index ae911ea529..84c0bc8ba9 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java @@ -20,6 +20,9 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.mockito.ArgumentCaptor; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; @@ -29,18 +32,18 @@ import org.thingsboard.rule.engine.util.TbMsgSource; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.msg.TbMsgType; -import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.queue.TbMsgCallback; import java.util.Map; import java.util.UUID; +import java.util.stream.Stream; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.BDDMockito.willCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -153,18 +156,28 @@ public class TbRenameKeysNodeTest { assertThat(newMsg).isSameAs(msg); } - @Test - void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception { + private static Stream givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig() { + return Stream.of( + Arguments.of(0, "{\"fromMetadata\":false,\"renameKeysMapping\":{\"temp\":\"temperature\"}}", true, "{\"renameIn\":\"DATA\",\"renameKeysMapping\":{\"temp\":\"temperature\"}}") + ); + } + + @ParameterizedTest + @MethodSource + void givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig(int givenVersion, String givenConfigStr, + boolean hasChanges, String expectedConfigStr) throws Exception { // GIVEN - var config = new TbRenameKeysNodeConfiguration().defaultConfiguration(); - var oldConfigJson = (ObjectNode) JacksonUtil.valueToTree(config); - oldConfigJson.remove("renameIn"); - oldConfigJson.put("fromMetadata", "false"); + willCallRealMethod().given(node).upgrade(anyInt(), any()); + JsonNode givenConfig = JacksonUtil.toJsonNode(givenConfigStr); + JsonNode expectedConfig = JacksonUtil.toJsonNode(expectedConfigStr); + // WHEN - TbPair upgrade = node.upgrade(0, oldConfigJson); + var upgradeResult = node.upgrade(givenVersion, givenConfig); + // THEN - assertTrue(upgrade.getFirst()); - assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass())); + assertThat(upgradeResult.getFirst()).isEqualTo(hasChanges); + ObjectNode upgradedConfig = (ObjectNode) upgradeResult.getSecond(); + assertThat(upgradedConfig).isEqualTo(expectedConfig); } private TbMsg getTbMsg(EntityId entityId, String data) { diff --git a/ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md index 422fc20756..204297957a 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md @@ -5,4 +5,77 @@ {% include rulenode/common_node_fields_templatization %} -##### Examples coming soon +##### Example + +Let's assume that a tenant manages two assets: + + - `TemperatureManager` asset - responsible for aggregating data from temperature sensors, essential for environmental monitoring and alerts. + - `HumidityManager` asset - collects data from humidity sensors, analyzing relative humidity levels + and correlating them with temperature data for comprehensive atmospheric condition monitoring and automated environmental adjustments. + +Each message received from the device includes `deviceType` property in the message metadata +with either `Temperature` or `Humidity` value according to the sensor type. + +In order to change the originator to the corresponding manager for further message processing, you can define the next node configuration: + +![image](${helpBaseUrl}/help/images/rulenode/examples/change-originator-ft.png) + +Imagine that you receive the message defined below from the `Temperature` sensor +and forwarded it to the **change originator** node with configuration added above. + +- incoming message definition: + +```json +{ + "msg": { + "temperature": 32 + }, + "metadata": { + "deviceType": "Temperature", + "deviceName": "TH-001", + "ts": "1685379440000" + } +} +``` +
+ +The same example for the `humidity` sensor: + +- incoming message definition: + +```json +{ + "msg": { + "humidity": 77 + }, + "metadata": { + "deviceType": "Humidity", + "deviceName": "HM-001", + "ts": "1685379440000" + } +} +``` + +
+ +To demonstrate that the originator was changed based on rule node configuration, the screenshots with debug events captured for the **change originator** rule node will be added below: + + - Debug events for processed message from the `Temperature` sensor: + +![image](${helpBaseUrl}/help/images/rulenode/examples/change-originator-ft-2.png) + + - Debug events for processed message from the `Humidity` sensor: + +![image](${helpBaseUrl}/help/images/rulenode/examples/change-originator-ft-3.png) + +In the debug events displayed, the `IN` message points to the incoming message received by the node, +with the originator type specified as `DEVICE`. This reflects the message from the actual sensor (e.g., a `Temperature` or `Humidity` sensor). +After processing through the `change originator` rule node, the `OUT` message has an originator type of `ASSET`, +indicating that the message originator has been successfully changed to the corresponding managing asset, such as `TemperatureManager` or `HumidityManager`. + +
+ +These examples showcases using the **change originator** node with dynamic configuration based on the substitution of metadata fields. + +
+
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md index dc1e721bd0..0ccbbd9058 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md @@ -1,9 +1,4 @@ -Fields templatization feature allows you to process the incoming messages with dynamic configuration by substitution templates specified in the configuration fields with values from message or message metadata. +Fields templatization feature allows you to process the incoming messages with dynamic configuration +by substitution of templates specified in the configuration fields with values from message or message metadata. +For more detailed information, please refer to the ThingsBoard [documentation](https://thingsboard.io/docs/user-guide/templatization/) -There are two types of rule node configuration templates defined: - - - `$[messageKey]` - templates with square brackets used to extract value from the message. - - - `${metadataKey}` - templates with curly brackets used to extract value from the message metadata. - -**Note:** `messageKey` and `metadataKey` are just samples of key names that might exist in the message or metadata. diff --git a/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md index 422fc20756..8fbc917327 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md @@ -5,4 +5,64 @@ {% include rulenode/common_node_fields_templatization %} -##### Examples coming soon +##### Example + +Let's assume that a tenant manages temperature sensors. +When a temperature sensor reports a high temperature, the platform creates an alarm. + +In addition, let's assume that each sensor has a group of alarm notification subscribers +associated with it and a primary user that responsible for updating the alarm status. + +Imagine that after alarm creation we fetched information about notification subscribers and +primary users with the help of enrichment rule nodes and after that our message looks like this: + +```json +{ + "msg": { + "temperature": 32 + }, + "metadata": { + "deviceType": "Thermostat", + "deviceName": "TH-001", + "ts": "1685379440000", + "primaryUser": "john.doe@example.com", + "subscribers": "mike.johnson@example.io,sarah.smith@example.org,emily.davis@example.co" + } +} +``` +
+ +Here is a node configuration: + +![image](${helpBaseUrl}/help/images/rulenode/examples/to-email-ft.png) + +After message evaluation by a rule node the outgoing message will be looks like this: + +```json +{ + "msg": { + "from": "info@testmail.org", + "to": "john.doe@example.com", + "cc": "mike.johnson@example.io,sarah.smith@example.org,emily.davis@example.co", + "bcc": null, + "subject": "Device Thermostat temperature high", + "body": "Device TH-001 has high temperature 32", + "images": null, + "html": false + }, + "metadata": { + "deviceType": "Thermostat", + "deviceName": "TH-001", + "ts": "1685379440000", + "primaryUser": "john.doe@example.com", + "subscribers": "mike.johnson@example.io,sarah.smith@example.org,emily.davis@example.co" + } +} +``` + +
+ +These examples showcases using the **to email** node with dynamic configuration based on the substitution of message and message metadata fields. + +
+
diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png new file mode 100644 index 0000000000..744b29d3da Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png differ diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png new file mode 100644 index 0000000000..4fa936482c Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png differ diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png new file mode 100644 index 0000000000..8b3c555e80 Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png differ diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png b/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png new file mode 100644 index 0000000000..544441de87 Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png differ