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: ", 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: + +![image](${helpBaseUrl}/help/images/rulenode/examples/change-originator-ft.png) + +Imagine that you receive the message defined below from the `Temperature` sensor +and forwarded it to the **change originator** node with configuration added above. + +- incoming message definition: + +```json +{ + "msg": { + "temperature": 32 + }, + "metadata": { + "deviceType": "Temperature", + "deviceName": "TH-001", + "ts": "1685379440000" + } +} +``` +
+ +The same example for the `humidity` sensor: + +- incoming message definition: + +```json +{ + "msg": { + "humidity": 77 + }, + "metadata": { + "deviceType": "Humidity", + "deviceName": "HM-001", + "ts": "1685379440000" + } +} +``` + +
+ +To demonstrate that the originator was changed based on rule node configuration, the screenshots with debug events captured for the **change originator** rule node will be added below: + + - Debug events for processed message from the `Temperature` sensor: + +![image](${helpBaseUrl}/help/images/rulenode/examples/change-originator-ft-2.png) + + - Debug events for processed message from the `Humidity` sensor: + +![image](${helpBaseUrl}/help/images/rulenode/examples/change-originator-ft-3.png) + +In the debug events displayed, the `IN` message points to the incoming message received by the node, +with the originator type specified as `DEVICE`. This reflects the message from the actual sensor (e.g., a `Temperature` or `Humidity` sensor). +After processing through the `change originator` rule node, the `OUT` message has an originator type of `ASSET`, +indicating that the message originator has been successfully changed to the corresponding managing asset, such as `TemperatureManager` or `HumidityManager`. + +
+ +These examples showcases using the **change originator** node with dynamic configuration based on the substitution of metadata fields. + +
+
diff --git a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md index dc1e721bd0..0ccbbd9058 100644 --- a/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md +++ b/ui-ngx/src/assets/help/en_US/rulenode/common_node_fields_templatization.md @@ -1,9 +1,4 @@ -Fields templatization feature allows you to process the incoming messages with dynamic configuration by substitution templates specified in the configuration fields with values from message or message metadata. +Fields templatization feature allows you to process the incoming messages with dynamic configuration +by substitution of templates specified in the configuration fields with values from message or message metadata. +For more detailed information, please refer to the ThingsBoard [documentation](https://thingsboard.io/docs/user-guide/templatization/) -There are two types of rule node configuration templates defined: - - - `$[messageKey]` - templates with square brackets used to extract value from the message. - - - `${metadataKey}` - templates with curly brackets used to extract value from the message metadata. - -**Note:** `messageKey` and `metadataKey` are just samples of key names that might exist in the message or metadata. diff --git a/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md b/ui-ngx/src/assets/help/en_US/rulenode/to_email_node_fields_templatization.md 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: + +![image](${helpBaseUrl}/help/images/rulenode/examples/to-email-ft.png) + +After message evaluation by a rule node the outgoing message will be looks like this: + +```json +{ + "msg": { + "from": "info@testmail.org", + "to": "john.doe@example.com", + "cc": "mike.johnson@example.io,sarah.smith@example.org,emily.davis@example.co", + "bcc": null, + "subject": "Device Thermostat temperature high", + "body": "Device TH-001 has high temperature 32", + "images": null, + "html": false + }, + "metadata": { + "deviceType": "Thermostat", + "deviceName": "TH-001", + "ts": "1685379440000", + "primaryUser": "john.doe@example.com", + "subscribers": "mike.johnson@example.io,sarah.smith@example.org,emily.davis@example.co" + } +} +``` + +
+ +These examples showcases using the **to email** node with dynamic configuration based on the substitution of message and message metadata fields. + +
+
diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png new file mode 100644 index 0000000000..744b29d3da Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-2.png differ diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png new file mode 100644 index 0000000000..4fa936482c Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft-3.png differ diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png new file mode 100644 index 0000000000..8b3c555e80 Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/change-originator-ft.png differ diff --git a/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png b/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png new file mode 100644 index 0000000000..544441de87 Binary files /dev/null and b/ui-ngx/src/assets/help/images/rulenode/examples/to-email-ft.png differ