init commit for transformation nodes && improved error handling on init for fetchTo based nodes

This commit is contained in:
ShvaykaD 2023-09-15 17:54:47 +03:00
parent cc441b4148
commit 40c957aabb
21 changed files with 315 additions and 147 deletions

View File

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

View File

@ -0,0 +1,57 @@
/**
* 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;
public abstract class TbAbstractTransformNodeWithTbMsgSource implements TbNode {
private static final String FROM_METADATA_PROPERTY = "fromMetadata";
protected abstract String getKeyToUpgradeFromVersionZero();
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ?
upgradeToUseTbMsgSource((ObjectNode) oldConfiguration, getKeyToUpgradeFromVersionZero()) :
new TbPair<>(false, oldConfiguration);
}
private TbPair<Boolean, JsonNode> upgradeToUseTbMsgSource(ObjectNode configToUpdate, String newProperty) throws TbNodeException {
if (!configToUpdate.has(FROM_METADATA_PROPERTY)) {
throw new TbNodeException("property to update: '" + FROM_METADATA_PROPERTY + "' doesn't exists in configuration!");
}
var value = configToUpdate.get(FROM_METADATA_PROPERTY).asText();
if ("true".equals(value)) {
configToUpdate.remove(FROM_METADATA_PROPERTY);
configToUpdate.put(newProperty, TbMsgSource.METADATA.name());
return new TbPair<>(true, configToUpdate);
}
if ("false".equals(value)) {
configToUpdate.remove(FROM_METADATA_PROPERTY);
configToUpdate.put(newProperty, TbMsgSource.DATA.name());
return new TbPair<>(true, configToUpdate);
}
throw new TbNodeException("property to update: '" + FROM_METADATA_PROPERTY + "' has unexpected value: "
+ value + ". Allowed values: true or false!");
}
}

View File

@ -43,10 +43,14 @@ import java.util.NoSuchElementException;
type = ComponentType.TRANSFORMATION, type = ComponentType.TRANSFORMATION,
name = "change originator", name = "change originator",
configClazz = TbChangeOriginatorNodeConfiguration.class, configClazz = TbChangeOriginatorNodeConfiguration.class,
nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity/Alarm Originator", nodeDescription = "Change Message Originator To Tenant/Customer/Related Entity/Alarm Originator/Entity by name pattern",
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " + nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
"If multiple Related Entities are found, only first Entity is used as new Originator, other entities are discarded.<br/>" + "If multiple related entities are found, only first entity is used as new originator, other entities are discarded.<br/>" +
"Alarm Originator found only in case original Originator is <code>Alarm</code> entity.", "Alarm Originator might be found only if the original Originator is <code>Alarm</code> entity.<br/>" +
"Entity by name pattern lookup only if found only in case original Originator is <code>Alarm</code> entity.<br/> " +
"Lookup of an entity by name pattern requires selection of entity type and name pattern to be specified in the configuration. " +
"Allowed entity types to select: 'DEVICE', 'ASSET', 'ENTITY_VIEW', 'EDGE' and 'USER'.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeChangeOriginatorConfig", configDirective = "tbTransformationNodeChangeOriginatorConfig",
icon = "find_replace" icon = "find_replace"

View File

@ -21,59 +21,63 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; 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.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; 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.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; 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.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Slf4j @Slf4j
@RuleNode( @RuleNode(
type = ComponentType.TRANSFORMATION, type = ComponentType.TRANSFORMATION,
name = "copy keys", name = "copy key-values",
version = 1,
configClazz = TbCopyKeysNodeConfiguration.class, configClazz = TbCopyKeysNodeConfiguration.class,
nodeDescription = "Copies the msg or metadata keys with specified key names selected in the list", nodeDescription = "Copies key-values from message to message metadata or vice-versa.",
nodeDetails = "Will fetch fields values specified in list. If specified field is not part of msg or metadata fields it will be ignored." + nodeDetails = "Fetches key-values from message or message metadata based on the keys list specified in the configuration " +
"Returns transformed messages via <code>Success</code> chain", "and copies them into message metadata or message. Keys that are absent in the fetch source will be ignored. " +
"Use regular expression(s) as a key(s) to copy keys by pattern.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeCopyKeysConfig", configDirective = "tbTransformationNodeCopyKeysConfig",
icon = "content_copy" icon = "content_copy"
) )
public class TbCopyKeysNode implements TbNode { public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbCopyKeysNodeConfiguration config; private TbCopyKeysNodeConfiguration config;
private List<Pattern> patternKeys; private List<Pattern> patternKeys;
private boolean fromMetadata; private TbMsgSource copyFrom;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbCopyKeysNodeConfiguration.class);
this.fromMetadata = config.isFromMetadata(); this.copyFrom = config.getCopyFrom();
this.patternKeys = new ArrayList<>(); if (copyFrom == null) {
config.getKeys().forEach(key -> { throw new TbNodeException("CopyFrom can't be null! Allowed values: " + Arrays.toString(TbMsgSource.values()));
this.patternKeys.add(Pattern.compile(key)); }
}); this.patternKeys = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList());
} }
@Override @Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
TbMsgMetaData metaData = msg.getMetaData(); var metaDataCopy = msg.getMetaData().copy();
String msgData = msg.getData(); String msgData = msg.getData();
boolean msgChanged = false; boolean msgChanged = false;
JsonNode dataNode = JacksonUtil.toJsonNode(msgData); JsonNode dataNode = JacksonUtil.toJsonNode(msgData);
if (dataNode.isObject()) { if (dataNode.isObject()) {
if (fromMetadata) { switch (copyFrom) {
case METADATA:
ObjectNode msgDataNode = (ObjectNode) dataNode; ObjectNode msgDataNode = (ObjectNode) dataNode;
Map<String, String> metaDataMap = metaData.getData(); Map<String, String> metaDataMap = metaDataCopy.getData();
for (Map.Entry<String, String> entry : metaDataMap.entrySet()) { for (Map.Entry<String, String> entry : metaDataMap.entrySet()) {
String keyData = entry.getKey(); String keyData = entry.getKey();
if (checkKey(keyData)) { if (checkKey(keyData)) {
@ -82,26 +86,33 @@ public class TbCopyKeysNode implements TbNode {
} }
} }
msgData = JacksonUtil.toString(msgDataNode); msgData = JacksonUtil.toString(msgDataNode);
} else { break;
case DATA:
Iterator<Map.Entry<String, JsonNode>> iteratorNode = dataNode.fields(); Iterator<Map.Entry<String, JsonNode>> iteratorNode = dataNode.fields();
while (iteratorNode.hasNext()) { while (iteratorNode.hasNext()) {
Map.Entry<String, JsonNode> entry = iteratorNode.next(); Map.Entry<String, JsonNode> entry = iteratorNode.next();
String keyData = entry.getKey(); String keyData = entry.getKey();
if (checkKey(keyData)) { if (checkKey(keyData)) {
msgChanged = true; msgChanged = true;
metaData.putValue(keyData, JacksonUtil.toString(entry.getValue())); metaDataCopy.putValue(keyData, JacksonUtil.toString(entry.getValue()));
} }
} }
break;
default:
log.debug("Unexpected CopyFrom value: {}. Allowed values: {}", copyFrom, TbMsgSource.values());
break;
} }
} }
if (msgChanged) { ctx.tellSuccess(msgChanged ? TbMsg.transformMsg(msg, metaDataCopy, msgData) : msg);
ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, msgData));
} else {
ctx.tellSuccess(msg);
} }
@Override
protected String getKeyToUpgradeFromVersionZero() {
return "copyFrom";
} }
boolean checkKey(String key) { boolean checkKey(String key) {
return patternKeys.stream().anyMatch(pattern -> pattern.matcher(key).matches()); return patternKeys.stream().anyMatch(pattern -> pattern.matcher(key).matches());
} }
} }

View File

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

View File

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

View File

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

View File

@ -38,10 +38,10 @@ import java.util.concurrent.ExecutionException;
name = "json path", name = "json path",
configClazz = TbJsonPathNodeConfiguration.class, configClazz = TbJsonPathNodeConfiguration.class,
nodeDescription = "Transforms incoming message body using JSONPath expression.", nodeDescription = "Transforms incoming message body using JSONPath expression.",
nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure. <br/>" nodeDetails = "JSONPath expression specifies a path to an element or a set of elements in a JSON structure. <br/>" +
+ "<b>'$'</b> represents the root object or array. <br/>" "<b>'$'</b> represents the root object or array. <br/>" +
+ "If JSONPath expression evaluation failed, incoming message routes via <code>Failure</code> chain, " "If JSONPath expression evaluation failed, incoming message routes via <code>Failure</code> chain.<br><br>" +
+ "otherwise <code>Success</code> chain is used.", "Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
icon = "functions", icon = "functions",
configDirective = "tbTransformationNodeJsonPathConfig" configDirective = "tbTransformationNodeJsonPathConfig"

View File

@ -21,14 +21,15 @@ import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext; 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.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils; 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.data.plugin.ComponentType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Arrays;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -36,34 +37,43 @@ import java.util.concurrent.ExecutionException;
@RuleNode( @RuleNode(
type = ComponentType.TRANSFORMATION, type = ComponentType.TRANSFORMATION,
name = "rename keys", name = "rename keys",
version = 1,
configClazz = TbRenameKeysNodeConfiguration.class, configClazz = TbRenameKeysNodeConfiguration.class,
nodeDescription = "Renames msg data or metadata keys to the new key names selected in the key mapping.", nodeDescription = "Renames message or message metadata key names 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." + nodeDetails = "If the key name that is selected in the key mapping is missed in the " +
" Returns transformed messages via <code>Success</code> chain", "selected source(message or message metadata), it will be ignored.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeRenameKeysConfig", configDirective = "tbTransformationNodeRenameKeysConfig",
icon = "find_replace" icon = "find_replace"
) )
public class TbRenameKeysNode implements TbNode { public class TbRenameKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbRenameKeysNodeConfiguration config; private TbRenameKeysNodeConfiguration config;
private Map<String, String> renameKeysMapping; private Map<String, String> renameKeysMapping;
private boolean fromMetadata; private TbMsgSource renameIn;
@Override @Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class); this.config = TbNodeUtils.convert(configuration, TbRenameKeysNodeConfiguration.class);
this.renameIn = config.getRenameIn();
this.renameKeysMapping = config.getRenameKeysMapping(); 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 @Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException { public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
TbMsgMetaData metaData = msg.getMetaData(); TbMsgMetaData metaDataCopy = msg.getMetaData().copy();
String data = msg.getData(); String data = msg.getData();
boolean msgChanged = false; boolean msgChanged = false;
if (fromMetadata) { switch (renameIn) {
Map<String, String> metaDataMap = metaData.getData(); case METADATA:
Map<String, String> metaDataMap = metaDataCopy.getData();
for (Map.Entry<String, String> entry : renameKeysMapping.entrySet()) { for (Map.Entry<String, String> entry : renameKeysMapping.entrySet()) {
String nameKey = entry.getKey(); String nameKey = entry.getKey();
if (metaDataMap.containsKey(nameKey)) { if (metaDataMap.containsKey(nameKey)) {
@ -72,8 +82,9 @@ public class TbRenameKeysNode implements TbNode {
metaDataMap.remove(nameKey); metaDataMap.remove(nameKey);
} }
} }
metaData = new TbMsgMetaData(metaDataMap); metaDataCopy = new TbMsgMetaData(metaDataMap);
} else { break;
case DATA:
JsonNode dataNode = JacksonUtil.toJsonNode(data); JsonNode dataNode = JacksonUtil.toJsonNode(data);
if (dataNode.isObject()) { if (dataNode.isObject()) {
ObjectNode msgData = (ObjectNode) dataNode; ObjectNode msgData = (ObjectNode) dataNode;
@ -87,11 +98,17 @@ public class TbRenameKeysNode implements TbNode {
} }
data = JacksonUtil.toString(msgData); data = JacksonUtil.toString(msgData);
} }
break;
default:
log.debug("Unexpected RenameIn value: {}. Allowed values: {}", renameIn, TbMsgSource.values());
break;
} }
if (msgChanged) { ctx.tellSuccess(msgChanged ? TbMsg.transformMsg(msg, metaDataCopy, data) : msg);
ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, data));
} else {
ctx.tellSuccess(msg);
} }
@Override
protected String getKeyToUpgradeFromVersionZero() {
return "renameIn";
} }
} }

View File

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

View File

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

View File

@ -39,7 +39,8 @@ import java.util.List;
"<code>msgType</code> - is a Message type.<br/>" + "<code>msgType</code> - is a Message type.<br/>" +
"Should return the following structure:<br/>" + "Should return the following structure:<br/>" +
"<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>&nbsp&nbsp&nbspmetadata: <i style=\"color: #666;\">new metadata</i>,<br/>&nbsp&nbsp&nbspmsgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" + "<code>{ msg: <i style=\"color: #666;\">new payload</i>,<br/>&nbsp&nbsp&nbspmetadata: <i style=\"color: #666;\">new metadata</i>,<br/>&nbsp&nbsp&nbspmsgType: <i style=\"color: #666;\">new msgType</i> }</code><br/>" +
"All fields in resulting object are optional and will be taken from original message if not specified.", "All fields in resulting object are optional and will be taken from original message if not specified.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"}, uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeScriptConfig" configDirective = "tbTransformationNodeScriptConfig"
) )

View File

@ -59,6 +59,7 @@ import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -119,7 +120,7 @@ public class TbGetCustomerAttributeNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN // 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()); verify(ctxMock, never()).tellSuccess(any());
} }

View File

@ -57,6 +57,7 @@ import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService; import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.NoSuchElementException; import java.util.NoSuchElementException;
@ -127,7 +128,7 @@ public class TbGetCustomerDetailsNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN // 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()); verify(ctxMock, never()).tellSuccess(any());
} }

View File

@ -40,6 +40,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.device.DeviceService;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -86,7 +87,7 @@ public class TbGetOriginatorFieldsNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN // 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()); verify(ctxMock, never()).tellSuccess(any());
} }

View File

@ -127,7 +127,7 @@ public class TbGetRelatedAttributeNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN // 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()); verify(ctxMock, never()).tellSuccess(any());
} }

View File

@ -50,6 +50,7 @@ import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -99,7 +100,7 @@ public class TbGetTenantAttributeNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN // 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()); verify(ctxMock, never()).tellSuccess(any());
} }

View File

@ -39,6 +39,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -96,7 +97,7 @@ public class TbGetTenantDetailsNodeTest {
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration)); var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN // 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()); verify(ctxMock, never()).tellSuccess(any());
} }

View File

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.transform; package org.thingsboard.rule.engine.transform;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -24,9 +25,11 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; 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.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.common.msg.queue.TbMsgCallback;
@ -37,6 +40,8 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@ -59,7 +64,7 @@ public class TbCopyKeysNodeTest {
ctx = mock(TbContext.class); ctx = mock(TbContext.class);
config = new TbCopyKeysNodeConfiguration().defaultConfiguration(); config = new TbCopyKeysNodeConfiguration().defaultConfiguration();
config.setKeys(Set.of("TestKey_1", "TestKey_2", "TestKey_3", "(\\w*)Data(\\w*)")); 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)); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node = spy(new TbCopyKeysNode()); node = spy(new TbCopyKeysNode());
node.init(ctx, nodeConfiguration); node.init(ctx, nodeConfiguration);
@ -74,7 +79,7 @@ public class TbCopyKeysNodeTest {
void givenDefaultConfig_whenVerify_thenOK() { void givenDefaultConfig_whenVerify_thenOK() {
TbCopyKeysNodeConfiguration defaultConfig = new TbCopyKeysNodeConfiguration().defaultConfiguration(); TbCopyKeysNodeConfiguration defaultConfig = new TbCopyKeysNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getKeys()).isEqualTo(Collections.emptySet()); assertThat(defaultConfig.getKeys()).isEqualTo(Collections.emptySet());
assertThat(defaultConfig.isFromMetadata()).isEqualTo(false); assertThat(defaultConfig.getCopyFrom()).isEqualTo(TbMsgSource.DATA);
} }
@Test @Test
@ -95,7 +100,7 @@ public class TbCopyKeysNodeTest {
@Test @Test
void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception { void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception {
config.setFromMetadata(false); config.setCopyFrom(TbMsgSource.DATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration); node.init(ctx, nodeConfiguration);
@ -149,6 +154,20 @@ public class TbCopyKeysNodeTest {
assertThat(newMsg).isSameAs(msg); assertThat(newMsg).isSameAs(msg);
} }
@Test
void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
// GIVEN
var config = new TbCopyKeysNodeConfiguration().defaultConfiguration();
var oldConfigJson = (ObjectNode) JacksonUtil.valueToTree(config);
oldConfigJson.remove("copyFrom");
oldConfigJson.put("fromMetadata", "false");
// WHEN
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, oldConfigJson);
// THEN
assertTrue(upgrade.getFirst());
assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass()));
}
private TbMsg getTbMsg(EntityId entityId, String data) { private TbMsg getTbMsg(EntityId entityId, String data) {
final Map<String, String> mdMap = Map.of( final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test", "TestKey_1", "Test",

View File

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.transform; package org.thingsboard.rule.engine.transform;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -24,9 +25,11 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; 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.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.common.msg.queue.TbMsgCallback;
@ -37,6 +40,8 @@ import java.util.Set;
import java.util.UUID; import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@ -59,7 +64,7 @@ public class TbDeleteKeysNodeTest {
ctx = mock(TbContext.class); ctx = mock(TbContext.class);
config = new TbDeleteKeysNodeConfiguration().defaultConfiguration(); config = new TbDeleteKeysNodeConfiguration().defaultConfiguration();
config.setKeys(Set.of("TestKey_1", "TestKey_2", "TestKey_3", "(\\w*)Data(\\w*)")); 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)); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node = spy(new TbDeleteKeysNode()); node = spy(new TbDeleteKeysNode());
node.init(ctx, nodeConfiguration); node.init(ctx, nodeConfiguration);
@ -74,7 +79,7 @@ public class TbDeleteKeysNodeTest {
void givenDefaultConfig_whenVerify_thenOK() { void givenDefaultConfig_whenVerify_thenOK() {
TbDeleteKeysNodeConfiguration defaultConfig = new TbDeleteKeysNodeConfiguration().defaultConfiguration(); TbDeleteKeysNodeConfiguration defaultConfig = new TbDeleteKeysNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getKeys()).isEqualTo(Collections.emptySet()); assertThat(defaultConfig.getKeys()).isEqualTo(Collections.emptySet());
assertThat(defaultConfig.isFromMetadata()).isEqualTo(false); assertThat(defaultConfig.getDeleteFrom()).isEqualTo(TbMsgSource.DATA);
} }
@Test @Test
@ -95,7 +100,7 @@ public class TbDeleteKeysNodeTest {
@Test @Test
void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception { void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception {
config.setFromMetadata(false); config.setDeleteFrom(TbMsgSource.DATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config)); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration); node.init(ctx, nodeConfiguration);
@ -133,6 +138,20 @@ public class TbDeleteKeysNodeTest {
assertThat(newMsg.getData()).isEqualTo(data); assertThat(newMsg.getData()).isEqualTo(data);
} }
@Test
void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
// GIVEN
var config = new TbDeleteKeysNodeConfiguration().defaultConfiguration();
var oldConfigJson = (ObjectNode) JacksonUtil.valueToTree(config);
oldConfigJson.remove("deleteFrom");
oldConfigJson.put("fromMetadata", "false");
// WHEN
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, oldConfigJson);
// THEN
assertTrue(upgrade.getFirst());
assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass()));
}
private TbMsg getTbMsg(EntityId entityId, String data) { private TbMsg getTbMsg(EntityId entityId, String data) {
final Map<String, String> mdMap = Map.of( final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test", "TestKey_1", "Test",

View File

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.transform; package org.thingsboard.rule.engine.transform;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -24,9 +25,11 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException; 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.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbMsgCallback; import org.thingsboard.server.common.msg.queue.TbMsgCallback;
@ -35,6 +38,8 @@ import java.util.Map;
import java.util.UUID; import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; import static org.mockito.Mockito.never;
@ -70,8 +75,8 @@ public class TbRenameKeysNodeTest {
@Test @Test
void givenDefaultConfig_whenVerify_thenOK() { void givenDefaultConfig_whenVerify_thenOK() {
TbRenameKeysNodeConfiguration defaultConfig = new TbRenameKeysNodeConfiguration().defaultConfiguration(); TbRenameKeysNodeConfiguration defaultConfig = new TbRenameKeysNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getRenameKeysMapping()).isEqualTo(Map.of("temp", "temperature")); assertThat(defaultConfig.getRenameKeysMapping()).isEqualTo(Map.of("temperatureCelsius", "temperature"));
assertThat(defaultConfig.isFromMetadata()).isEqualTo(false); assertThat(defaultConfig.getRenameIn()).isEqualTo(TbMsgSource.DATA);
} }
@Test @Test
@ -95,7 +100,7 @@ public class TbRenameKeysNodeTest {
void givenMetadata_whenOnMsg_thenVerifyOutput() throws Exception { void givenMetadata_whenOnMsg_thenVerifyOutput() throws Exception {
config = new TbRenameKeysNodeConfiguration().defaultConfiguration(); config = new TbRenameKeysNodeConfiguration().defaultConfiguration();
config.setRenameKeysMapping(Map.of("TestKey_1", "Attribute_1", "TestKey_2", "Attribute_2")); 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)); nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration); node.init(ctx, nodeConfiguration);
@ -148,6 +153,20 @@ public class TbRenameKeysNodeTest {
assertThat(newMsg).isSameAs(msg); assertThat(newMsg).isSameAs(msg);
} }
@Test
void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
// GIVEN
var config = new TbRenameKeysNodeConfiguration().defaultConfiguration();
var oldConfigJson = (ObjectNode) JacksonUtil.valueToTree(config);
oldConfigJson.remove("renameIn");
oldConfigJson.put("fromMetadata", "false");
// WHEN
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, oldConfigJson);
// THEN
assertTrue(upgrade.getFirst());
assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass()));
}
private TbMsg getTbMsg(EntityId entityId, String data) { private TbMsg getTbMsg(EntityId entityId, String data) {
final Map<String, String> mdMap = Map.of( final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test", "TestKey_1", "Test",