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

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,
name = "change originator",
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. " +
"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.",
"If multiple related entities are found, only first entity is used as new originator, other entities are discarded.<br/>" +
"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"},
configDirective = "tbTransformationNodeChangeOriginatorConfig",
icon = "find_replace"

View File

@ -21,87 +21,98 @@ 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-values",
version = 1,
configClazz = TbCopyKeysNodeConfiguration.class,
nodeDescription = "Copies the msg or metadata keys with specified key names selected in the list",
nodeDetails = "Will fetch fields values specified in list. If specified field is not part of msg or metadata fields it will be ignored." +
"Returns transformed messages via <code>Success</code> chain",
nodeDescription = "Copies key-values from message to message metadata or vice-versa.",
nodeDetails = "Fetches key-values from message or message metadata based on the keys list specified in the configuration " +
"and copies them into message metadata or 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"},
configDirective = "tbTransformationNodeCopyKeysConfig",
icon = "content_copy"
)
public class TbCopyKeysNode implements TbNode {
public class TbCopyKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbCopyKeysNodeConfiguration config;
private List<Pattern> patternKeys;
private boolean fromMetadata;
private TbMsgSource copyFrom;
@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.patternKeys = config.getKeys().stream().map(Pattern::compile).collect(Collectors.toList());
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
TbMsgMetaData metaData = msg.getMetaData();
var metaDataCopy = msg.getMetaData().copy();
String msgData = msg.getData();
boolean msgChanged = false;
JsonNode dataNode = JacksonUtil.toJsonNode(msgData);
if (dataNode.isObject()) {
if (fromMetadata) {
ObjectNode msgDataNode = (ObjectNode) dataNode;
Map<String, String> metaDataMap = metaData.getData();
for (Map.Entry<String, String> entry : metaDataMap.entrySet()) {
String keyData = entry.getKey();
if (checkKey(keyData)) {
msgChanged = true;
msgDataNode.put(keyData, entry.getValue());
switch (copyFrom) {
case METADATA:
ObjectNode msgDataNode = (ObjectNode) dataNode;
Map<String, String> metaDataMap = metaDataCopy.getData();
for (Map.Entry<String, String> entry : metaDataMap.entrySet()) {
String keyData = entry.getKey();
if (checkKey(keyData)) {
msgChanged = true;
msgDataNode.put(keyData, entry.getValue());
}
}
}
msgData = JacksonUtil.toString(msgDataNode);
} else {
Iterator<Map.Entry<String, JsonNode>> iteratorNode = dataNode.fields();
while (iteratorNode.hasNext()) {
Map.Entry<String, JsonNode> entry = iteratorNode.next();
String keyData = entry.getKey();
if (checkKey(keyData)) {
msgChanged = true;
metaData.putValue(keyData, JacksonUtil.toString(entry.getValue()));
msgData = JacksonUtil.toString(msgDataNode);
break;
case DATA:
Iterator<Map.Entry<String, JsonNode>> iteratorNode = dataNode.fields();
while (iteratorNode.hasNext()) {
Map.Entry<String, JsonNode> entry = iteratorNode.next();
String keyData = entry.getKey();
if (checkKey(keyData)) {
msgChanged = true;
metaDataCopy.putValue(keyData, JacksonUtil.toString(entry.getValue()));
}
}
}
break;
default:
log.debug("Unexpected CopyFrom value: {}. Allowed values: {}", copyFrom, TbMsgSource.values());
break;
}
}
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());
}
}

View File

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

View File

@ -21,84 +21,96 @@ 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-values",
version = 1,
configClazz = TbDeleteKeysNodeConfiguration.class,
nodeDescription = "Removes keys from the msg data or metadata with the specified key names selected in the list",
nodeDetails = "Will fetch fields (regex) values specified in list. If specified field (regex) is not part of msg " +
"or metadata fields it will be ignored. Returns transformed messages via <code>Success</code> chain",
nodeDescription = "Removes key-values from message or metadata.",
nodeDetails = "Removes key-values from message or message metadata based on the keys list specified in the configuration. " +
"Use regular expression(s) as a key(s) to remove keys by pattern.<br><br>" +
"Output connections: <code>Success</code>, <code>Failure</code>.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeDeleteKeysConfig",
icon = "remove_circle"
)
public class TbDeleteKeysNode implements TbNode {
public class TbDeleteKeysNode extends TbAbstractTransformNodeWithTbMsgSource {
private TbDeleteKeysNodeConfiguration config;
private List<Pattern> patternKeys;
private boolean fromMetadata;
private TbMsgSource deleteFrom;
@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.patternKeys = 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();
TbMsgMetaData metaDataCopy = msg.getMetaData().copy();
String msgData = msg.getData();
List<String> keysToDelete = new ArrayList<>();
if (fromMetadata) {
Map<String, String> metaDataMap = metaData.getData();
metaDataMap.forEach((keyMetaData, valueMetaData) -> {
if (checkKey(keyMetaData)) {
keysToDelete.add(keyMetaData);
}
});
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);
switch (deleteFrom) {
case METADATA:
Map<String, String> metaDataMap = metaDataCopy.getData();
metaDataMap.forEach((keyMetaData, valueMetaData) -> {
if (checkKey(keyMetaData)) {
keysToDelete.add(keyMetaData);
}
});
msgDataObject.remove(keysToDelete);
msgData = JacksonUtil.toString(msgDataObject);
}
}
if (keysToDelete.isEmpty()) {
ctx.tellSuccess(msg);
} else {
ctx.tellSuccess(TbMsg.transformMsg(msg, metaData, msgData));
keysToDelete.forEach(metaDataMap::remove);
metaDataCopy = new TbMsgMetaData(metaDataMap);
break;
case DATA:
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);
}
break;
default:
log.debug("Unexpected DeleteFrom value: {}. Allowed values: {}", deleteFrom, TbMsgSource.values());
break;
}
ctx.tellSuccess(keysToDelete.isEmpty() ? msg : TbMsg.transformMsg(msg, metaDataCopy, msgData));
}
@Override
protected String getKeyToUpgradeFromVersionZero() {
return "deleteFrom";
}
boolean checkKey(String key) {
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 org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.util.TbMsgSource;
import java.util.Collections;
import java.util.Set;
@ -24,14 +25,14 @@ import java.util.Set;
@Data
public class TbDeleteKeysNodeConfiguration implements NodeConfiguration<TbDeleteKeysNodeConfiguration> {
private boolean fromMetadata;
private TbMsgSource deleteFrom;
private Set<String> keys;
@Override
public TbDeleteKeysNodeConfiguration defaultConfiguration() {
TbDeleteKeysNodeConfiguration configuration = new TbDeleteKeysNodeConfiguration();
configuration.setKeys(Collections.emptySet());
configuration.setFromMetadata(false);
configuration.setDeleteFrom(TbMsgSource.DATA);
return configuration;
}

View File

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

View File

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

View File

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

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.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,11 @@ 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 "
nodeDescription = "Split array message into several messages",
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 "
+ "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"},
icon = "content_copy",
configDirective = "tbNodeEmptyConfig"

View File

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

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.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());
}

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.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());
}

View File

@ -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());
}

View File

@ -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());
}

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.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());
}

View File

@ -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());
}

View File

@ -16,6 +16,7 @@
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;
@ -24,9 +25,11 @@ 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;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
@ -37,6 +40,8 @@ import java.util.Set;
import java.util.UUID;
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.Mockito.mock;
import static org.mockito.Mockito.never;
@ -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
@ -95,7 +100,7 @@ public class TbCopyKeysNodeTest {
@Test
void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception {
config.setFromMetadata(false);
config.setCopyFrom(TbMsgSource.DATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
@ -149,6 +154,20 @@ public class TbCopyKeysNodeTest {
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) {
final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test",

View File

@ -16,6 +16,7 @@
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;
@ -24,9 +25,11 @@ 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;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
@ -37,6 +40,8 @@ import java.util.Set;
import java.util.UUID;
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.Mockito.mock;
import static org.mockito.Mockito.never;
@ -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,7 +79,7 @@ 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
@ -95,7 +100,7 @@ public class TbDeleteKeysNodeTest {
@Test
void givenMsgFromMsg_whenOnMsg_thenVerifyOutput() throws Exception {
config.setFromMetadata(false);
config.setDeleteFrom(TbMsgSource.DATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctx, nodeConfiguration);
@ -133,6 +138,20 @@ public class TbDeleteKeysNodeTest {
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) {
final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test",

View File

@ -16,6 +16,7 @@
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;
@ -24,9 +25,11 @@ 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;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
@ -35,6 +38,8 @@ import java.util.Map;
import java.util.UUID;
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.Mockito.mock;
import static org.mockito.Mockito.never;
@ -70,8 +75,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 +100,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 +153,20 @@ public class TbRenameKeysNodeTest {
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) {
final Map<String, String> mdMap = Map.of(
"TestKey_1", "Test",