diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java
index 81bb3d6772..043cc7793f 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/deduplication/TbMsgDeduplicationNode.java
+++ b/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:
" +
- "FIRST - return first message that arrived during deduplication period.
" +
- "LAST - return last message that arrived during deduplication period.
" +
- "ALL - return all messages as a single JSON array message. " +
- "Where each element represents object with msg and metadata inner properties.
",
+ nodeDetails = "Deduplication strategies:
- FIRST - return first message that arrived during deduplication period.
" +
+ "- LAST - return last message that arrived during deduplication period.
" +
+ "- ALL - return all messages as a single JSON array message. " +
+ "Where each element represents object with msg and metadata inner properties.
",
icon = "content_copy",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbActionNodeMsgDeduplicationConfig"
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
index a52641bedc..6869268517 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNode.java
@@ -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 SEND_EMAIL.
" +
+ "Output connections: Success, Failure.",
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 imgMap = JacksonUtil.fromString(imagesStr, new TypeReference>() {});
@@ -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;
}
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
index 7b8ddefa7f..f31b5a3893 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeConfiguration.java
+++ b/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 {
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;
}
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java
index a3b7fead3f..ef149aa4ad 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java
+++ b/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 {
- private Map dataMapping;
private boolean ignoreNullStrings;
@Override
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNodeWithTbMsgSource.java
new file mode 100644
index 0000000000..bed00659ea
--- /dev/null
+++ b/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 upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
+ return fromVersion == 0 ?
+ upgradeToUseTbMsgSource((ObjectNode) oldConfiguration, getKeyToUpgradeFromVersionZero()) :
+ new TbPair<>(false, oldConfiguration);
+ }
+
+ private TbPair 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!");
+ }
+
+}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
index 4d1a092097..ea412be3fe 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
@@ -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.
" +
- "Alarm Originator found only in case original Originator is Alarm entity.",
+ nodeDescription = "Change message originator to Tenant/Customer/Related Entity/Alarm Originator/Entity by name pattern.",
+ nodeDetails = "Configuration: - Customer - use customer of incoming message originator as new originator. " +
+ "Only for assigned to customer originators with one of the following type: 'User', 'Asset', 'Device'.
" +
+ "- Tenant - use current tenant as new originator.
" +
+ "- Related Entity - use related entity as new originator. Lookup based on configured relation query. " +
+ "If multiple related entities are found, only first entity is used as new originator, other entities are discarded.
" +
+ "- Alarm Originator - use alarm originator as new originator. Only if incoming message originator is alarm entity.
" +
+ "- Entity by name pattern - specify entity type and name pattern of new originator. Following entity types are supported: " +
+ "'Device', 'Asset', 'Entity View', 'Edge' or 'User'.
" +
+ "Output connections: Success, Failure.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeChangeOriginatorConfig",
icon = "find_replace"
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java
index f413be43c1..ef950ff563 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNode.java
@@ -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 Success 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.
" +
+ "Output connections: Success, Failure.",
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 patternKeys;
- private boolean fromMetadata;
+ private TbMsgSource copyFrom;
+ private List 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 metaDataMap = metaData.getData();
- for (Map.Entry 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 metaDataMap = metaDataCopy.getData();
+ for (Map.Entry 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> iteratorNode = dataNode.fields();
- while (iteratorNode.hasNext()) {
- Map.Entry 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> iteratorNode = dataNode.fields();
+ while (iteratorNode.hasNext()) {
+ Map.Entry 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());
}
+
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeConfiguration.java
index 6bf9f1e343..5c8d7edf4b 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeConfiguration.java
+++ b/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 {
- private boolean fromMetadata;
+ private TbMsgSource copyFrom;
private Set keys;
@Override
public TbCopyKeysNodeConfiguration defaultConfiguration() {
TbCopyKeysNodeConfiguration configuration = new TbCopyKeysNodeConfiguration();
configuration.setKeys(Collections.emptySet());
- configuration.setFromMetadata(false);
+ configuration.setCopyFrom(TbMsgSource.DATA);
return configuration;
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java
index 9ea31bc56d..7bb63ee1bd 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNode.java
@@ -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 Success 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.
" +
+ "Output connections: Success, Failure.",
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 patternKeys;
- private boolean fromMetadata;
+ private TbMsgSource deleteFrom;
+ private List 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 keysToDelete = new ArrayList<>();
- if (fromMetadata) {
- Map 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();
+ 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());
}
+
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeConfiguration.java
index d0b22df409..d68b391df6 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeConfiguration.java
+++ b/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 {
- private boolean fromMetadata;
+ private TbMsgSource deleteFrom;
private Set keys;
@Override
public TbDeleteKeysNodeConfiguration defaultConfiguration() {
TbDeleteKeysNodeConfiguration configuration = new TbDeleteKeysNodeConfiguration();
configuration.setKeys(Collections.emptySet());
- configuration.setFromMetadata(false);
+ configuration.setDeleteFrom(TbMsgSource.DATA);
return configuration;
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java
index 0e85a50c34..b22ce20dcb 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbJsonPathNode.java
@@ -38,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.
"
- + "'$' represents the root object or array.
"
- + "If JSONPath expression evaluation failed, incoming message routes via Failure chain, "
- + "otherwise Success chain is used.",
+ nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure.
" +
+ "Output connections: Success, Failure.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
icon = "functions",
configDirective = "tbTransformationNodeJsonPathConfig"
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java
index 88e6dc1a8b..705ce3fc42 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNode.java
@@ -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 Success 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.
" +
+ "Output connections: Success, Failure.",
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 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 metaDataMap = metaData.getData();
- for (Map.Entry 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 metaDataMap = metaDataCopy.getData();
for (Map.Entry 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 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";
+ }
+
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeConfiguration.java
index 0475ba4ca4..1e6d5e1483 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeConfiguration.java
+++ b/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 {
- private boolean fromMetadata;
+ private TbMsgSource renameIn;
private Map 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;
}
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java
index 8959d12c9f..40ddccbea7 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbSplitArrayMsgNode.java
@@ -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 Failure chain, otherwise returns "
- + "inner objects of the extracted array as separate messages via Success 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.
" +
+ "Output connections: Success, Failure.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
icon = "content_copy",
configDirective = "tbNodeEmptyConfig"
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
index ba78970c54..79549edbd7 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
+++ b/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
" +
- "metadata - is a Message metadata.
" +
- "msg - is a Message payload.
" +
- "msgType - is a Message type.
" +
+ "msg - is a message payload.
" +
+ "metadata - is a message metadata.
" +
+ "msgType - is a message type.
" +
"Should return the following structure:
" +
"{ msg: new payload,
   metadata: new metadata,
   msgType: new msgType }
" +
- "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.
" +
+ "Output connections: Success, Failure.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeScriptConfig"
)
diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
index 9579c73058..5a19fa860f 100644
--- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
+++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
@@ -44,8 +44,7 @@ public class EntitiesCustomerIdAsyncLoader {
}
private static ListenableFuture toCustomerIdAsync(TbContext ctx, ListenableFuture 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());
}
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
index bf4d090c1c..9d732301f8 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/mail/TbMsgToEmailNodeTest.java
@@ -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 msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
- ArgumentCaptor typeCaptor = ArgumentCaptor.forClass(TbMsgType.class);
- ArgumentCaptor originatorCaptor = ArgumentCaptor.forClass(EntityId.class);
- ArgumentCaptor metadataCaptor = ArgumentCaptor.forClass(TbMsgMetaData.class);
- ArgumentCaptor 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() {
+ 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;
}
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
index 291614464a..d200ec70d8 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
+++ b/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());
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java
index 51625eafe7..16db712ef9 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java
+++ b/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());
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java
index d681deb9c0..ded7400ea5 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java
+++ b/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());
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java
index dcf5c22ea5..b236c3097c 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java
+++ b/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());
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java
index 87b34cffe0..f0c0e76419 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java
+++ b/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());
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java
index bfbaa9a92e..93da628de0 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java
+++ b/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());
}
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java
index 0fb9589e90..5f9efed3fb 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbCopyKeysNodeTest.java
@@ -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 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 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 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 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 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 mdMap = Map.of(
"TestKey_1", "Test",
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java
index 6eee16e7d3..176979a546 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbDeleteKeysNodeTest.java
@@ -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 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 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 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 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 mdMap = Map.of(
"TestKey_1", "Test",
diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java
index c602aab5df..ea46db2e2b 100644
--- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java
+++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbRenameKeysNodeTest.java
@@ -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 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 mdMap = Map.of(
"TestKey_1", "Test",
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md
new file mode 100644
index 0000000000..204297957a
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/rulenode/change_originator_node_fields_templatization.md
@@ -0,0 +1,81 @@
+#### Fields templatization
+
+
+
+
+{% 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:
+
+
+
+Imagine that you receive the message defined below from the `Temperature` sensor
+and forwarded it to the **change originator** node with configuration added above.
+
+- incoming message definition:
+
+```json
+{
+ "msg": {
+ "temperature": 32
+ },
+ "metadata": {
+ "deviceType": "Temperature",
+ "deviceName": "TH-001",
+ "ts": "1685379440000"
+ }
+}
+```
+
+
+The same example for the `humidity` sensor:
+
+- incoming message definition:
+
+```json
+{
+ "msg": {
+ "humidity": 77
+ },
+ "metadata": {
+ "deviceType": "Humidity",
+ "deviceName": "HM-001",
+ "ts": "1685379440000"
+ }
+}
+```
+
+
+
+To demonstrate that the originator was changed based on rule node configuration, the screenshots with debug events captured for the **change originator** rule node will be added below:
+
+ - Debug events for processed message from the `Temperature` sensor:
+
+
+
+ - Debug events for processed message from the `Humidity` sensor:
+
+
+
+In the debug events displayed, the `IN` message points to the incoming message received by the node,
+with the originator type specified as `DEVICE`. This reflects the message from the actual sensor (e.g., a `Temperature` or `Humidity` sensor).
+After processing through the `change originator` rule node, the `OUT` message has an originator type of `ASSET`,
+indicating that the message originator has been successfully changed to the corresponding managing asset, such as `TemperatureManager` or `HumidityManager`.
+
+
+
+These examples showcases using the **change originator** node with dynamic configuration based on the substitution of metadata fields.
+
+
+
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md
index dc1e721bd0..0ccbbd9058 100644
--- a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md
+++ b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md
@@ -1,9 +1,4 @@
-Fields templatization feature allows you to process the incoming messages with dynamic configuration by substitution templates specified in the configuration fields with values from message or message metadata.
+Fields templatization feature allows you to process the incoming messages with dynamic configuration
+by substitution of templates specified in the configuration fields with values from message or message metadata.
+For more detailed information, please refer to the ThingsBoard [documentation](https://thingsboard.io/docs/user-guide/templatization/)
-There are two types of rule node configuration templates defined:
-
- - `$[messageKey]` - templates with square brackets used to extract value from the message.
-
- - `${metadataKey}` - templates with curly brackets used to extract value from the message metadata.
-
-**Note:** `messageKey` and `metadataKey` are just samples of key names that might exist in the message or metadata.
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md
new file mode 100644
index 0000000000..8fbc917327
--- /dev/null
+++ b/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md
@@ -0,0 +1,68 @@
+#### Fields templatization
+
+
+
+
+{% 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"
+ }
+}
+```
+
+
+Here is a node configuration:
+
+
+
+After message evaluation by a rule node the outgoing message will be looks like this:
+
+```json
+{
+ "msg": {
+ "from": "info@testmail.org",
+ "to": "john.doe@example.com",
+ "cc": "mike.johnson@example.io,sarah.smith@example.org,emily.davis@example.co",
+ "bcc": null,
+ "subject": "Device Thermostat temperature high",
+ "body": "Device TH-001 has high temperature 32",
+ "images": null,
+ "html": false
+ },
+ "metadata": {
+ "deviceType": "Thermostat",
+ "deviceName": "TH-001",
+ "ts": "1685379440000",
+ "primaryUser": "john.doe@example.com",
+ "subscribers": "mike.johnson@example.io,sarah.smith@example.org,emily.davis@example.co"
+ }
+}
+```
+
+
+
+These examples showcases using the **to email** node with dynamic configuration based on the substitution of message and message metadata fields.
+
+
+
diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png
new file mode 100644
index 0000000000..744b29d3da
Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png differ
diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png
new file mode 100644
index 0000000000..4fa936482c
Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png differ
diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png
new file mode 100644
index 0000000000..8b3c555e80
Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png differ
diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png b/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png
new file mode 100644
index 0000000000..544441de87
Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png differ