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: - Customer - use customer of incoming message originator as new originator. " +
"Only for assigned to customer originators with one of the following type: 'User', 'Asset', 'Device'.
" +
"- Tenant - use current tenant as new originator.
" +
- "- Related Entity - use related entity as new originator. Lookup based on configured relation type and direction. " +
+ "
- Related Entity - use related entity as new originator. Lookup based on configured relation query. " +
"If multiple related entities are found, only first entity is used as new originator, other entities are discarded.
" +
"- Alarm Originator - use alarm originator as new originator. Only if incoming message originator is alarm entity.
" +
- "- Entity by name pattern - new originator lookup requires selection one of the following entity types: " +
+ "
- Entity by name pattern - specify entity type and name pattern of new originator. Following entity types are supported: " +
"'Device', 'Asset', 'Entity View', 'Edge' or 'User' and input of name pattern.
" +
"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:
+
+
+
+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:
+
+
+
+ - Debug events for processed message from the `Humidity` sensor:
+
+
+
+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:
+
+
+
+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