Browse Source

added see examples templatization docs && refactoring after review

pull/9284/head
ShvaykaD 3 years ago
parent
commit
057c26f8ea
  1. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
  2. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java
  3. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  4. 36
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java
  5. 55
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java
  6. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java
  7. 27
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java
  8. 4
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java
  9. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
  10. 37
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java
  11. 48
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java
  12. 37
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java
  13. 75
      ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md
  14. 11
      ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md
  15. 62
      ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md
  16. BIN
      ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png
  17. BIN
      ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png
  18. BIN
      ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png
  19. BIN
      ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png

9
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.<br><br>" +
"Additionally, supports adding inline base64 images to the mail body from message metadata " +
"by extracting them from <strong><i>images</i></strong> metadata key-value.<br>" +
"<strong><i>images</strong></i> key-value should represent a map of entries: <strong><i>cid --> encodedImage</i></strong>.<br>" +
"E.g., <strong><i>cid</i></strong> is a <strong><i>tb.example.png</i></strong> and<br>" +
"<strong><i>encodedImage</i></strong> is a <strong><i>data:image/png;base64,...</i></strong>, " +
"then you can use <i><strong>img</i></strong> HTML tag with src: <i><strong>cid:tb.example.png</i></strong>.<br><br>" +
"Set <code>SEND_EMAIL</code> output message type if transformation completed successfully.<br><br>" +
nodeDetails = "Transforms message to email message. If transformation completed successfully output message type will be set to <code>SEND_EMAIL</code>.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeToEmailConfig",

5
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<Pattern> compiledKeyPatterns;
protected abstract String getKeyToUpgradeFromVersionZero();
@Override

4
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: <ul><li><strong>Customer</strong> - use customer of incoming message originator as new originator. " +
"Only for assigned to customer originators with one of the following type: 'User', 'Asset', 'Device'.</li>" +
"<li><strong>Tenant</strong> - use current tenant as new originator.</li>" +
"<li><strong>Related Entity</strong> - use related entity as new originator. Lookup based on configured relation type and direction. " +
"<li><strong>Related Entity</strong> - 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.</li>" +
"<li><strong>Alarm Originator</strong> - use alarm originator as new originator. Only if incoming message originator is alarm entity.</li>" +
"<li><strong>Entity by name pattern</strong> - new originator lookup requires selection one of the following entity types: " +
"<li><strong>Entity by name pattern</strong> - 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.</li></ul>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},

36
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.<br><br>" +
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.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
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<Pattern> 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<String, String> metaDataMap = metaDataCopy.getData();
for (Map.Entry<String, String> 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<Map.Entry<String, JsonNode>> iteratorNode = dataNode.fields();
while (iteratorNode.hasNext()) {
Map.Entry<String, JsonNode> 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());
}
}

55
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.<br><br>" +
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.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
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<Pattern> 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<String> keysToDelete = new ArrayList<>();
var metaDataCopy = msg.getMetaData().copy();
var msgDataStr = msg.getData();
boolean hasNoChanges = false;
switch (deleteFrom) {
case METADATA:
Map<String, String> 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<String>();
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());
}
}

3
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. " +
"<strong><i>$</i></strong> represents the root object or array.<br><br>" +
nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
icon = "functions",

27
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.<br><br>" +
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.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeRenameKeysConfig",
@ -75,11 +75,13 @@ public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
case METADATA:
Map<String, String> metaDataMap = metaDataCopy.getData();
for (Map.Entry<String, String> 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<String, String> 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);
}

4
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.<br><br>" +
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.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
icon = "content_copy",

3
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(),

37
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<Arguments> 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<Boolean, JsonNode> 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) {

48
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<TbMsg> 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<TbMsg> 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<TbMsg> 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<Arguments> 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<Boolean, JsonNode> 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) {

37
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<Arguments> 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<Boolean, JsonNode> 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) {

75
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"
}
}
```
<br>
The same example for the `humidity` sensor:
- incoming message definition:
```json
{
"msg": {
"humidity": 77
},
"metadata": {
"deviceType": "Humidity",
"deviceName": "HM-001",
"ts": "1685379440000"
}
}
```
<br>
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`.
<br>
These examples showcases using the **change originator** node with dynamic configuration based on the substitution of metadata fields.
<br>
<br>

11
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.

62
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"
}
}
```
<br>
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"
}
}
```
<br>
These examples showcases using the **to email** node with dynamic configuration based on the substitution of message and message metadata fields.
<br>
<br>

BIN
ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Loading…
Cancel
Save