Browse Source

Merge pull request #9284 from ShvaykaD/feature/transformation-nodes-enhancements

Transformation rule nodes enhancements
pull/9771/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
1cbb6e39ad
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java
  2. 40
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
  3. 16
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
  4. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java
  5. 1
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java
  6. 60
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java
  7. 14
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  8. 99
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java
  9. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeConfiguration.java
  10. 101
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java
  11. 5
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeConfiguration.java
  12. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java
  13. 88
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java
  14. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeConfiguration.java
  15. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java
  16. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
  17. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
  18. 188
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
  19. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
  20. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java
  21. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java
  22. 2
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java
  23. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java
  24. 3
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java
  25. 45
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java
  26. 47
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java
  27. 36
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java
  28. 81
      ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md
  29. 11
      ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md
  30. 68
      ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md
  31. BIN
      ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png
  32. BIN
      ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png
  33. BIN
      ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png
  34. 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/deduplication/TbMsgDeduplicationNode.java

@ -49,11 +49,10 @@ import java.util.concurrent.TimeUnit;
configClazz = TbMsgDeduplicationNodeConfiguration.class,
nodeDescription = "Deduplicate messages within the same originator entity for a configurable period " +
"based on a specified deduplication strategy.",
nodeDetails = "Rule node allows you to select one of the following strategy to deduplicate messages: <br></br>" +
"<b>FIRST</b> - return first message that arrived during deduplication period.<br></br>" +
"<b>LAST</b> - return last message that arrived during deduplication period.<br></br>" +
"<b>ALL</b> - return all messages as a single JSON array message. " +
"Where each element represents object with <b>msg</b> and <b>metadata</b> inner properties.<br></br>",
nodeDetails = "Deduplication strategies: <ul><li><strong>FIRST</strong> - return first message that arrived during deduplication period.</li>" +
"<li><strong>LAST</strong> - return last message that arrived during deduplication period.</li>" +
"<li><strong>ALL</strong> - return all messages as a single JSON array message. " +
"Where each element represents object with <strong><i>msg</i></strong> and <strong><i>metadata</i></strong> inner properties.</li></ul>",
icon = "content_copy",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeMsgDeduplicationConfig"

40
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.rule.engine.mail;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
@ -32,7 +31,6 @@ import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@ -42,8 +40,8 @@ import java.util.Map;
name = "to email",
configClazz = TbMsgToEmailNodeConfiguration.class,
nodeDescription = "Transforms message to email message",
nodeDetails = "Transforms message to email message by populating email fields using values derived from message metadata. " +
"Set 'SEND_EMAIL' output message type.",
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",
icon = "email"
@ -54,12 +52,12 @@ public class TbMsgToEmailNode implements TbNode {
private static final String DYNAMIC = "dynamic";
private TbMsgToEmailNodeConfiguration config;
private boolean isDynamicHtmlTemplate;
private boolean dynamicMailBodyType;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbMsgToEmailNodeConfiguration.class);
this.isDynamicHtmlTemplate = DYNAMIC.equals(this.config.getMailBodyType());
this.dynamicMailBodyType = DYNAMIC.equals(this.config.getMailBodyType());
}
@Override
@ -74,24 +72,22 @@ public class TbMsgToEmailNode implements TbNode {
}
}
private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) throws JsonProcessingException {
private TbMsg buildEmailMsg(TbContext ctx, TbMsg msg, TbEmail email) {
String emailJson = JacksonUtil.toString(email);
return ctx.transformMsg(msg, TbMsgType.SEND_EMAIL, msg.getOriginator(), msg.getMetaData().copy(), emailJson);
}
private TbEmail convert(TbMsg msg) throws IOException {
private TbEmail convert(TbMsg msg) {
TbEmail.TbEmailBuilder builder = TbEmail.builder();
builder.from(fromTemplate(this.config.getFromTemplate(), msg));
builder.to(fromTemplate(this.config.getToTemplate(), msg));
builder.cc(fromTemplate(this.config.getCcTemplate(), msg));
builder.bcc(fromTemplate(this.config.getBccTemplate(), msg));
if(isDynamicHtmlTemplate) {
builder.html(Boolean.parseBoolean(fromTemplate(this.config.getIsHtmlTemplate(), msg)));
} else {
builder.html(Boolean.parseBoolean(this.config.getMailBodyType()));
}
builder.subject(fromTemplate(this.config.getSubjectTemplate(), msg));
builder.body(fromTemplate(this.config.getBodyTemplate(), msg));
builder.from(fromTemplate(config.getFromTemplate(), msg));
builder.to(fromTemplate(config.getToTemplate(), msg));
builder.cc(fromTemplate(config.getCcTemplate(), msg));
builder.bcc(fromTemplate(config.getBccTemplate(), msg));
String htmlStr = dynamicMailBodyType ?
fromTemplate(config.getIsHtmlTemplate(), msg) : config.getMailBodyType();
builder.html(Boolean.parseBoolean(htmlStr));
builder.subject(fromTemplate(config.getSubjectTemplate(), msg));
builder.body(fromTemplate(config.getBodyTemplate(), msg));
String imagesStr = msg.getMetaData().getValue(IMAGES);
if (!StringUtils.isEmpty(imagesStr)) {
Map<String, String> imgMap = JacksonUtil.fromString(imagesStr, new TypeReference<HashMap<String, String>>() {});
@ -101,11 +97,7 @@ public class TbMsgToEmailNode implements TbNode {
}
private String fromTemplate(String template, TbMsg msg) {
if (!StringUtils.isEmpty(template)) {
return TbNodeUtils.processPattern(template, msg);
} else {
return null;
}
return StringUtils.isNotEmpty(template) ? TbNodeUtils.processPattern(template, msg) : null;
}
}

16
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java

@ -19,7 +19,7 @@ import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
public class TbMsgToEmailNodeConfiguration implements NodeConfiguration {
public class TbMsgToEmailNodeConfiguration implements NodeConfiguration<TbMsgToEmailNodeConfiguration> {
private String fromTemplate;
private String toTemplate;
@ -28,16 +28,16 @@ public class TbMsgToEmailNodeConfiguration implements NodeConfiguration {
private String subjectTemplate;
private String bodyTemplate;
private String isHtmlTemplate;
private String mailBodyType;
private String mailBodyType; // Plain Text -> false. HTML - true. Dynamic - value used from isHtmlTemplate.
@Override
public TbMsgToEmailNodeConfiguration defaultConfiguration() {
TbMsgToEmailNodeConfiguration configuration = new TbMsgToEmailNodeConfiguration();
configuration.fromTemplate = "info@testmail.org";
configuration.toTemplate = "${userEmail}";
configuration.subjectTemplate = "Device ${deviceType} temperature high";
configuration.bodyTemplate = "Device ${deviceName} has high temperature ${temp}";
configuration.mailBodyType = "false";
var configuration = new TbMsgToEmailNodeConfiguration();
configuration.setFromTemplate("info@testmail.org");
configuration.setToTemplate("${userEmail}");
configuration.setSubjectTemplate("Device ${deviceType} temperature high");
configuration.setBodyTemplate("Device ${deviceName} has high temperature $[temperature]");
configuration.setMailBodyType("false");
return configuration;
}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java

@ -32,6 +32,7 @@ import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Arrays;
import java.util.NoSuchElementException;
@Slf4j
@ -46,10 +47,9 @@ public abstract class TbAbstractNodeWithFetchTo<C extends TbAbstractFetchToNodeC
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
config = loadNodeConfiguration(configuration);
if (config.getFetchTo() == null) {
throw new TbNodeException("FetchTo cannot be null!");
} else {
fetchTo = config.getFetchTo();
throw new TbNodeException("FetchTo option can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
}
fetchTo = config.getFetchTo();
}
protected abstract C loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException;

1
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java

@ -27,7 +27,6 @@ import java.util.Map;
@EqualsAndHashCode(callSuper = true)
public class TbGetOriginatorFieldsConfiguration extends TbGetMappedDataNodeConfiguration implements NodeConfiguration<TbGetOriginatorFieldsConfiguration> {
private Map<String, String> dataMapping;
private boolean ignoreNullStrings;
@Override

60
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java

@ -0,0 +1,60 @@
/**
* Copyright © 2016-2023 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.transform;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.thingsboard.rule.engine.api.TbNode;
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 abstract String getKeyToUpgradeFromVersionZero();
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ?
upgradeToUseTbMsgSource((ObjectNode) oldConfiguration, getKeyToUpgradeFromVersionZero()) :
new TbPair<>(false, oldConfiguration);
}
private TbPair<Boolean, JsonNode> upgradeToUseTbMsgSource(ObjectNode configToUpdate, String newProperty) throws TbNodeException {
if (!configToUpdate.has(FROM_METADATA_PROPERTY)) {
throw new TbNodeException("property to update: '" + FROM_METADATA_PROPERTY + "' doesn't exists in configuration!");
}
var value = configToUpdate.get(FROM_METADATA_PROPERTY).asText();
if ("true".equals(value)) {
configToUpdate.remove(FROM_METADATA_PROPERTY);
configToUpdate.put(newProperty, TbMsgSource.METADATA.name());
return new TbPair<>(true, configToUpdate);
}
if ("false".equals(value)) {
configToUpdate.remove(FROM_METADATA_PROPERTY);
configToUpdate.put(newProperty, TbMsgSource.DATA.name());
return new TbPair<>(true, configToUpdate);
}
throw new TbNodeException("property to update: '" + FROM_METADATA_PROPERTY + "' has unexpected value: "
+ value + ". Allowed values: true or false!");
}
}

14
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -43,10 +43,16 @@ import java.util.NoSuchElementException;
type = ComponentType.TRANSFORMATION,
name = "change originator",
configClazz = TbChangeOriginatorNodeConfiguration.class,
nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity/Alarm Originator",
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
"If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded.<br/>" +
"Alarm Originator found only in case original Originator is <code>Alarm</code> entity.",
nodeDescription = "Change message originator to Tenant/Customer/Related Entity/Alarm Originator/Entity by name pattern.",
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 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> - specify entity type and name pattern of new originator. Following entity types are supported: " +
"'Device', 'Asset', 'Entity View', 'Edge' or 'User'.</li></ul>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeChangeOriginatorConfig",
icon = "find_replace"

99
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java

@ -21,89 +21,100 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.TbMsgSource;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.ArrayList;
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;
@Slf4j
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "copy keys",
name = "copy key-value pairs",
version = 1,
configClazz = TbCopyKeysNodeConfiguration.class,
nodeDescription = "Copies the msg or metadata keys with specified key names selected in the list",
nodeDetails = "Will fetch fields values specified in list. If specified field is not part of msg or metadata fields it will be ignored." +
"Returns transformed messages via <code>Success</code> chain",
nodeDescription = "Copies key-value pairs from message to message metadata or vice-versa.",
nodeDetails = "Copies key-value pairs from the message to message 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",
icon = "content_copy"
)
public class TbCopyKeysNode implements TbNode {
public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbCopyKeysNodeConfiguration config;
private List<Pattern> patternKeys;
private boolean fromMetadata;
private TbMsgSource copyFrom;
private List<Pattern> compiledKeyPatterns;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class);
this.fromMetadata = config.isFromMetadata();
this.patternKeys = new ArrayList<>();
config.getKeys().forEach(key -> {
this.patternKeys.add(Pattern.compile(key));
});
this.copyFrom = config.getCopyFrom();
if (copyFrom == null) {
throw new TbNodeException("CopyFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
}
this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList());
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
TbMsgMetaData metaData = msg.getMetaData();
var metaDataCopy = msg.getMetaData().copy();
String msgData = msg.getData();
boolean msgChanged = false;
JsonNode dataNode = JacksonUtil.toJsonNode(msgData);
if (dataNode.isObject()) {
if (fromMetadata) {
ObjectNode msgDataNode = (ObjectNode) dataNode;
Map<String, String> metaDataMap = metaData.getData();
for (Map.Entry<String, String> entry : metaDataMap.entrySet()) {
String keyData = entry.getKey();
if (checkKey(keyData)) {
msgChanged = true;
msgDataNode.put(keyData, entry.getValue());
switch (copyFrom) {
case METADATA:
ObjectNode msgDataNode = (ObjectNode) dataNode;
Map<String, String> metaDataMap = metaDataCopy.getData();
for (Map.Entry<String, String> entry : metaDataMap.entrySet()) {
String mdKey = entry.getKey();
String mdValue = entry.getValue();
if (matches(mdKey)) {
msgChanged = true;
msgDataNode.put(mdKey, mdValue);
}
}
}
msgData = JacksonUtil.toString(msgDataNode);
} else {
Iterator<Map.Entry<String, JsonNode>> iteratorNode = dataNode.fields();
while (iteratorNode.hasNext()) {
Map.Entry<String, JsonNode> entry = iteratorNode.next();
String keyData = entry.getKey();
if (checkKey(keyData)) {
msgChanged = true;
String value = entry.getValue().isTextual() ?
entry.getValue().asText() : JacksonUtil.toString(entry.getValue());
metaData.putValue(keyData, value);
msgData = JacksonUtil.toString(msgDataNode);
break;
case DATA:
Iterator<Map.Entry<String, JsonNode>> iteratorNode = dataNode.fields();
while (iteratorNode.hasNext()) {
Map.Entry<String, JsonNode> entry = iteratorNode.next();
String msgKey = entry.getKey();
JsonNode msgValue = entry.getValue();
if (matches(msgKey)) {
msgChanged = true;
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());
}
}
if (msgChanged) {
ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, msgData));
} else {
ctx.tellSuccess(msg);
}
ctx.tellSuccess(msgChanged ? TbMsg.transformMsg(msg, metaDataCopy, msgData) : msg);
}
@Override
protected String getKeyToUpgradeFromVersionZero() {
return "copyFrom";
}
boolean checkKey(String key) {
return patternKeys.stream().anyMatch(pattern -> pattern.matcher(key).matches());
boolean matches(String key) {
return compiledKeyPatterns.stream().anyMatch(pattern -> pattern.matcher(key).matches());
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeConfiguration.java

@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.transform;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.util.TbMsgSource;
import java.util.Collections;
import java.util.Set;
@ -24,14 +25,14 @@ import java.util.Set;
@Data
public class TbCopyKeysNodeConfiguration implements NodeConfiguration<TbCopyKeysNodeConfiguration> {
private boolean fromMetadata;
private TbMsgSource copyFrom;
private Set<String> keys;
@Override
public TbCopyKeysNodeConfiguration defaultConfiguration() {
TbCopyKeysNodeConfiguration configuration = new TbCopyKeysNodeConfiguration();
configuration.setKeys(Collections.emptySet());
configuration.setFromMetadata(false);
configuration.setCopyFrom(TbMsgSource.DATA);
return configuration;
}

101
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java

@ -21,84 +21,95 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.TbMsgSource;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
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;
@Slf4j
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "delete keys",
name = "delete key-value pairs",
version = 1,
configClazz = TbDeleteKeysNodeConfiguration.class,
nodeDescription = "Removes keys from the msg data or metadata with the specified key names selected in the list",
nodeDetails = "Will fetch fields (regex) values specified in list. If specified field (regex) is not part of msg " +
"or metadata fields it will be ignored. Returns transformed messages via <code>Success</code> chain",
nodeDescription = "Deletes key-value pairs from message or message metadata.",
nodeDetails = "Deletes key-value pairs from the message or message 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",
icon = "remove_circle"
)
public class TbDeleteKeysNode implements TbNode {
public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbDeleteKeysNodeConfiguration config;
private List<Pattern> patternKeys;
private boolean fromMetadata;
private TbMsgSource deleteFrom;
private List<Pattern> compiledKeyPatterns;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbDeleteKeysNodeConfiguration.class);
this.fromMetadata = config.isFromMetadata();
this.patternKeys = new ArrayList<>();
config.getKeys().forEach(key -> {
this.patternKeys.add(Pattern.compile(key));
});
this.deleteFrom = config.getDeleteFrom();
if (deleteFrom == null) {
throw new TbNodeException("DeleteFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
}
this.compiledKeyPatterns = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList());
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
TbMsgMetaData metaData = msg.getMetaData();
String msgData = msg.getData();
List<String> keysToDelete = new ArrayList<>();
if (fromMetadata) {
Map<String, String> metaDataMap = metaData.getData();
metaDataMap.forEach((keyMetaData, valueMetaData) -> {
if (checkKey(keyMetaData)) {
keysToDelete.add(keyMetaData);
var metaDataCopy = msg.getMetaData().copy();
var msgDataStr = msg.getData();
boolean hasNoChanges = false;
switch (deleteFrom) {
case METADATA:
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(msgDataStr);
if (dataNode.isObject()) {
var msgDataObject = (ObjectNode) dataNode;
var msgKeysToDelete = new ArrayList<String>();
dataNode.fieldNames().forEachRemaining(key -> {
if (matches(key)) {
msgKeysToDelete.add(key);
}
});
msgDataObject.remove(msgKeysToDelete);
msgDataStr = JacksonUtil.toString(msgDataObject);
hasNoChanges = msgKeysToDelete.isEmpty();
}
});
keysToDelete.forEach(metaDataMap::remove);
metaData = new TbMsgMetaData(metaDataMap);
} else {
JsonNode dataNode = JacksonUtil.toJsonNode(msgData);
if (dataNode.isObject()) {
ObjectNode msgDataObject = (ObjectNode) dataNode;
dataNode.fields().forEachRemaining(entry -> {
String keyData = entry.getKey();
if (checkKey(keyData)) {
keysToDelete.add(keyData);
}
});
msgDataObject.remove(keysToDelete);
msgData = JacksonUtil.toString(msgDataObject);
}
}
if (keysToDelete.isEmpty()) {
ctx.tellSuccess(msg);
} else {
ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, msgData));
break;
default:
log.debug("Unexpected DeleteFrom value: {}. Allowed values: {}", deleteFrom, TbMsgSource.values());
}
ctx.tellSuccess(hasNoChanges ? msg : TbMsg.transformMsg(msg, metaDataCopy, msgDataStr));
}
@Override
protected String getKeyToUpgradeFromVersionZero() {
return "deleteFrom";
}
boolean checkKey(String key) {
return patternKeys.stream().anyMatch(pattern -> pattern.matcher(key).matches());
boolean matches(String key) {
return compiledKeyPatterns.stream().anyMatch(pattern -> pattern.matcher(key).matches());
}
}

5
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeConfiguration.java

@ -17,6 +17,7 @@ package org.thingsboard.rule.engine.transform;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.util.TbMsgSource;
import java.util.Collections;
import java.util.Set;
@ -24,14 +25,14 @@ import java.util.Set;
@Data
public class TbDeleteKeysNodeConfiguration implements NodeConfiguration<TbDeleteKeysNodeConfiguration> {
private boolean fromMetadata;
private TbMsgSource deleteFrom;
private Set<String> keys;
@Override
public TbDeleteKeysNodeConfiguration defaultConfiguration() {
TbDeleteKeysNodeConfiguration configuration = new TbDeleteKeysNodeConfiguration();
configuration.setKeys(Collections.emptySet());
configuration.setFromMetadata(false);
configuration.setDeleteFrom(TbMsgSource.DATA);
return configuration;
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java

@ -38,10 +38,8 @@ 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. <br/>"
+ "<b>'$'</b> represents the root object or array. <br/>"
+ "If JSONPath expression evaluation failed, incoming message routes via <code>Failure</code> chain, "
+ "otherwise <code>Success</code> chain is used.",
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",
configDirective = "tbTransformationNodeJsonPathConfig"

88
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java

@ -21,14 +21,15 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.TbMsgSource;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ExecutionException;
@ -36,62 +37,81 @@ import java.util.concurrent.ExecutionException;
@RuleNode(
type = ComponentType.TRANSFORMATION,
name = "rename keys",
version = 1,
configClazz = TbRenameKeysNodeConfiguration.class,
nodeDescription = "Renames msg data or metadata keys to the new key names selected in the key mapping.",
nodeDetails = "If the key that is selected in the key mapping is missed in the selected msg source(data or metadata), it will be ignored." +
" Returns transformed messages via <code>Success</code> chain",
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 exist 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",
icon = "find_replace"
)
public class TbRenameKeysNode implements TbNode {
public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbRenameKeysNodeConfiguration config;
private Map<String, String> renameKeysMapping;
private boolean fromMetadata;
private TbMsgSource renameIn;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class);
this.renameIn = config.getRenameIn();
this.renameKeysMapping = config.getRenameKeysMapping();
this.fromMetadata = config.isFromMetadata();
if (renameIn == null) {
throw new TbNodeException("RenameIn can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
}
if (renameKeysMapping == null || renameKeysMapping.isEmpty()) {
throw new TbNodeException("At least one mapping entry should be specified!");
}
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
TbMsgMetaData metaData = msg.getMetaData();
TbMsgMetaData metaDataCopy = msg.getMetaData().copy();
String data = msg.getData();
boolean msgChanged = false;
if (fromMetadata) {
Map<String, String> metaDataMap = metaData.getData();
for (Map.Entry<String, String> entry : renameKeysMapping.entrySet()) {
String nameKey = entry.getKey();
if (metaDataMap.containsKey(nameKey)) {
msgChanged = true;
metaDataMap.put(entry.getValue(), metaDataMap.get(nameKey));
metaDataMap.remove(nameKey);
}
}
metaData = new TbMsgMetaData(metaDataMap);
} else {
JsonNode dataNode = JacksonUtil.toJsonNode(data);
if (dataNode.isObject()) {
ObjectNode msgData = (ObjectNode) dataNode;
switch (renameIn) {
case METADATA:
Map<String, String> metaDataMap = metaDataCopy.getData();
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 (metaDataMap.containsKey(currentKeyName)) {
msgChanged = true;
msgData.set(entry.getValue(), msgData.get(nameKey));
msgData.remove(nameKey);
String value = metaDataMap.get(currentKeyName);
metaDataMap.put(newKeyName, value);
metaDataMap.remove(currentKeyName);
}
}
data = JacksonUtil.toString(msgData);
}
}
if (msgChanged) {
ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, data));
} else {
ctx.tellSuccess(msg);
metaDataCopy = new TbMsgMetaData(metaDataMap);
break;
case DATA:
JsonNode dataNode = JacksonUtil.toJsonNode(data);
if (dataNode.isObject()) {
ObjectNode msgData = (ObjectNode) dataNode;
for (Map.Entry<String, String> entry : renameKeysMapping.entrySet()) {
String currentKeyName = entry.getKey();
String newKeyName = entry.getValue();
if (msgData.has(currentKeyName)) {
msgChanged = true;
JsonNode value = msgData.get(currentKeyName);
msgData.set(newKeyName, value);
msgData.remove(currentKeyName);
}
}
data = JacksonUtil.toString(msgData);
}
break;
default:
log.debug("Unexpected RenameIn value: {}. Allowed values: {}", renameIn, TbMsgSource.values());
}
ctx.tellSuccess(msgChanged ? TbMsg.transformMsg(msg, metaDataCopy, data) : msg);
}
@Override
protected String getKeyToUpgradeFromVersionZero() {
return "renameIn";
}
}

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeConfiguration.java

@ -17,20 +17,21 @@ package org.thingsboard.rule.engine.transform;
import lombok.Data;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.util.TbMsgSource;
import java.util.Map;
@Data
public class TbRenameKeysNodeConfiguration implements NodeConfiguration<TbRenameKeysNodeConfiguration> {
private boolean fromMetadata;
private TbMsgSource renameIn;
private Map<String, String> renameKeysMapping;
@Override
public TbRenameKeysNodeConfiguration defaultConfiguration() {
TbRenameKeysNodeConfiguration configuration = new TbRenameKeysNodeConfiguration();
configuration.setRenameKeysMapping(Map.of("temp", "temperature"));
configuration.setFromMetadata(false);
configuration.setRenameKeysMapping(Map.of("temperatureCelsius", "temperature"));
configuration.setRenameIn(TbMsgSource.DATA);
return configuration;
}

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java

@ -25,8 +25,8 @@ import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNode;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.msg.TbNodeConnectionType;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
@ -39,10 +39,10 @@ import java.util.concurrent.ExecutionException;
type = ComponentType.TRANSFORMATION,
name = "split array msg",
configClazz = EmptyNodeConfiguration.class,
nodeDescription = "Split array message into several msgs",
nodeDetails = "Split the array fetched from the msg body. If the msg data is not a JSON array returns the "
+ "incoming message as outbound message with <code>Failure</code> chain, otherwise returns "
+ "inner objects of the extracted array as separate messages via <code>Success</code> chain.",
nodeDescription = "Split array message into several messages",
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",
configDirective = "tbNodeEmptyConfig"

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java

@ -34,12 +34,13 @@ import java.util.List;
configClazz = TbTransformMsgNodeConfiguration.class,
nodeDescription = "Change Message payload, Metadata or Message type using JavaScript",
nodeDetails = "JavaScript function receive 3 input parameters <br/> " +
"<code>metadata</code> - is a Message metadata.<br/>" +
"<code>msg</code> - is a Message payload.<br/>" +
"<code>msgType</code> - is a Message type.<br/>" +
"<code>msg</code> - is a message payload.<br/>" +
"<code>metadata</code> - is a message metadata.<br/>" +
"<code>msgType</code> - is a message type.<br/>" +
"Should return the following structure:<br/>" +
"<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>&nbsp&nbsp&nbspmetadata: <i style=\"color: #666;\">new metadata</i>,<br/>&nbsp&nbsp&nbspmsgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" +
"All fields in resulting object are optional and will be taken from original message if not specified.",
"All fields in resulting object are optional and will be taken from original message if not specified.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeScriptConfig"
)

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java

@ -44,8 +44,7 @@ public class EntitiesCustomerIdAsyncLoader {
}
private static <T extends HasCustomerId> ListenableFuture<CustomerId> toCustomerIdAsync(TbContext ctx, ListenableFuture<T> future) {
return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getCustomerId())
: Futures.immediateFuture(null), ctx.getDbCallbackExecutor());
return Futures.transform(future, in -> in != null ? in.getCustomerId() : null, ctx.getDbCallbackExecutor());
}
}

188
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java

@ -15,12 +15,15 @@
*/
package org.thingsboard.rule.engine.mail;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import org.junit.Test;
import org.junit.runner.RunWith;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbEmail;
@ -28,85 +31,134 @@ import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.io.IOException;
import java.util.UUID;
import java.util.stream.Stream;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotSame;
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.verify;
@RunWith(MockitoJUnitRunner.class)
public class TbMsgToEmailNodeTest {
private TbMsgToEmailNode emailNode;
private static final int EXPECTED_TEMPERATURE = 30;
private static final String EXPECTED_DEVICE_NAME = "TH-001";
private static final String EXPECTED_DEVICE_TYPE = "thermostat";
private static final String EXPECTED_SUBJECT = "Device " + EXPECTED_DEVICE_TYPE + " temperature high";
private static final String EXPECTED_BODY = "Device " + EXPECTED_DEVICE_NAME + " has high temperature " + EXPECTED_TEMPERATURE;
private static final String EXPECTED_TO_EMAIL = "user@email.io";
private static final String DYNAMIC_MAIL_BODY_TYPE = "dynamic";
private EntityId originator;
private TbMsgToEmailNode node;
private TbMsgToEmailNodeConfiguration config;
private TbContext ctxMock;
@BeforeEach
void setUp() throws TbNodeException {
ctxMock = mock(TbContext.class);
originator = new DeviceId(UUID.randomUUID());
config = new TbMsgToEmailNodeConfiguration().defaultConfiguration();
node = new TbMsgToEmailNode();
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
}
@AfterEach
void tearDown() {
node.destroy();
}
@Mock
private TbContext ctx;
@Test
void givenDefaultConfig_whenVerify_thenOK() {
// GIVEN-WHEN-THEN
assertThat(config.getFromTemplate()).isEqualTo("info@testmail.org");
assertThat(config.getToTemplate()).isEqualTo("${userEmail}");
assertThat(config.getSubjectTemplate()).isEqualTo("Device ${deviceType} temperature high");
assertThat(config.getBodyTemplate()).isEqualTo("Device ${deviceName} has high temperature $[temperature]");
}
private EntityId originator = new DeviceId(Uuids.timeBased());
private TbMsgMetaData metaData = new TbMsgMetaData();
private String rawJson = "{\"name\": \"temp\", \"passed\": 5 , \"complex\": {\"val\":12, \"count\":100}}";
@ParameterizedTest
@MethodSource("MailBodyTypeTestConfig")
public void givenMailBodyTypeTestConfig_whenOnMsg_thenVerify(MailBodyTypeTestConfig testConfig) throws TbNodeException {
// GIVEN
String mailBodyType = testConfig.getMailBodyType();
config.setMailBodyType(mailBodyType);
if (DYNAMIC_MAIL_BODY_TYPE.equals(mailBodyType)) {
config.setIsHtmlTemplate("${html}");
}
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
var md = new TbMsgMetaData();
md.putValue("userEmail", EXPECTED_TO_EMAIL);
md.putValue("deviceType", EXPECTED_DEVICE_TYPE);
md.putValue("deviceName", EXPECTED_DEVICE_NAME);
if (testConfig.getIsHtmlTemplateMdValue() != null) {
md.putValue("html", testConfig.getIsHtmlTemplateMdValue());
}
private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased());
private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
var msgDataStr = "{\"temperature\": " + EXPECTED_TEMPERATURE + "}";
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, md, msgDataStr);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
var typeCaptor = ArgumentCaptor.forClass(TbMsgType.class);
var originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
var metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
var dataCaptor = ArgumentCaptor.forClass(String.class);
verify(ctxMock).transformMsg(
msgCaptor.capture(),
typeCaptor.capture(),
originatorCaptor.capture(),
metadataCaptor.capture(),
dataCaptor.capture()
);
verify(ctxMock, never()).tellFailure(any(), any());
Assertions.assertEquals(TbMsgType.SEND_EMAIL, typeCaptor.getValue());
Assertions.assertEquals(originator, originatorCaptor.getValue());
Assertions.assertNotSame(md, metadataCaptor.getValue());
var actual = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), TbEmail.class);
var expected = getExpectedTbEmail(testConfig.isExpectedHtmlValue());
Assertions.assertEquals(expected, actual);
}
@Test
public void msgCanBeConverted() {
initWithScript();
metaData.putValue("username", "oreo");
metaData.putValue("userEmail", "user@email.io");
metaData.putValue("name", "temp");
metaData.putValue("passed", "5");
metaData.putValue("count", "100");
TbMsg msg = TbMsg.newMsg(TbMsgType.POST_TELEMETRY_REQUEST, originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId);
emailNode.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<TbMsgType> typeCaptor = ArgumentCaptor.forClass(TbMsgType.class);
ArgumentCaptor<EntityId> originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
ArgumentCaptor<TbMsgMetaData> metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
ArgumentCaptor<String> dataCaptor = ArgumentCaptor.forClass(String.class);
verify(ctx).transformMsg(msgCaptor.capture(), typeCaptor.capture(), originatorCaptor.capture(), metadataCaptor.capture(), dataCaptor.capture());
assertEquals(TbMsgType.SEND_EMAIL, typeCaptor.getValue());
assertEquals(originator, originatorCaptor.getValue());
assertEquals("oreo", metadataCaptor.getValue().getValue("username"));
assertNotSame(metaData, metadataCaptor.getValue());
TbEmail actual = JacksonUtil.fromBytes(dataCaptor.getValue().getBytes(), TbEmail.class);
TbEmail expected = TbEmail.builder()
.from("test@mail.org")
.to("user@email.io")
.subject("Hi oreo there")
.body("temp is to high. Current 5 and 100")
private TbEmail getExpectedTbEmail(boolean html) {
return TbEmail.builder()
.from(config.getFromTemplate())
.to(EXPECTED_TO_EMAIL)
.subject(EXPECTED_SUBJECT)
.body(EXPECTED_BODY)
.html(html)
.build();
assertEquals(expected, actual);
}
private void initWithScript() {
try {
TbMsgToEmailNodeConfiguration config = new TbMsgToEmailNodeConfiguration();
config.setFromTemplate("test@mail.org");
config.setToTemplate("${userEmail}");
config.setSubjectTemplate("Hi ${username} there");
config.setBodyTemplate("${name} is to high. Current ${passed} and ${count}");
config.setMailBodyType("false");
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
emailNode = new TbMsgToEmailNode();
emailNode.init(ctx, nodeConfiguration);
} catch (TbNodeException ex) {
throw new IllegalStateException(ex);
}
static Stream<MailBodyTypeTestConfig> MailBodyTypeTestConfig() {
return Stream.of(
new MailBodyTypeTestConfig(false, "false", null),
new MailBodyTypeTestConfig(false, null, null),
new MailBodyTypeTestConfig(false, DYNAMIC_MAIL_BODY_TYPE, "false"),
new MailBodyTypeTestConfig(true, DYNAMIC_MAIL_BODY_TYPE, "true"),
new MailBodyTypeTestConfig(true, "true", null)
);
}
@Data
@RequiredArgsConstructor
static class MailBodyTypeTestConfig {
private final boolean expectedHtmlValue;
private final String mailBodyType;
private final String isHtmlTemplateMdValue;
}
}

3
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java

@ -59,6 +59,7 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -119,7 +120,7 @@ public class TbGetCustomerAttributeNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
assertThat(exception.getMessage()).isEqualTo("FetchTo option can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
verify(ctxMock, never()).tellSuccess(any());
}

3
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java

@ -57,6 +57,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
@ -127,7 +128,7 @@ public class TbGetCustomerDetailsNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
assertThat(exception.getMessage()).isEqualTo("FetchTo option can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
verify(ctxMock, never()).tellSuccess(any());
}

3
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
@ -86,7 +87,7 @@ public class TbGetOriginatorFieldsNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
assertThat(exception.getMessage()).isEqualTo("FetchTo option can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
verify(ctxMock, never()).tellSuccess(any());
}

2
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java

@ -127,7 +127,7 @@ public class TbGetRelatedAttributeNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
assertThat(exception.getMessage()).isEqualTo("FetchTo option can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
verify(ctxMock, never()).tellSuccess(any());
}

3
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java

@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@ -99,7 +100,7 @@ public class TbGetTenantAttributeNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
assertThat(exception.getMessage()).isEqualTo("FetchTo option can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
verify(ctxMock, never()).tellSuccess(any());
}

3
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java

@ -39,6 +39,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.tenant.TenantService;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
@ -96,7 +97,7 @@ public class TbGetTenantDetailsNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
assertThat(exception.getMessage()).isEqualTo("FetchTo option can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
verify(ctxMock, never()).tellSuccess(any());
}

45
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java

@ -16,14 +16,19 @@
package org.thingsboard.rule.engine.transform;
import com.fasterxml.jackson.databind.JsonNode;
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;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
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;
@ -35,13 +40,13 @@ 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.mockito.ArgumentMatchers.any;
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 TbCopyKeysNodeTest {
@ -59,7 +64,7 @@ public class TbCopyKeysNodeTest {
ctx = mock(TbContext.class);
config = new TbCopyKeysNodeConfiguration().defaultConfiguration();
config.setKeys(Set.of("TestKey_1", "TestKey_2", "TestKey_3", "(\\w*)Data(\\w*)"));
config.setFromMetadata(true);
config.setCopyFrom(TbMsgSource.METADATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node = spy(new TbCopyKeysNode());
node.init(ctx, nodeConfiguration);
@ -74,7 +79,7 @@ public class TbCopyKeysNodeTest {
void givenDefaultConfig_whenVerify_thenOK() {
TbCopyKeysNodeConfiguration defaultConfig = new TbCopyKeysNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getKeys()).isEqualTo(Collections.emptySet());
assertThat(defaultConfig.isFromMetadata()).isEqualTo(false);
assertThat(defaultConfig.getCopyFrom()).isEqualTo(TbMsgSource.DATA);
}
@Test
@ -82,7 +87,7 @@ public class TbCopyKeysNodeTest {
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();
@ -95,7 +100,7 @@ public class TbCopyKeysNodeTest {
@Test
void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception {
config.setFromMetadata(false);
config.setCopyFrom(TbMsgSource.DATA);
config.setKeys(Set.of(".*Key$"));
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
@ -105,7 +110,7 @@ public class TbCopyKeysNodeTest {
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();
@ -132,7 +137,7 @@ public class TbCopyKeysNodeTest {
node.onMsg(ctx, msg);
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();
@ -147,7 +152,7 @@ public class TbCopyKeysNodeTest {
node.onMsg(ctx, msg);
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();
@ -156,6 +161,30 @@ public class TbCopyKeysNodeTest {
assertThat(newMsg).isSameAs(msg);
}
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig() {
return Stream.of(
Arguments.of(0, "{\"fromMetadata\":false,\"keys\":[\"temperature\"]}", true, "{\"copyFrom\":\"DATA\",\"keys\":[\"temperature\"]}"),
Arguments.of(0, "{\"fromMetadata\":true,\"keys\":[\"temperature\"]}", true, "{\"copyFrom\":\"METADATA\",\"keys\":[\"temperature\"]}")
);
}
@ParameterizedTest
@MethodSource
void givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig(int givenVersion, String givenConfigStr,
boolean hasChanges, String expectedConfigStr) throws Exception {
// GIVEN
JsonNode givenConfig = JacksonUtil.toJsonNode(givenConfigStr);
JsonNode expectedConfig = JacksonUtil.toJsonNode(expectedConfigStr);
// WHEN
var upgradeResult = node.upgrade(givenVersion, givenConfig);
// THEN
assertThat(upgradeResult.getFirst()).isEqualTo(hasChanges);
ObjectNode upgradedConfig = (ObjectNode) upgradeResult.getSecond();
assertThat(upgradedConfig).isEqualTo(expectedConfig);
}
private TbMsg getTbMsg(EntityId entityId, String data) {
final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test",

47
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java

@ -16,14 +16,19 @@
package org.thingsboard.rule.engine.transform;
import com.fasterxml.jackson.databind.JsonNode;
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;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
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;
@ -35,13 +40,13 @@ 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.mockito.ArgumentMatchers.any;
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 {
@ -59,7 +64,7 @@ public class TbDeleteKeysNodeTest {
ctx = mock(TbContext.class);
config = new TbDeleteKeysNodeConfiguration().defaultConfiguration();
config.setKeys(Set.of("TestKey_1", "TestKey_2", "TestKey_3", "(\\w*)Data(\\w*)"));
config.setFromMetadata(true);
config.setDeleteFrom(TbMsgSource.METADATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node = spy(new TbDeleteKeysNode());
node.init(ctx, nodeConfiguration);
@ -74,15 +79,15 @@ public class TbDeleteKeysNodeTest {
void givenDefaultConfig_whenVerify_thenOK() {
TbDeleteKeysNodeConfiguration defaultConfig = new TbDeleteKeysNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getKeys()).isEqualTo(Collections.emptySet());
assertThat(defaultConfig.isFromMetadata()).isEqualTo(false);
assertThat(defaultConfig.getDeleteFrom()).isEqualTo(TbMsgSource.DATA);
}
@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();
@ -94,8 +99,8 @@ public class TbDeleteKeysNodeTest {
}
@Test
void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception {
config.setFromMetadata(false);
void givenDeleteFromMsgConfig_whenOnMsg_thenVerifyOutput() throws Exception {
config.setDeleteFrom(TbMsgSource.DATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
@ -103,7 +108,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();
@ -124,7 +129,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();
@ -133,6 +138,30 @@ public class TbDeleteKeysNodeTest {
assertThat(newMsg.getData()).isEqualTo(data);
}
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig() {
return Stream.of(
Arguments.of(0, "{\"fromMetadata\":false,\"keys\":[\"temperature\"]}", true, "{\"deleteFrom\":\"DATA\",\"keys\":[\"temperature\"]}"),
Arguments.of(0, "{\"fromMetadata\":true,\"keys\":[\"temperature\"]}", true, "{\"deleteFrom\":\"METADATA\",\"keys\":[\"temperature\"]}")
);
}
@ParameterizedTest
@MethodSource
void givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig(int givenVersion, String givenConfigStr,
boolean hasChanges, String expectedConfigStr) throws Exception {
// GIVEN
JsonNode givenConfig = JacksonUtil.toJsonNode(givenConfigStr);
JsonNode expectedConfig = JacksonUtil.toJsonNode(expectedConfigStr);
// WHEN
var upgradeResult = node.upgrade(givenVersion, givenConfig);
// THEN
assertThat(upgradeResult.getFirst()).isEqualTo(hasChanges);
ObjectNode upgradedConfig = (ObjectNode) upgradeResult.getSecond();
assertThat(upgradedConfig).isEqualTo(expectedConfig);
}
private TbMsg getTbMsg(EntityId entityId, String data) {
final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test",

36
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java

@ -16,14 +16,19 @@
package org.thingsboard.rule.engine.transform;
import com.fasterxml.jackson.databind.JsonNode;
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;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
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;
@ -33,6 +38,7 @@ 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.mockito.ArgumentMatchers.any;
@ -70,8 +76,8 @@ public class TbRenameKeysNodeTest {
@Test
void givenDefaultConfig_whenVerify_thenOK() {
TbRenameKeysNodeConfiguration defaultConfig = new TbRenameKeysNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getRenameKeysMapping()).isEqualTo(Map.of("temp", "temperature"));
assertThat(defaultConfig.isFromMetadata()).isEqualTo(false);
assertThat(defaultConfig.getRenameKeysMapping()).isEqualTo(Map.of("temperatureCelsius", "temperature"));
assertThat(defaultConfig.getRenameIn()).isEqualTo(TbMsgSource.DATA);
}
@Test
@ -95,7 +101,7 @@ public class TbRenameKeysNodeTest {
void givenMetadata_whenOnMsg_thenVerifyOutput() throws Exception {
config = new TbRenameKeysNodeConfiguration().defaultConfiguration();
config.setRenameKeysMapping(Map.of("TestKey_1", "Attribute_1", "TestKey_2", "Attribute_2"));
config.setFromMetadata(true);
config.setRenameIn(TbMsgSource.METADATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
@ -148,6 +154,30 @@ public class TbRenameKeysNodeTest {
assertThat(newMsg).isSameAs(msg);
}
private static Stream<Arguments> givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig() {
return Stream.of(
Arguments.of(0, "{\"fromMetadata\":false,\"renameKeysMapping\":{\"temp\":\"temperature\"}}", true, "{\"renameIn\":\"DATA\",\"renameKeysMapping\":{\"temp\":\"temperature\"}}"),
Arguments.of(0, "{\"fromMetadata\":true,\"renameKeysMapping\":{\"temp\":\"temperature\"}}", true, "{\"renameIn\":\"METADATA\",\"renameKeysMapping\":{\"temp\":\"temperature\"}}")
);
}
@ParameterizedTest
@MethodSource
void givenFromVersionAndConfig_whenUpgrade_thenVerifyUpgradeResultAndConfig(int givenVersion, String givenConfigStr,
boolean hasChanges, String expectedConfigStr) throws Exception {
// GIVEN
JsonNode givenConfig = JacksonUtil.toJsonNode(givenConfigStr);
JsonNode expectedConfig = JacksonUtil.toJsonNode(expectedConfigStr);
// WHEN
var upgradeResult = node.upgrade(givenVersion, givenConfig);
// THEN
assertThat(upgradeResult.getFirst()).isEqualTo(hasChanges);
ObjectNode upgradedConfig = (ObjectNode) upgradeResult.getSecond();
assertThat(upgradedConfig).isEqualTo(expectedConfig);
}
private TbMsg getTbMsg(EntityId entityId, String data) {
final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test",

81
ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md

@ -0,0 +1,81 @@
#### Fields templatization
<div class="divider"></div>
<br/>
{% include rulenode/common_node_fields_templatization %}
##### 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.

68
ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md

@ -0,0 +1,68 @@
#### Fields templatization
<div class="divider"></div>
<br/>
{% include rulenode/common_node_fields_templatization %}
##### 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