AI rule node: add support for all data/metadata patterns in rule nodes

This commit is contained in:
Dmytro Skarzhynets 2025-06-17 18:17:38 +03:00
parent 9567dd3090
commit d2d22a44c2
No known key found for this signature in database
GPG Key ID: 2B51652F224037DF
3 changed files with 191 additions and 30 deletions

View File

@ -23,9 +23,6 @@ import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Created by ashvayka on 13.01.18.
*/
@Data
public final class TbMsgMetaData implements Serializable {
@ -34,7 +31,7 @@ public final class TbMsgMetaData implements Serializable {
private final Map<String, String> data;
public TbMsgMetaData() {
this.data = new ConcurrentHashMap<>();
data = new ConcurrentHashMap<>();
}
public TbMsgMetaData(Map<String, String> data) {
@ -46,24 +43,29 @@ public final class TbMsgMetaData implements Serializable {
* Internal constructor to create immutable TbMsgMetaData.EMPTY
* */
private TbMsgMetaData(int ignored) {
this.data = Collections.emptyMap();
data = Collections.emptyMap();
}
public String getValue(String key) {
return this.data.get(key);
return data.get(key);
}
public void putValue(String key, String value) {
if (key != null && value != null) {
this.data.put(key, value);
data.put(key, value);
}
}
public Map<String, String> values() {
return new HashMap<>(this.data);
return new HashMap<>(data);
}
public TbMsgMetaData copy() {
return new TbMsgMetaData(this.data);
return new TbMsgMetaData(data);
}
public boolean isEmpty() {
return data == null || data.isEmpty();
}
}

View File

@ -16,11 +16,11 @@
package org.thingsboard.rule.engine.api.util;
import com.fasterxml.jackson.databind.JsonNode;
import org.springframework.util.CollectionUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.util.CollectionsUtil;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -29,15 +29,18 @@ import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Created by ashvayka on 19.01.18.
*/
public class TbNodeUtils {
public final class TbNodeUtils {
private TbNodeUtils() {
throw new IllegalStateException("Utility class");
}
private static final Pattern DATA_PATTERN = Pattern.compile("(\\$\\[)(.*?)(])");
private static final String ALL_DATA_TEMPLATE = "$[*]";
private static final String ALL_METADATA_TEMPLATE = "${*}";
public static <T> T convert(TbNodeConfiguration configuration, Class<T> clazz) throws TbNodeException {
try {
return JacksonUtil.treeToValue(configuration.getData(), clazz);
@ -47,16 +50,19 @@ public class TbNodeUtils {
}
public static List<String> processPatterns(List<String> patterns, TbMsg tbMsg) {
if (!CollectionUtils.isEmpty(patterns)) {
return patterns.stream().map(p -> processPattern(p, tbMsg)).collect(Collectors.toList());
if (CollectionsUtil.isEmpty(patterns)) {
return Collections.emptyList();
}
return Collections.emptyList();
return patterns.stream().map(p -> processPattern(p, tbMsg)).toList();
}
public static String processPattern(String pattern, TbMsg tbMsg) {
try {
String result = processPattern(pattern, tbMsg.getMetaData());
JsonNode json = JacksonUtil.toJsonNode(tbMsg.getData());
result = result.replace(ALL_DATA_TEMPLATE, JacksonUtil.toString(json));
if (json.isObject()) {
Matcher matcher = DATA_PATTERN.matcher(result);
while (matcher.find()) {
@ -64,7 +70,7 @@ public class TbNodeUtils {
String[] keys = group.split("\\.");
JsonNode jsonNode = json;
for (String key : keys) {
if (!StringUtils.isEmpty(key) && jsonNode != null) {
if (StringUtils.isNotEmpty(key) && jsonNode != null) {
jsonNode = jsonNode.get(key);
} else {
jsonNode = null;
@ -83,15 +89,9 @@ public class TbNodeUtils {
}
}
@Deprecated(since = "3.6.1", forRemoval = true)
public static List<String> processPatterns(List<String> patterns, TbMsgMetaData metaData) {
if (!CollectionUtils.isEmpty(patterns)) {
return patterns.stream().map(p -> processPattern(p, metaData)).collect(Collectors.toList());
}
return Collections.emptyList();
}
public static String processPattern(String pattern, TbMsgMetaData metaData) {
private static String processPattern(String pattern, TbMsgMetaData metaData) {
String replacement = metaData.isEmpty() ? "{}" : JacksonUtil.toString(metaData.getData());
pattern = pattern.replace(ALL_METADATA_TEMPLATE, replacement);
return processTemplate(pattern, metaData.values());
}
@ -108,10 +108,11 @@ public class TbNodeUtils {
}
static String formatDataVarTemplate(String key) {
return "$[" + key + ']';
return "$[" + key + "]";
}
static String formatMetadataVarTemplate(String key) {
return "${" + key + '}';
return "${" + key + "}";
}
}

View File

@ -26,6 +26,8 @@ import org.thingsboard.server.common.data.msg.TbMsgType;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.Map;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@ -167,4 +169,160 @@ public class TbNodeUtilsTest {
assertThat(TbNodeUtils.formatMetadataVarTemplate(null), is("${null}"));
assertThat(TbNodeUtils.formatMetadataVarTemplate(null), is(String.format(METADATA_VARIABLE_TEMPLATE, (String) null)));
}
@Test
public void testAllMetadataTemplateReplacement() {
// GIVEN
String pattern = "META ${*}";
var metadata = new TbMsgMetaData();
metadata.putValue("meta_key", "meta_value");
var msg = TbMsg.newMsg()
.data(TbMsg.EMPTY_JSON_OBJECT)
.metaData(metadata)
.build();
// WHEN
String actual = TbNodeUtils.processPattern(pattern, msg);
// THEN
String expected = "META {\"meta_key\":\"meta_value\"}";
assertThat(actual, is(expected));
}
@Test
public void testMultipleAllMetadataTemplatesReplacement() {
// GIVEN
String pattern = "${*} then again ${*}";
var metadata = new TbMsgMetaData();
metadata.putValue("meta_key", "meta_value");
var msg = TbMsg.newMsg()
.data(TbMsg.EMPTY_JSON_OBJECT)
.metaData(metadata)
.build();
// WHEN
String actual = TbNodeUtils.processPattern(pattern, msg);
// THEN
String expected = "{\"meta_key\":\"meta_value\"} then again {\"meta_key\":\"meta_value\"}";
assertThat(actual, is(expected));
}
@Test
public void testAllDataTemplateReplacement() {
// GIVEN
String pattern = "DATA $[*]";
var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value");
var msg = TbMsg.newMsg()
.data(JacksonUtil.toString(dataJson))
.metaData(TbMsgMetaData.EMPTY)
.build();
// WHEN
String actual = TbNodeUtils.processPattern(pattern, msg);
// THEN
String expected = "DATA {\"data_key\":\"data_value\"}";
assertThat(actual, is(expected));
}
@Test
public void testMultipleAllDataTemplatesReplacement() {
// GIVEN
String pattern = "$[*] then again $[*]";
var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value");
var msg = TbMsg.newMsg()
.data(JacksonUtil.toString(dataJson))
.metaData(TbMsgMetaData.EMPTY)
.build();
// WHEN
String actual = TbNodeUtils.processPattern(pattern, msg);
// THEN
String expected = "{\"data_key\":\"data_value\"} then again {\"data_key\":\"data_value\"}";
assertThat(actual, is(expected));
}
@Test
public void testAllDataAndAllMetadataTemplatesSimultaneously() {
// GIVEN
String pattern = "META ${*} DATA $[*]";
var metadata = new TbMsgMetaData(Map.of("meta_key", "meta_value"));
var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value");
var msg = TbMsg.newMsg()
.data(JacksonUtil.toString(dataJson))
.metaData(metadata)
.build();
// WHEN
String actual = TbNodeUtils.processPattern(pattern, msg);
// THEN
String expected = "META {\"meta_key\":\"meta_value\"} DATA {\"data_key\":\"data_value\"}";
assertThat(actual, is(expected));
}
@Test
public void testAllDataAndAllMetadataTemplatesSimultaneouslyEmpty() {
// GIVEN
String pattern = "META ${*} DATA $[*]";
var msg = TbMsg.newMsg()
.data(TbMsg.EMPTY_JSON_OBJECT)
.metaData(TbMsgMetaData.EMPTY)
.build();
// WHEN
String actual = TbNodeUtils.processPattern(pattern, msg);
// THEN
String expected = "META {} DATA {}";
assertThat(actual, is(expected));
}
@Test
public void testAllDataTemplateArray() {
// GIVEN
String pattern = "DATA $[*]";
var msg = TbMsg.newMsg()
.data("[1, \"two\", true]")
.metaData(TbMsgMetaData.EMPTY)
.build();
// WHEN
String actual = TbNodeUtils.processPattern(pattern, msg);
// THEN
String expected = "DATA [1,\"two\",true]";
assertThat(actual, is(expected));
}
@Test
public void testMixedAllDataMetadataAndNormalTemplates() {
// GIVEN
String pattern = "fullMeta=${*}, singleMeta=${meta_key}, fullData=$[*], singleData=$[data_key]";
var metadata = new TbMsgMetaData(Map.of("meta_key", "meta_value"));
var dataJson = JacksonUtil.newObjectNode().put("data_key", "data_value");
var msg = TbMsg.newMsg()
.data(JacksonUtil.toString(dataJson))
.metaData(metadata)
.build();
// WHEN
String actual = TbNodeUtils.processPattern(pattern, msg);
// THEN
String expected = "fullMeta={\"meta_key\":\"meta_value\"}, singleMeta=meta_value, fullData={\"data_key\":\"data_value\"}, singleData=data_value";
assertThat(actual, is(expected));
}
}