Browse Source

Merge pull request #8634 from thingsboard/feature/rule-nodes-versioning

Rule Node Versioning + Improvements
pull/8759/head
Andrew Shvayka 3 years ago
committed by GitHub
parent
commit
7fa9be1a68
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
  2. 2
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  3. 82
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java
  5. 40
      application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java
  6. 3
      application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
  7. 12
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  8. 1
      application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java
  9. 62
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  10. 50
      application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java
  11. 3
      application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java
  12. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java
  13. 76
      application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java
  14. 10
      application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java
  15. 4
      application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java
  16. 9
      application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
  17. 7
      common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java
  18. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java
  19. 17
      common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
  20. 6
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
  21. 4
      dao/pom.xml
  22. 2
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  23. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java
  24. 5
      dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java
  25. 29
      dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java
  26. 2
      dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java
  27. 2
      dao/src/main/java/org/thingsboard/server/dao/service/Validator.java
  28. 1
      dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java
  29. 4
      dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java
  30. 10
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java
  31. 6
      dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java
  32. 19
      dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java
  33. 9
      dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java
  34. 11
      dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java
  35. 2
      dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java
  36. 2
      dao/src/main/resources/sql/schema-entities-idx.sql
  37. 2
      dao/src/main/resources/sql/schema-entities.sql
  38. 3
      dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java
  39. 9
      dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java
  40. 2
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java
  41. 6
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java
  42. 35
      rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbVersionedNode.java
  43. 120
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java
  44. 3
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java
  45. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/DataToFetch.java
  46. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchTo.java
  47. 25
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java
  48. 170
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java
  49. 96
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java
  50. 197
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java
  51. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNodeConfiguration.java
  52. 153
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetMappedDataNode.java
  53. 117
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java
  54. 109
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java
  55. 66
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java
  56. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java
  57. 29
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java
  58. 10
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java
  59. 46
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java
  60. 93
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java
  61. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java
  62. 33
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java
  63. 9
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java
  64. 22
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java
  65. 29
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetMappedDataNodeConfiguration.java
  66. 18
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java
  67. 66
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java
  68. 55
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java
  69. 25
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java
  70. 11
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java
  71. 35
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java
  72. 39
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java
  73. 7
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java
  74. 57
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java
  75. 39
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java
  76. 6
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java
  77. 15
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java
  78. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java
  79. 41
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/ContactBasedEntityDetails.java
  80. 20
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesAlarmOriginatorIdAsyncLoader.java
  81. 21
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java
  82. 51
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java
  83. 37
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java
  84. 41
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java
  85. 2
      rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntityContainer.java
  86. 2
      rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js
  87. 231
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java
  88. 449
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java
  89. 368
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNodeTest.java
  90. 132
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java
  91. 388
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java
  92. 489
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java
  93. 483
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java
  94. 45
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java
  95. 357
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java
  96. 633
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java
  97. 422
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java
  98. 298
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java
  99. 12
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java
  100. 170
      rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java

2
application/src/main/java/org/thingsboard/server/controller/RuleChainController.java

@ -460,7 +460,7 @@ public class RuleChainController extends BaseController {
@ApiParam(value = "Enables overwrite for existing rule chains with the same name.")
@RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException {
TenantId tenantId = getCurrentUser().getTenantId();
List<RuleChainImportResult> importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite);
List<RuleChainImportResult> importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite, tbRuleChainService::updateRuleNodeConfiguration);
for (RuleChainImportResult importResult : importResults) {
if (importResult.getError() == null) {
tbClusterService.broadcastEntityStateChangeEvent(importResult.getTenantId(), importResult.getRuleChainId(),

2
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java

@ -260,6 +260,7 @@ public class ThingsboardInstallService {
case "3.5.1":
log.info("Upgrading ThingsBoard from version 3.5.1 to 3.5.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.5.1");
dataUpdateService.updateData("3.5.1");
//TODO DON'T FORGET to update switch statement in the CacheCleanupService if you need to clear the cache
break;
default:
@ -268,6 +269,7 @@ public class ThingsboardInstallService {
entityDatabaseSchemaService.createOrUpdateViewsAndFunctions();
entityDatabaseSchemaService.createOrUpdateDeviceInfoView(persistToTelemetry);
log.info("Updating system data...");
dataUpdateService.upgradeRuleNodes();
systemDataLoaderService.updateSystemWidgets();
installScripts.loadSystemLwm2mResources();
}

82
application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java

@ -31,6 +31,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.rule.engine.api.NodeDefinition;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbRelationTypes;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
@ -48,12 +49,14 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@Slf4j
public class AnnotationComponentDiscoveryService implements ComponentDiscoveryService {
public static final int MAX_OPTIMISITC_RETRIES = 3;
@Value("${plugins.scan_packages}")
private String[] scanPackages;
@ -63,11 +66,13 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@Autowired
private ComponentDescriptorService componentDescriptorService;
private Map<String, ComponentDescriptor> components = new HashMap<>();
private final Map<String, RuleNodeClassInfo> ruleNodeClasses = new HashMap<>();
private final Map<String, ComponentDescriptor> components = new HashMap<>();
private Map<ComponentType, List<ComponentDescriptor>> coreComponentsMap = new HashMap<>();
private final Map<ComponentType, List<ComponentDescriptor>> coreComponentsMap = new HashMap<>();
private Map<ComponentType, List<ComponentDescriptor>> edgeComponentsMap = new HashMap<>();
private final Map<ComponentType, List<ComponentDescriptor>> edgeComponentsMap = new HashMap<>();
private boolean isInstall() {
return environment.acceptsProfiles(Profiles.of("install"));
@ -75,28 +80,62 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
@PostConstruct
public void init() {
for (var def : discoverBeansByAnnotationType(RuleNode.class)) {
String clazzName = def.getBeanClassName();
try {
var clazz = Class.forName(clazzName);
RuleNode annotation = clazz.getAnnotation(RuleNode.class);
boolean versioned = false;
if (annotation.version() > 0) { // No need to process nodes that has version = 0;
if (TbVersionedNode.class.isAssignableFrom(clazz)) {
versioned = true;
} else {
log.error("RuleNode [{}] has version {} but does not implement TbVersionedNode interface! Any update procedures for this rule node will be skipped!", clazzName, annotation.version());
}
}
ruleNodeClasses.put(clazzName, new RuleNodeClassInfo(clazz, annotation, versioned));
} catch (Exception e) {
log.warn("Failed to create instance of rule node type: {} due to: ", clazzName, e);
}
}
if (!isInstall()) {
discoverComponents();
}
}
private Set<BeanDefinition> discoverBeansByAnnotationType(Class<? extends Annotation> annotationType) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(annotationType));
Set<BeanDefinition> defs = new HashSet<>();
for (String scanPackage : scanPackages) {
defs.addAll(scanner.findCandidateComponents(scanPackage));
}
return defs;
}
@Override
public Optional<RuleNodeClassInfo> getRuleNodeInfo(String clazz) {
return Optional.ofNullable(ruleNodeClasses.get(clazz));
}
@Override
public List<RuleNodeClassInfo> getVersionedNodes() {
return ruleNodeClasses.values().stream().filter(RuleNodeClassInfo::isVersioned).collect(Collectors.toList());
}
private void registerRuleNodeComponents() {
Set<BeanDefinition> ruleNodeBeanDefinitions = getBeanDefinitions(RuleNode.class);
for (BeanDefinition def : ruleNodeBeanDefinitions) {
for (RuleNodeClassInfo def : ruleNodeClasses.values()) {
int retryCount = 0;
Exception cause = null;
while (retryCount < MAX_OPTIMISITC_RETRIES) {
try {
String clazzName = def.getBeanClassName();
Class<?> clazz = Class.forName(clazzName);
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
ComponentType type = ruleNodeAnnotation.type();
ComponentType type = def.getAnnotation().type();
ComponentDescriptor component = scanAndPersistComponent(def, type);
components.put(component.getClazz(), component);
putComponentIntoMaps(type, ruleNodeAnnotation, component);
putComponentIntoMaps(type, def.getAnnotation(), component);
break;
} catch (Exception e) {
log.trace("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
log.trace("Can't initialize component {}, due to {}", def.getClassName(), e.getMessage(), e);
cause = e;
retryCount++;
try {
@ -107,7 +146,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
}
}
if (cause != null && retryCount == MAX_OPTIMISITC_RETRIES) {
log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), cause.getMessage(), cause);
log.error("Can't initialize component {}, due to {}", def.getClassName(), cause.getMessage(), cause);
throw new RuntimeException(cause);
}
}
@ -144,13 +183,14 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return false;
}
private ComponentDescriptor scanAndPersistComponent(BeanDefinition def, ComponentType type) {
private ComponentDescriptor scanAndPersistComponent(RuleNodeClassInfo def, ComponentType type) {
ComponentDescriptor scannedComponent = new ComponentDescriptor();
String clazzName = def.getBeanClassName();
String clazzName = def.getClassName();
try {
scannedComponent.setType(type);
Class<?> clazz = Class.forName(clazzName);
Class<?> clazz = def.getClazz();
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
scannedComponent.setConfigurationVersion(def.isVersioned() ? def.getCurrentVersion() : 0);
scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
@ -162,7 +202,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
scannedComponent.setClazz(clazzName);
log.debug("Processing scanned component: {}", scannedComponent);
} catch (Exception e) {
log.error("Can't initialize component {}, due to {}", def.getBeanClassName(), e.getMessage(), e);
log.error("Can't initialize component {}, due to {}", clazzName, e.getMessage(), e);
throw new RuntimeException(e);
}
ComponentDescriptor persistedComponent = componentDescriptorService.findByClazz(TenantId.SYS_TENANT_ID, clazzName);
@ -210,16 +250,6 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
return relationTypes.toArray(new String[relationTypes.size()]);
}
private Set<BeanDefinition> getBeanDefinitions(Class<? extends Annotation> componentType) {
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(componentType));
Set<BeanDefinition> defs = new HashSet<>();
for (String scanPackage : scanPackages) {
defs.addAll(scanner.findCandidateComponents(scanPackage));
}
return defs;
}
@Override
public void discoverComponents() {
registerRuleNodeComponents();

6
application/src/main/java/org/thingsboard/server/service/component/ComponentDiscoveryService.java

@ -19,7 +19,9 @@ import org.thingsboard.server.common.data.plugin.ComponentDescriptor;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.rule.RuleChainType;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@ -30,6 +32,10 @@ public interface ComponentDiscoveryService {
void discoverComponents();
Optional<RuleNodeClassInfo> getRuleNodeInfo(String clazz);
List<RuleNodeClassInfo> getVersionedNodes();
List<ComponentDescriptor> getComponents(ComponentType type, RuleChainType ruleChainType);
List<ComponentDescriptor> getComponents(Set<ComponentType> types, RuleChainType ruleChainType);

40
application/src/main/java/org/thingsboard/server/service/component/RuleNodeClassInfo.java

@ -0,0 +1,40 @@
/**
* 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.server.service.component;
import lombok.Data;
import org.thingsboard.rule.engine.api.RuleNode;
@Data
public class RuleNodeClassInfo {
private final Class<?> clazz;
private final RuleNode annotation;
private final boolean versioned;
public String getClassName(){
return clazz.getName();
}
public String getSimpleName() {
return clazz.getSimpleName();
}
public int getCurrentVersion() {
return annotation.version();
}
}

3
application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java

@ -48,6 +48,7 @@ import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.Optional;
import java.util.function.Function;
import static org.thingsboard.server.utils.LwM2mObjectModelUtils.toLwm2mResource;
@ -173,7 +174,7 @@ public class InstallScripts {
ruleChain = ruleChainService.saveRuleChain(ruleChain);
ruleChainMetaData.setRuleChainId(ruleChain.getId());
ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData);
ruleChainService.saveRuleChainMetaData(TenantId.SYS_TENANT_ID, ruleChainMetaData, Function.identity());
return ruleChain;
}

12
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -744,6 +744,18 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
} catch (Exception e) {
}
}
try {
conn.createStatement().execute("ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS configuration_version int DEFAULT 0;");
} catch (Exception e) {
}
try {
conn.createStatement().execute("CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);");
} catch (Exception e) {
}
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3005002;");
}

1
application/src/main/java/org/thingsboard/server/service/install/update/DataUpdateService.java

@ -19,4 +19,5 @@ public interface DataUpdateService {
void updateData(String fromVersion) throws Exception;
void upgradeRuleNodes();
}

62
application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.install.update;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Iterables;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -26,6 +27,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.profile.TbDeviceProfileNode;
@ -46,6 +48,7 @@ import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.query.DynamicValue;
@ -62,6 +65,7 @@ import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.audit.AuditLogDao;
@ -77,15 +81,18 @@ import org.thingsboard.server.dao.sql.device.DeviceProfileRepository;
import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.install.SystemDataLoaderService;
import org.thingsboard.server.service.install.TbRuleEngineQueueConfigService;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.StringUtils.isBlank;
@ -133,7 +140,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
private QueueService queueService;
@Autowired
private TbRuleEngineQueueConfigService queueConfig;
private ComponentDiscoveryService componentDiscoveryService;
@Autowired
private SystemDataLoaderService systemDataLoaderService;
@ -203,11 +210,60 @@ public class DefaultDataUpdateService implements DataUpdateService {
log.info("Skipping edge events migration");
}
break;
case "3.5.1":
log.info("Updating data from version 3.5.1 to 3.5.2 ...");
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
}
@Override
public void upgradeRuleNodes() {
try {
log.info("Lookup rule nodes to upgrade ...");
var nodeClassToVersionMap = componentDiscoveryService.getVersionedNodes();
log.info("Found {} versioned nodes to check for upgrade!", nodeClassToVersionMap.size());
nodeClassToVersionMap.forEach(clazz -> {
var ruleNodeType = clazz.getClassName();
var ruleNodeTypeForLogs = clazz.getSimpleName();
var toVersion = clazz.getCurrentVersion();
log.info("Going to check for nodes with type: {} to upgrade to version: {}.", ruleNodeTypeForLogs, toVersion);
var ruleNodesToUpdate = new PageDataIterable<>(
pageLink -> ruleChainService.findAllRuleNodesByTypeAndVersionLessThan(ruleNodeType, toVersion, pageLink), 1024
);
if (Iterables.isEmpty(ruleNodesToUpdate)) {
log.info("There are no active nodes with type: {}, or all nodes with this type already set to latest version!", ruleNodeTypeForLogs);
} else {
for (var ruleNode : ruleNodesToUpdate) {
var ruleNodeId = ruleNode.getId();
var oldConfiguration = ruleNode.getConfiguration();
int fromVersion = ruleNode.getConfigurationVersion();
log.info("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
try {
var tbVersionedNode = (TbVersionedNode) clazz.getClazz().getDeclaredConstructor().newInstance();
TbPair<Boolean, JsonNode> upgradeRuleNodeConfigurationResult = tbVersionedNode.upgrade(fromVersion, oldConfiguration);
if (upgradeRuleNodeConfigurationResult.getFirst()) {
ruleNode.setConfiguration(upgradeRuleNodeConfigurationResult.getSecond());
}
ruleNode.setConfigurationVersion(toVersion);
ruleChainService.saveRuleNode(TenantId.SYS_TENANT_ID, ruleNode);
log.info("Successfully upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion);
} catch (Exception e) {
log.warn("Failed to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {} due to: ",
ruleNodeId, ruleNodeTypeForLogs, fromVersion, toVersion, e);
}
}
}
});
log.info("Finished rule nodes upgrade!");
} catch (Exception e) {
log.error("Unexpected error during rule nodes upgrade: ", e);
}
}
private final PaginatedUpdater<String, DeviceProfileEntity> deviceProfileEntityDynamicConditionsUpdater =
new PaginatedUpdater<>() {
@ -442,7 +498,7 @@ public class DefaultDataUpdateService implements DataUpdateService {
md.getNodes().add(ruleNode);
md.setFirstNodeIndex(newIdx);
md.addConnectionInfo(newIdx, oldIdx, "Success");
ruleChainService.saveRuleChainMetaData(tenant.getId(), md);
ruleChainService.saveRuleChainMetaData(tenant.getId(), md, Function.identity());
}
} catch (Exception e) {
log.error("[{}] Unable to update Tenant: {}", tenant.getId(), tenant.getName(), e);

50
application/src/main/java/org/thingsboard/server/service/rule/DefaultTbRuleChainService.java

@ -15,10 +15,13 @@
*/
package org.thingsboard.server.service.rule;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.flow.TbRuleChainOutputNode;
@ -42,12 +45,13 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import java.util.ArrayList;
import java.util.Collections;
@ -70,8 +74,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
private final RuleChainService ruleChainService;
private final RelationService relationService;
private final InstallScripts installScripts;
private final EntitiesVersionControlService vcService;
private final ComponentDiscoveryService componentDiscoveryService;
@Override
public Set<String> getRuleChainOutputLabels(TenantId tenantId, RuleChainId ruleChainId) {
@ -277,7 +280,7 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
RuleChainId ruleChainId = ruleChain.getId();
RuleChainId ruleChainMetaDataId = ruleChainMetaData.getRuleChainId();
try {
RuleChainUpdateResult result = ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData);
RuleChainUpdateResult result = ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, this::updateRuleNodeConfiguration);
checkNotNull(result.isSuccess() ? true : null);
List<RuleChain> updatedRuleChains;
@ -404,6 +407,45 @@ public class DefaultTbRuleChainService extends AbstractTbEntityService implement
}
}
@Override
public RuleNode updateRuleNodeConfiguration(RuleNode node) {
var ruleChainId = node.getRuleChainId();
var ruleNodeId = node.getId();
var ruleNodeType = node.getType();
try {
var ruleNodeClass = componentDiscoveryService.getRuleNodeInfo(ruleNodeType)
.orElseThrow(() -> new RuntimeException("Rule node " + ruleNodeType + " is not supported!"));
if (ruleNodeClass.isVersioned()) {
TbVersionedNode tbVersionedNode = (TbVersionedNode) ruleNodeClass.getClazz().getDeclaredConstructor().newInstance();
int fromVersion = node.getConfigurationVersion();
int toVersion = ruleNodeClass.getCurrentVersion();
if (fromVersion < toVersion) {
log.debug("Going to upgrade rule node with id: {} type: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeType, fromVersion, toVersion);
try {
TbPair<Boolean, JsonNode> upgradeResult = tbVersionedNode.upgrade(fromVersion, node.getConfiguration());
if (upgradeResult.getFirst()) {
node.setConfiguration(upgradeResult.getSecond());
}
node.setConfigurationVersion(toVersion);
log.debug("Successfully upgrade rule node with id: {} type: {}, rule chain id: {} fromVersion: {} toVersion: {}",
ruleNodeId, ruleNodeType, ruleChainId, fromVersion, toVersion);
} catch (TbNodeException e) {
log.warn("Failed to upgrade rule node with id: {} type: {} rule chain id: {} fromVersion: {} toVersion: {} due to: ",
ruleNodeId, ruleNodeType, ruleChainId, fromVersion, toVersion, e);
}
} else {
log.debug("Rule node with id: {} type: {} ruleChainId: {} already set to latest version!",
ruleNodeId, ruleChainId, ruleNodeType);
}
}
} catch (Exception e) {
log.error("Failed to update the rule node with id: {} type: {}, rule chain id: {}",
ruleNodeId, ruleNodeType, ruleChainId, e);
}
return node;
}
private Set<RuleChainId> updateRelatedRuleChains(TenantId tenantId, RuleChainId ruleChainId, Map<String, String> labelsMap) {
Set<RuleChainId> updatedRuleChains = new HashSet<>();
List<RuleChainOutputLabelsUsage> usageList = getOutputLabelUsage(tenantId, ruleChainId);

3
application/src/main/java/org/thingsboard/server/service/rule/TbRuleChainService.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleChainOutputLabelsUsage;
import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import java.util.List;
@ -54,4 +55,6 @@ public interface TbRuleChainService extends SimpleTbEntityService<RuleChain> {
RuleChain setAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChain ruleChain, User user) throws ThingsboardException;
RuleChain unsetAutoAssignToEdgeRuleChain(TenantId tenantId, RuleChain ruleChain, User user) throws ThingsboardException;
RuleNode updateRuleNodeConfiguration(RuleNode ruleNode);
}

4
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/RuleChainImportService.java

@ -35,6 +35,7 @@ import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.rule.RuleNodeDao;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.rule.TbRuleChainService;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
import java.util.Arrays;
@ -52,6 +53,7 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
private static final LinkedHashSet<EntityType> HINTS = new LinkedHashSet<>(Arrays.asList(EntityType.RULE_CHAIN, EntityType.DEVICE, EntityType.ASSET));
private final TbRuleChainService tbRuleChainService;
private final RuleChainService ruleChainService;
private final RuleNodeDao ruleNodeDao;
@ -106,7 +108,7 @@ public class RuleChainImportService extends BaseEntityImportService<RuleChainId,
ruleChain = ruleChainService.saveRuleChain(ruleChain);
if (ctx.isFinalImportAttempt() || ctx.getCurrentImportResult().isUpdatedAllExternalIds()) {
exportData.getMetaData().setRuleChainId(ruleChain.getId());
ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), exportData.getMetaData());
ruleChainService.saveRuleChainMetaData(ctx.getTenantId(), exportData.getMetaData(), tbRuleChainService::updateRuleNodeConfiguration);
return ruleChainService.findRuleChainById(ctx.getTenantId(), ruleChain.getId());
} else {
return ruleChain;

76
application/src/test/java/org/thingsboard/server/controller/RuleChainControllerTest.java

@ -21,6 +21,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
@ -30,6 +31,9 @@ import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.action.TbCreateAlarmNode;
import org.thingsboard.rule.engine.action.TbCreateAlarmNodeConfiguration;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.rule.engine.metadata.TbGetRelatedAttributeNode;
import org.thingsboard.rule.engine.metadata.TbGetRelatedDataNodeConfiguration;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@ -129,6 +133,78 @@ public class RuleChainControllerTest extends AbstractControllerTest {
ActionType.UPDATED);
}
@Test
public void testSaveRuleChainMetadataWithVersionedNodes() throws Exception {
RuleChain ruleChain = new RuleChain();
ruleChain.setName("RuleChain");
RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
Assert.assertNotNull(savedRuleChain);
RuleChainId ruleChainId = savedRuleChain.getId();
Assert.assertNotNull(ruleChainId);
Assert.assertTrue(savedRuleChain.getCreatedTime() > 0);
Assert.assertEquals(ruleChain.getName(), savedRuleChain.getName());
var annotation = TbGetRelatedAttributeNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class);
String ruleNodeType = TbGetRelatedAttributeNode.class.getName();
int currentVersion = annotation.version();
String oldConfig = "{\"attrMapping\":{\"serialNumber\":\"sn\"}," +
"\"relationsQuery\":{\"direction\":\"FROM\",\"maxLevel\":1," +
"\"filters\":[{\"relationType\":\"Contains\",\"entityTypes\":[]}]," +
"\"fetchLastLevelOnly\":false},\"telemetry\":false}";
TbGetRelatedDataNodeConfiguration defaultConfiguration = new TbGetRelatedDataNodeConfiguration().defaultConfiguration();
String newConfig = JacksonUtil.toString(defaultConfiguration);
var ruleChainMetaData = createRuleChainMetadataWithTbVersionedNodes(
ruleChainId,
ruleNodeType,
currentVersion,
oldConfig,
newConfig
);
var savedRuleChainMetaData = doPost("/api/ruleChain/metadata", ruleChainMetaData, RuleChainMetaData.class);
Assert.assertEquals(ruleChainId, savedRuleChainMetaData.getRuleChainId());
Assert.assertEquals(2, savedRuleChainMetaData.getNodes().size());
for (RuleNode ruleNode : savedRuleChainMetaData.getNodes()) {
Assert.assertNotNull(ruleNode.getId());
Assert.assertEquals(currentVersion, ruleNode.getConfigurationVersion());
Assert.assertEquals(defaultConfiguration, JacksonUtil.treeToValue(ruleNode.getConfiguration(), defaultConfiguration.getClass()));
}
}
private RuleChainMetaData createRuleChainMetadataWithTbVersionedNodes(
RuleChainId ruleChainId,
String ruleNodeType,
int currentVersion,
String oldConfig,
String newConfig
) {
RuleChainMetaData ruleChainMetaData = new RuleChainMetaData();
ruleChainMetaData.setRuleChainId(ruleChainId);
var ruleNodeWithOldConfig = new RuleNode();
ruleNodeWithOldConfig.setName("Old Rule Node");
ruleNodeWithOldConfig.setType(ruleNodeType);
ruleNodeWithOldConfig.setConfiguration(JacksonUtil.toJsonNode(oldConfig));
var ruleNodeWithNewConfig = new RuleNode();
ruleNodeWithNewConfig.setName("New Rule Node");
ruleNodeWithNewConfig.setType(ruleNodeType);
ruleNodeWithNewConfig.setConfigurationVersion(currentVersion);
ruleNodeWithNewConfig.setConfiguration(JacksonUtil.toJsonNode(newConfig));
List<RuleNode> ruleNodes = new ArrayList<>();
ruleNodes.add(ruleNodeWithOldConfig);
ruleNodes.add(ruleNodeWithNewConfig);
ruleChainMetaData.setFirstNodeIndex(0);
ruleChainMetaData.setNodes(ruleNodes);
return ruleChainMetaData;
}
@Test
public void testSaveRuleChainWithViolationOfLengthValidation() throws Exception {

10
application/src/test/java/org/thingsboard/server/rules/flow/AbstractRuleEngineFlowIntegrationTest.java

@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.ReflectionTestUtils;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNodeConfiguration;
import org.thingsboard.rule.engine.metadata.FetchTo;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
@ -137,16 +139,20 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setFetchTo(FetchTo.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setFetchTo(FetchTo.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));
@ -239,8 +245,10 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setFetchTo(FetchTo.METADATA);
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
ruleNode1.setConfiguration(JacksonUtil.valueToTree(configuration1));
@ -271,8 +279,10 @@ public abstract class AbstractRuleEngineFlowIntegrationTest extends AbstractRule
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setFetchTo(FetchTo.METADATA);
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
ruleNode2.setConfiguration(JacksonUtil.valueToTree(configuration2));

4
application/src/test/java/org/thingsboard/server/rules/lifecycle/AbstractRuleEngineLifecycleIntegrationTest.java

@ -24,6 +24,8 @@ import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.metadata.FetchTo;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.DataConstants;
@ -92,8 +94,10 @@ public abstract class AbstractRuleEngineLifecycleIntegrationTest extends Abstrac
RuleNode ruleNode = new RuleNode();
ruleNode.setName("Simple Rule Node");
ruleNode.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
configuration.setFetchTo(FetchTo.METADATA);
configuration.setServerAttributeNames(Collections.singletonList("serverAttributeKey"));
ruleNode.setConfiguration(JacksonUtil.valueToTree(configuration));

9
application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java

@ -23,6 +23,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNode;
import org.thingsboard.rule.engine.debug.TbMsgGeneratorNodeConfiguration;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNode;
import org.thingsboard.rule.engine.metadata.TbGetAttributesNodeConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
@ -86,6 +87,7 @@ import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import java.util.function.Function;
import static org.assertj.core.api.Assertions.assertThat;
@ -333,6 +335,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
@ -341,7 +344,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
metaData.setFirstNodeIndex(0);
metaData.addConnectionInfo(0, 1, "Success");
ruleChainService.saveRuleChainMetaData(tenantId, metaData);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}
@ -361,6 +364,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
RuleNode ruleNode1 = new RuleNode();
ruleNode1.setName("Simple Rule Node 1");
ruleNode1.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode1.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode1.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration1 = new TbGetAttributesNodeConfiguration();
configuration1.setServerAttributeNames(Collections.singletonList("serverAttributeKey1"));
@ -369,6 +373,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
RuleNode ruleNode2 = new RuleNode();
ruleNode2.setName("Simple Rule Node 2");
ruleNode2.setType(org.thingsboard.rule.engine.metadata.TbGetAttributesNode.class.getName());
ruleNode2.setConfigurationVersion(TbGetAttributesNode.class.getAnnotation(org.thingsboard.rule.engine.api.RuleNode.class).version());
ruleNode2.setDebugMode(true);
TbGetAttributesNodeConfiguration configuration2 = new TbGetAttributesNodeConfiguration();
configuration2.setServerAttributeNames(Collections.singletonList("serverAttributeKey2"));
@ -377,7 +382,7 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
metaData.setNodes(Arrays.asList(ruleNode1, ruleNode2));
metaData.setFirstNodeIndex(0);
metaData.addConnectionInfo(0, 1, "Success");
ruleChainService.saveRuleChainMetaData(tenantId, metaData);
ruleChainService.saveRuleChainMetaData(tenantId, metaData, Function.identity());
return ruleChainService.findRuleChainById(tenantId, ruleChain.getId());
}

7
common/dao-api/src/main/java/org/thingsboard/server/dao/rule/RuleChainService.java

@ -35,6 +35,7 @@ import org.thingsboard.server.dao.entity.EntityDaoService;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
/**
* Created by igor on 3/12/18.
@ -45,7 +46,7 @@ public interface RuleChainService extends EntityDaoService {
boolean setRootRuleChain(TenantId tenantId, RuleChainId ruleChainId);
RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData);
RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function<RuleNode, RuleNode> ruleNodeUpdater);
RuleChainMetaData loadRuleChainMetaData(TenantId tenantId, RuleChainId ruleChainId);
@ -75,7 +76,7 @@ public interface RuleChainService extends EntityDaoService {
RuleChainData exportTenantRuleChains(TenantId tenantId, PageLink pageLink) throws ThingsboardException;
List<RuleChainImportResult> importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite);
List<RuleChainImportResult> importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite, Function<RuleNode, RuleNode> ruleNodeUpdater);
RuleChain assignRuleChainToEdge(TenantId tenantId, RuleChainId ruleChainId, EdgeId edgeId);
@ -99,6 +100,8 @@ public interface RuleChainService extends EntityDaoService {
PageData<RuleNode> findAllRuleNodesByType(String type, PageLink pageLink);
PageData<RuleNode> findAllRuleNodesByTypeAndVersionLessThan(String type, int version, PageLink pageLink);
RuleNode saveRuleNode(TenantId tenantId, RuleNode ruleNode);
void deleteRuleNodes(TenantId tenantId, RuleChainId ruleChainId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

@ -42,6 +42,8 @@ public interface TimeseriesService {
ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys);
List<TsKvEntry> findLatestSync(TenantId tenantId, EntityId entityId, Collection<String> keys);
ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId);
ListenableFuture<Integer> save(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry);

17
common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java

@ -26,6 +26,8 @@ import org.thingsboard.server.common.data.BaseDataWithAdditionalInfo;
import org.thingsboard.server.common.data.id.ComponentDescriptorId;
import org.thingsboard.server.common.data.validation.Length;
import java.util.Objects;
/**
* @author Andrew Shvayka
*/
@ -48,8 +50,10 @@ public class ComponentDescriptor extends BaseData<ComponentDescriptorId> {
@Getter @Setter private String clazz;
@ApiModelProperty(position = 8, value = "Complex JSON object that represents the Rule Node configuration.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private transient JsonNode configurationDescriptor;
@ApiModelProperty(position = 9, value = "Rule node configuration version. By default, this value is 0. If the rule node is a versioned node, this value might be greater than 0.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private int configurationVersion;
@Length(fieldName = "actions")
@ApiModelProperty(position = 9, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@ApiModelProperty(position = 10, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private String actions;
public ComponentDescriptor() {
@ -64,9 +68,11 @@ public class ComponentDescriptor extends BaseData<ComponentDescriptorId> {
super(plugin);
this.type = plugin.getType();
this.scope = plugin.getScope();
this.clusteringMode = plugin.getClusteringMode();
this.name = plugin.getName();
this.clazz = plugin.getClazz();
this.configurationDescriptor = plugin.getConfigurationDescriptor();
this.configurationVersion = plugin.getConfigurationVersion();
this.actions = plugin.getActions();
}
@ -94,10 +100,11 @@ public class ComponentDescriptor extends BaseData<ComponentDescriptorId> {
if (type != that.type) return false;
if (scope != that.scope) return false;
if (name != null ? !name.equals(that.name) : that.name != null) return false;
if (actions != null ? !actions.equals(that.actions) : that.actions != null) return false;
if (configurationDescriptor != null ? !configurationDescriptor.equals(that.configurationDescriptor) : that.configurationDescriptor != null) return false;
return clazz != null ? clazz.equals(that.clazz) : that.clazz == null;
if (!Objects.equals(name, that.name)) return false;
if (!Objects.equals(actions, that.actions)) return false;
if (!Objects.equals(configurationDescriptor, that.configurationDescriptor)) return false;
if (configurationVersion != that.configurationVersion) return false;
return Objects.equals(clazz, that.clazz);
}
@Override

6
common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java

@ -50,7 +50,9 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
private boolean debugMode;
@ApiModelProperty(position = 7, value = "Enable/disable singleton mode. ", example = "false")
private boolean singletonMode;
@ApiModelProperty(position = 8, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode")
@ApiModelProperty(position = 8, value = "Version of rule node configuration. ", example = "0")
private int configurationVersion;
@ApiModelProperty(position = 9, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode")
private transient JsonNode configuration;
@JsonIgnore
private byte[] configurationBytes;
@ -104,7 +106,7 @@ public class RuleNode extends BaseDataWithAdditionalInfo<RuleNodeId> implements
return super.getCreatedTime();
}
@ApiModelProperty(position = 8, value = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", dataType = "com.fasterxml.jackson.databind.JsonNode")
@ApiModelProperty(position = 10, value = "Additional parameters of the rule node. Contains 'layoutX' and 'layoutY' properties for visualization.", dataType = "com.fasterxml.jackson.databind.JsonNode")
@Override
public JsonNode getAdditionalInfo() {
return super.getAdditionalInfo();

4
dao/pom.xml

@ -222,6 +222,10 @@
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
</dependency>
<dependency>
<groupId>org.thingsboard.rule-engine</groupId>
<artifactId>rule-engine-api</artifactId>
</dependency>
</dependencies>
<build>
<plugins>

2
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -337,6 +337,7 @@ public class ModelConstants {
public static final String COMPONENT_DESCRIPTOR_NAME_PROPERTY = "name";
public static final String COMPONENT_DESCRIPTOR_CLASS_PROPERTY = "clazz";
public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY = "configuration_descriptor";
public static final String COMPONENT_DESCRIPTOR_CONFIGURATION_VERSION_PROPERTY = "configuration_version";
public static final String COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY = "actions";
/**
@ -392,6 +393,7 @@ public class ModelConstants {
public static final String RULE_NODE_CHAIN_ID_PROPERTY = "rule_chain_id";
public static final String RULE_NODE_TYPE_PROPERTY = "type";
public static final String RULE_NODE_NAME_PROPERTY = "name";
public static final String RULE_NODE_VERSION_PROPERTY = "configuration_version";
public static final String RULE_NODE_CONFIGURATION_PROPERTY = "configuration";
/**

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/ComponentDescriptorEntity.java

@ -64,6 +64,9 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_CONFIGURATION_DESCRIPTOR_PROPERTY)
private JsonNode configurationDescriptor;
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_CONFIGURATION_VERSION_PROPERTY)
private int configurationVersion;
@Column(name = ModelConstants.COMPONENT_DESCRIPTOR_ACTIONS_PROPERTY)
private String actions;
@ -82,6 +85,7 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
this.name = component.getName();
this.clazz = component.getClazz();
this.configurationDescriptor = component.getConfigurationDescriptor();
this.configurationVersion = component.getConfigurationVersion();
}
@Override
@ -95,6 +99,7 @@ public class ComponentDescriptorEntity extends BaseSqlEntity<ComponentDescriptor
data.setClazz(this.getClazz());
data.setActions(this.getActions());
data.setConfigurationDescriptor(configurationDescriptor);
data.setConfigurationVersion(configurationVersion);
return data;
}
}

5
dao/src/main/java/org/thingsboard/server/dao/model/sql/RuleNodeEntity.java

@ -49,6 +49,9 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
@Column(name = ModelConstants.RULE_NODE_NAME_PROPERTY)
private String name;
@Column(name = ModelConstants.RULE_NODE_VERSION_PROPERTY)
private int configurationVersion;
@Type(type = "json")
@Column(name = ModelConstants.RULE_NODE_CONFIGURATION_PROPERTY)
private JsonNode configuration;
@ -81,6 +84,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode();
this.singletonMode = ruleNode.isSingletonMode();
this.configurationVersion = ruleNode.getConfigurationVersion();
this.configuration = ruleNode.getConfiguration();
this.additionalInfo = ruleNode.getAdditionalInfo();
if (ruleNode.getExternalId() != null) {
@ -99,6 +103,7 @@ public class RuleNodeEntity extends BaseSqlEntity<RuleNode> {
ruleNode.setName(name);
ruleNode.setDebugMode(debugMode);
ruleNode.setSingletonMode(singletonMode);
ruleNode.setConfigurationVersion(configurationVersion);
ruleNode.setConfiguration(configuration);
ruleNode.setAdditionalInfo(additionalInfo);
if (externalId != null) {

29
dao/src/main/java/org/thingsboard/server/dao/rule/BaseRuleChainService.java

@ -27,6 +27,8 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.TbVersionedNode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.edge.Edge;
@ -52,6 +54,7 @@ import org.thingsboard.server.common.data.rule.RuleChainUpdateResult;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.rule.RuleNodeUpdateResult;
import org.thingsboard.server.common.data.util.ReflectionUtils;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.dao.entity.AbstractEntityService;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.exception.DataValidationException;
@ -60,6 +63,7 @@ import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.service.Validator;
import org.thingsboard.server.dao.service.validator.RuleChainDataValidator;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -69,11 +73,13 @@ import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import static org.thingsboard.server.common.data.DataConstants.TENANT;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
import static org.thingsboard.server.dao.service.Validator.validatePositiveNumber;
import static org.thingsboard.server.dao.service.Validator.validateString;
/**
@ -140,7 +146,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
@Override
public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData) {
public RuleChainUpdateResult saveRuleChainMetaData(TenantId tenantId, RuleChainMetaData ruleChainMetaData, Function<RuleNode, RuleNode> ruleNodeUpdater) {
Validator.validateId(ruleChainMetaData.getRuleChainId(), "Incorrect rule chain id.");
RuleChain ruleChain = findRuleChainById(tenantId, ruleChainMetaData.getRuleChainId());
if (ruleChain == null) {
@ -180,9 +186,11 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
updatedRuleNodes.add(new RuleNodeUpdateResult(existingNode, newRuleNode));
}
RuleChainId ruleChainId = ruleChain.getId();
if (nodes != null) {
for (RuleNode node : toAddOrUpdate) {
node.setRuleChainId(ruleChain.getId());
node.setRuleChainId(ruleChainId);
node = ruleNodeUpdater.apply(node);
RuleNode savedNode = ruleNodeDao.save(tenantId, node);
relations.add(new EntityRelation(ruleChainMetaData.getRuleChainId(), savedNode.getId(),
EntityRelation.CONTAINS_TYPE, RelationTypeGroup.RULE_CHAIN));
@ -218,7 +226,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
RuleChain targetRuleChain = findRuleChainById(TenantId.SYS_TENANT_ID, targetRuleChainId);
RuleNode targetNode = new RuleNode();
targetNode.setName(targetRuleChain != null ? targetRuleChain.getName() : "Rule Chain Input");
targetNode.setRuleChainId(ruleChain.getId());
targetNode.setRuleChainId(ruleChainId);
targetNode.setType("org.thingsboard.rule.engine.flow.TbRuleChainInputNode");
var configuration = JacksonUtil.newObjectNode();
configuration.put("ruleChainId", targetRuleChainId.getId().toString());
@ -231,7 +239,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
targetNode = ruleNodeDao.save(tenantId, targetNode);
EntityRelation sourceRuleChainToRuleNode = new EntityRelation();
sourceRuleChainToRuleNode.setFrom(ruleChain.getId());
sourceRuleChainToRuleNode.setFrom(ruleChainId);
sourceRuleChainToRuleNode.setTo(targetNode.getId());
sourceRuleChainToRuleNode.setType(EntityRelation.CONTAINS_TYPE);
sourceRuleChainToRuleNode.setTypeGroup(RelationTypeGroup.RULE_CHAIN);
@ -446,7 +454,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
@Override
public List<RuleChainImportResult> importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite) {
public List<RuleChainImportResult> importTenantRuleChains(TenantId tenantId, RuleChainData ruleChainData, boolean overwrite, Function<RuleNode, RuleNode> ruleNodeUpdater) {
List<RuleChainImportResult> importResults = new ArrayList<>();
setRandomRuleChainIds(ruleChainData);
@ -483,7 +491,7 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
}
if (CollectionUtils.isNotEmpty(ruleChainData.getMetadata())) {
ruleChainData.getMetadata().forEach(md -> saveRuleChainMetaData(tenantId, md));
ruleChainData.getMetadata().forEach(md -> saveRuleChainMetaData(tenantId, md, ruleNodeUpdater));
}
return importResults;
@ -700,6 +708,15 @@ public class BaseRuleChainService extends AbstractEntityService implements RuleC
return ruleNodeDao.findAllRuleNodesByType(type, pageLink);
}
@Override
public PageData<RuleNode> findAllRuleNodesByTypeAndVersionLessThan(String type, int version, PageLink pageLink) {
log.trace("Executing findAllRuleNodesByTypeAndVersionLessThan, type {}, pageLink {}, version {}", type, pageLink, version);
validateString(type, "Incorrect type of the rule node");
validatePositiveNumber(version, "Incorrect version to compare with. Version should be greater than 0!");
validatePageLink(pageLink);
return ruleNodeDao.findAllRuleNodesByTypeAndVersionLessThan(type, version, pageLink);
}
@Override
public RuleNode saveRuleNode(TenantId tenantId, RuleNode ruleNode) {
return ruleNodeDao.save(tenantId, ruleNode);

2
dao/src/main/java/org/thingsboard/server/dao/rule/RuleNodeDao.java

@ -34,6 +34,8 @@ public interface RuleNodeDao extends Dao<RuleNode> {
PageData<RuleNode> findAllRuleNodesByType(String type, PageLink pageLink);
PageData<RuleNode> findAllRuleNodesByTypeAndVersionLessThan(String type, int version, PageLink pageLink);
List<RuleNode> findByExternalIds(RuleChainId ruleChainId, List<RuleNodeId> externalIds);
void deleteByIdIn(List<RuleNodeId> ruleNodeIds);

2
dao/src/main/java/org/thingsboard/server/dao/service/Validator.java

@ -61,7 +61,7 @@ public class Validator {
/**
* This method validate <code>String</code> string. If string is invalid than throw
* This method validate <code>long</code> value. If value isn't possitive than throw
* <code>IncorrectParameterException</code> exception
*
* @param val the val

1
dao/src/main/java/org/thingsboard/server/dao/sql/component/AbstractComponentDescriptorInsertRepository.java

@ -73,6 +73,7 @@ public abstract class AbstractComponentDescriptorInsertRepository implements Com
.setParameter("actions", entity.getActions())
.setParameter("clazz", entity.getClazz())
.setParameter("configuration_descriptor", entity.getConfigurationDescriptor().toString())
.setParameter("configuration_version", entity.getConfigurationVersion())
.setParameter("name", entity.getName())
.setParameter("scope", entity.getScope().name())
.setParameter("type", entity.getType().name())

4
dao/src/main/java/org/thingsboard/server/dao/sql/component/SqlComponentDescriptorInsertRepository.java

@ -44,10 +44,10 @@ public class SqlComponentDescriptorInsertRepository extends AbstractComponentDes
}
private static String getInsertOrUpdateStatement(String conflictKeyStatement, String updateKeyStatement) {
return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, name, scope, type, clustering_mode) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :name, :scope, :type, :clustering_mode) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *";
return "INSERT INTO component_descriptor (id, created_time, actions, clazz, configuration_descriptor, configuration_version, name, scope, type, clustering_mode) VALUES (:id, :created_time, :actions, :clazz, :configuration_descriptor, :configuration_version, :name, :scope, :type, :clustering_mode) ON CONFLICT " + conflictKeyStatement + " DO UPDATE SET " + updateKeyStatement + " returning *";
}
private static String getUpdateStatement(String id) {
return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, name = :name, scope = :scope, type = :type, clustering_mode = :clustering_mode";
return "actions = :actions, " + id + ",created_time = :created_time, configuration_descriptor = :configuration_descriptor, configuration_version = :configuration_version, name = :name, scope = :scope, type = :type, clustering_mode = :clustering_mode";
}
}

10
dao/src/main/java/org/thingsboard/server/dao/sql/rule/JpaRuleNodeDao.java

@ -69,6 +69,16 @@ public class JpaRuleNodeDao extends JpaAbstractDao<RuleNodeEntity, RuleNode> imp
DaoUtil.toPageable(pageLink)));
}
@Override
public PageData<RuleNode> findAllRuleNodesByTypeAndVersionLessThan(String type, int version, PageLink pageLink) {
return DaoUtil.toPageData(ruleNodeRepository
.findAllRuleNodesByTypeAndVersionLessThan(
type,
version,
Objects.toString(pageLink.getTextSearch(), ""),
DaoUtil.toPageable(pageLink)));
}
@Override
public List<RuleNode> findByExternalIds(RuleChainId ruleChainId, List<RuleNodeId> externalIds) {
return DaoUtil.convertDataList(ruleNodeRepository.findRuleNodesByRuleChainIdAndExternalIdIn(ruleChainId.getId(),

6
dao/src/main/java/org/thingsboard/server/dao/sql/rule/RuleNodeRepository.java

@ -41,6 +41,12 @@ public interface RuleNodeRepository extends JpaRepository<RuleNodeEntity, UUID>
@Param("searchText") String searchText,
Pageable pageable);
@Query("SELECT r FROM RuleNodeEntity r WHERE r.type = :ruleType AND r.configurationVersion < :version AND LOWER(r.configuration) LIKE LOWER(CONCAT('%', :searchText, '%')) ")
Page<RuleNodeEntity> findAllRuleNodesByTypeAndVersionLessThan(@Param("ruleType") String ruleType,
@Param("version") int version,
@Param("searchText") String searchText,
Pageable pageable);
List<RuleNodeEntity> findRuleNodesByRuleChainIdAndExternalIdIn(UUID ruleChainId, List<UUID> externalIds);
@Transactional

19
dao/src/main/java/org/thingsboard/server/dao/sqlts/SqlTimeseriesLatestDao.java

@ -156,11 +156,12 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
@Override
public ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key) {
TsKvEntry latest = doFindLatest(entityId, key);
if (latest == null) {
latest = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
}
return Futures.immediateFuture(latest);
return Futures.immediateFuture(getLatestTsKvEntry(entityId, key));
}
@Override
public TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key) {
return getLatestTsKvEntry(entityId, key);
}
@Override
@ -268,4 +269,12 @@ public class SqlTimeseriesLatestDao extends BaseAbstractSqlTimeseriesDao impleme
return tsLatestQueue.add(latestEntity);
}
private TsKvEntry getLatestTsKvEntry(EntityId entityId, String key) {
TsKvEntry latest = doFindLatest(entityId, key);
if (latest == null) {
latest = new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry(key, null));
}
return latest;
}
}

9
dao/src/main/java/org/thingsboard/server/dao/timeseries/BaseTimeseriesService.java

@ -132,6 +132,15 @@ public class BaseTimeseriesService implements TimeseriesService {
return Futures.allAsList(futures);
}
@Override
public List<TsKvEntry> findLatestSync(TenantId tenantId, EntityId entityId, Collection<String> keys) {
validate(entityId);
List<TsKvEntry> latestEntries = Lists.newArrayListWithExpectedSize(keys.size());
keys.forEach(key -> Validator.validateString(key, "Incorrect key " + key));
keys.forEach(key -> latestEntries.add(timeseriesLatestDao.findLatestSync(tenantId, entityId, key)));
return latestEntries;
}
@Override
public ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId) {
validate(entityId);

11
dao/src/main/java/org/thingsboard/server/dao/timeseries/CassandraBaseTimeseriesLatestDao.java

@ -44,6 +44,7 @@ import org.thingsboard.server.dao.util.NoSqlTsLatestDao;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import static com.datastax.oss.driver.api.querybuilder.QueryBuilder.literal;
@ -69,6 +70,16 @@ public class CassandraBaseTimeseriesLatestDao extends AbstractCassandraBaseTimes
return findLatest(tenantId, entityId, key, rs -> convertResultToTsKvEntry(key, rs.one()));
}
@Override
public TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key) {
try {
return findLatest(tenantId, entityId, key, rs -> convertResultToTsKvEntry(key, rs.one())).get();
} catch (InterruptedException | ExecutionException e) {
log.error("[{}][{}] Failed to get latest entry for key: {} due to: ", tenantId, entityId, key, e);
throw new RuntimeException(e);
}
}
private <T> ListenableFuture<T> findLatest(TenantId tenantId, EntityId entityId, String key, java.util.function.Function<TbResultSet, T> function) {
BoundStatementBuilder stmtBuilder = new BoundStatementBuilder(getFindLatestStmt().bind());
stmtBuilder.setString(0, entityId.getEntityType().name());

2
dao/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesLatestDao.java

@ -40,6 +40,8 @@ public interface TimeseriesLatestDao {
*/
ListenableFuture<TsKvEntry> findLatest(TenantId tenantId, EntityId entityId, String key);
TsKvEntry findLatestSync(TenantId tenantId, EntityId entityId, String key);
ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId);
ListenableFuture<Void> saveLatest(TenantId tenantId, EntityId entityId, TsKvEntry tsKvEntry);

2
dao/src/main/resources/sql/schema-entities-idx.sql

@ -89,6 +89,8 @@ CREATE INDEX IF NOT EXISTS idx_rule_node_external_id ON rule_node(rule_chain_id,
CREATE INDEX IF NOT EXISTS idx_rule_node_type ON rule_node(type);
CREATE INDEX IF NOT EXISTS idx_rule_node_type_configuration_version ON rule_node(type, configuration_version);
CREATE INDEX IF NOT EXISTS idx_api_usage_state_entity_id ON api_usage_state(entity_id);
CREATE INDEX IF NOT EXISTS idx_alarm_comment_alarm_id ON alarm_comment(alarm_id);

2
dao/src/main/resources/sql/schema-entities.sql

@ -122,6 +122,7 @@ CREATE TABLE IF NOT EXISTS component_descriptor (
actions varchar(255),
clazz varchar UNIQUE,
configuration_descriptor varchar,
configuration_version int DEFAULT 0,
name varchar(255),
scope varchar(255),
type varchar(255),
@ -180,6 +181,7 @@ CREATE TABLE IF NOT EXISTS rule_node (
created_time bigint NOT NULL,
rule_chain_id uuid,
additional_info varchar,
configuration_version int DEFAULT 0,
configuration varchar(10000000),
type varchar(255),
name varchar(255),

3
dao/src/test/java/org/thingsboard/server/dao/service/EdgeServiceTest.java

@ -44,6 +44,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@ -639,7 +640,7 @@ public class EdgeServiceTest extends AbstractServiceTest {
ruleChainMetaData3.setNodes(Arrays.asList(ruleNode1, ruleNode2));
ruleChainMetaData3.setFirstNodeIndex(0);
ruleChainMetaData3.setRuleChainId(ruleChain3.getId());
ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData3);
ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData3, Function.identity());
ruleChainService.assignRuleChainToEdge(tenantId, ruleChain3.getId(), savedEdge.getId());

9
dao/src/test/java/org/thingsboard/server/dao/service/RuleChainServiceTest.java

@ -40,6 +40,7 @@ import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
/**
* Created by igor on 3/13/18.
@ -280,7 +281,7 @@ public class RuleChainServiceTest extends AbstractServiceTest {
ruleNodes.set(name3Index, ruleNode4);
Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, savedRuleChainMetaData).isSuccess());
Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, savedRuleChainMetaData, Function.identity()).isSuccess());
RuleChainMetaData updatedRuleChainMetaData = ruleChainService.loadRuleChainMetaData(tenantId, savedRuleChainMetaData.getRuleChainId());
Assert.assertEquals(3, updatedRuleChainMetaData.getNodes().size());
@ -311,14 +312,14 @@ public class RuleChainServiceTest extends AbstractServiceTest {
@Test
public void testUpdateRuleChainMetaDataWithCirclingRelation() {
Assertions.assertThrows(DataValidationException.class, () -> {
ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation());
ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation(), Function.identity());
});
}
@Test
public void testUpdateRuleChainMetaDataWithCirclingRelation2() {
Assertions.assertThrows(DataValidationException.class, () -> {
ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation2());
ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation2(), Function.identity());
});
}
@ -395,7 +396,7 @@ public class RuleChainServiceTest extends AbstractServiceTest {
ruleChainMetaData.addConnectionInfo(0,2,"fail");
ruleChainMetaData.addConnectionInfo(1,2,"success");
Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData).isSuccess());
Assert.assertTrue(ruleChainService.saveRuleChainMetaData(tenantId, ruleChainMetaData, Function.identity()).isSuccess());
return ruleChainService.loadRuleChainMetaData(tenantId, ruleChainMetaData.getRuleChainId());
}

2
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleNode.java

@ -65,4 +65,6 @@ public @interface RuleNode {
RuleChainType[] ruleChainTypes() default {RuleChainType.CORE, RuleChainType.EDGE};
int version() default 0;
}

6
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbNode.java

@ -29,8 +29,10 @@ public interface TbNode {
void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException;
default void destroy() {}
default void destroy() {
}
default void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {}
default void onPartitionChangeMsg(TbContext ctx, PartitionChangeMsg msg) {
}
}

35
rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/TbVersionedNode.java

@ -0,0 +1,35 @@
/**
* 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.api;
import com.fasterxml.jackson.databind.JsonNode;
import org.thingsboard.server.common.data.util.TbPair;
public interface TbVersionedNode extends TbNode {
/**
* Upgrades the configuration from a specific version to the current version specified in the
* {@link RuleNode} annotation for the instance of {@link TbVersionedNode}.
*
* @param fromVersion The version from which the configuration needs to be upgraded.
* @param oldConfiguration The old configuration to be upgraded.
* @return A pair consisting of a Boolean flag indicating the success of the upgrade
* and a JsonNode representing the upgraded configuration.
* @throws TbNodeException If an error occurs during the upgrade process.
*/
TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException;
}

120
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNode.java

@ -20,14 +20,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.DonAsynchron;
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.TbRelationTypes;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.TsKvEntry;
@ -39,21 +37,23 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "calculate delta", relationTypes = {"Success", "Failure", "Other"},
configClazz = CalculateDeltaNodeConfiguration.class,
nodeDescription = "Calculates and adds 'delta' value into message based on the incoming and previous value",
nodeDetails = "Calculates delta and period based on the previous time-series reading and current data. " +
"Delta calculation is done in scope of the message originator, e.g. device, asset or customer. " +
"If there is input key, the output relation will be 'Success' unless delta is negative and corresponding configuration parameter is set. " +
"If there is no input value key in the incoming message, the output relation will be 'Other'.",
nodeDescription = "Calculates delta and amount of time passed between previous timeseries key reading " +
"and current value for this key from the incoming message",
nodeDetails = "Useful for metering use cases, when you need to calculate consumption based on pulse counter reading.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeCalculateDeltaConfig")
public class CalculateDeltaNode implements TbNode {
private Map<EntityId, ValueWithTs> cache;
private CalculateDeltaNodeConfiguration config;
private TbContext ctx;
@ -66,7 +66,6 @@ public class CalculateDeltaNode implements TbNode {
this.ctx = ctx;
this.timeseriesService = ctx.getTimeseriesService();
this.useCache = config.isUseCache();
if (useCache) {
cache = new ConcurrentHashMap<>();
}
@ -74,51 +73,50 @@ public class CalculateDeltaNode implements TbNode {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
if (msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) {
JsonNode json = JacksonUtil.toJsonNode(msg.getData());
String inputKey = config.getInputValueKey();
if (json.has(inputKey)) {
DonAsynchron.withCallback(getLastValue(msg.getOriginator()),
previousData -> {
double currentValue = json.get(inputKey).asDouble();
long currentTs = msg.getMetaDataTs();
if (useCache) {
cache.put(msg.getOriginator(), new ValueWithTs(currentTs, currentValue));
}
BigDecimal delta = BigDecimal.valueOf(previousData != null ? currentValue - previousData.value : 0.0);
if (config.isTellFailureIfDeltaIsNegative() && delta.doubleValue() < 0) {
ctx.tellNext(msg, TbRelationTypes.FAILURE);
return;
}
if (config.getRound() != null) {
delta = delta.setScale(config.getRound(), RoundingMode.HALF_UP);
}
ObjectNode result = (ObjectNode) json;
if (delta.stripTrailingZeros().scale() > 0) {
result.put(config.getOutputValueKey(), delta.doubleValue());
} else {
result.put(config.getOutputValueKey(), delta.longValueExact());
}
if (config.isAddPeriodBetweenMsgs()) {
long period = previousData != null ? currentTs - previousData.ts : 0;
result.put(config.getPeriodValueKey(), period);
}
ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(result)));
},
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
} else {
ctx.tellNext(msg, "Other");
}
} else {
if (!msg.getType().equals(SessionMsgType.POST_TELEMETRY_REQUEST.name())) {
ctx.tellNext(msg, "Other");
return;
}
JsonNode json = JacksonUtil.toJsonNode(msg.getData());
String inputKey = config.getInputValueKey();
if (!json.has(inputKey)) {
ctx.tellNext(msg, "Other");
return;
}
withCallback(getLastValue(msg.getOriginator()),
previousData -> {
double currentValue = json.get(inputKey).asDouble();
long currentTs = msg.getMetaDataTs();
if (useCache) {
cache.put(msg.getOriginator(), new ValueWithTs(currentTs, currentValue));
}
BigDecimal delta = BigDecimal.valueOf(previousData != null ? currentValue - previousData.value : 0.0);
if (config.isTellFailureIfDeltaIsNegative() && delta.doubleValue() < 0) {
ctx.tellFailure(msg, new IllegalArgumentException("Delta value is negative!"));
return;
}
if (config.getRound() != null) {
delta = delta.setScale(config.getRound(), RoundingMode.HALF_UP);
}
ObjectNode result = (ObjectNode) json;
if (delta.stripTrailingZeros().scale() > 0) {
result.put(config.getOutputValueKey(), delta.doubleValue());
} else {
result.put(config.getOutputValueKey(), delta.longValueExact());
}
if (config.isAddPeriodBetweenMsgs()) {
long period = previousData != null ? currentTs - previousData.ts : 0;
result.put(config.getPeriodValueKey(), period);
}
ctx.tellSuccess(TbMsg.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), JacksonUtil.toString(result)));
},
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
@Override
@ -128,18 +126,29 @@ public class CalculateDeltaNode implements TbNode {
}
}
private ListenableFuture<ValueWithTs> fetchLatestValue(EntityId entityId) {
private ListenableFuture<ValueWithTs> fetchLatestValueAsync(EntityId entityId) {
return Futures.transform(timeseriesService.findLatest(ctx.getTenantId(), entityId, Collections.singletonList(config.getInputValueKey())),
list -> extractValue(list.get(0))
, ctx.getDbCallbackExecutor());
}
private ValueWithTs fetchLatestValue(EntityId entityId) {
List<TsKvEntry> tsKvEntries = timeseriesService.findLatestSync(
ctx.getTenantId(),
entityId,
Collections.singletonList(config.getInputValueKey()));
return extractValue(tsKvEntries.get(0));
}
private ListenableFuture<ValueWithTs> getLastValue(EntityId entityId) {
ValueWithTs latestValue;
if (useCache && (latestValue = cache.get(entityId)) != null) {
if (useCache) {
ValueWithTs latestValue;
if ((latestValue = cache.get(entityId)) == null) {
latestValue = fetchLatestValue(entityId);
}
return Futures.immediateFuture(latestValue);
} else {
return fetchLatestValue(entityId);
return fetchLatestValueAsync(entityId);
}
}
@ -181,4 +190,5 @@ public class CalculateDeltaNode implements TbNode {
this.value = value;
}
}
}

3
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeConfiguration.java

@ -22,6 +22,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CalculateDeltaNodeConfiguration implements NodeConfiguration<CalculateDeltaNodeConfiguration> {
private String inputValueKey;
private String outputValueKey;
private boolean useCache;
@ -32,7 +33,7 @@ public class CalculateDeltaNodeConfiguration implements NodeConfiguration<Calcul
@Override
public CalculateDeltaNodeConfiguration defaultConfiguration() {
CalculateDeltaNodeConfiguration configuration = new CalculateDeltaNodeConfiguration();
var configuration = new CalculateDeltaNodeConfiguration();
configuration.setInputValueKey("pulseCounter");
configuration.setOutputValueKey("delta");
configuration.setUseCache(true);

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformNodeConfiguration.java → rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/DataToFetch.java

@ -13,11 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.transform;
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
public enum DataToFetch {
@Data
public class TbTransformNodeConfiguration {
ATTRIBUTES, LATEST_TELEMETRY, FIELDS
}

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntityDetails.java → rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/FetchTo.java

@ -13,10 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.rule.engine.util;
package org.thingsboard.rule.engine.metadata;
public enum EntityDetails {
public enum FetchTo {
ID, TITLE, COUNTRY, CITY, STATE, ZIP, ADDRESS, ADDRESS2, PHONE, EMAIL, ADDITIONAL_INFO
DATA,
METADATA
}

25
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractFetchToNodeConfiguration.java

@ -0,0 +1,25 @@
/**
* 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.metadata;
import lombok.Data;
@Data
public abstract class TbAbstractFetchToNodeConfiguration {
private FetchTo fetchTo;
}

170
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNode.java

@ -15,17 +15,15 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.thingsboard.common.util.JacksonUtil;
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;
@ -35,132 +33,108 @@ import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
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.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE;
import static org.thingsboard.server.common.data.DataConstants.LATEST_TS;
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
import static org.thingsboard.server.common.data.DataConstants.SHARED_SCOPE;
public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeConfiguration, T extends EntityId> implements TbNode {
@Slf4j
public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeConfiguration, T extends EntityId> extends TbAbstractNodeWithFetchTo<C> {
private static final String VALUE = "value";
private static final String TS = "ts";
protected C config;
private boolean fetchToData;
private boolean isTellFailureIfAbsent;
private boolean getLatestValueWithTs;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadGetAttributesNodeConfig(configuration);
this.fetchToData = config.isFetchToData();
this.getLatestValueWithTs = config.isGetLatestValueWithTs();
this.isTellFailureIfAbsent = BooleanUtils.toBooleanDefaultIfNull(this.config.isTellFailureIfAbsent(), true);
super.init(ctx, configuration);
getLatestValueWithTs = config.isGetLatestValueWithTs();
isTellFailureIfAbsent = BooleanUtils.toBooleanDefaultIfNull(config.isTellFailureIfAbsent(), true);
}
protected abstract C loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException;
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws TbNodeException {
try {
withCallback(
findEntityIdAsync(ctx, msg),
entityId -> safePutAttributes(ctx, msg, entityId),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
} catch (Throwable th) {
ctx.tellFailure(msg, th);
}
var msgDataAsObjectNode = FetchTo.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null;
withCallback(
findEntityIdAsync(ctx, msg),
entityId -> safePutAttributes(ctx, msg, msgDataAsObjectNode, entityId),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
protected abstract ListenableFuture<T> findEntityIdAsync(TbContext ctx, TbMsg msg);
private void safePutAttributes(TbContext ctx, TbMsg msg, T entityId) {
if (entityId == null || entityId.isNullUid()) {
ctx.tellNext(msg, FAILURE);
return;
}
JsonNode msgDataNode;
if (fetchToData) {
msgDataNode = JacksonUtil.toJsonNode(msg.getData());
if (!msgDataNode.isObject()) {
ctx.tellFailure(msg, new IllegalArgumentException("Msg body is not an object!"));
return;
}
} else {
msgDataNode = null;
}
ConcurrentHashMap<String, List<String>> failuresMap = new ConcurrentHashMap<>();
ListenableFuture<List<Map<String, ? extends List<? extends KvEntry>>>> allFutures = Futures.allAsList(
getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresMap),
getAttrAsync(ctx, entityId, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg), failuresMap),
getAttrAsync(ctx, entityId, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), msg), failuresMap),
getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresMap)
private void safePutAttributes(TbContext ctx, TbMsg msg, ObjectNode msgDataNode, T entityId) {
Set<TbPair<String, List<String>>> failuresPairSet = ConcurrentHashMap.newKeySet();
var getKvEntryPairFutures = Futures.allAsList(
getLatestTelemetry(ctx, entityId, TbNodeUtils.processPatterns(config.getLatestTsKeyNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, CLIENT_SCOPE, TbNodeUtils.processPatterns(config.getClientAttributeNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, SHARED_SCOPE, TbNodeUtils.processPatterns(config.getSharedAttributeNames(), msg), failuresPairSet),
getAttrAsync(ctx, entityId, SERVER_SCOPE, TbNodeUtils.processPatterns(config.getServerAttributeNames(), msg), failuresPairSet)
);
withCallback(allFutures, futuresList -> {
TbMsgMetaData msgMetaData = msg.getMetaData().copy();
futuresList.stream().filter(Objects::nonNull).forEach(kvEntriesMap -> {
kvEntriesMap.forEach((keyScope, kvEntryList) -> {
String prefix = getPrefix(keyScope);
kvEntryList.forEach(kvEntry -> {
String key = prefix + kvEntry.getKey();
if (fetchToData) {
JacksonUtil.addKvEntry((ObjectNode) msgDataNode, kvEntry, key);
} else {
msgMetaData.putValue(key, kvEntry.getValueAsString());
}
});
withCallback(getKvEntryPairFutures, futuresList -> {
var msgMetaData = msg.getMetaData().copy();
futuresList.stream().filter(Objects::nonNull).forEach(kvEntriesPair -> {
var keyScope = kvEntriesPair.getFirst();
var kvEntryList = kvEntriesPair.getSecond();
var prefix = getPrefix(keyScope);
kvEntryList.forEach(kvEntry -> {
String targetKey = prefix + kvEntry.getKey();
enrichMessage(msgDataNode, msgMetaData, kvEntry, targetKey);
});
});
TbMsg outMsg = fetchToData ?
TbMsg.transformMsgData(msg, JacksonUtil.toString(msgDataNode)) :
TbMsg.transformMsg(msg, msgMetaData);
if (failuresMap.isEmpty()) {
TbMsg outMsg = transformMessage(msg, msgDataNode, msgMetaData);
if (failuresPairSet.isEmpty()) {
ctx.tellSuccess(outMsg);
} else {
ctx.tellFailure(outMsg, reportFailures(failuresMap));
ctx.tellFailure(outMsg, reportFailures(failuresPairSet));
}
}, t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}, t -> ctx.tellFailure(msg, t), MoreExecutors.directExecutor());
}
private ListenableFuture<Map<String, List<AttributeKvEntry>>> getAttrAsync(TbContext ctx, EntityId entityId, String scope, List<String> keys, ConcurrentHashMap<String, List<String>> failuresMap) {
private ListenableFuture<TbPair<String, List<AttributeKvEntry>>> getAttrAsync(
TbContext ctx,
EntityId entityId,
String scope,
List<String> keys,
Set<TbPair<String, List<String>>> failuresPairSet
) {
if (CollectionUtils.isEmpty(keys)) {
return Futures.immediateFuture(null);
}
ListenableFuture<List<AttributeKvEntry>> attributeKvEntryListFuture = ctx.getAttributesService().find(ctx.getTenantId(), entityId, scope, keys);
var attributeKvEntryListFuture = ctx.getAttributesService().find(ctx.getTenantId(), entityId, scope, keys);
return Futures.transform(attributeKvEntryListFuture, attributeKvEntryList -> {
if (isTellFailureIfAbsent && attributeKvEntryList.size() != keys.size()) {
getNotExistingKeys(attributeKvEntryList, keys).forEach(key -> computeFailuresMap(scope, failuresMap, key));
List<String> nonExistentKeys = getNonExistentKeys(attributeKvEntryList, keys);
failuresPairSet.add(new TbPair<>(scope, nonExistentKeys));
}
Map<String, List<AttributeKvEntry>> mapAttributeKvEntry = new HashMap<>();
mapAttributeKvEntry.put(scope, attributeKvEntryList);
return mapAttributeKvEntry;
}, MoreExecutors.directExecutor());
return new TbPair<>(scope, attributeKvEntryList);
}, ctx.getDbCallbackExecutor());
}
private ListenableFuture<Map<String, List<TsKvEntry>>> getLatestTelemetry(TbContext ctx, EntityId entityId, List<String> keys, ConcurrentHashMap<String, List<String>> failuresMap) {
private ListenableFuture<TbPair<String, List<TsKvEntry>>> getLatestTelemetry(TbContext ctx, EntityId entityId, List<String> keys, Set<TbPair<String, List<String>>> failuresPairSet) {
if (CollectionUtils.isEmpty(keys)) {
return Futures.immediateFuture(null);
}
ListenableFuture<List<TsKvEntry>> latestTelemetryFutures = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), entityId, keys);
return Futures.transform(latestTelemetryFutures, tsKvEntries -> {
List<TsKvEntry> listTsKvEntry = new ArrayList<>();
var listTsKvEntry = new ArrayList<TsKvEntry>();
var nonExistentKeys = new ArrayList<String>();
tsKvEntries.forEach(tsKvEntry -> {
if (tsKvEntry.getValue() == null) {
if (isTellFailureIfAbsent) {
computeFailuresMap(LATEST_TS, failuresMap, tsKvEntry.getKey());
nonExistentKeys.add(tsKvEntry.getKey());
}
} else if (getLatestValueWithTs) {
listTsKvEntry.add(getValueWithTs(tsKvEntry));
@ -168,22 +142,23 @@ public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeC
listTsKvEntry.add(new BasicTsKvEntry(tsKvEntry.getTs(), tsKvEntry));
}
});
Map<String, List<TsKvEntry>> mapTsKvEntry = new HashMap<>();
mapTsKvEntry.put(LATEST_TS, listTsKvEntry);
return mapTsKvEntry;
}, MoreExecutors.directExecutor());
if (isTellFailureIfAbsent && !nonExistentKeys.isEmpty()) {
failuresPairSet.add(new TbPair<>(LATEST_TS, nonExistentKeys));
}
return new TbPair<>(LATEST_TS, listTsKvEntry);
}, ctx.getDbCallbackExecutor());
}
private TsKvEntry getValueWithTs(TsKvEntry tsKvEntry) {
ObjectMapper mapper = fetchToData ? JacksonUtil.OBJECT_MAPPER : JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER;
ObjectNode value = JacksonUtil.newObjectNode(mapper);
var mapper = FetchTo.DATA.equals(fetchTo) ? JacksonUtil.OBJECT_MAPPER : JacksonUtil.ALLOW_UNQUOTED_FIELD_NAMES_MAPPER;
var value = JacksonUtil.newObjectNode(mapper);
value.put(TS, tsKvEntry.getTs());
JacksonUtil.addKvEntry(value, tsKvEntry, VALUE, mapper);
return new BasicTsKvEntry(tsKvEntry.getTs(), new JsonDataEntry(tsKvEntry.getKey(), value.toString()));
}
private String getPrefix(String scope) {
String prefix = "";
var prefix = "";
switch (scope) {
case CLIENT_SCOPE:
prefix = "cs_";
@ -198,31 +173,20 @@ public abstract class TbAbstractGetAttributesNode<C extends TbGetAttributesNodeC
return prefix;
}
private List<String> getNotExistingKeys(List<AttributeKvEntry> existingAttributesKvEntry, List<String> allKeys) {
private List<String> getNonExistentKeys(List<AttributeKvEntry> existingAttributesKvEntry, List<String> allKeys) {
List<String> existingKeys = existingAttributesKvEntry.stream().map(KvEntry::getKey).collect(Collectors.toList());
return allKeys.stream().filter(key -> !existingKeys.contains(key)).collect(Collectors.toList());
}
private void computeFailuresMap(String scope, ConcurrentHashMap<String, List<String>> failuresMap, String key) {
List<String> failures = failuresMap.computeIfAbsent(scope, k -> new ArrayList<>());
failures.add(key);
}
private RuntimeException reportFailures(ConcurrentHashMap<String, List<String>> failuresMap) {
StringBuilder errorMessage = new StringBuilder("The following attribute/telemetry keys is not present in the DB: ").append("\n");
if (failuresMap.containsKey(CLIENT_SCOPE)) {
errorMessage.append("\t").append("[" + CLIENT_SCOPE + "]:").append(failuresMap.get(CLIENT_SCOPE).toString()).append("\n");
}
if (failuresMap.containsKey(SERVER_SCOPE)) {
errorMessage.append("\t").append("[" + SERVER_SCOPE + "]:").append(failuresMap.get(SERVER_SCOPE).toString()).append("\n");
}
if (failuresMap.containsKey(SHARED_SCOPE)) {
errorMessage.append("\t").append("[" + SHARED_SCOPE + "]:").append(failuresMap.get(SHARED_SCOPE).toString()).append("\n");
}
if (failuresMap.containsKey(LATEST_TS)) {
errorMessage.append("\t").append("[" + LATEST_TS + "]:").append(failuresMap.get(LATEST_TS).toString()).append("\n");
}
failuresMap.clear();
private RuntimeException reportFailures(Set<TbPair<String, List<String>>> failuresPairSet) {
var errorMessage = new StringBuilder("The following attribute/telemetry keys is not present in the DB: ").append("\n");
failuresPairSet.forEach(failurePair -> {
String scope = failurePair.getFirst();
List<String> nonExistentKeys = failurePair.getSecond();
errorMessage.append("\t").append("[").append(scope).append("]:").append(nonExistentKeys.toString()).append("\n");
});
failuresPairSet.clear();
return new RuntimeException(errorMessage.toString());
}
}

96
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDataNode.java

@ -0,0 +1,96 @@
/**
* 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.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
public abstract class TbAbstractGetEntityDataNode<T extends EntityId> extends TbAbstractGetMappedDataNode<T, TbGetEntityDataNodeConfiguration> {
private final static String DATA_TO_FETCH_PROPERTY_NAME = "dataToFetch";
private static final String OLD_DATA_TO_FETCH_PROPERTY_NAME = "telemetry";
private final static String DATA_MAPPING_PROPERTY_NAME = "dataMapping";
private static final String OLD_DATA_MAPPING_PROPERTY_NAME = "attrMapping";
private static final String DATA_TO_FETCH_VALIDATION_MSG = "DataToFetch property has invalid value: %s." +
" Only ATTRIBUTES and LATEST_TELEMETRY values supported!";
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
var msgDataAsObjectNode = FetchTo.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null;
withCallback(findEntityAsync(ctx, msg.getOriginator()),
entityId -> processDataAndTell(ctx, msg, entityId, msgDataAsObjectNode),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
protected void checkDataToFetchSupportedOrElseThrow(DataToFetch dataToFetch) throws TbNodeException {
if (dataToFetch == null || dataToFetch.equals(DataToFetch.FIELDS)) {
throw new TbNodeException(String.format(DATA_TO_FETCH_VALIDATION_MSG, dataToFetch));
}
}
protected void processDataAndTell(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) {
DataToFetch dataToFetch = config.getDataToFetch();
switch (dataToFetch) {
case ATTRIBUTES:
processAttributesKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
break;
case LATEST_TELEMETRY:
processTsKvEntryData(ctx, msg, entityId, msgDataAsJsonNode);
break;
case FIELDS:
processFieldsData(ctx, msg, entityId, msgDataAsJsonNode, true);
break;
}
}
protected TbPair<Boolean, JsonNode> upgradeToUseFetchToAndDataToFetch(JsonNode oldConfiguration) throws TbNodeException {
var newConfigObjectNode = (ObjectNode) oldConfiguration;
if (!newConfigObjectNode.has(OLD_DATA_TO_FETCH_PROPERTY_NAME)) {
throw new TbNodeException("property to update: '" + OLD_DATA_TO_FETCH_PROPERTY_NAME + "' doesn't exists in configuration!");
}
if (!newConfigObjectNode.has(OLD_DATA_MAPPING_PROPERTY_NAME)) {
throw new TbNodeException("property to update: '" + OLD_DATA_MAPPING_PROPERTY_NAME + "' doesn't exists in configuration!");
}
newConfigObjectNode.set(DATA_MAPPING_PROPERTY_NAME, newConfigObjectNode.get(OLD_DATA_MAPPING_PROPERTY_NAME));
newConfigObjectNode.remove(OLD_DATA_MAPPING_PROPERTY_NAME);
var value = newConfigObjectNode.get(OLD_DATA_TO_FETCH_PROPERTY_NAME).asText();
if ("true".equals(value)) {
newConfigObjectNode.remove(OLD_DATA_TO_FETCH_PROPERTY_NAME);
newConfigObjectNode.put(DATA_TO_FETCH_PROPERTY_NAME, DataToFetch.LATEST_TELEMETRY.name());
} else if ("false".equals(value)) {
newConfigObjectNode.remove(OLD_DATA_TO_FETCH_PROPERTY_NAME);
newConfigObjectNode.put(DATA_TO_FETCH_PROPERTY_NAME, DataToFetch.ATTRIBUTES.name());
} else {
throw new TbNodeException("property to update: '" + OLD_DATA_TO_FETCH_PROPERTY_NAME + "' has unexpected value: " + value + ". Allowed values: true or false!");
}
newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, FetchTo.METADATA.name());
return new TbPair<>(true, newConfigObjectNode);
}
}

197
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNode.java

@ -15,167 +15,110 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.google.gson.reflect.TypeToken;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
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.util.EntityDetails;
import org.thingsboard.rule.engine.util.ContactBasedEntityDetails;
import org.thingsboard.server.common.data.ContactBased;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.List;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
@Slf4j
public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEntityDetailsNodeConfiguration> implements TbNode {
private static final Gson gson = new Gson();
private static final JsonParser jsonParser = new JsonParser();
private static final Type TYPE = new TypeToken<Map<String, String>>() {
}.getType();
protected C config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = loadGetEntityDetailsNodeConfiguration(configuration);
}
public abstract class TbAbstractGetEntityDetailsNode<C extends TbAbstractGetEntityDetailsNodeConfiguration, I extends UUIDBased> extends TbAbstractNodeWithFetchTo<C> {
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
withCallback(getDetails(ctx, msg),
var msgDataAsObjectNode = FetchTo.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null;
withCallback(getDetails(ctx, msg, msgDataAsObjectNode),
ctx::tellSuccess,
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
protected abstract C loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException;
protected abstract ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg);
protected abstract String getPrefix();
protected abstract ListenableFuture<? extends ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg);
protected abstract ListenableFuture<? extends ContactBased<I>> getContactBasedFuture(TbContext ctx, TbMsg msg);
protected MessageData getDataAsJson(TbMsg msg) {
if (this.config.isAddToMetadata()) {
return new MessageData(gson.toJsonTree(msg.getMetaData().getData(), TYPE), "metadata");
} else {
return new MessageData(jsonParser.parse(msg.getData()), "data");
protected void checkIfDetailsListIsNotEmptyOrElseThrow(List<ContactBasedEntityDetails> detailsList) throws TbNodeException {
if (detailsList == null || detailsList.isEmpty()) {
throw new TbNodeException("No entity details selected!");
}
}
protected ListenableFuture<TbMsg> getTbMsgListenableFuture(TbContext ctx, TbMsg msg, MessageData messageData, String prefix) {
if (!this.config.getDetailsList().isEmpty()) {
ListenableFuture<? extends ContactBased> contactBasedListenableFuture = getContactBasedListenableFuture(ctx, msg);
ListenableFuture<JsonElement> resultObject = addContactProperties(messageData.getData(), contactBasedListenableFuture, prefix);
return transformMsg(ctx, msg, resultObject, messageData);
} else {
return Futures.immediateFuture(msg);
}
}
private ListenableFuture<TbMsg> transformMsg(TbContext ctx, TbMsg msg, ListenableFuture<JsonElement> propertiesFuture, MessageData messageData) {
return Futures.transformAsync(propertiesFuture, jsonElement -> {
if (jsonElement != null) {
if (messageData.getDataType().equals("metadata")) {
Map<String, String> metadataMap = gson.fromJson(jsonElement.toString(), TYPE);
return Futures.immediateFuture(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), new TbMsgMetaData(metadataMap), msg.getData()));
} else {
return Futures.immediateFuture(ctx.transformMsg(msg, msg.getType(), msg.getOriginator(), msg.getMetaData(), gson.toJson(jsonElement)));
}
} else {
return Futures.immediateFuture(null);
private ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg, ObjectNode messageData) {
ListenableFuture<? extends ContactBased<I>> contactBasedFuture = getContactBasedFuture(ctx, msg);
return Futures.transformAsync(contactBasedFuture, contactBased -> {
if (contactBased == null) {
return Futures.immediateFuture(msg);
}
var msgMetaData = msg.getMetaData().copy();
fetchEntityDetailsToMsg(contactBased, messageData, msgMetaData);
return Futures.immediateFuture(transformMessage(msg, messageData, msgMetaData));
}, MoreExecutors.directExecutor());
}
private ListenableFuture<JsonElement> addContactProperties(JsonElement data, ListenableFuture<? extends ContactBased> entityFuture, String prefix) {
return Futures.transformAsync(entityFuture, contactBased -> {
if (contactBased != null) {
JsonElement jsonElement = null;
for (EntityDetails entityDetails : this.config.getDetailsList()) {
jsonElement = setProperties(contactBased, data, entityDetails, prefix);
}
return Futures.immediateFuture(jsonElement);
} else {
return Futures.immediateFuture(null);
private void fetchEntityDetailsToMsg(ContactBased<I> contactBased, ObjectNode messageData, TbMsgMetaData msgMetaData) {
String value = null;
for (var entityDetail : config.getDetailsList()) {
switch (entityDetail) {
case ID:
value = contactBased.getId().getId().toString();
break;
case TITLE:
value = contactBased.getName();
break;
case ADDRESS:
value = contactBased.getAddress();
break;
case ADDRESS2:
value = contactBased.getAddress2();
break;
case CITY:
value = contactBased.getCity();
break;
case COUNTRY:
value = contactBased.getCountry();
break;
case STATE:
value = contactBased.getState();
break;
case EMAIL:
value = contactBased.getEmail();
break;
case PHONE:
value = contactBased.getPhone();
break;
case ZIP:
value = contactBased.getZip();
break;
case ADDITIONAL_INFO:
if (contactBased.getAdditionalInfo().hasNonNull("description")) {
value = contactBased.getAdditionalInfo().get("description").asText();
}
break;
}
}, MoreExecutors.directExecutor());
}
private JsonElement setProperties(ContactBased entity, JsonElement data, EntityDetails entityDetails, String prefix) {
JsonObject dataAsObject = data.getAsJsonObject();
switch (entityDetails) {
case ID:
dataAsObject.addProperty(prefix + "id", entity.getId().toString());
break;
case TITLE:
dataAsObject.addProperty(prefix + "title", entity.getName());
break;
case ADDRESS:
if (entity.getAddress() != null) {
dataAsObject.addProperty(prefix + "address", entity.getAddress());
}
break;
case ADDRESS2:
if (entity.getAddress2() != null) {
dataAsObject.addProperty(prefix + "address2", entity.getAddress2());
}
break;
case CITY:
if (entity.getCity() != null) dataAsObject.addProperty(prefix + "city", entity.getCity());
break;
case COUNTRY:
if (entity.getCountry() != null)
dataAsObject.addProperty(prefix + "country", entity.getCountry());
break;
case STATE:
if (entity.getState() != null) {
dataAsObject.addProperty(prefix + "state", entity.getState());
}
break;
case EMAIL:
if (entity.getEmail() != null) {
dataAsObject.addProperty(prefix + "email", entity.getEmail());
}
break;
case PHONE:
if (entity.getPhone() != null) {
dataAsObject.addProperty(prefix + "phone", entity.getPhone());
}
break;
case ZIP:
if (entity.getZip() != null) {
dataAsObject.addProperty(prefix + "zip", entity.getZip());
}
break;
case ADDITIONAL_INFO:
if (entity.getAdditionalInfo().hasNonNull("description")) {
dataAsObject.addProperty(prefix + "additionalInfo", entity.getAdditionalInfo().get("description").asText());
}
break;
if (value == null) {
continue;
}
setDetail(entityDetail.getRuleEngineName(), value, messageData, msgMetaData);
}
return dataAsObject;
}
@Data
@AllArgsConstructor
private static class MessageData {
private JsonElement data;
private String dataType;
private void setDetail(String property, String value, ObjectNode messageData, TbMsgMetaData msgMetaData) {
String fieldName = getPrefix() + property;
if (FetchTo.METADATA.equals(fetchTo)) {
msgMetaData.putValue(fieldName, value);
} else if (FetchTo.DATA.equals(fetchTo)) {
messageData.put(fieldName, value);
}
}
}

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetEntityDetailsNodeConfiguration.java

@ -16,16 +16,15 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import org.thingsboard.rule.engine.util.EntityDetails;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.util.ContactBasedEntityDetails;
import java.util.List;
@Data
public abstract class TbAbstractGetEntityDetailsNodeConfiguration {
@EqualsAndHashCode(callSuper = true)
public abstract class TbAbstractGetEntityDetailsNodeConfiguration extends TbAbstractFetchToNodeConfiguration {
private List<EntityDetails> detailsList;
private boolean addToMetadata;
private List<ContactBasedEntityDetails> detailsList;
}

153
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractGetMappedDataNode.java

@ -0,0 +1,153 @@
/**
* 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.metadata;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntitiesFieldsAsyncLoader;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
@Slf4j
public abstract class TbAbstractGetMappedDataNode<T extends EntityId, C extends TbGetMappedDataNodeConfiguration> extends TbAbstractNodeWithFetchTo<C> {
protected void checkIfMappingIsNotEmptyOrElseThrow(Map<String, String> dataMapping) throws TbNodeException {
if (dataMapping == null || dataMapping.isEmpty()) {
throw new TbNodeException("At least one mapping entry should be specified!");
}
}
protected void processFieldsData(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode, boolean ignoreNullStrings) {
var mappingsMap = processFieldsMappingPatterns(msg);
withCallback(getEntityFieldsAsync(ctx, entityId, mappingsMap, ignoreNullStrings),
data -> putFieldsDataAndTell(ctx, msg, msgDataAsJsonNode, data),
t -> ctx.tellFailure(msg, t),
MoreExecutors.directExecutor());
}
protected void processAttributesKvEntryData(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) {
var mappingsMap = processKvEntryMappingPatterns(msg);
var sourceKeys = List.copyOf(mappingsMap.keySet());
withCallback(getAttributesAsync(ctx, entityId, sourceKeys),
data -> putKvEntryDataAndTell(ctx, msg, data, mappingsMap, msgDataAsJsonNode),
t -> ctx.tellFailure(msg, t),
MoreExecutors.directExecutor());
}
protected void processTsKvEntryData(TbContext ctx, TbMsg msg, T entityId, ObjectNode msgDataAsJsonNode) {
var mappingsMap = processKvEntryMappingPatterns(msg);
var sourceKeys = List.copyOf(mappingsMap.keySet());
withCallback(getLatestTelemetryAsync(ctx, entityId, sourceKeys),
data -> putKvEntryDataAndTell(ctx, msg, data, mappingsMap, msgDataAsJsonNode),
t -> ctx.tellFailure(msg, t),
MoreExecutors.directExecutor());
}
private void putFieldsDataAndTell(TbContext ctx, TbMsg msg, ObjectNode msgDataAsJsonNode, Map<String, String> targetKeysToSourceValuesMap) {
TbMsgMetaData msgMetaData = msg.getMetaData().copy();
for (var entry : targetKeysToSourceValuesMap.entrySet()) {
var targetKeyName = entry.getKey();
var sourceFieldValue = entry.getValue();
if (FetchTo.DATA.equals(fetchTo)) {
msgDataAsJsonNode.put(targetKeyName, sourceFieldValue);
} else if (FetchTo.METADATA.equals(fetchTo)) {
msgMetaData.putValue(targetKeyName, sourceFieldValue);
}
}
TbMsg outMsg = transformMessage(msg, msgDataAsJsonNode, msgMetaData);
ctx.tellSuccess(outMsg);
}
private void putKvEntryDataAndTell(TbContext ctx, TbMsg msg, List<? extends KvEntry> data, Map<String, String> map, ObjectNode msgData) {
var msgMetaData = msg.getMetaData().copy();
for (KvEntry entry : data) {
String targetKey = map.get(entry.getKey());
enrichMessage(msgData, msgMetaData, entry, targetKey);
}
ctx.tellSuccess(transformMessage(msg, msgData, msgMetaData));
}
private Map<String, String> processFieldsMappingPatterns(TbMsg msg) {
var mappingsMap = new HashMap<String, String>();
config.getDataMapping().forEach((sourceField, targetKey) -> {
String patternProcessedTargetKey = TbNodeUtils.processPattern(targetKey, msg);
mappingsMap.put(sourceField, patternProcessedTargetKey);
});
return mappingsMap;
}
private Map<String, String> processKvEntryMappingPatterns(TbMsg msg) {
var mappingsMap = new HashMap<String, String>();
config.getDataMapping().forEach((sourceKey, targetKey) -> {
String patternProcessedSourceKey = TbNodeUtils.processPattern(sourceKey, msg);
String patternProcessedTargetKey = TbNodeUtils.processPattern(targetKey, msg);
mappingsMap.put(patternProcessedSourceKey, patternProcessedTargetKey);
});
return mappingsMap;
}
private ListenableFuture<Map<String, String>> getEntityFieldsAsync(TbContext ctx, EntityId entityId, Map<String, String> mappingsMap, boolean ignoreNullStrings) {
return Futures.transform(EntitiesFieldsAsyncLoader.findAsync(ctx, entityId),
fieldsData -> {
var targetKeysToSourceValuesMap = new HashMap<String, String>();
for (var mappingEntry : mappingsMap.entrySet()) {
var sourceFieldName = mappingEntry.getKey();
var targetKeyName = mappingEntry.getValue();
var sourceFieldValue = fieldsData.getFieldValue(sourceFieldName, ignoreNullStrings);
if (sourceFieldValue != null) {
targetKeysToSourceValuesMap.put(targetKeyName, sourceFieldValue);
}
}
return targetKeysToSourceValuesMap;
}, ctx.getDbCallbackExecutor()
);
}
private ListenableFuture<List<KvEntry>> getAttributesAsync(TbContext ctx, EntityId entityId, List<String> attrKeys) {
var latest = ctx.getAttributesService().find(ctx.getTenantId(), entityId, SERVER_SCOPE, attrKeys);
return Futures.transform(latest, l ->
l.stream()
.map(i -> (KvEntry) i)
.collect(Collectors.toList()),
ctx.getDbCallbackExecutor());
}
private ListenableFuture<List<KvEntry>> getLatestTelemetryAsync(TbContext ctx, EntityId entityId, List<String> timeseriesKeys) {
var latest = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), entityId, timeseriesKeys);
return Futures.transform(latest, l ->
l.stream()
.map(i -> (KvEntry) i)
.collect(Collectors.toList()),
ctx.getDbCallbackExecutor());
}
}

117
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbAbstractNodeWithFetchTo.java

@ -0,0 +1,117 @@
/**
* 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.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import lombok.extern.slf4j.Slf4j;
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.api.TbVersionedNode;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.KvEntry;
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.NoSuchElementException;
@Slf4j
public abstract class TbAbstractNodeWithFetchTo<C extends TbAbstractFetchToNodeConfiguration> implements TbVersionedNode {
protected final static String FETCH_TO_PROPERTY_NAME = "fetchTo";
protected C config;
protected FetchTo fetchTo;
@Override
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();
}
}
protected abstract C loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException;
protected <I extends EntityId> AsyncFunction<I, I> checkIfEntityIsPresentOrThrow(String message) {
return id -> {
if (id == null || id.isNullUid()) {
return Futures.immediateFailedFuture(new NoSuchElementException(message));
}
return Futures.immediateFuture(id);
};
}
protected ObjectNode getMsgDataAsObjectNode(TbMsg msg) {
var msgDataNode = JacksonUtil.toJsonNode(msg.getData());
if (msgDataNode == null || !msgDataNode.isObject()) {
throw new IllegalArgumentException("Message body is not an object!");
}
return (ObjectNode) msgDataNode;
}
protected void enrichMessage(ObjectNode msgData, TbMsgMetaData metaData, KvEntry kvEntry, String targetKey) {
if (FetchTo.DATA.equals(fetchTo)) {
JacksonUtil.addKvEntry(msgData, kvEntry, targetKey);
} else if (FetchTo.METADATA.equals(fetchTo)) {
metaData.putValue(targetKey, kvEntry.getValueAsString());
}
}
protected TbMsg transformMessage(TbMsg msg, ObjectNode msgDataNode, TbMsgMetaData msgMetaData) {
switch (fetchTo) {
case DATA:
return TbMsg.transformMsgData(msg, JacksonUtil.toString(msgDataNode));
case METADATA:
return TbMsg.transformMsg(msg, msgMetaData);
default:
log.debug("Unexpected FetchTo value: {}. Allowed values: {}", fetchTo, FetchTo.values());
return msg;
}
}
protected TbPair<Boolean, JsonNode> upgradeRuleNodesWithOldPropertyToUseFetchTo(
JsonNode oldConfiguration,
String oldProperty,
String ifTrue,
String ifFalse
) throws TbNodeException {
var newConfigObjectNode = (ObjectNode) oldConfiguration;
if (!newConfigObjectNode.has(oldProperty)) {
throw new TbNodeException("property to update: '" + oldProperty + "' doesn't exists in configuration!");
}
var value = newConfigObjectNode.get(oldProperty).asText();
if ("true".equals(value)) {
newConfigObjectNode.remove(oldProperty);
newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, ifTrue);
return new TbPair<>(true, newConfigObjectNode);
} else if ("false".equals(value)) {
newConfigObjectNode.remove(oldProperty);
newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, ifFalse);
return new TbPair<>(true, newConfigObjectNode);
} else {
throw new TbNodeException("property to update: '" + oldProperty + "' has unexpected value: " + value + ". Allowed values: true or false!");
}
}
}

109
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbEntityGetAttrNode.java

@ -1,109 +0,0 @@
/**
* 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.metadata;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
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.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
@Slf4j
public abstract class TbEntityGetAttrNode<T extends EntityId> implements TbNode {
private TbGetEntityAttrNodeConfiguration config;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbGetEntityAttrNodeConfiguration.class);
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
try {
withCallback(findEntityAsync(ctx, msg.getOriginator()),
entityId -> safeGetAttributes(ctx, msg, entityId),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
} catch (Throwable th) {
ctx.tellFailure(msg, th);
}
}
private void safeGetAttributes(TbContext ctx, TbMsg msg, T entityId) {
if (entityId == null || entityId.isNullUid()) {
ctx.tellNext(msg, FAILURE);
return;
}
Map<String, String> mappingsMap = new HashMap<>();
config.getAttrMapping().forEach((key, value) -> {
String processPatternKey = TbNodeUtils.processPattern(key, msg);
String processPatternValue = TbNodeUtils.processPattern(value, msg);
mappingsMap.put(processPatternKey, processPatternValue);
});
List<String> keys = List.copyOf(mappingsMap.keySet());
withCallback(config.isTelemetry() ? getLatestTelemetry(ctx, entityId, keys) : getAttributesAsync(ctx, entityId, keys),
attributes -> putAttributesAndTell(ctx, msg, attributes, mappingsMap),
t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
}
private ListenableFuture<List<KvEntry>> getAttributesAsync(TbContext ctx, EntityId entityId, List<String> attrKeys) {
ListenableFuture<List<AttributeKvEntry>> latest = ctx.getAttributesService().find(ctx.getTenantId(), entityId, SERVER_SCOPE, attrKeys);
return Futures.transform(latest, l ->
l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()), MoreExecutors.directExecutor());
}
private ListenableFuture<List<KvEntry>> getLatestTelemetry(TbContext ctx, EntityId entityId, List<String> timeseriesKeys) {
ListenableFuture<List<TsKvEntry>> latest = ctx.getTimeseriesService().findLatest(ctx.getTenantId(), entityId, timeseriesKeys);
return Futures.transform(latest, l ->
l.stream().map(i -> (KvEntry) i).collect(Collectors.toList()), MoreExecutors.directExecutor());
}
private void putAttributesAndTell(TbContext ctx, TbMsg msg, List<? extends KvEntry> attributes, Map<String, String> map) {
attributes.forEach(r -> {
String attrName = map.get(r.getKey());
msg.getMetaData().putValue(attrName, r.getValueAsString());
});
ctx.tellSuccess(msg);
}
protected abstract ListenableFuture<T> findEntityAsync(TbContext ctx, EntityId originator);
public void setConfig(TbGetEntityAttrNodeConfiguration config) {
this.config = config;
}
}

66
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNode.java

@ -16,23 +16,19 @@
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
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.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
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.concurrent.ExecutionException;
@ -40,60 +36,66 @@ import java.util.concurrent.ExecutionException;
@RuleNode(
type = ComponentType.ENRICHMENT,
name = "fetch device credentials",
version = 1,
configClazz = TbFetchDeviceCredentialsNodeConfiguration.class,
nodeDescription = "Enrich the message body or metadata with the device credentials",
nodeDetails = "Adds <b>credentialsType</b> and <b>credentials</b> properties to the message metadata if the " +
"configuration parameter <b>fetchToMetadata</b> is set to <code>true</code>, otherwise, adds properties " +
"to the message data. If originator type is not <b>DEVICE</b> or rule node failed to get device credentials " +
"- send Message via <code>Failure</code> chain, otherwise <code>Success</code> chain is used.",
nodeDescription = "Adds device credentials to the message or message metadata",
nodeDetails = "if message originator type is Device and device credentials was successfully fetched, " +
"rule node enriches message or message metadata with <i>credentialsType</i> and <i>credentials</i> properties. " +
"Useful when you need to fetch device credentials and use them for further message processing. For example, use device credentials to interact with external systems.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeFetchDeviceCredentialsConfig")
public class TbFetchDeviceCredentialsNode implements TbNode {
public class TbFetchDeviceCredentialsNode extends TbAbstractNodeWithFetchTo<TbFetchDeviceCredentialsNodeConfiguration> {
private static final String CREDENTIALS = "credentials";
private static final String CREDENTIALS_TYPE = "credentialsType";
TbFetchDeviceCredentialsNodeConfiguration config;
boolean fetchToMetadata;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbFetchDeviceCredentialsNodeConfiguration.class);
this.fetchToMetadata = config.isFetchToMetadata();
protected TbFetchDeviceCredentialsNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbFetchDeviceCredentialsNodeConfiguration.class);
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
EntityId originator = msg.getOriginator();
var originator = msg.getOriginator();
var msgDataAsObjectNode = FetchTo.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null;
if (!EntityType.DEVICE.equals(originator.getEntityType())) {
ctx.tellFailure(msg, new RuntimeException("Unsupported originator type: " + originator.getEntityType() + "!"));
return;
}
DeviceId deviceId = new DeviceId(msg.getOriginator().getId());
DeviceCredentials deviceCredentials = ctx.getDeviceCredentialsService().findDeviceCredentialsByDeviceId(ctx.getTenantId(), deviceId);
var deviceId = new DeviceId(msg.getOriginator().getId());
var deviceCredentials = ctx.getDeviceCredentialsService().findDeviceCredentialsByDeviceId(ctx.getTenantId(), deviceId);
if (deviceCredentials == null) {
ctx.tellFailure(msg, new RuntimeException("Failed to get Device Credentials for device: " + deviceId + "!"));
return;
}
TbMsg transformedMsg;
DeviceCredentialsType credentialsType = deviceCredentials.getCredentialsType();
JsonNode credentialsInfo = ctx.getDeviceCredentialsService().toCredentialsInfo(deviceCredentials);
if (fetchToMetadata) {
TbMsgMetaData metaData = msg.getMetaData();
var credentialsType = deviceCredentials.getCredentialsType();
var credentialsInfo = ctx.getDeviceCredentialsService().toCredentialsInfo(deviceCredentials);
var metaData = msg.getMetaData().copy();
if (FetchTo.METADATA.equals(fetchTo)) {
metaData.putValue(CREDENTIALS_TYPE, credentialsType.name());
if (credentialsType.equals(DeviceCredentialsType.ACCESS_TOKEN) || credentialsType.equals(DeviceCredentialsType.X509_CERTIFICATE)) {
metaData.putValue(CREDENTIALS, credentialsInfo.asText());
} else {
metaData.putValue(CREDENTIALS, JacksonUtil.toString(credentialsInfo));
}
transformedMsg = TbMsg.transformMsg(msg, msg.getType(), originator, metaData, msg.getData());
} else {
ObjectNode data = (ObjectNode) JacksonUtil.toJsonNode(msg.getData());
data.put(CREDENTIALS_TYPE, credentialsType.name());
data.set(CREDENTIALS, credentialsInfo);
transformedMsg = TbMsg.transformMsg(msg, msg.getType(), originator, msg.getMetaData(), JacksonUtil.toString(data));
} else if (FetchTo.DATA.equals(fetchTo)) {
msgDataAsObjectNode.put(CREDENTIALS_TYPE, credentialsType.name());
msgDataAsObjectNode.set(CREDENTIALS, credentialsInfo);
}
TbMsg transformedMsg = transformMessage(msg, msgDataAsObjectNode, metaData);
ctx.tellSuccess(transformedMsg);
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ?
upgradeRuleNodesWithOldPropertyToUseFetchTo(
oldConfiguration,
"fetchToMetadata",
FetchTo.METADATA.name(),
FetchTo.DATA.name()) :
new TbPair<>(false, oldConfiguration);
}
}

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeConfiguration.java

@ -17,18 +17,19 @@ package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
@Data
@EqualsAndHashCode(callSuper = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public class TbFetchDeviceCredentialsNodeConfiguration implements NodeConfiguration<TbFetchDeviceCredentialsNodeConfiguration> {
private boolean fetchToMetadata;
public class TbFetchDeviceCredentialsNodeConfiguration extends TbAbstractFetchToNodeConfiguration implements NodeConfiguration<TbFetchDeviceCredentialsNodeConfiguration> {
@Override
public TbFetchDeviceCredentialsNodeConfiguration defaultConfiguration() {
TbFetchDeviceCredentialsNodeConfiguration configuration = new TbFetchDeviceCredentialsNodeConfiguration();
configuration.setFetchToMetadata(true);
var configuration = new TbFetchDeviceCredentialsNodeConfiguration();
configuration.setFetchTo(FetchTo.METADATA);
return configuration;
}
}

29
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNode.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
@ -25,6 +26,7 @@ import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
/**
@ -32,19 +34,19 @@ import org.thingsboard.server.common.msg.TbMsg;
*/
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "originator attributes",
configClazz = TbGetAttributesNodeConfiguration.class,
nodeDescription = "Enrich the message body or metadata with the originator attributes and/or timeseries data",
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message data/metadata " +
"with specific prefix: <i>cs/shared/ss</i>. Latest telemetry value added into Message data/metadata without prefix. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> ",
name = "originator attributes",
configClazz = TbGetAttributesNodeConfiguration.class,
version = 1,
nodeDescription = "Adds attributes and/or latest timeseries data for the message originator to the message or message metadata",
nodeDetails = "Useful when you need to retrieve some attributes or the latest telemetry readings from the message originator " +
"that are not included in the incoming message to use them for further message processing. " +
"For example to filter messages based on the threshold value stored in the attributes.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeOriginatorAttributesConfig")
public class TbGetAttributesNode extends TbAbstractGetAttributesNode<TbGetAttributesNodeConfiguration, EntityId> {
@Override
protected TbGetAttributesNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
protected TbGetAttributesNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbGetAttributesNodeConfiguration.class);
}
@ -53,4 +55,15 @@ public class TbGetAttributesNode extends TbAbstractGetAttributesNode<TbGetAttrib
return Futures.immediateFuture(msg.getOriginator());
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ?
upgradeRuleNodesWithOldPropertyToUseFetchTo(
oldConfiguration,
"fetchToData",
FetchTo.DATA.name(),
FetchTo.METADATA.name()) :
new TbPair<>(false, oldConfiguration);
}
}

10
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeConfiguration.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Collections;
@ -25,7 +26,8 @@ import java.util.List;
* Created by ashvayka on 19.01.18.
*/
@Data
public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
@EqualsAndHashCode(callSuper = true)
public class TbGetAttributesNodeConfiguration extends TbAbstractFetchToNodeConfiguration implements NodeConfiguration<TbGetAttributesNodeConfiguration> {
private List<String> clientAttributeNames;
private List<String> sharedAttributeNames;
@ -35,18 +37,18 @@ public class TbGetAttributesNodeConfiguration implements NodeConfiguration<TbGet
private boolean tellFailureIfAbsent;
private boolean getLatestValueWithTs;
private boolean fetchToData;
@Override
public TbGetAttributesNodeConfiguration defaultConfiguration() {
TbGetAttributesNodeConfiguration configuration = new TbGetAttributesNodeConfiguration();
var configuration = new TbGetAttributesNodeConfiguration();
configuration.setClientAttributeNames(Collections.emptyList());
configuration.setSharedAttributeNames(Collections.emptyList());
configuration.setServerAttributeNames(Collections.emptyList());
configuration.setLatestTsKeyNames(Collections.emptyList());
configuration.setTellFailureIfAbsent(true);
configuration.setGetLatestValueWithTs(false);
configuration.setFetchToData(false);
configuration.setFetchTo(FetchTo.METADATA);
return configuration;
}
}

46
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNode.java

@ -15,30 +15,58 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
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.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntitiesCustomerIdAsyncLoader;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
@Slf4j
@RuleNode(
type = ComponentType.ENRICHMENT,
name="customer attributes",
configClazz = TbGetEntityAttrNodeConfiguration.class,
nodeDescription = "Add Originators Customer Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "Enrich the message metadata with the corresponding customer's latest attributes or telemetry value. " +
"The customer is selected based on the originator of the message: device, asset, etc. " +
"</br>" +
"Useful when you store some parameters on the customer level and would like to use them for message processing.",
name = "customer attributes",
configClazz = TbGetEntityDataNodeConfiguration.class,
version = 1,
nodeDescription = "Adds message originator customer attributes or latest telemetry into message or message metadata",
nodeDetails = "Useful in multi-customer solutions where each customer has a different configuration or threshold set " +
"that is stored as customer attributes or telemetry data and used for dynamic message filtering, transformation, " +
"or actions such as alarm creation if the threshold is exceeded.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeCustomerAttributesConfig")
public class TbGetCustomerAttributeNode extends TbEntityGetAttrNode<CustomerId> {
public class TbGetCustomerAttributeNode extends TbAbstractGetEntityDataNode<CustomerId> {
private static final String CUSTOMER_NOT_FOUND_MESSAGE = "Failed to find customer for entity with id: %s and type: %s";
@Override
protected TbGetEntityDataNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbGetEntityDataNodeConfiguration.class);
checkIfMappingIsNotEmptyOrElseThrow(config.getDataMapping());
checkDataToFetchSupportedOrElseThrow(config.getDataToFetch());
return config;
}
@Override
protected ListenableFuture<CustomerId> findEntityAsync(TbContext ctx, EntityId originator) {
return EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator);
return Futures.transformAsync(EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctx, originator),
checkIfEntityIsPresentOrThrow(String.format(CUSTOMER_NOT_FOUND_MESSAGE, originator.getId(), originator.getEntityType().getNormalName())),
ctx.getDbCallbackExecutor()
);
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ?
upgradeToUseFetchToAndDataToFetch(oldConfiguration) :
new TbPair<>(false, oldConfiguration);
}
}

93
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNode.java

@ -15,90 +15,105 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
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.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.ContactBased;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import java.util.NoSuchElementException;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "customer details",
configClazz = TbGetCustomerDetailsNodeConfiguration.class,
nodeDescription = "Enrich the message body or metadata with the corresponding customer details: title, address, email, phone, etc.",
nodeDetails = "If checkbox: <b>Add selected details to the message metadata</b> is selected, existing fields will be added to the message metadata instead of message data.<br><br>" +
"<b>Note:</b> only Device, Asset, and Entity View type are allowed.<br><br>" +
"If the originator of the message is not assigned to Customer, or originator type is not supported - Message will be forwarded to <b>Failure</b> chain, otherwise, <b>Success</b> chain will be used.",
version = 1,
nodeDescription = "Adds message originator customer details into message or message metadata",
nodeDetails = "Useful in multi-customer solutions where we need dynamically use customer contact information " +
"such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeEntityDetailsConfig")
public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetCustomerDetailsNodeConfiguration> {
public class TbGetCustomerDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetCustomerDetailsNodeConfiguration, CustomerId> {
private static final String CUSTOMER_PREFIX = "customer_";
@Override
protected TbGetCustomerDetailsNodeConfiguration loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbGetCustomerDetailsNodeConfiguration.class);
protected TbGetCustomerDetailsNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbGetCustomerDetailsNodeConfiguration.class);
checkIfDetailsListIsNotEmptyOrElseThrow(config.getDetailsList());
return config;
}
@Override
protected ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg) {
return getTbMsgListenableFuture(ctx, msg, getDataAsJson(msg), CUSTOMER_PREFIX);
protected String getPrefix() {
return CUSTOMER_PREFIX;
}
@Override
protected ListenableFuture<? extends ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg) {
return getCustomer(ctx, msg);
}
private ListenableFuture<Customer> getCustomer(TbContext ctx, TbMsg msg) {
ListenableFuture<? extends HasCustomerId> entityFuture;
switch (msg.getOriginator().getEntityType()) { // TODO: use EntityServiceRegistry
protected ListenableFuture<Customer> getContactBasedFuture(TbContext ctx, TbMsg msg) {
switch (msg.getOriginator().getEntityType()) {
case DEVICE:
entityFuture = Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) msg.getOriginator()));
break;
return Futures.transformAsync(ctx.getDeviceService().findDeviceByIdAsync(ctx.getTenantId(), new DeviceId(msg.getOriginator().getId())),
device -> getCustomerFuture(ctx, device, msg.getOriginator()), ctx.getDbCallbackExecutor());
case ASSET:
entityFuture = ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) msg.getOriginator());
break;
return Futures.transformAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), new AssetId(msg.getOriginator().getId())),
asset -> getCustomerFuture(ctx, asset, msg.getOriginator()), ctx.getDbCallbackExecutor());
case ENTITY_VIEW:
entityFuture = ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) msg.getOriginator());
break;
return Futures.transformAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), new EntityViewId(msg.getOriginator().getId())),
entityView -> getCustomerFuture(ctx, entityView, msg.getOriginator()), ctx.getDbCallbackExecutor());
case USER:
entityFuture = ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) msg.getOriginator());
break;
return Futures.transformAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), new UserId(msg.getOriginator().getId())),
user -> getCustomerFuture(ctx, user, msg.getOriginator()), ctx.getDbCallbackExecutor());
case EDGE:
entityFuture = ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), (EdgeId) msg.getOriginator());
break;
return Futures.transformAsync(ctx.getEdgeService().findEdgeByIdAsync(ctx.getTenantId(), new EdgeId(msg.getOriginator().getId())),
edge -> getCustomerFuture(ctx, edge, msg.getOriginator()), ctx.getDbCallbackExecutor());
default:
throw new RuntimeException(msg.getOriginator().getEntityType().getNormalName() + " entities not supported");
return Futures.immediateFailedFuture(new NoSuchElementException("Entity with entityType '" + msg.getOriginator().getEntityType() + "' is not supported."));
}
return Futures.transformAsync(entityFuture, entity -> {
if (entity != null) {
if (!entity.getCustomerId().isNullUid()) {
return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), entity.getCustomerId());
} else {
throw new RuntimeException(msg.getOriginator().getEntityType().getNormalName() +
(entity instanceof HasName ? " with name '" + ((HasName) entity).getName() + "'" : "")
+ " is not assigned to Customer");
}
private ListenableFuture<Customer> getCustomerFuture(TbContext ctx, HasCustomerId hasCustomerId, EntityId originator) {
if (hasCustomerId == null) {
return Futures.immediateFuture(null);
} else {
if (hasCustomerId.getCustomerId() == null || hasCustomerId.getCustomerId().isNullUid()) {
if (hasCustomerId instanceof HasName) {
var hasName = (HasName) hasCustomerId;
throw new RuntimeException(originator.getEntityType().getNormalName() + " with name '" + hasName.getName() + "' is not assigned to Customer!");
}
throw new RuntimeException(originator.getEntityType().getNormalName() + " with id '" + originator + "' is not assigned to Customer!");
} else {
return Futures.immediateFuture(null);
return ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), hasCustomerId.getCustomerId());
}
}, MoreExecutors.directExecutor());
}
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ?
upgradeRuleNodesWithOldPropertyToUseFetchTo(
oldConfiguration,
"addToMetadata",
FetchTo.METADATA.name(),
FetchTo.DATA.name()) :
new TbPair<>(false, oldConfiguration);
}
}

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeConfiguration.java

@ -16,18 +16,21 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Collections;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbGetCustomerDetailsNodeConfiguration extends TbAbstractGetEntityDetailsNodeConfiguration implements NodeConfiguration<TbGetCustomerDetailsNodeConfiguration> {
@Override
public TbGetCustomerDetailsNodeConfiguration defaultConfiguration() {
TbGetCustomerDetailsNodeConfiguration configuration = new TbGetCustomerDetailsNodeConfiguration();
var configuration = new TbGetCustomerDetailsNodeConfiguration();
configuration.setDetailsList(Collections.emptyList());
configuration.setFetchTo(FetchTo.DATA);
return configuration;
}
}

33
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNode.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
@ -25,29 +27,46 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntitiesRelatedDeviceIdAsyncLoader;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "related device attributes",
configClazz = TbGetDeviceAttrNodeConfiguration.class,
nodeDescription = "Add Originators Related Device Attributes and Latest Telemetry value into Message Data or Metadata",
nodeDetails = "If Attributes enrichment configured, <b>CLIENT/SHARED/SERVER</b> attributes are added into Message data/metadata " +
"with specific prefix: <i>cs/shared/ss</i>. Latest telemetry value added into Message data/metadata without prefix. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.cs_temperature</code> or <code>metadata.shared_limit</code> ",
version = 1,
nodeDescription = "Add originators related device attributes and/or latest telemetry values into message or message metadata",
nodeDetails = "Related device lookup based on the configured relation query. " +
"If multiple related devices are found, only first device is used for message enrichment, other entities are discarded. " +
"Useful when you need to retrieve attributes and/or latest telemetry values from device that has a relation to the message originator and use them for further message processing.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeDeviceAttributesConfig")
public class TbGetDeviceAttrNode extends TbAbstractGetAttributesNode<TbGetDeviceAttrNodeConfiguration, DeviceId> {
private static final String RELATED_DEVICE_NOT_FOUND_MESSAGE = "Failed to find related device to message originator using relation query specified in the configuration!";
@Override
protected TbGetDeviceAttrNodeConfiguration loadGetAttributesNodeConfig(TbNodeConfiguration configuration) throws TbNodeException {
protected TbGetDeviceAttrNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbGetDeviceAttrNodeConfiguration.class);
}
@Override
protected ListenableFuture<DeviceId> findEntityIdAsync(TbContext ctx, TbMsg msg) {
return EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, msg.getOriginator(), config.getDeviceRelationsQuery());
return Futures.transformAsync(
EntitiesRelatedDeviceIdAsyncLoader.findDeviceAsync(ctx, msg.getOriginator(), config.getDeviceRelationsQuery()),
checkIfEntityIsPresentOrThrow(RELATED_DEVICE_NOT_FOUND_MESSAGE),
ctx.getDbCallbackExecutor());
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ?
upgradeRuleNodesWithOldPropertyToUseFetchTo(
oldConfiguration,
"fetchToData",
FetchTo.DATA.name(),
FetchTo.METADATA.name()) :
new TbPair<>(false, oldConfiguration);
}
}

9
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeConfiguration.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.data.DeviceRelationsQuery;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@ -23,22 +24,23 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import java.util.Collections;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbGetDeviceAttrNodeConfiguration extends TbGetAttributesNodeConfiguration {
private DeviceRelationsQuery deviceRelationsQuery;
@Override
public TbGetDeviceAttrNodeConfiguration defaultConfiguration() {
TbGetDeviceAttrNodeConfiguration configuration = new TbGetDeviceAttrNodeConfiguration();
var configuration = new TbGetDeviceAttrNodeConfiguration();
configuration.setClientAttributeNames(Collections.emptyList());
configuration.setSharedAttributeNames(Collections.emptyList());
configuration.setServerAttributeNames(Collections.emptyList());
configuration.setLatestTsKeyNames(Collections.emptyList());
configuration.setTellFailureIfAbsent(true);
configuration.setGetLatestValueWithTs(false);
configuration.setFetchToData(false);
configuration.setFetchTo(FetchTo.METADATA);
DeviceRelationsQuery deviceRelationsQuery = new DeviceRelationsQuery();
var deviceRelationsQuery = new DeviceRelationsQuery();
deviceRelationsQuery.setDirection(EntitySearchDirection.FROM);
deviceRelationsQuery.setMaxLevel(1);
deviceRelationsQuery.setRelationType(EntityRelation.CONTAINS_TYPE);
@ -48,4 +50,5 @@ public class TbGetDeviceAttrNodeConfiguration extends TbGetAttributesNodeConfigu
return configuration;
}
}

22
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityAttrNodeConfiguration.java → rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetEntityDataNodeConfiguration.java

@ -16,24 +16,26 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.HashMap;
import java.util.Map;
@Data
public class TbGetEntityAttrNodeConfiguration implements NodeConfiguration<TbGetEntityAttrNodeConfiguration> {
@EqualsAndHashCode(callSuper = true)
public class TbGetEntityDataNodeConfiguration extends TbGetMappedDataNodeConfiguration implements NodeConfiguration<TbGetEntityDataNodeConfiguration> {
private Map<String, String> attrMapping;
private boolean isTelemetry = false;
private DataToFetch dataToFetch;
@Override
public TbGetEntityAttrNodeConfiguration defaultConfiguration() {
TbGetEntityAttrNodeConfiguration configuration = new TbGetEntityAttrNodeConfiguration();
Map<String, String> attrMapping = new HashMap<>();
attrMapping.putIfAbsent("temperature", "tempo");
configuration.setAttrMapping(attrMapping);
configuration.setTelemetry(false);
public TbGetEntityDataNodeConfiguration defaultConfiguration() {
var configuration = new TbGetEntityDataNodeConfiguration();
var dataMapping = new HashMap<String, String>();
dataMapping.putIfAbsent("alarmThreshold", "threshold");
configuration.setDataMapping(dataMapping);
configuration.setDataToFetch(DataToFetch.ATTRIBUTES);
configuration.setFetchTo(FetchTo.METADATA);
return configuration;
}
}

29
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetMappedDataNodeConfiguration.java

@ -0,0 +1,29 @@
/**
* 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.metadata;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Map;
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class TbGetMappedDataNodeConfiguration extends TbAbstractFetchToNodeConfiguration {
private Map<String, String> dataMapping;
}

18
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsConfiguration.java

@ -16,25 +16,29 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.HashMap;
import java.util.Map;
@Data
public class TbGetOriginatorFieldsConfiguration implements NodeConfiguration<TbGetOriginatorFieldsConfiguration> {
@EqualsAndHashCode(callSuper = true)
public class TbGetOriginatorFieldsConfiguration extends TbGetMappedDataNodeConfiguration implements NodeConfiguration<TbGetOriginatorFieldsConfiguration> {
private Map<String, String> fieldsMapping;
private Map<String, String> dataMapping;
private boolean ignoreNullStrings;
@Override
public TbGetOriginatorFieldsConfiguration defaultConfiguration() {
TbGetOriginatorFieldsConfiguration configuration = new TbGetOriginatorFieldsConfiguration();
Map<String, String> fieldsMapping = new HashMap<>();
fieldsMapping.put("name", "originatorName");
fieldsMapping.put("type", "originatorType");
configuration.setFieldsMapping(fieldsMapping);
var configuration = new TbGetOriginatorFieldsConfiguration();
var dataMapping = new HashMap<String, String>();
dataMapping.put("name", "originatorName");
dataMapping.put("type", "originatorType");
configuration.setDataMapping(dataMapping);
configuration.setIgnoreNullStrings(false);
configuration.setFetchTo(FetchTo.METADATA);
return configuration;
}
}

66
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNode.java

@ -15,22 +15,20 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
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.EntitiesFieldsAsyncLoader;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
import java.util.concurrent.ExecutionException;
/**
* Created by ashvayka on 19.01.18.
@ -39,47 +37,43 @@ import static org.thingsboard.common.util.DonAsynchron.withCallback;
@RuleNode(type = ComponentType.ENRICHMENT,
name = "originator fields",
configClazz = TbGetOriginatorFieldsConfiguration.class,
nodeDescription = "Add Message Originator fields values into Message Metadata",
nodeDetails = "Will fetch fields values specified in mapping. If specified field is not part of originator fields it will be ignored.",
version = 1,
nodeDescription = "Adds message originator fields values into message or message metadata",
nodeDetails = "Fetches fields values specified in the mapping. If specified field is not part of originator fields it will be ignored. " +
"Useful when you need to retrieve originator fields and use them for further message processing.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeOriginatorFieldsConfig")
public class TbGetOriginatorFieldsNode implements TbNode {
public class TbGetOriginatorFieldsNode extends TbAbstractGetMappedDataNode<EntityId, TbGetOriginatorFieldsConfiguration> {
private TbGetOriginatorFieldsConfiguration config;
private boolean ignoreNullStrings;
protected final static String DATA_MAPPING_PROPERTY_NAME = "dataMapping";
protected static final String OLD_DATA_MAPPING_PROPERTY_NAME = "fieldsMapping";
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
config = TbNodeUtils.convert(configuration, TbGetOriginatorFieldsConfiguration.class);
ignoreNullStrings = config.isIgnoreNullStrings();
protected TbGetOriginatorFieldsConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbGetOriginatorFieldsConfiguration.class);
checkIfMappingIsNotEmptyOrElseThrow(config.getDataMapping());
return config;
}
@Override
public void onMsg(TbContext ctx, TbMsg msg) {
try {
withCallback(putEntityFields(ctx, msg.getOriginator(), msg),
i -> ctx.tellSuccess(msg), t -> ctx.tellFailure(msg, t), ctx.getDbCallbackExecutor());
} catch (Throwable th) {
ctx.tellFailure(msg, th);
}
public void onMsg(TbContext ctx, TbMsg msg) throws ExecutionException, InterruptedException, TbNodeException {
var msgDataAsJsonNode = FetchTo.DATA.equals(fetchTo) ? getMsgDataAsObjectNode(msg) : null;
processFieldsData(ctx, msg, msg.getOriginator(), msgDataAsJsonNode, config.isIgnoreNullStrings());
}
private ListenableFuture<Void> putEntityFields(TbContext ctx, EntityId entityId, TbMsg msg) {
if (config.getFieldsMapping().isEmpty()) {
return Futures.immediateFuture(null);
} else {
return Futures.transform(EntitiesFieldsAsyncLoader.findAsync(ctx, entityId),
data -> {
config.getFieldsMapping().forEach((field, metaKey) -> {
String val = data.getFieldValue(field, ignoreNullStrings);
if (val != null) {
msg.getMetaData().putValue(metaKey, val);
}
});
return null;
}, MoreExecutors.directExecutor()
);
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
if (fromVersion == 0) {
var newConfigObjectNode = (ObjectNode) oldConfiguration;
if (!newConfigObjectNode.has(OLD_DATA_MAPPING_PROPERTY_NAME)) {
throw new TbNodeException("property to update: '" + OLD_DATA_MAPPING_PROPERTY_NAME + "' doesn't exists in configuration!");
}
newConfigObjectNode.set(DATA_MAPPING_PROPERTY_NAME, newConfigObjectNode.get(OLD_DATA_MAPPING_PROPERTY_NAME));
newConfigObjectNode.remove(OLD_DATA_MAPPING_PROPERTY_NAME);
newConfigObjectNode.put(FETCH_TO_PROPERTY_NAME, FetchTo.METADATA.name());
return new TbPair<>(true, newConfigObjectNode);
}
return new TbPair<>(false, oldConfiguration);
}
}

55
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNode.java

@ -15,7 +15,10 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -24,33 +27,53 @@ import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.rule.engine.util.EntitiesRelatedEntityIdAsyncLoader;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import java.util.Arrays;
@Slf4j
@RuleNode(
type = ComponentType.ENRICHMENT,
name="related attributes",
configClazz = TbGetRelatedAttrNodeConfiguration.class,
nodeDescription = "Add Originators Related Entity Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "Related Entity found using configured relation direction and Relation Type. " +
"If multiple Related Entities are found, only first Entity is used for attributes enrichment, other entities are discarded. " +
"If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>.",
name = "related entity data",
configClazz = TbGetRelatedDataNodeConfiguration.class,
version = 1,
nodeDescription = "Adds originators related entity attributes or latest telemetry or fields into message or message metadata",
nodeDetails = "Related entity lookup based on the configured relation query. " +
"If multiple related entities are found, only first entity is used for message enrichment, other entities are discarded. " +
"Useful when you need to retrieve data from an entity that has a relation to the message originator and use them for further message processing.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeRelatedAttributesConfig")
public class TbGetRelatedAttributeNode extends TbAbstractGetEntityDataNode<EntityId> {
public class TbGetRelatedAttributeNode extends TbEntityGetAttrNode<EntityId> {
private static final String RELATED_ENTITY_NOT_FOUND_MESSAGE = "Failed to find related entity to message originator using relation query specified in the configuration!";
private TbGetRelatedAttrNodeConfiguration config;
@Override
public TbGetRelatedDataNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbGetRelatedDataNodeConfiguration.class);
checkIfMappingIsNotEmptyOrElseThrow(config.getDataMapping());
checkDataToFetchSupportedOrElseThrow(config.getDataToFetch());
return config;
}
@Override
public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbGetRelatedAttrNodeConfiguration.class);
setConfig(config);
public ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
var relatedAttrConfig = (TbGetRelatedDataNodeConfiguration) config;
return Futures.transformAsync(
EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, relatedAttrConfig.getRelationsQuery()),
checkIfEntityIsPresentOrThrow(RELATED_ENTITY_NOT_FOUND_MESSAGE),
ctx.getDbCallbackExecutor());
}
@Override
protected ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator) {
return EntitiesRelatedEntityIdAsyncLoader.findEntityAsync(ctx, originator, config.getRelationsQuery());
protected void checkDataToFetchSupportedOrElseThrow(DataToFetch dataToFetch) throws TbNodeException {
if (dataToFetch == null) {
throw new TbNodeException("DataToFetch property cannot be null! Supported values are: " + Arrays.toString(DataToFetch.values()));
}
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ? upgradeToUseFetchToAndDataToFetch(oldConfiguration) : new TbPair<>(false, oldConfiguration);
}
}

25
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttrNodeConfiguration.java → rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetRelatedDataNodeConfiguration.java

@ -16,6 +16,7 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
@ -23,28 +24,30 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@Data
public class TbGetRelatedAttrNodeConfiguration extends TbGetEntityAttrNodeConfiguration {
@EqualsAndHashCode(callSuper = true)
public class TbGetRelatedDataNodeConfiguration extends TbGetEntityDataNodeConfiguration {
private RelationsQuery relationsQuery;
@Override
public TbGetRelatedAttrNodeConfiguration defaultConfiguration() {
TbGetRelatedAttrNodeConfiguration configuration = new TbGetRelatedAttrNodeConfiguration();
Map<String, String> attrMapping = new HashMap<>();
attrMapping.putIfAbsent("temperature", "tempo");
configuration.setAttrMapping(attrMapping);
configuration.setTelemetry(false);
RelationsQuery relationsQuery = new RelationsQuery();
public TbGetRelatedDataNodeConfiguration defaultConfiguration() {
var configuration = new TbGetRelatedDataNodeConfiguration();
var dataMapping = new HashMap<String, String>();
dataMapping.putIfAbsent("serialNumber", "sn");
configuration.setDataMapping(dataMapping);
configuration.setDataToFetch(DataToFetch.ATTRIBUTES);
configuration.setFetchTo(FetchTo.METADATA);
var relationsQuery = new RelationsQuery();
var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setDirection(EntitySearchDirection.FROM);
relationsQuery.setMaxLevel(1);
RelationEntityTypeFilter relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter));
configuration.setRelationsQuery(relationsQuery);
return configuration;
}
}

11
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNode.java

@ -54,13 +54,10 @@ import static org.thingsboard.rule.engine.metadata.TbGetTelemetryNodeConfigurati
@RuleNode(type = ComponentType.ENRICHMENT,
name = "originator telemetry",
configClazz = TbGetTelemetryNodeConfiguration.class,
nodeDescription = "Add Message Originator Telemetry for selected time range into Message Metadata\n",
nodeDetails = "The node allows you to select fetch mode: <b>FIRST/LAST/ALL</b> to fetch telemetry of certain time range that are added into Message metadata without any prefix. " +
"If selected fetch mode <b>ALL</b> Telemetry will be added like array into Message Metadata where <b>key</b> is Timestamp and <b>value</b> is value of Telemetry.</br>" +
"If selected fetch mode <b>FIRST</b> or <b>LAST</b> Telemetry will be added like string without Timestamp.</br>" +
"Also, the rule node allows you to select telemetry sampling order: <b>ASC</b> or <b>DESC</b>. </br>" +
"Aggregation feature allows you to fetch aggregated telemetry as a single value by <b>AVG, COUNT, SUM, MIN, MAX, NONE</b>. </br>" +
"<b>Note</b>: The maximum size of the fetched array is 1000 records.\n ",
nodeDescription = "Adds message originator telemetry for selected time range into message metadata",
nodeDetails = "Useful when you need to get telemetry data set from the message originator for a specific time range " +
"instead of fetching just the latest telemetry or if you need to get the closest telemetry to the fetch interval start or end. " +
"Also, this node can be used for telemetry aggregation within configured fetch interval.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeGetTelemetryFromDatabase")
public class TbGetTelemetryNode implements TbNode {

35
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNode.java

@ -15,32 +15,49 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
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.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
@Slf4j
@RuleNode(
type = ComponentType.ENRICHMENT,
name="tenant attributes",
configClazz = TbGetEntityAttrNodeConfiguration.class,
nodeDescription = "Add Originators Tenant Attributes or Latest Telemetry into Message Metadata",
nodeDetails = "If Attributes enrichment configured, server scope attributes are added into Message metadata. " +
"If Latest Telemetry enrichment configured, latest telemetry added into metadata. " +
"To access those attributes in other nodes this template can be used " +
"<code>metadata.temperature</code>.",
name = "tenant attributes",
configClazz = TbGetEntityDataNodeConfiguration.class,
version = 1,
nodeDescription = "Adds message originator tenant attributes or latest telemetry into message or message metadata",
nodeDetails = "Useful when you need to retrieve some common configuration or threshold set " +
"that is stored as tenant attributes or telemetry data and use it for further message processing.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeTenantAttributesConfig")
public class TbGetTenantAttributeNode extends TbEntityGetAttrNode<TenantId> {
public class TbGetTenantAttributeNode extends TbAbstractGetEntityDataNode<TenantId> {
@Override
protected ListenableFuture<TenantId> findEntityAsync(TbContext ctx, EntityId originator) {
public TbGetEntityDataNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbGetEntityDataNodeConfiguration.class);
checkIfMappingIsNotEmptyOrElseThrow(config.getDataMapping());
checkDataToFetchSupportedOrElseThrow(config.getDataToFetch());
return config;
}
@Override
public ListenableFuture<TenantId> findEntityAsync(TbContext ctx, EntityId originator) {
return Futures.immediateFuture(ctx.getTenantId());
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ? upgradeToUseFetchToAndDataToFetch(oldConfiguration) : new TbPair<>(false, oldConfiguration);
}
}

39
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNode.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.rule.engine.api.RuleNode;
@ -22,36 +23,52 @@ 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.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.ContactBased;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentType;
import org.thingsboard.server.common.data.util.TbPair;
import org.thingsboard.server.common.msg.TbMsg;
@Slf4j
@RuleNode(type = ComponentType.ENRICHMENT,
name = "tenant details",
configClazz = TbGetTenantDetailsNodeConfiguration.class,
nodeDescription = "Adds fields from Tenant details to the message body or metadata",
nodeDetails = "If checkbox: <b>Add selected details to the message metadata</b> is selected, existing fields will be added to the message metadata instead of message data.<br><br>" +
"<b>Note:</b> only Device, Asset, and Entity View type are allowed.<br><br>" +
"If the originator of the message is not assigned to Tenant, or originator type is not supported - Message will be forwarded to <b>Failure</b> chain, otherwise, <b>Success</b> chain will be used.",
version = 1,
nodeDescription = "Adds message originator tenant details into message or message metadata",
nodeDetails = "Useful when we need to retrieve contact information from your tenant " +
"such as email, phone, address, etc., for notifications via email, SMS, and other notification providers.",
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbEnrichmentNodeEntityDetailsConfig")
public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetTenantDetailsNodeConfiguration> {
public class TbGetTenantDetailsNode extends TbAbstractGetEntityDetailsNode<TbGetTenantDetailsNodeConfiguration, TenantId> {
private static final String TENANT_PREFIX = "tenant_";
@Override
protected TbGetTenantDetailsNodeConfiguration loadGetEntityDetailsNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
return TbNodeUtils.convert(configuration, TbGetTenantDetailsNodeConfiguration.class);
protected TbGetTenantDetailsNodeConfiguration loadNodeConfiguration(TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbGetTenantDetailsNodeConfiguration.class);
checkIfDetailsListIsNotEmptyOrElseThrow(config.getDetailsList());
return config;
}
@Override
protected ListenableFuture<TbMsg> getDetails(TbContext ctx, TbMsg msg) {
return getTbMsgListenableFuture(ctx, msg, getDataAsJson(msg), TENANT_PREFIX);
protected String getPrefix() {
return TENANT_PREFIX;
}
@Override
protected ListenableFuture<? extends ContactBased> getContactBasedListenableFuture(TbContext ctx, TbMsg msg) {
protected ListenableFuture<Tenant> getContactBasedFuture(TbContext ctx, TbMsg msg) {
return ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), ctx.getTenantId());
}
@Override
public TbPair<Boolean, JsonNode> upgrade(int fromVersion, JsonNode oldConfiguration) throws TbNodeException {
return fromVersion == 0 ?
upgradeRuleNodesWithOldPropertyToUseFetchTo(
oldConfiguration,
"addToMetadata",
FetchTo.METADATA.name(),
FetchTo.DATA.name()) :
new TbPair<>(false, oldConfiguration);
}
}

7
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeConfiguration.java

@ -16,18 +16,21 @@
package org.thingsboard.rule.engine.metadata;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.rule.engine.api.NodeConfiguration;
import java.util.Collections;
@Data
@EqualsAndHashCode(callSuper = true)
public class TbGetTenantDetailsNodeConfiguration extends TbAbstractGetEntityDetailsNodeConfiguration implements NodeConfiguration<TbGetTenantDetailsNodeConfiguration> {
@Override
public TbGetTenantDetailsNodeConfiguration defaultConfiguration() {
TbGetTenantDetailsNodeConfiguration configuration = new TbGetTenantDetailsNodeConfiguration();
var configuration = new TbGetTenantDetailsNodeConfiguration();
configuration.setDetailsList(Collections.emptyList());
configuration.setFetchTo(FetchTo.DATA);
return configuration;
}
}

57
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbAbstractTransformNode.java

@ -23,7 +23,6 @@ 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.TbRelationTypes;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
import org.thingsboard.server.common.msg.queue.TbMsgCallback;
@ -31,19 +30,18 @@ import org.thingsboard.server.common.msg.queue.TbMsgCallback;
import java.util.List;
import static org.thingsboard.common.util.DonAsynchron.withCallback;
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
/**
* Created by ashvayka on 19.01.18.
*/
@Slf4j
public abstract class TbAbstractTransformNode implements TbNode {
public abstract class TbAbstractTransformNode<C> implements TbNode {
private TbTransformNodeConfiguration config;
protected C config;
@Override
public void init(TbContext context, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbTransformNodeConfiguration.class);
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
config = loadNodeConfiguration(ctx, configuration);
}
@Override
@ -54,44 +52,33 @@ public abstract class TbAbstractTransformNode implements TbNode {
MoreExecutors.directExecutor());
}
protected abstract C loadNodeConfiguration(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException;
protected void transformFailure(TbContext ctx, TbMsg msg, Throwable t) {
ctx.tellFailure(msg, t);
}
protected void transformSuccess(TbContext ctx, TbMsg msg, TbMsg m) {
if (m != null) {
ctx.tellSuccess(m);
} else {
ctx.tellNext(msg, FAILURE);
}
}
protected void transformSuccess(TbContext ctx, TbMsg msg, List<TbMsg> msgs) {
if (msgs != null && !msgs.isEmpty()) {
if (msgs.size() == 1) {
ctx.tellSuccess(msgs.get(0));
} else {
TbMsgCallbackWrapper wrapper = new MultipleTbMsgsCallbackWrapper(msgs.size(), new TbMsgCallback() {
@Override
public void onSuccess() {
ctx.ack(msg);
}
@Override
public void onFailure(RuleEngineException e) {
ctx.tellFailure(msg, e);
}
});
msgs.forEach(newMsg -> ctx.enqueueForTellNext(newMsg, TbRelationTypes.SUCCESS, wrapper::onSuccess, wrapper::onFailure));
}
if (msgs == null || msgs.isEmpty()) {
ctx.tellFailure(msg, new RuntimeException("Message or messages list are empty!"));
} else if (msgs.size() == 1) {
ctx.tellSuccess(msgs.get(0));
} else {
ctx.tellNext(msg, FAILURE);
TbMsgCallbackWrapper wrapper = new MultipleTbMsgsCallbackWrapper(msgs.size(), new TbMsgCallback() {
@Override
public void onSuccess() {
ctx.ack(msg);
}
@Override
public void onFailure(RuleEngineException e) {
ctx.tellFailure(msg, e);
}
});
msgs.forEach(newMsg -> ctx.enqueueForTellNext(newMsg, TbRelationTypes.SUCCESS, wrapper::onSuccess, wrapper::onFailure));
}
}
protected abstract ListenableFuture<List<TbMsg>> transform(TbContext ctx, TbMsg msg);
public void setConfig(TbTransformNodeConfiguration config) {
this.config = config;
}
}

39
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNode.java

@ -37,6 +37,7 @@ import org.thingsboard.server.common.msg.TbMsg;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.NoSuchElementException;
@Slf4j
@RuleNode(
@ -51,31 +52,36 @@ import java.util.List;
configDirective = "tbTransformationNodeChangeOriginatorConfig",
icon = "find_replace"
)
public class TbChangeOriginatorNode extends TbAbstractTransformNode {
public class TbChangeOriginatorNode extends TbAbstractTransformNode<TbChangeOriginatorNodeConfiguration> {
protected static final String CUSTOMER_SOURCE = "CUSTOMER";
protected static final String TENANT_SOURCE = "TENANT";
protected static final String RELATED_SOURCE = "RELATED";
protected static final String ALARM_ORIGINATOR_SOURCE = "ALARM_ORIGINATOR";
protected static final String ENTITY_SOURCE = "ENTITY";
private TbChangeOriginatorNodeConfiguration config;
private static final String CUSTOMER_SOURCE = "CUSTOMER";
private static final String TENANT_SOURCE = "TENANT";
private static final String RELATED_SOURCE = "RELATED";
private static final String ALARM_ORIGINATOR_SOURCE = "ALARM_ORIGINATOR";
private static final String ENTITY_SOURCE = "ENTITY";
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
protected TbChangeOriginatorNodeConfiguration loadNodeConfiguration(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbChangeOriginatorNodeConfiguration.class);
validateConfig(config);
setConfig(config);
return config;
}
@Override
protected ListenableFuture<List<TbMsg>> transform(TbContext ctx, TbMsg msg) {
ListenableFuture<? extends EntityId> newOriginator = getNewOriginator(ctx, msg);
return Futures.transform(newOriginator, n -> {
if (n == null || n.isNullUid()) {
return null;
ListenableFuture<? extends EntityId> newOriginatorFuture = getNewOriginator(ctx, msg);
return Futures.transformAsync(newOriginatorFuture, newOriginator -> {
if (newOriginator == null || newOriginator.isNullUid()) {
return Futures.immediateFailedFuture(new NoSuchElementException("Failed to find new originator!"));
}
return Collections.singletonList((ctx.transformMsg(msg, msg.getType(), n, msg.getMetaData(), msg.getData())));
return Futures.immediateFuture(
Collections.singletonList(
ctx.transformMsg(
msg,
msg.getType(),
newOriginator,
msg.getMetaData(),
msg.getData())));
}, ctx.getDbCallbackExecutor());
}
@ -129,7 +135,6 @@ public class TbChangeOriginatorNode extends TbAbstractTransformNode {
}
EntitiesByNameAndTypeLoader.checkEntityType(EntityType.valueOf(conf.getEntityType()));
}
}
}

6
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeConfiguration.java

@ -25,7 +25,9 @@ import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
import java.util.Collections;
@Data
public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
public class TbChangeOriginatorNodeConfiguration implements NodeConfiguration<TbChangeOriginatorNodeConfiguration> {
private static final String CUSTOMER_SOURCE = "CUSTOMER";
private String originatorSource;
@ -36,7 +38,7 @@ public class TbChangeOriginatorNodeConfiguration extends TbTransformNodeConfigur
@Override
public TbChangeOriginatorNodeConfiguration defaultConfiguration() {
TbChangeOriginatorNodeConfiguration configuration = new TbChangeOriginatorNodeConfiguration();
configuration.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
configuration.setOriginatorSource(CUSTOMER_SOURCE);
RelationsQuery relationsQuery = new RelationsQuery();
relationsQuery.setDirection(EntitySearchDirection.FROM);

15
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNode.java

@ -43,17 +43,16 @@ import java.util.List;
uiResources = {"static/rulenode/rulenode-core-config.js"},
configDirective = "tbTransformationNodeScriptConfig"
)
public class TbTransformMsgNode extends TbAbstractTransformNode {
public class TbTransformMsgNode extends TbAbstractTransformNode<TbTransformMsgNodeConfiguration> {
private TbTransformMsgNodeConfiguration config;
private ScriptEngine scriptEngine;
@Override
public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
this.config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
protected TbTransformMsgNodeConfiguration loadNodeConfiguration(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException {
var config = TbNodeUtils.convert(configuration, TbTransformMsgNodeConfiguration.class);
scriptEngine = ctx.createScriptEngine(config.getScriptLang(),
ScriptLanguage.TBEL.equals(config.getScriptLang()) ? config.getTbelScript() : config.getJsScript());
setConfig(config);
return config;
}
@Override
@ -62,12 +61,6 @@ public class TbTransformMsgNode extends TbAbstractTransformNode {
return scriptEngine.executeUpdateAsync(msg);
}
@Override
protected void transformSuccess(TbContext ctx, TbMsg msg, TbMsg m) {
ctx.logJsEvalResponse();
super.transformSuccess(ctx, msg, m);
}
@Override
protected void transformFailure(TbContext ctx, TbMsg msg, Throwable t) {
ctx.logJsEvalFailure();

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/transform/TbTransformMsgNodeConfiguration.java

@ -20,7 +20,7 @@ import org.thingsboard.rule.engine.api.NodeConfiguration;
import org.thingsboard.server.common.data.script.ScriptLanguage;
@Data
public class TbTransformMsgNodeConfiguration extends TbTransformNodeConfiguration implements NodeConfiguration {
public class TbTransformMsgNodeConfiguration implements NodeConfiguration<TbTransformMsgNodeConfiguration> {
private ScriptLanguage scriptLang;
private String jsScript;

41
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/ContactBasedEntityDetails.java

@ -0,0 +1,41 @@
/**
* 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.util;
import lombok.Getter;
public enum ContactBasedEntityDetails {
ID("id"),
TITLE("title"),
COUNTRY("country"),
CITY("city"),
STATE("state"),
ZIP("zip"),
ADDRESS("address"),
ADDRESS2("address2"),
PHONE("phone"),
EMAIL("email"),
ADDITIONAL_INFO("additionalInfo");
@Getter
private final String ruleEngineName;
ContactBasedEntityDetails(String ruleEngineName) {
this.ruleEngineName = ruleEngineName;
}
}

20
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesAlarmOriginatorIdAsyncLoader.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.alarm.Alarm;
@ -26,20 +25,19 @@ import org.thingsboard.server.common.data.id.EntityId;
public class EntitiesAlarmOriginatorIdAsyncLoader {
public static ListenableFuture<EntityId> findEntityIdAsync(TbContext ctx, EntityId original) {
switch (original.getEntityType()) {
public static ListenableFuture<EntityId> findEntityIdAsync(TbContext ctx, EntityId originator) {
switch (originator.getEntityType()) {
case ALARM:
return getAlarmOriginatorAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) original));
return getAlarmOriginatorAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) originator), ctx);
default:
return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType()));
return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType " + originator.getEntityType()));
}
}
private static ListenableFuture<EntityId> getAlarmOriginatorAsync(ListenableFuture<Alarm> future) {
return Futures.transformAsync(future, in -> {
return in != null ? Futures.immediateFuture(in.getOriginator())
: Futures.immediateFuture(null);
}, MoreExecutors.directExecutor());
private static ListenableFuture<EntityId> getAlarmOriginatorAsync(ListenableFuture<Alarm> future, TbContext ctx) {
return Futures.transformAsync(future, in -> in != null ?
Futures.immediateFuture(in.getOriginator())
: Futures.immediateFuture(null), ctx.getDbCallbackExecutor());
}
}

21
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoader.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.HasCustomerId;
@ -29,24 +28,24 @@ import org.thingsboard.server.common.data.id.UserId;
public class EntitiesCustomerIdAsyncLoader {
public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId original) {
switch (original.getEntityType()) {
public static ListenableFuture<CustomerId> findEntityIdAsync(TbContext ctx, EntityId originator) {
switch (originator.getEntityType()) {
case CUSTOMER:
return Futures.immediateFuture((CustomerId) original);
return Futures.immediateFuture((CustomerId) originator);
case USER:
return getCustomerAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) original));
return toCustomerIdAsync(ctx, ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) originator));
case ASSET:
return getCustomerAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) original));
return toCustomerIdAsync(ctx, ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) originator));
case DEVICE:
return getCustomerAsync(Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) original)));
return toCustomerIdAsync(ctx, Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) originator)));
default:
return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType()));
return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originator.getEntityType()));
}
}
private static <T extends HasCustomerId> ListenableFuture<CustomerId> getCustomerAsync(ListenableFuture<T> future) {
private static <T extends HasCustomerId> ListenableFuture<CustomerId> toCustomerIdAsync(TbContext ctx, ListenableFuture<T> future) {
return Futures.transformAsync(future, in -> in != null ? Futures.immediateFuture(in.getCustomerId())
: Futures.immediateFuture(null), MoreExecutors.directExecutor());
: Futures.immediateFuture(null), ctx.getDbCallbackExecutor());
}
}

51
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesFieldsAsyncLoader.java

@ -17,7 +17,6 @@ package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.BaseData;
@ -30,47 +29,53 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import java.util.NoSuchElementException;
import java.util.function.Function;
public class EntitiesFieldsAsyncLoader {
public static ListenableFuture<EntityFieldsData> findAsync(TbContext ctx, EntityId original) {
switch (original.getEntityType()) { // TODO: use EntityServiceRegistry
public static ListenableFuture<EntityFieldsData> findAsync(TbContext ctx, EntityId originatorId) {
switch (originatorId.getEntityType()) { // TODO: use EntityServiceRegistry
case TENANT:
return getAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), (TenantId) original),
EntityFieldsData::new);
return toEntityFieldsDataAsync(ctx.getTenantService().findTenantByIdAsync(ctx.getTenantId(), (TenantId) originatorId),
EntityFieldsData::new, ctx);
case CUSTOMER:
return getAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) original),
EntityFieldsData::new);
return toEntityFieldsDataAsync(ctx.getCustomerService().findCustomerByIdAsync(ctx.getTenantId(), (CustomerId) originatorId),
EntityFieldsData::new, ctx);
case USER:
return getAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) original),
EntityFieldsData::new);
return toEntityFieldsDataAsync(ctx.getUserService().findUserByIdAsync(ctx.getTenantId(), (UserId) originatorId),
EntityFieldsData::new, ctx);
case ASSET:
return getAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) original),
EntityFieldsData::new);
return toEntityFieldsDataAsync(ctx.getAssetService().findAssetByIdAsync(ctx.getTenantId(), (AssetId) originatorId),
EntityFieldsData::new, ctx);
case DEVICE:
return getAsync(Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) original)),
EntityFieldsData::new);
return toEntityFieldsDataAsync(Futures.immediateFuture(ctx.getDeviceService().findDeviceById(ctx.getTenantId(), (DeviceId) originatorId)),
EntityFieldsData::new, ctx);
case ALARM:
return getAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) original),
EntityFieldsData::new);
return toEntityFieldsDataAsync(ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), (AlarmId) originatorId),
EntityFieldsData::new, ctx);
case RULE_CHAIN:
return getAsync(ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) original),
EntityFieldsData::new);
return toEntityFieldsDataAsync(ctx.getRuleChainService().findRuleChainByIdAsync(ctx.getTenantId(), (RuleChainId) originatorId),
EntityFieldsData::new, ctx);
case ENTITY_VIEW:
return getAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) original),
EntityFieldsData::new);
return toEntityFieldsDataAsync(ctx.getEntityViewService().findEntityViewByIdAsync(ctx.getTenantId(), (EntityViewId) originatorId),
EntityFieldsData::new, ctx);
default:
return Futures.immediateFailedFuture(new TbNodeException("Unexpected original EntityType " + original.getEntityType()));
return Futures.immediateFailedFuture(new TbNodeException("Unexpected originator EntityType: " + originatorId.getEntityType()));
}
}
private static <T extends BaseData> ListenableFuture<EntityFieldsData> getAsync(
ListenableFuture<T> future, Function<T, EntityFieldsData> converter) {
private static <T extends BaseData<? extends UUIDBased>> ListenableFuture<EntityFieldsData> toEntityFieldsDataAsync(
ListenableFuture<T> future,
Function<T, EntityFieldsData> converter,
TbContext ctx
) {
return Futures.transformAsync(future, in -> in != null ?
Futures.immediateFuture(converter.apply(in))
: Futures.immediateFailedFuture(new RuntimeException("Entity not found!")), MoreExecutors.directExecutor());
: Futures.immediateFailedFuture(new NoSuchElementException("Entity not found!")), ctx.getDbCallbackExecutor());
}
}

37
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedDeviceIdAsyncLoader.java

@ -17,39 +17,42 @@ package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.apache.commons.collections.CollectionUtils;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.data.DeviceRelationsQuery;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.device.DeviceSearchQuery;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.device.DeviceService;
import java.util.List;
public class EntitiesRelatedDeviceIdAsyncLoader {
public static ListenableFuture<DeviceId> findDeviceAsync(TbContext ctx, EntityId originator,
DeviceRelationsQuery deviceRelationsQuery) {
DeviceService deviceService = ctx.getDeviceService();
DeviceSearchQuery query = buildQuery(originator, deviceRelationsQuery);
ListenableFuture<List<Device>> asyncDevices = deviceService.findDevicesByQuery(ctx.getTenantId(), query);
return Futures.transformAsync(asyncDevices, d -> CollectionUtils.isNotEmpty(d) ? Futures.immediateFuture(d.get(0).getId())
: Futures.immediateFuture(null), MoreExecutors.directExecutor());
public static ListenableFuture<DeviceId> findDeviceAsync(
TbContext ctx,
EntityId originator,
DeviceRelationsQuery deviceRelationsQuery
) {
var deviceService = ctx.getDeviceService();
var query = buildQuery(originator, deviceRelationsQuery);
var devicesListFuture = deviceService.findDevicesByQuery(ctx.getTenantId(), query);
return Futures.transformAsync(devicesListFuture,
deviceList -> CollectionUtils.isNotEmpty(deviceList) ?
Futures.immediateFuture(deviceList.get(0).getId())
: Futures.immediateFuture(null), ctx.getDbCallbackExecutor());
}
private static DeviceSearchQuery buildQuery(EntityId originator, DeviceRelationsQuery deviceRelationsQuery) {
DeviceSearchQuery query = new DeviceSearchQuery();
RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
deviceRelationsQuery.getDirection(), deviceRelationsQuery.getMaxLevel(), deviceRelationsQuery.isFetchLastLevelOnly());
var query = new DeviceSearchQuery();
var parameters = new RelationsSearchParameters(
originator,
deviceRelationsQuery.getDirection(),
deviceRelationsQuery.getMaxLevel(),
deviceRelationsQuery.isFetchLastLevelOnly()
);
query.setParameters(parameters);
query.setRelationType(deviceRelationsQuery.getRelationType());
query.setDeviceTypes(deviceRelationsQuery.getDeviceTypes());
return query;
}
}

41
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntitiesRelatedEntityIdAsyncLoader.java

@ -17,42 +17,49 @@ package org.thingsboard.rule.engine.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import org.apache.commons.collections.CollectionUtils;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.data.RelationsQuery;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.dao.relation.RelationService;
import java.util.List;
public class EntitiesRelatedEntityIdAsyncLoader {
public static ListenableFuture<EntityId> findEntityAsync(TbContext ctx, EntityId originator,
RelationsQuery relationsQuery) {
RelationService relationService = ctx.getRelationService();
EntityRelationsQuery query = buildQuery(originator, relationsQuery);
ListenableFuture<List<EntityRelation>> asyncRelation = relationService.findByQuery(ctx.getTenantId(), query);
public static ListenableFuture<EntityId> findEntityAsync(
TbContext ctx,
EntityId originator,
RelationsQuery relationsQuery
) {
var relationService = ctx.getRelationService();
var query = buildQuery(originator, relationsQuery);
var relationListFuture = relationService.findByQuery(ctx.getTenantId(), query);
if (relationsQuery.getDirection() == EntitySearchDirection.FROM) {
return Futures.transformAsync(asyncRelation, r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getTo())
: Futures.immediateFuture(null), MoreExecutors.directExecutor());
return Futures.transformAsync(relationListFuture,
relationList -> CollectionUtils.isNotEmpty(relationList) ?
Futures.immediateFuture(relationList.get(0).getTo())
: Futures.immediateFuture(null), ctx.getDbCallbackExecutor());
} else if (relationsQuery.getDirection() == EntitySearchDirection.TO) {
return Futures.transformAsync(asyncRelation, r -> CollectionUtils.isNotEmpty(r) ? Futures.immediateFuture(r.get(0).getFrom())
: Futures.immediateFuture(null), MoreExecutors.directExecutor());
return Futures.transformAsync(relationListFuture,
relationList -> CollectionUtils.isNotEmpty(relationList) ?
Futures.immediateFuture(relationList.get(0).getFrom())
: Futures.immediateFuture(null), ctx.getDbCallbackExecutor());
}
return Futures.immediateFailedFuture(new IllegalStateException("Unknown direction"));
}
private static EntityRelationsQuery buildQuery(EntityId originator, RelationsQuery relationsQuery) {
EntityRelationsQuery query = new EntityRelationsQuery();
RelationsSearchParameters parameters = new RelationsSearchParameters(originator,
relationsQuery.getDirection(), relationsQuery.getMaxLevel(), relationsQuery.isFetchLastLevelOnly());
var query = new EntityRelationsQuery();
var parameters = new RelationsSearchParameters(
originator,
relationsQuery.getDirection(),
relationsQuery.getMaxLevel(),
relationsQuery.isFetchLastLevelOnly()
);
query.setParameters(parameters);
query.setFilters(relationsQuery.getFilters());
return query;
}
}

2
rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/util/EntityContainer.java

@ -25,4 +25,4 @@ public class EntityContainer {
private EntityId entityId;
private EntityType entityType;
}
}

2
rule-engine/rule-engine-components/src/main/resources/public/static/rulenode/rulenode-core-config.js

File diff suppressed because one or more lines are too long

231
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/AbstractAttributeNodeTest.java

@ -1,231 +0,0 @@
/**
* 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.metadata;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Futures;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyCollection;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
@RunWith(MockitoJUnitRunner.class)
public abstract class AbstractAttributeNodeTest {
final CustomerId customerId = new CustomerId(Uuids.timeBased());
final TenantId tenantId = TenantId.fromUUID(Uuids.timeBased());
final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased());
final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased());
final String keyAttrConf = "${word}";
final String valueAttrConf = "${result}";
@Mock
TbContext ctx;
@Mock
AttributesService attributesService;
@Mock
TimeseriesService timeseriesService;
@Mock
UserService userService;
@Mock
AssetService assetService;
@Mock
DeviceService deviceService;
TbMsg msg;
Map<String, String> metaData;
TbEntityGetAttrNode node;
void init(TbEntityGetAttrNode node) throws TbNodeException {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(getTbNodeConfig()));
metaData = new HashMap<>();
metaData.putIfAbsent("word", "temperature");
metaData.putIfAbsent("result", "answer");
this.node = node;
this.node.init(null, nodeConfiguration);
}
void errorThrownIfCannotLoadAttributes(User user) {
msg = TbMsg.newMsg("USER", user.getId(), new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
when(ctx.getAttributesService()).thenReturn(attributesService);
when(attributesService.find(any(), eq(getEntityId()), eq(SERVER_SCOPE), anyCollection()))
.thenThrow(new IllegalStateException("something wrong"));
node.onMsg(ctx, msg);
final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(same(msg), captor.capture());
Throwable value = captor.getValue();
assertEquals("something wrong", value.getMessage());
assertTrue(msg.getMetaData().getData().isEmpty());
}
void errorThrownIfCannotLoadAttributesAsync(User user) {
msg = TbMsg.newMsg("USER", user.getId(), new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
when(ctx.getAttributesService()).thenReturn(attributesService);
when(attributesService.find(any(), eq(getEntityId()), eq(SERVER_SCOPE), anyCollection()))
.thenReturn(Futures.immediateFailedFuture(new IllegalStateException("something wrong")));
node.onMsg(ctx, msg);
final ArgumentCaptor<Throwable> captor = ArgumentCaptor.forClass(Throwable.class);
verify(ctx).tellFailure(same(msg), captor.capture());
Throwable value = captor.getValue();
assertEquals("something wrong", value.getMessage());
assertTrue(msg.getMetaData().getData().isEmpty());
}
void failedChainUsedIfCustomerCannotBeFound(User user) {
msg = TbMsg.newMsg("USER", user.getId(), new TbMsgMetaData(), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
node.onMsg(ctx, msg);
verify(ctx).tellNext(msg, FAILURE);
assertTrue(msg.getMetaData().getData().isEmpty());
}
void entityAttributeAddedInMetadata(EntityId entityId, String type) {
msg = TbMsg.newMsg(type, entityId, new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
entityAttributeFetched(getEntityId());
}
void usersCustomerAttributesFetched(User user) {
msg = TbMsg.newMsg("USER", user.getId(), new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
entityAttributeFetched(getEntityId());
}
void assetsCustomerAttributesFetched(Asset asset) {
msg = TbMsg.newMsg("ASSET", asset.getId(), new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
entityAttributeFetched(getEntityId());
}
void deviceCustomerAttributesFetched(Device device) {
msg = TbMsg.newMsg("DEVICE", device.getId(), new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
entityAttributeFetched(getEntityId());
}
void deviceCustomerTelemetryFetched(Device device) throws TbNodeException {
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(getTbNodeConfigForTelemetry()));
TbEntityGetAttrNode node = getEmptyNode();
node.init(null, nodeConfiguration);
msg = TbMsg.newMsg("DEVICE", device.getId(), new TbMsgMetaData(metaData), TbMsgDataType.JSON, "{}", ruleChainId, ruleNodeId);
List<TsKvEntry> timeseries = Lists.newArrayList(new BasicTsKvEntry(1L, new StringDataEntry("temperature", "highest")));
when(ctx.getTimeseriesService()).thenReturn(timeseriesService);
when(timeseriesService.findLatest(any(), eq(getEntityId()), anyCollection()))
.thenReturn(Futures.immediateFuture(timeseries));
node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg);
assertEquals(msg.getMetaData().getValue("answer"), "highest");
}
void entityAttributeFetched(EntityId entityId) {
List<AttributeKvEntry> attributes = Lists.newArrayList(new BaseAttributeKvEntry(new StringDataEntry("temperature", "high"), 1L));
when(ctx.getAttributesService()).thenReturn(attributesService);
when(attributesService.find(any(), eq(entityId), eq(SERVER_SCOPE), anyCollection()))
.thenReturn(Futures.immediateFuture(attributes));
node.onMsg(ctx, msg);
verify(ctx).tellSuccess(msg);
assertEquals(msg.getMetaData().getValue("answer"), "high");
}
TbGetEntityAttrNodeConfiguration getTbNodeConfig() {
return getConfig(false);
}
TbGetEntityAttrNodeConfiguration getTbNodeConfigForTelemetry() {
return getConfig(true);
}
private TbGetEntityAttrNodeConfiguration getConfig(boolean isTelemetry) {
TbGetEntityAttrNodeConfiguration config = new TbGetEntityAttrNodeConfiguration();
Map<String, String> conf = new HashMap<>();
conf.put(keyAttrConf, valueAttrConf);
config.setAttrMapping(conf);
config.setTelemetry(isTelemetry);
return config;
}
protected abstract TbEntityGetAttrNode getEmptyNode();
abstract EntityId getEntityId();
void mockFindDevice(Device device) {
when(ctx.getDeviceService()).thenReturn(deviceService);
when(deviceService.findDeviceById(any(), eq(device.getId()))).thenReturn(device);
}
void mockFindAsset(Asset asset) {
when(ctx.getAssetService()).thenReturn(assetService);
when(assetService.findAssetByIdAsync(any(), eq(asset.getId()))).thenReturn(Futures.immediateFuture(asset));
}
void mockFindUser(User user) {
when(ctx.getUserService()).thenReturn(userService);
when(userService.findUserByIdAsync(any(), eq(user.getId()))).thenReturn(Futures.immediateFuture(user));
}
}

449
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/CalculateDeltaNodeTest.java

@ -0,0 +1,449 @@
/**
* 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.metadata;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.assertj.core.api.Assertions;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.DoubleDataEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.Callable;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anySet;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class CalculateDeltaNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() {
@Override
public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void execute(@NotNull Runnable command) {
command.run();
}
};
@Mock
private TbContext ctxMock;
@Mock
private TimeseriesService timeseriesServiceMock;
private CalculateDeltaNode node;
private CalculateDeltaNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
@BeforeEach
public void setUp() throws TbNodeException {
node = new CalculateDeltaNode();
config = new CalculateDeltaNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
node.init(ctxMock, nodeConfiguration);
}
@Test
public void givenDefaultConfig_whenDefaultConfiguration_thenVerify() {
assertEquals(config.getInputValueKey(), "pulseCounter");
assertEquals(config.getOutputValueKey(), "delta");
assertTrue(config.isUseCache());
assertFalse(config.isAddPeriodBetweenMsgs());
assertEquals(config.getPeriodValueKey(), "periodInMs");
assertTrue(config.isTellFailureIfDeltaIsNegative());
}
@Test
public void givenInvalidMsgType_whenOnMsg_thenShouldTellNextOther() {
// GIVEN
var msgData = "{\"pulseCounter\": 42}";
var msg = TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other"));
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, never()).tellFailure(any(), any());
}
@Test
public void givenInvalidMsgDataType_whenOnMsg_thenShouldTellNextOther() {
// GIVEN
var msgData = "[]";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other"));
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, never()).tellFailure(any(), any());
}
@Test
public void givenInputKeyIsNotPresent_whenOnMsg_thenShouldTellNextOther() {
// GIVEN
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "{}");
// WHEN
node.onMsg(ctxMock, msg);
// THEN
verify(ctxMock, times(1)).tellNext(eq(msg), eq("Other"));
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, never()).tellFailure(any(), any());
}
@Test
public void givenDoubleValue_whenOnMsgAndCachingOff_thenShouldTellSuccess() throws TbNodeException {
// GIVEN
config.setRound(1);
config.setInputValueKey("temperature");
config.setOutputValueKey("temp_delta");
config.setUseCache(false);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", 40.5)));
var msgData = "{\"temperature\": 42,\"airPressure\":123}";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":1.5}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
}
@Test
public void givenLongStringValue_whenOnMsgAndCachingOff_thenShouldTellSuccess() throws TbNodeException {
// GIVEN
config.setInputValueKey("temperature");
config.setOutputValueKey("temp_delta");
config.setUseCache(false);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("temperature", 40L)));
var msgData = "{\"temperature\": 42,\"airPressure\":123}";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":2}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
}
@Test
public void givenValidStringValue_whenOnMsgAndCachingOff_thenShouldTellSuccess() throws TbNodeException {
// GIVEN
config.setInputValueKey("temperature");
config.setOutputValueKey("temp_delta");
config.setUseCache(false);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("temperature", "40.0")));
var msgData = "{\"temperature\": 42,\"airPressure\":123}";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":2}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
}
@Test
public void givenTwoMessagesAndPeriodOnAndCachingOn_whenOnMsg_thenVerify() throws TbNodeException {
// STAGE 1
// GIVEN
config.setInputValueKey("temperature");
config.setOutputValueKey("temp_delta");
config.setPeriodValueKey("ts_delta");
config.setAddPeriodBetweenMsgs(true);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
mockFindLatest(new BasicTsKvEntry(1L, new DoubleDataEntry("temperature", 40.0)));
var msgData = "{\"temperature\": 42,\"airPressure\":123}";
var firstMsgMetaData = new TbMsgMetaData();
firstMsgMetaData.putValue("ts", String.valueOf(3L));
var firstMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, firstMsgMetaData, msgData);
// WHEN
node.onMsg(ctxMock, firstMsg);
// THEN
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":2,\"ts_delta\":2}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
// STAGE 2
// GIVEN
reset(ctxMock);
reset(timeseriesServiceMock);
var secondMsgMetaData = new TbMsgMetaData();
secondMsgMetaData.putValue("ts", String.valueOf(6L));
var secondMsg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, secondMsgMetaData, msgData);
// WHEN
node.onMsg(ctxMock, secondMsg);
// THEN
actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(timeseriesServiceMock, never()).findLatest(any(), any(), anyList());
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":0,\"ts_delta\":3}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
}
@Test
public void givenLastValueIsNull_whenOnMsgAndCachingOff_thenDeltaShouldBeZero() throws TbNodeException {
// GIVEN
config.setInputValueKey("temperature");
config.setOutputValueKey("temp_delta");
config.setUseCache(false);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
mockFindLatestAsync(new BasicTsKvEntry(System.currentTimeMillis(), new DoubleDataEntry("temperature", null)));
var msgData = "{\"temperature\": 42,\"airPressure\":123}";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMsgCaptor.capture());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temperature\":42,\"airPressure\":123,\"temp_delta\":0}";
assertEquals(expectedMsgData, actualMsgCaptor.getValue().getData());
}
@Test
public void givenNegativeDeltaAndTellFailureIfNegativeDeltaTrue_whenOnMsg_thenShouldTellFailure() throws TbNodeException {
// GIVEN
config.setTellFailureIfDeltaIsNegative(true);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node.init(ctxMock, nodeConfiguration);
mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new LongDataEntry("pulseCounter", 200L)));
var msgData = "{\"pulseCounter\":\"123\"}";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
var actualExceptionCaptor = ArgumentCaptor.forClass(Exception.class);
verify(ctxMock, times(1)).tellFailure(actualMsgCaptor.capture(), actualExceptionCaptor.capture());
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, never()).tellNext(any(), anyString());
verify(ctxMock, never()).tellNext(any(), anySet());
var expectedExceptionMsg = "Delta value is negative!";
var actualException = actualExceptionCaptor.getValue();
assertEquals(msg, actualMsgCaptor.getValue());
assertInstanceOf(IllegalArgumentException.class, actualException);
assertEquals(expectedExceptionMsg, actualException.getMessage());
}
@Test
public void givenInvalidStringValue_whenOnMsg_thenException() {
// GIVEN
mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new StringDataEntry("pulseCounter", "high")));
var msgData = "{\"pulseCounter\":\"123\"}";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN-THEN
Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Calculation failed. Unable to parse value [high] of telemetry [pulseCounter] to Double");
}
@Test
public void givenBooleanValue_whenOnMsg_thenException() {
// GIVEN
mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new BooleanDataEntry("pulseCounter", false)));
var msgData = "{\"pulseCounter\":true}";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN-THEN
Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Calculation failed. Boolean values are not supported!");
}
@Test
public void givenJsonValue_whenOnMsg_thenException() {
// GIVEN
mockFindLatest(new BasicTsKvEntry(System.currentTimeMillis(), new JsonDataEntry("pulseCounter", "{\"isActive\":false}")));
var msgData = "{\"pulseCounter\":{\"isActive\":true}}";
var msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), msgData);
// WHEN-THEN
Assertions.assertThatThrownBy(() -> node.onMsg(ctxMock, msg))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Calculation failed. JSON values are not supported!");
}
private void mockFindLatest(TsKvEntry tsKvEntry) {
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(timeseriesServiceMock.findLatestSync(
eq(TENANT_ID), eq(DUMMY_DEVICE_ORIGINATOR), argThat(new ListMatcher<>(List.of(tsKvEntry.getKey())))
)).thenReturn(List.of(tsKvEntry));
}
private void mockFindLatestAsync(TsKvEntry tsKvEntry) {
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(timeseriesServiceMock.findLatest(
eq(TENANT_ID), eq(DUMMY_DEVICE_ORIGINATOR), argThat(new ListMatcher<>(List.of(tsKvEntry.getKey())))
)).thenReturn(Futures.immediateFuture(List.of(tsKvEntry)));
}
@RequiredArgsConstructor
private static class ListMatcher<T> implements ArgumentMatcher<List<T>> {
private final List<T> expectedList;
@Override
public boolean matches(List<T> actualList) {
if (actualList == expectedList) {
return true;
}
if (actualList.size() != expectedList.size()) {
return false;
}
return actualList.containsAll(expectedList);
}
}
}

368
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbAbstractGetAttributesNodeTest.java

@ -1,368 +0,0 @@
/**
* 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.metadata;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.common.util.AbstractListeningExecutor;
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.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
@RunWith(MockitoJUnitRunner.class)
public class TbAbstractGetAttributesNodeTest {
private EntityId originator = new DeviceId(Uuids.timeBased());
private TenantId tenantId = TenantId.fromUUID(Uuids.timeBased());
@Mock
private TbContext ctx;
@Mock
private AttributesService attributesService;
@Mock
private TimeseriesService tsService;
private AbstractListeningExecutor dbExecutor;
private List<String> clientAttributes;
private List<String> serverAttributes;
private List<String> sharedAttributes;
private List<String> tsKeys;
private long ts;
@Before
public void before() throws TbNodeException {
dbExecutor = new AbstractListeningExecutor() {
@Override
protected int getThreadPollSize() {
return 3;
}
};
dbExecutor.init();
Mockito.reset(ctx);
Mockito.reset(attributesService);
Mockito.reset(tsService);
Mockito.reset(ctx);
Mockito.reset(attributesService);
Mockito.reset(tsService);
lenient().when(ctx.getAttributesService()).thenReturn(attributesService);
lenient().when(ctx.getTimeseriesService()).thenReturn(tsService);
lenient().when(ctx.getTenantId()).thenReturn(tenantId);
lenient().when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);
clientAttributes = getAttributeNames("client");
serverAttributes = getAttributeNames("server");
sharedAttributes = getAttributeNames("shared");
tsKeys = List.of("temperature", "humidity", "unknown");
ts = System.currentTimeMillis();
Mockito.when(attributesService.find(tenantId, originator, DataConstants.CLIENT_SCOPE, clientAttributes))
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(clientAttributes, ts)));
Mockito.when(attributesService.find(tenantId, originator, DataConstants.SERVER_SCOPE, serverAttributes))
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(serverAttributes, ts)));
Mockito.when(attributesService.find(tenantId, originator, DataConstants.SHARED_SCOPE, sharedAttributes))
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(sharedAttributes, ts)));
Mockito.when(tsService.findLatest(tenantId, originator, tsKeys))
.thenReturn(Futures.immediateFuture(getListTsKvEntry(tsKeys, ts)));
}
@After
public void after() {
dbExecutor.destroy();
}
@Test
public void fetchToMetadata_whenOnMsg_then_success() throws Exception {
TbGetAttributesNode node = initNode(false, false, false);
TbMsg msg = getTbMsg(originator);
node.onMsg(ctx, msg);
// check msg
TbMsg resultMsg = checkMsg(true);
//check attributes
checkAttributes(resultMsg, false, "cs_", clientAttributes);
checkAttributes(resultMsg, false, "ss_", serverAttributes);
checkAttributes(resultMsg, false, "shared_", sharedAttributes);
//check timeseries
checkTs(resultMsg, false, false, tsKeys);
}
@Test
public void fetchToMetadata_latestWithTs_whenOnMsg_then_success() throws Exception {
TbGetAttributesNode node = initNode(false, true, false);
TbMsg msg = getTbMsg(originator);
node.onMsg(ctx, msg);
// check msg
TbMsg resultMsg = checkMsg(true);
//check attributes
checkAttributes(resultMsg, false, "cs_", clientAttributes);
checkAttributes(resultMsg, false, "ss_", serverAttributes);
checkAttributes(resultMsg, false, "shared_", sharedAttributes);
//check timeseries with ts
checkTs(resultMsg, false, true, tsKeys);
}
@Test
public void fetchToData_whenOnMsg_then_success() throws Exception {
TbGetAttributesNode node = initNode(true, false, false);
TbMsg msg = getTbMsg(originator);
node.onMsg(ctx, msg);
// check msg
TbMsg resultMsg = checkMsg(true);
//check attributes
checkAttributes(resultMsg, true, "cs_", clientAttributes);
checkAttributes(resultMsg, true, "ss_", serverAttributes);
checkAttributes(resultMsg, true, "shared_", sharedAttributes);
//check timeseries
checkTs(resultMsg, true, false, tsKeys);
}
@Test
public void fetchToData_latestWithTs_whenOnMsg_then_success() throws Exception {
TbGetAttributesNode node = initNode(true, true, false);
TbMsg msg = getTbMsg(originator);
node.onMsg(ctx, msg);
// check msg
TbMsg resultMsg = checkMsg(true);
//check attributes
checkAttributes(resultMsg, true, "cs_", clientAttributes);
checkAttributes(resultMsg, true, "ss_", serverAttributes);
checkAttributes(resultMsg, true, "shared_", sharedAttributes);
//check timeseries with ts
checkTs(resultMsg, true, true, tsKeys);
}
@Test
public void fetchToMetadata_whenOnMsg_then_failure() throws Exception {
TbGetAttributesNode node = initNode(false, false, true);
TbMsg msg = getTbMsg(originator);
node.onMsg(ctx, msg);
// check msg
TbMsg actualMsg = checkMsg(false);
//check attributes
checkAttributes(actualMsg, false, "cs_", clientAttributes);
checkAttributes(actualMsg, false, "ss_", serverAttributes);
checkAttributes(actualMsg, false, "shared_", sharedAttributes);
//check timeseries with ts
checkTs(actualMsg, false, false, tsKeys);
}
@Test
public void fetchToData_whenOnMsg_then_failure() throws Exception {
TbGetAttributesNode node = initNode(true, true, true);
TbMsg msg = getTbMsg(originator);
node.onMsg(ctx, msg);
// check msg
TbMsg actualMsg = checkMsg(false);
//check attributes
checkAttributes(actualMsg, true, "cs_", clientAttributes);
checkAttributes(actualMsg, true, "ss_", serverAttributes);
checkAttributes(actualMsg, true, "shared_", sharedAttributes);
//check timeseries with ts
checkTs(actualMsg, true, true, tsKeys);
}
@Test
public void fetchToData_whenOnMsg_and_data_is_not_object_then_failure() throws Exception {
TbGetAttributesNode node = initNode(true, true, true);
TbMsg msg = TbMsg.newMsg("TEST", originator, new TbMsgMetaData(), "[]");
node.onMsg(ctx, msg);
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(IllegalArgumentException.class);
Mockito.verify(ctx, never()).tellSuccess(any());
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture());
Assert.assertSame(msg, newMsgCaptor.getValue());
Assert.assertNotNull(exceptionCaptor.getValue());
}
private TbMsg checkMsg(boolean checkSuccess) {
ArgumentCaptor<TbMsg> msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
if (checkSuccess) {
Mockito.verify(ctx, Mockito.timeout(5000)).tellSuccess(msgCaptor.capture());
} else {
ArgumentCaptor<RuntimeException> exceptionCaptor = ArgumentCaptor.forClass(RuntimeException.class);
Mockito.verify(ctx, never()).tellSuccess(any());
Mockito.verify(ctx, Mockito.timeout(5000)).tellFailure(msgCaptor.capture(), exceptionCaptor.capture());
RuntimeException exception = exceptionCaptor.getValue();
Assert.assertNotNull(exception);
Assert.assertNotNull(exception.getMessage());
Assert.assertTrue(exception.getMessage().startsWith("The following attribute/telemetry keys is not present in the DB:"));
}
TbMsg resultMsg = msgCaptor.getValue();
Assert.assertNotNull(resultMsg);
Assert.assertNotNull(resultMsg.getMetaData());
Assert.assertNotNull(resultMsg.getData());
return resultMsg;
}
private void checkAttributes(TbMsg actualMsg, boolean fetchToData, String prefix, List<String> attributes) {
JsonNode msgData = JacksonUtil.toJsonNode(actualMsg.getData());
attributes.stream()
.filter(attribute -> !attribute.equals("unknown"))
.forEach(attribute -> {
String result;
if (fetchToData) {
result = msgData.get(prefix + attribute).asText();
} else {
result = actualMsg.getMetaData().getValue(prefix + attribute);
}
Assert.assertNotNull(result);
Assert.assertEquals(attribute + "_value", result);
});
}
private void checkTs(TbMsg actualMsg, boolean fetchToData, boolean getLatestValueWithTs, List<String> tsKeys) {
JsonNode msgData = JacksonUtil.toJsonNode(actualMsg.getData());
long value = 1L;
for (String key : tsKeys) {
if (key.equals("unknown")) {
continue;
}
String actualValue;
String expectedValue;
if (getLatestValueWithTs) {
expectedValue = "{\"ts\":" + ts + ",\"value\":{\"data\":" + value + "}}";
} else {
expectedValue = "{\"data\":" + value + "}";
}
if (fetchToData) {
actualValue = JacksonUtil.toString(msgData.get(key));
} else {
actualValue = actualMsg.getMetaData().getValue(key);
}
Assert.assertNotNull(actualValue);
Assert.assertEquals(expectedValue, actualValue);
value++;
}
}
private TbGetAttributesNode initNode(boolean fetchToData, boolean getLatestValueWithTs, boolean isTellFailureIfAbsent) throws TbNodeException {
TbGetAttributesNodeConfiguration config = new TbGetAttributesNodeConfiguration();
config.setClientAttributeNames(List.of("client_attr_1", "client_attr_2", "${client_attr_metadata}", "unknown"));
config.setServerAttributeNames(List.of("server_attr_1", "server_attr_2", "${server_attr_metadata}", "unknown"));
config.setSharedAttributeNames(List.of("shared_attr_1", "shared_attr_2", "$[shared_attr_data]", "unknown"));
config.setLatestTsKeyNames(List.of("temperature", "humidity", "unknown"));
config.setFetchToData(fetchToData);
config.setGetLatestValueWithTs(getLatestValueWithTs);
config.setTellFailureIfAbsent(isTellFailureIfAbsent);
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
TbGetAttributesNode node = new TbGetAttributesNode();
node.init(ctx, nodeConfiguration);
return node;
}
private TbMsg getTbMsg(EntityId entityId) {
ObjectNode msgData = JacksonUtil.newObjectNode();
msgData.put("shared_attr_data", "shared_attr_3");
TbMsgMetaData msgMetaData = new TbMsgMetaData();
msgMetaData.putValue("client_attr_metadata", "client_attr_3");
msgMetaData.putValue("server_attr_metadata", "server_attr_3");
return TbMsg.newMsg("TEST", entityId, msgMetaData, JacksonUtil.toString(msgData));
}
private List<String> getAttributeNames(String prefix) {
return List.of(prefix + "_attr_1", prefix + "_attr_2", prefix + "_attr_3", "unknown");
}
private List<AttributeKvEntry> getListAttributeKvEntry(List<String> attributes, long ts) {
return attributes.stream()
.filter(attribute -> !attribute.equals("unknown"))
.map(attribute -> toAttributeKvEntry(ts, attribute))
.collect(Collectors.toList());
}
private BaseAttributeKvEntry toAttributeKvEntry(long ts, String attribute) {
return new BaseAttributeKvEntry(ts, new StringDataEntry(attribute, attribute + "_value"));
}
private List<TsKvEntry> getListTsKvEntry(List<String> keys, long ts) {
long value = 1L;
List<TsKvEntry> kvEntries = new ArrayList<>();
for (String key : keys) {
if (key.equals("unknown")) {
continue;
}
String dataValue = "{\"data\":" + value + "}";
kvEntries.add(new BasicTsKvEntry(ts, new JsonDataEntry(key, dataValue)));
value++;
}
return kvEntries;
}
}

132
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbFetchDeviceCredentialsNodeTest.java

@ -15,10 +15,15 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
@ -27,6 +32,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
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;
@ -36,46 +42,36 @@ 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.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.thingsboard.server.common.data.security.DeviceCredentialsType.ACCESS_TOKEN;
@ExtendWith(MockitoExtension.class)
public class TbFetchDeviceCredentialsNodeTest {
DeviceId deviceId;
TbFetchDeviceCredentialsNode node;
TbFetchDeviceCredentialsNodeConfiguration config;
TbNodeConfiguration nodeConfiguration;
TbContext ctx;
TbMsgCallback callback;
DeviceCredentialsService deviceCredentialsService;
@Mock
private TbContext ctxMock;
@Mock
private TbMsgCallback callbackMock;
@Mock
private DeviceCredentialsService deviceCredentialsServiceMock;
@Spy
private TbFetchDeviceCredentialsNode node;
private DeviceId deviceId;
private TbFetchDeviceCredentialsNodeConfiguration config;
@BeforeEach
void setUp() throws TbNodeException {
deviceId = new DeviceId(UUID.randomUUID());
callback = mock(TbMsgCallback.class);
ctx = mock(TbContext.class);
config = new TbFetchDeviceCredentialsNodeConfiguration().defaultConfiguration();
config.setFetchToMetadata(true);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
node = spy(new TbFetchDeviceCredentialsNode());
node.init(ctx, nodeConfiguration);
deviceCredentialsService = mock(DeviceCredentialsService.class);
willReturn(deviceCredentialsService).given(ctx).getDeviceCredentialsService();
willAnswer(invocation -> {
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setCredentialsType(ACCESS_TOKEN);
return deviceCredentials;
}).given(deviceCredentialsService).findDeviceCredentialsByDeviceId(any(), any());
willAnswer(invocation -> {
return JacksonUtil.newObjectNode();
}).given(deviceCredentialsService).toCredentialsInfo(any());
node.init(ctxMock, new TbNodeConfiguration(JacksonUtil.valueToTree(config)));
}
@AfterEach
@ -86,25 +82,36 @@ public class TbFetchDeviceCredentialsNodeTest {
@Test
void givenDefaultConfig_whenInit_thenOK() {
assertThat(node.config).isEqualTo(config);
assertThat(node.fetchToMetadata).isEqualTo(true);
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA);
}
@Test
void givenDefaultConfig_whenVerify_thenOK() {
TbFetchDeviceCredentialsNodeConfiguration defaultConfig = new TbFetchDeviceCredentialsNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.isFetchToMetadata()).isEqualTo(true);
var defaultConfig = new TbFetchDeviceCredentialsNodeConfiguration().defaultConfiguration();
assertThat(defaultConfig.getFetchTo()).isEqualTo(FetchTo.METADATA);
}
@Test
void givenMsg_whenOnMsg_thenVerifyOutput() throws Exception {
node.onMsg(ctx, getTbMsg(deviceId));
void givenValidMsg_whenOnMsg_thenVerifyOutput() throws Exception {
// GIVEN
doReturn(deviceCredentialsServiceMock).when(ctxMock).getDeviceCredentialsService();
doAnswer(invocation -> {
DeviceCredentials deviceCredentials = new DeviceCredentials();
deviceCredentials.setCredentialsType(ACCESS_TOKEN);
return deviceCredentials;
}).when(deviceCredentialsServiceMock).findDeviceCredentialsByDeviceId(any(), any());
doAnswer(invocation -> JacksonUtil.newObjectNode()).when(deviceCredentialsServiceMock).toCredentialsInfo(any());
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctx, times(1)).tellSuccess(newMsgCaptor.capture());
verify(ctx, never()).tellFailure(any(), any());
verify(deviceCredentialsService, times(1)).findDeviceCredentialsByDeviceId(any(), any());
// WHEN
node.onMsg(ctxMock, getTbMsg(deviceId));
TbMsg newMsg = newMsgCaptor.getValue();
// THEN
var newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(newMsgCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
verify(deviceCredentialsServiceMock, times(1)).findDeviceCredentialsByDeviceId(any(), any());
var newMsg = newMsgCaptor.getValue();
assertThat(newMsg).isNotNull();
assertThat(newMsg.getMetaData().getData().containsKey("credentials")).isEqualTo(true);
@ -112,42 +119,59 @@ public class TbFetchDeviceCredentialsNodeTest {
}
@Test
void givenUnsupportedOriginatorType_whenOnMsg_thenTellFailure() throws Exception {
node.onMsg(ctx, getTbMsg(new CustomerId(UUID.randomUUID())));
void givenUnsupportedOriginatorType_whenOnMsg_thenShouldTellFailure() throws Exception {
// GIVEN
var randomCustomerId = new CustomerId(UUID.randomUUID());
// WHEN
node.onMsg(ctxMock, getTbMsg(randomCustomerId));
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(Exception.class);
verify(ctx, never()).tellSuccess(any());
verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture());
// THEN
var newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
var exceptionCaptor = ArgumentCaptor.forClass(Exception.class);
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture());
assertThat(exceptionCaptor.getValue()).isInstanceOf(RuntimeException.class);
}
@Test
void givenGetDeviceCredentials_whenOnMsg_thenTellFailure() throws Exception {
willAnswer(invocation -> {
return null;
}).given(deviceCredentialsService).findDeviceCredentialsByDeviceId(any(), any());
void givenGetDeviceCredentials_whenOnMsg_thenShouldTellFailure() throws Exception {
// GIVEN
doReturn(deviceCredentialsServiceMock).when(ctxMock).getDeviceCredentialsService();
willAnswer(invocation -> null).given(deviceCredentialsServiceMock).findDeviceCredentialsByDeviceId(any(), any());
node.onMsg(ctx, getTbMsg(deviceId));
// WHEN
node.onMsg(ctxMock, getTbMsg(deviceId));
ArgumentCaptor<TbMsg> newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
ArgumentCaptor<Exception> exceptionCaptor = ArgumentCaptor.forClass(Exception.class);
verify(ctx, never()).tellSuccess(any());
verify(ctx, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture());
// THEN
var newMsgCaptor = ArgumentCaptor.forClass(TbMsg.class);
var exceptionCaptor = ArgumentCaptor.forClass(Exception.class);
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, times(1)).tellFailure(newMsgCaptor.capture(), exceptionCaptor.capture());
assertThat(exceptionCaptor.getValue()).isInstanceOf(RuntimeException.class);
}
@Test
void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
String oldConfig = "{\"fetchToMetadata\":true}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
assertTrue(upgrade.getFirst());
assertEquals(config, JacksonUtil.treeToValue(upgrade.getSecond(), config.getClass()));
}
private TbMsg getTbMsg(EntityId entityId) {
final Map<String, String> mdMap = Map.of(
"country", "US",
"city", "NY"
);
final TbMsgMetaData metaData = new TbMsgMetaData(mdMap);
final var metaData = new TbMsgMetaData(mdMap);
final String data = "{\"TestAttribute_1\": \"humidity\", \"TestAttribute_2\": \"voltage\"}";
return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, metaData, data, callback);
return TbMsg.newMsg("POST_ATTRIBUTES_REQUEST", entityId, metaData, data, callbackMock);
}
}

388
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetAttributesNodeTest.java

@ -0,0 +1,388 @@
/**
* 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.metadata;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.thingsboard.common.util.AbstractListeningExecutor;
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.server.common.data.DataConstants;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.JsonDataEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
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.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class)
public class TbGetAttributesNodeTest {
private static final EntityId ORIGINATOR = new DeviceId(Uuids.timeBased());
private static final TenantId TENANT_ID = TenantId.fromUUID(Uuids.timeBased());
private AbstractListeningExecutor dbExecutor;
@Mock
private TbContext ctxMock;
@Mock
private AttributesService attributesServiceMock;
@Mock
private TimeseriesService timeseriesServiceMock;
private List<String> clientAttributes;
private List<String> serverAttributes;
private List<String> sharedAttributes;
private List<String> tsKeys;
private long ts;
private TbGetAttributesNode node;
@Before
public void before() throws TbNodeException {
dbExecutor = new AbstractListeningExecutor() {
@Override
protected int getThreadPollSize() {
return 3;
}
};
dbExecutor.init();
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDbCallbackExecutor()).thenReturn(dbExecutor);
clientAttributes = getAttributeNames("client");
serverAttributes = getAttributeNames("server");
sharedAttributes = getAttributeNames("shared");
tsKeys = List.of("temperature", "humidity", "unknown");
ts = System.currentTimeMillis();
when(attributesServiceMock.find(TENANT_ID, ORIGINATOR, DataConstants.CLIENT_SCOPE, clientAttributes))
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(clientAttributes, ts)));
when(attributesServiceMock.find(TENANT_ID, ORIGINATOR, DataConstants.SERVER_SCOPE, serverAttributes))
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(serverAttributes, ts)));
when(attributesServiceMock.find(TENANT_ID, ORIGINATOR, DataConstants.SHARED_SCOPE, sharedAttributes))
.thenReturn(Futures.immediateFuture(getListAttributeKvEntry(sharedAttributes, ts)));
when(timeseriesServiceMock.findLatest(TENANT_ID, ORIGINATOR, tsKeys))
.thenReturn(Futures.immediateFuture(getListTsKvEntry(tsKeys, ts)));
}
@After
public void after() {
dbExecutor.destroy();
}
@Test
public void givenFetchAttributesToMetadata_whenOnMsg_thenShouldTellSuccess() throws Exception {
// GIVEN
node = initNode(FetchTo.METADATA, false, false);
var msg = getTbMsg(ORIGINATOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var resultMsg = checkMsg(true);
checkAttributes(resultMsg, FetchTo.METADATA, "cs_", clientAttributes);
checkAttributes(resultMsg, FetchTo.METADATA, "ss_", serverAttributes);
checkAttributes(resultMsg, FetchTo.METADATA, "shared_", sharedAttributes);
checkTs(resultMsg, FetchTo.METADATA, false, tsKeys);
}
@Test
public void givenFetchLatestTimeseriesToMetadata_whenOnMsg_thenShouldTellSuccess() throws Exception {
// GIVEN
node = initNode(FetchTo.METADATA, true, false);
var msg = getTbMsg(ORIGINATOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var resultMsg = checkMsg(true);
checkAttributes(resultMsg, FetchTo.METADATA, "cs_", clientAttributes);
checkAttributes(resultMsg, FetchTo.METADATA, "ss_", serverAttributes);
checkAttributes(resultMsg, FetchTo.METADATA, "shared_", sharedAttributes);
checkTs(resultMsg, FetchTo.METADATA, true, tsKeys);
}
@Test
public void givenFetchAttributesToData_whenOnMsg_thenShouldTellSuccess() throws Exception {
// GIVEN
node = initNode(FetchTo.DATA, false, false);
var msg = getTbMsg(ORIGINATOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var resultMsg = checkMsg(true);
checkAttributes(resultMsg, FetchTo.DATA, "cs_", clientAttributes);
checkAttributes(resultMsg, FetchTo.DATA, "ss_", serverAttributes);
checkAttributes(resultMsg, FetchTo.DATA, "shared_", sharedAttributes);
checkTs(resultMsg, FetchTo.DATA, false, tsKeys);
}
@Test
public void givenFetchLatestTimeseriesToData_whenOnMsg_thenShouldTellSuccess() throws Exception {
// GIVEN
node = initNode(FetchTo.DATA, true, false);
var msg = getTbMsg(ORIGINATOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var resultMsg = checkMsg(true);
checkAttributes(resultMsg, FetchTo.DATA, "cs_", clientAttributes);
checkAttributes(resultMsg, FetchTo.DATA, "ss_", serverAttributes);
checkAttributes(resultMsg, FetchTo.DATA, "shared_", sharedAttributes);
checkTs(resultMsg, FetchTo.DATA, true, tsKeys);
}
@Test
public void givenFetchAttributesToMetadata_whenOnMsg_thenShouldTellFailure() throws Exception {
// GIVEN
node = initNode(FetchTo.METADATA, false, true);
var msg = getTbMsg(ORIGINATOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMsg = checkMsg(false);
checkAttributes(actualMsg, FetchTo.METADATA, "cs_", clientAttributes);
checkAttributes(actualMsg, FetchTo.METADATA, "ss_", serverAttributes);
checkAttributes(actualMsg, FetchTo.METADATA, "shared_", sharedAttributes);
checkTs(actualMsg, FetchTo.METADATA, false, tsKeys);
}
@Test
public void givenFetchLatestTimeseriesToData_whenOnMsg_thenShouldTellFailure() throws Exception {
// GIVEN
node = initNode(FetchTo.DATA, true, true);
var msg = getTbMsg(ORIGINATOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMsg = checkMsg(false);
checkAttributes(actualMsg, FetchTo.DATA, "cs_", clientAttributes);
checkAttributes(actualMsg, FetchTo.DATA, "ss_", serverAttributes);
checkAttributes(actualMsg, FetchTo.DATA, "shared_", sharedAttributes);
checkTs(actualMsg, FetchTo.DATA, true, tsKeys);
}
@Test
public void givenFetchLatestTimeseriesToDataAndDataIsNotJsonObject_whenOnMsg_thenException() throws Exception {
// GIVEN
node = initNode(FetchTo.DATA, true, true);
var msg = TbMsg.newMsg("TEST", ORIGINATOR, new TbMsgMetaData(), "[]");
// WHEN
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg));
// THEN
verify(ctxMock, never()).tellSuccess(any());
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!");
}
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbGetAttributesNodeConfiguration().defaultConfiguration();
var node = new TbGetAttributesNode();
String oldConfig = "{\"fetchToData\":false," +
"\"clientAttributeNames\":[]," +
"\"sharedAttributeNames\":[]," +
"\"serverAttributeNames\":[]," +
"\"latestTsKeyNames\":[]," +
"\"tellFailureIfAbsent\":true," +
"\"getLatestValueWithTs\":false}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()));
}
private TbMsg checkMsg(boolean checkSuccess) {
var msgCaptor = ArgumentCaptor.forClass(TbMsg.class);
if (checkSuccess) {
verify(ctxMock, timeout(5000)).tellSuccess(msgCaptor.capture());
} else {
var exceptionCaptor = ArgumentCaptor.forClass(RuntimeException.class);
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, timeout(5000)).tellFailure(msgCaptor.capture(), exceptionCaptor.capture());
var exception = exceptionCaptor.getValue();
assertNotNull(exception);
assertNotNull(exception.getMessage());
assertTrue(exception.getMessage().startsWith("The following attribute/telemetry keys is not present in the DB:"));
}
var resultMsg = msgCaptor.getValue();
assertNotNull(resultMsg);
assertNotNull(resultMsg.getMetaData());
assertNotNull(resultMsg.getData());
return resultMsg;
}
private void checkAttributes(TbMsg actualMsg, FetchTo fetchTo, String prefix, List<String> attributes) {
var msgData = JacksonUtil.toJsonNode(actualMsg.getData());
attributes.stream()
.filter(attribute -> !attribute.equals("unknown"))
.forEach(attribute -> {
String result = null;
if (FetchTo.DATA.equals(fetchTo)) {
result = msgData.get(prefix + attribute).asText();
} else if (FetchTo.METADATA.equals(fetchTo)) {
result = actualMsg.getMetaData().getValue(prefix + attribute);
}
assertNotNull(result);
assertEquals(attribute + "_value", result);
});
}
private void checkTs(TbMsg actualMsg, FetchTo fetchTo, boolean getLatestValueWithTs, List<String> tsKeys) {
var msgData = JacksonUtil.toJsonNode(actualMsg.getData());
long value = 1L;
for (var key : tsKeys) {
if (key.equals("unknown")) {
continue;
}
String actualValue = null;
String expectedValue;
if (getLatestValueWithTs) {
expectedValue = "{\"ts\":" + ts + ",\"value\":{\"data\":" + value + "}}";
} else {
expectedValue = "{\"data\":" + value + "}";
}
if (FetchTo.DATA.equals(fetchTo)) {
actualValue = JacksonUtil.toString(msgData.get(key));
} else if (FetchTo.METADATA.equals(fetchTo)) {
actualValue = actualMsg.getMetaData().getValue(key);
}
assertNotNull(actualValue);
assertEquals(expectedValue, actualValue);
value++;
}
}
private TbGetAttributesNode initNode(FetchTo fetchTo, boolean getLatestValueWithTs, boolean isTellFailureIfAbsent) throws TbNodeException {
var config = new TbGetAttributesNodeConfiguration();
config.setClientAttributeNames(List.of("client_attr_1", "client_attr_2", "${client_attr_metadata}", "unknown"));
config.setServerAttributeNames(List.of("server_attr_1", "server_attr_2", "${server_attr_metadata}", "unknown"));
config.setSharedAttributeNames(List.of("shared_attr_1", "shared_attr_2", "$[shared_attr_data]", "unknown"));
config.setLatestTsKeyNames(List.of("temperature", "humidity", "unknown"));
config.setFetchTo(fetchTo);
config.setGetLatestValueWithTs(getLatestValueWithTs);
config.setTellFailureIfAbsent(isTellFailureIfAbsent);
var nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
var node = new TbGetAttributesNode();
node.init(ctxMock, nodeConfiguration);
return node;
}
private TbMsg getTbMsg(EntityId entityId) {
var msgData = JacksonUtil.newObjectNode();
msgData.put("shared_attr_data", "shared_attr_3");
var msgMetaData = new TbMsgMetaData();
msgMetaData.putValue("client_attr_metadata", "client_attr_3");
msgMetaData.putValue("server_attr_metadata", "server_attr_3");
return TbMsg.newMsg("TEST", entityId, msgMetaData, JacksonUtil.toString(msgData));
}
private List<String> getAttributeNames(String prefix) {
return List.of(prefix + "_attr_1", prefix + "_attr_2", prefix + "_attr_3", "unknown");
}
private List<AttributeKvEntry> getListAttributeKvEntry(List<String> attributesList, long ts) {
return attributesList.stream()
.filter(attribute -> !attribute.equals("unknown"))
.map(attribute -> toAttributeKvEntry(ts, attribute))
.collect(Collectors.toList());
}
private BaseAttributeKvEntry toAttributeKvEntry(long ts, String attribute) {
return new BaseAttributeKvEntry(ts, new StringDataEntry(attribute, attribute + "_value"));
}
private List<TsKvEntry> getListTsKvEntry(List<String> keysList, long ts) {
long value = 1L;
var kvEntriesList = new ArrayList<TsKvEntry>();
for (var key : keysList) {
if (key.equals("unknown")) {
continue;
}
String dataValue = "{\"data\":" + value + "}";
kvEntriesList.add(new BasicTsKvEntry(ts, new JsonDataEntry(key, dataValue)));
value++;
}
return kvEntriesList;
}
}

489
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerAttributeNodeTest.java

@ -15,101 +15,492 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
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.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.concurrent.Callable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
@RunWith(MockitoJUnitRunner.class)
public class TbGetCustomerAttributeNodeTest extends AbstractAttributeNodeTest {
User user = new User();
Asset asset = new Asset();
Device device = new Device();
@ExtendWith(MockitoExtension.class)
public class TbGetCustomerAttributeNodeTest {
@Before
public void initDataForTests() throws TbNodeException {
init(new TbGetCustomerAttributeNode());
user.setCustomerId(customerId);
user.setId(new UserId(UUID.randomUUID()));
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final CustomerId CUSTOMER_ID = new CustomerId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() {
@Override
public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
asset.setCustomerId(customerId);
asset.setId(new AssetId(UUID.randomUUID()));
@Override
public void execute(@NotNull Runnable command) {
command.run();
}
};
@Mock
private TbContext ctxMock;
@Mock
private AttributesService attributesServiceMock;
@Mock
private TimeseriesService timeseriesServiceMock;
@Mock
private UserService userServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
private TbGetCustomerAttributeNode node;
private TbGetEntityDataNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private TbMsg msg;
device.setCustomerId(customerId);
device.setId(new DeviceId(Uuids.timeBased()));
@BeforeEach
public void setUp() {
node = new TbGetCustomerAttributeNode();
config = new TbGetEntityDataNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
}
@Override
protected TbEntityGetAttrNode getEmptyNode() {
return new TbGetCustomerAttributeNode();
@Test
public void givenConfigWithNullFetchTo_whenInit_thenException() {
// GIVEN
config.setFetchTo(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
verify(ctxMock, never()).tellSuccess(any());
}
@Override
EntityId getEntityId() {
return customerId;
@Test
public void givenConfigWithNullDataToFetch_whenInit_thenException() {
// GIVEN
config.setDataToFetch(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("DataToFetch property has invalid value: null. Only ATTRIBUTES and LATEST_TELEMETRY values supported!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void errorThrownIfCannotLoadAttributes() {
mockFindUser(user);
errorThrownIfCannotLoadAttributes(user);
public void givenConfigWithUnsupportedDataToFetch_whenInit_thenException() {
// GIVEN
config.setDataToFetch(DataToFetch.FIELDS);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("DataToFetch property has invalid value: FIELDS. Only ATTRIBUTES and LATEST_TELEMETRY values supported!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void errorThrownIfCannotLoadAttributesAsync() {
mockFindUser(user);
errorThrownIfCannotLoadAttributesAsync(user);
public void givenDefaultConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
assertThat(node.config).isEqualTo(config);
assertThat(config.getDataMapping()).isEqualTo(Map.of("alarmThreshold", "threshold"));
assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES);
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA);
}
@Test
public void failedChainUsedIfCustomerCannotBeFound() {
when(ctx.getUserService()).thenReturn(userService);
when(userService.findUserByIdAsync(any(), eq(user.getId()))).thenReturn(Futures.immediateFuture(null));
failedChainUsedIfCustomerCannotBeFound(user);
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
config.setDataMapping(Map.of(
"sourceAttr1", "targetKey1",
"sourceAttr2", "targetKey2",
"sourceAttr3", "targetKey3"));
config.setDataToFetch(DataToFetch.LATEST_TELEMETRY);
config.setFetchTo(FetchTo.DATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
assertThat(node.config).isEqualTo(config);
assertThat(config.getDataMapping()).isEqualTo(Map.of(
"sourceAttr1", "targetKey1",
"sourceAttr2", "targetKey2",
"sourceAttr3", "targetKey3"));
assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY);
assertThat(node.fetchTo).isEqualTo(FetchTo.DATA);
}
@Test
public void customerAttributeAddedInMetadata() {
entityAttributeAddedInMetadata(customerId, "CUSTOMER");
public void givenEmptyAttributesMapping_whenInit_thenException() {
// GIVEN
var expectedExceptionMessage = "At least one mapping entry should be specified!";
config.setDataMapping(Collections.emptyMap());
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo(expectedExceptionMessage);
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void usersCustomerAttributesFetched() {
mockFindUser(user);
usersCustomerAttributesFetched(user);
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() {
// GIVEN
node.fetchTo = FetchTo.DATA;
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]");
// WHEN
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void assetsCustomerAttributesFetched() {
mockFindAsset(asset);
assetsCustomerAttributesFetched(asset);
public void givenDidNotFindEntity_whenOnMsg_thenShouldTellFailure() {
// GIVEN
var userId = new UserId(UUID.randomUUID());
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", userId, new TbMsgMetaData(), "{}");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(null)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(userId));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
var actualExceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, times(1))
.tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture());
var actualMessage = actualMessageCaptor.getValue();
var actualException = actualExceptionCaptor.getValue();
var expectedExceptionMessage = String.format(
"Failed to find customer for entity with id: %s and type: %s",
userId.getId(), userId.getEntityType().getNormalName());
assertEquals(msg, actualMessage);
assertEquals(expectedExceptionMessage, actualException.getMessage());
assertInstanceOf(NoSuchElementException.class, actualException);
}
@Test
public void deviceCustomerAttributesFetched() {
mockFindDevice(device);
deviceCustomerAttributesFetched(device);
public void givenFetchAttributesToData_whenOnMsg_thenShouldFetchAttributesToData() {
// GIVEN
var device = new Device(new DeviceId(UUID.randomUUID()));
device.setCustomerId(CUSTOMER_ID);
prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, device.getId());
List<AttributeKvEntry> attributesList = List.of(
new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey2", "sourceValue2"), 2L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey3", "sourceValue3"), 3L)
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
doReturn(device).when(deviceServiceMock).findDeviceById(eq(TENANT_ID), eq(device.getId()));
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
"\"humidity\":77," +
"\"messageBodyPattern1\":\"targetKey2\"," +
"\"messageBodyPattern2\":\"sourceKey3\"," +
"\"targetKey1\":\"sourceValue1\"," +
"\"targetKey2\":\"sourceValue2\"," +
"\"targetKey3\":\"sourceValue3\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void deviceCustomerTelemetryFetched() throws TbNodeException {
mockFindDevice(device);
deviceCustomerTelemetryFetched(device);
public void givenFetchAttributesToMetaData_whenOnMsg_thenShouldFetchAttributesToMetaData() {
// GIVEN
var user = new User(new UserId(UUID.randomUUID()));
user.setCustomerId(CUSTOMER_ID);
prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, user.getId());
List<AttributeKvEntry> attributesList = List.of(
new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey2", "sourceValue2"), 2L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey3", "sourceValue3"), 3L)
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(eq(TENANT_ID), eq(user.getId()));
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(CUSTOMER_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
"metaDataPattern1", "sourceKey2",
"metaDataPattern2", "targetKey3",
"targetKey1", "sourceValue1",
"targetKey2", "sourceValue2",
"targetKey3", "sourceValue3"
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void givenFetchTelemetryToData_whenOnMsg_thenShouldFetchTelemetryToData() {
// GIVEN
var customer = new Customer(new CustomerId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, customer.getId());
List<TsKvEntry> timeseriesList = List.of(
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey2", "sourceValue2")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey3", "sourceValue3"))
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(customer.getId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseriesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
"\"humidity\":77," +
"\"messageBodyPattern1\":\"targetKey2\"," +
"\"messageBodyPattern2\":\"sourceKey3\"," +
"\"targetKey1\":\"sourceValue1\"," +
"\"targetKey2\":\"sourceValue2\"," +
"\"targetKey3\":\"sourceValue3\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenFetchTelemetryToMetaData_whenOnMsg_thenShouldFetchTelemetryToMetaData() {
// GIVEN
var asset = new Asset(new AssetId(UUID.randomUUID()));
asset.setCustomerId(new CustomerId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, asset.getId());
List<TsKvEntry> timeseriesList = List.of(
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey2", "sourceValue2")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey3", "sourceValue3"))
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId()));
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(asset.getCustomerId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseriesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
"metaDataPattern1", "sourceKey2",
"metaDataPattern2", "targetKey3",
"targetKey1", "sourceValue1",
"targetKey2", "sourceValue2",
"targetKey3", "sourceValue3"
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbGetEntityDataNodeConfiguration().defaultConfiguration();
var node = new TbGetCustomerAttributeNode();
String oldConfig = "{\"attrMapping\":{\"alarmThreshold\":\"threshold\"},\"telemetry\":false}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()));
}
private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) {
config.setDataMapping(Map.of(
"sourceKey1", "targetKey1",
"${metaDataPattern1}", "$[messageBodyPattern1]",
"$[messageBodyPattern2]", "${metaDataPattern2}"));
config.setDataToFetch(dataToFetch);
config.setFetchTo(fetchTo);
node.config = config;
node.fetchTo = fetchTo;
var msgMetaData = new TbMsgMetaData();
msgMetaData.putValue("metaDataPattern1", "sourceKey2");
msgMetaData.putValue("metaDataPattern2", "targetKey3");
var msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}";
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData);
}
@RequiredArgsConstructor
private static class ListMatcher<T> implements ArgumentMatcher<List<T>> {
private final List<T> expectedList;
@Override
public boolean matches(List<T> actualList) {
if (actualList == expectedList) {
return true;
}
if (actualList.size() != expectedList.size()) {
return false;
}
return actualList.containsAll(expectedList);
}
}
}

483
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetCustomerDetailsNodeTest.java

@ -0,0 +1,483 @@
/**
* 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.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
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.ContactBasedEntityDetails;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
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.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.user.UserService;
import java.util.Collections;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.concurrent.Callable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbGetCustomerDetailsNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() {
@Override
public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void execute(@NotNull Runnable command) {
command.run();
}
};
@Mock
private TbContext ctxMock;
@Mock
private CustomerService customerServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private EntityViewService entityViewServiceMock;
@Mock
private UserService userServiceMock;
@Mock
private EdgeService edgeServiceMock;
private TbGetCustomerDetailsNode node;
private TbGetCustomerDetailsNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private TbMsg msg;
private Customer customer;
@BeforeEach
public void setUp() {
node = new TbGetCustomerDetailsNode();
config = new TbGetCustomerDetailsNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
customer = new Customer();
customer.setId(new CustomerId(UUID.randomUUID()));
customer.setTitle("Customer title");
customer.setCountry("Customer country");
customer.setCity("Customer city");
customer.setState("Customer state");
customer.setZip("123456");
customer.setAddress("Customer address 1");
customer.setAddress2("Customer address 2");
customer.setPhone("+123456789");
customer.setEmail("email@tenant.com");
customer.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":\"Customer description\"}"));
}
@Test
public void givenConfigWithNullFetchTo_whenInit_thenException() {
// GIVEN
config.setDetailsList(List.of(ContactBasedEntityDetails.ID));
config.setFetchTo(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void givenDefaultConfig_whenInit_thenOK() {
assertThat(config.getDetailsList()).isEqualTo(Collections.emptyList());
assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA);
}
@Test
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
config.setDetailsList(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE));
config.setFetchTo(FetchTo.METADATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
assertThat(node.config).isEqualTo(config);
assertThat(config.getDetailsList()).isEqualTo(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE));
assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA);
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA);
}
@Test
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() {
// GIVEN
node.fetchTo = FetchTo.DATA;
msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]");
// WHEN
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void givenAllEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchAllToData() {
// GIVEN
var device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
device.setCustomerId(customer.getId());
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.values()), device.getId());
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device));
mockFindCustomer();
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"," +
"\"customer_id\":\"" + customer.getId() + "\"," +
"\"customer_title\":\"" + customer.getTitle() + "\"," +
"\"customer_country\":\"" + customer.getCountry() + "\"," +
"\"customer_city\":\"" + customer.getCity() + "\"," +
"\"customer_state\":\"" + customer.getState() + "\"," +
"\"customer_zip\":\"" + customer.getZip() + "\"," +
"\"customer_address\":\"" + customer.getAddress() + "\"," +
"\"customer_address2\":\"" + customer.getAddress2() + "\"," +
"\"customer_phone\":\"" + customer.getPhone() + "\"," +
"\"customer_email\":\"" + customer.getEmail() + "\"," +
"\"customer_additionalInfo\":\"" + customer.getAdditionalInfo().get("description").asText() + "\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenSomeEntityDetailsAndFetchToMetadata_whenOnMsg_thenShouldTellSuccessAndFetchSomeToMetaData() {
// GIVEN
var asset = new Asset();
asset.setId(new AssetId(UUID.randomUUID()));
asset.setCustomerId(customer.getId());
prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE), asset.getId());
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
when(assetServiceMock.findAssetByIdAsync(eq(TENANT_ID), eq(asset.getId()))).thenReturn(Futures.immediateFuture(asset));
mockFindCustomer();
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(msg.getMetaData().getData());
expectedMsgMetaData.putValue("customer_id", customer.getId().getId().toString());
expectedMsgMetaData.putValue("customer_title", customer.getTitle());
expectedMsgMetaData.putValue("customer_phone", customer.getPhone());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void givenNotPresentEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() {
// GIVEN
customer.setZip(null);
customer.setAddress(null);
customer.setAddress2(null);
var user = new User();
user.setId(new UserId(UUID.randomUUID()));
user.setCustomerId(customer.getId());
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), user.getId());
when(ctxMock.getUserService()).thenReturn(userServiceMock);
when(userServiceMock.findUserByIdAsync(eq(TENANT_ID), eq(user.getId()))).thenReturn(Futures.immediateFuture(user));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
mockFindCustomer();
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenDidNotFindCustomer_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() {
// GIVEN
var edge = new Edge();
edge.setId(new EdgeId(UUID.randomUUID()));
edge.setCustomerId(customer.getId());
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId());
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByIdAsync(eq(TENANT_ID), eq(edge.getId()))).thenReturn(Futures.immediateFuture(edge));
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByIdAsync(eq(TENANT_ID), eq(customer.getId()))).thenReturn(Futures.immediateFuture(null));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenDidNotFindOriginator_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() {
// GIVEN
var edge = new Edge();
edge.setId(new EdgeId(UUID.randomUUID()));
edge.setCustomerId(customer.getId());
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), edge.getId());
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getEdgeService()).thenReturn(edgeServiceMock);
when(edgeServiceMock.findEdgeByIdAsync(eq(TENANT_ID), eq(edge.getId()))).thenReturn(Futures.immediateFuture(null));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenOriginatorNotAssignedToCustomer_whenOnMsg_thenShouldTellFailureAndFetchNothingToData() {
// GIVEN
var device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
device.setName("Thermostat");
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2), device.getId());
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
var actualExceptionCaptor = ArgumentCaptor.forClass(Exception.class);
verify(ctxMock, times(1)).tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture());
verify(ctxMock, never()).tellSuccess(any());
var actualMsg = actualMessageCaptor.getValue();
var actualException = actualExceptionCaptor.getValue();
assertThat(actualMsg.getData()).isEqualTo(msg.getData());
assertThat(actualMsg.getMetaData()).isEqualTo(msg.getMetaData());
assertThat(actualException).isInstanceOf(RuntimeException.class);
assertThat(actualException.getMessage()).isEqualTo("Device with name 'Thermostat' is not assigned to Customer!");
}
@Test
public void givenNullDescriptionAndAddInfoEntityDetails_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() {
// GIVEN
customer.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":null}"));
var device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
device.setCustomerId(customer.getId());
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO), device.getId());
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(deviceServiceMock.findDeviceByIdAsync(eq(TENANT_ID), eq(device.getId()))).thenReturn(Futures.immediateFuture(device));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
mockFindCustomer();
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenUnsupportedEntityType_whenOnMsg_thenShouldTellFailureAndFetchNothingToMetaData() {
// GIVEN
var dashboard = new Dashboard();
dashboard.setId(new DashboardId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.STATE), dashboard.getId());
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
var actualExceptionCaptor = ArgumentCaptor.forClass(Exception.class);
verify(ctxMock, times(1)).tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture());
verify(ctxMock, never()).tellSuccess(any());
var actualMsg = actualMessageCaptor.getValue();
var actualException = actualExceptionCaptor.getValue();
assertThat(actualMsg.getData()).isEqualTo(msg.getData());
assertThat(actualMsg.getMetaData()).isEqualTo(msg.getMetaData());
assertThat(actualException).isInstanceOf(NoSuchElementException.class);
assertThat(actualException.getMessage()).isEqualTo("Entity with entityType 'DASHBOARD' is not supported.");
}
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbGetCustomerDetailsNodeConfiguration().defaultConfiguration();
var node = new TbGetCustomerDetailsNode();
String oldConfig = "{\"detailsList\":[],\"addToMetadata\":false}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()));
}
private void prepareMsgAndConfig(FetchTo fetchTo, List<ContactBasedEntityDetails> detailsList, EntityId originator) {
config.setDetailsList(detailsList);
config.setFetchTo(fetchTo);
node.config = config;
node.fetchTo = fetchTo;
var msgMetaData = new TbMsgMetaData();
msgMetaData.putValue("metaKey1", "metaValue1");
msgMetaData.putValue("metaKey2", "metaValue2");
var msgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"}";
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData);
}
private void mockFindCustomer() {
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getCustomerService()).thenReturn(customerServiceMock);
when(customerServiceMock.findCustomerByIdAsync(eq(TENANT_ID), eq(customer.getId()))).thenReturn(Futures.immediateFuture(customer));
}
}

45
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetDeviceAttrNodeTest.java

@ -0,0 +1,45 @@
/**
* 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.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.util.TbPair;
public class TbGetDeviceAttrNodeTest {
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbGetDeviceAttrNodeConfiguration().defaultConfiguration();
var node = new TbGetDeviceAttrNode();
String oldConfig = "{\"fetchToData\":false," +
"\"clientAttributeNames\":[]," +
"\"sharedAttributeNames\":[]," +
"\"serverAttributeNames\":[]," +
"\"latestTsKeyNames\":[]," +
"\"tellFailureIfAbsent\":true," +
"\"getLatestValueWithTs\":false," +
"\"deviceRelationsQuery\":{\"direction\":\"FROM\",\"maxLevel\":1,\"relationType\":\"Contains\",\"deviceTypes\":[\"default\"]," +
"\"fetchLastLevelOnly\":false}}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()));
}
}

357
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetOriginatorFieldsNodeTest.java

@ -0,0 +1,357 @@
/**
* 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.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
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.dao.device.DeviceService;
import java.util.Collections;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbGetOriginatorFieldsNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId DUMMY_TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() {
@Override
public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void execute(@NotNull Runnable command) {
command.run();
}
};
@Mock
private TbContext ctxMock;
@Mock
private DeviceService deviceServiceMock;
private TbGetOriginatorFieldsNode node;
private TbGetOriginatorFieldsConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private TbMsg msg;
@BeforeEach
public void setUp() {
node = new TbGetOriginatorFieldsNode();
config = new TbGetOriginatorFieldsConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
}
@Test
public void givenConfigWithNullFetchTo_whenInit_thenException() {
// GIVEN
config.setFetchTo(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void givenDefaultConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN-WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
assertThat(node.config).isEqualTo(config);
assertThat(config.getDataMapping()).isEqualTo(Map.of(
"name", "originatorName",
"type", "originatorType"));
assertThat(config.isIgnoreNullStrings()).isEqualTo(false);
assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA);
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA);
}
@Test
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
config.setDataMapping(Map.of(
"email", "originatorEmail",
"title", "originatorTitle",
"country", "originatorCountry"));
config.setIgnoreNullStrings(true);
config.setFetchTo(FetchTo.DATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
assertThat(node.config).isEqualTo(config);
assertThat(config.getDataMapping()).isEqualTo(Map.of(
"email", "originatorEmail",
"title", "originatorTitle",
"country", "originatorCountry"));
assertThat(config.isIgnoreNullStrings()).isEqualTo(true);
assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA);
assertThat(node.fetchTo).isEqualTo(FetchTo.DATA);
}
@Test
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() {
// GIVEN
node.fetchTo = FetchTo.DATA;
msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]");
// WHEN
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void givenValidMsgAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchToData() throws TbNodeException, ExecutionException, InterruptedException {
// GIVEN
var device = new Device();
device.setId(DUMMY_DEVICE_ORIGINATOR);
device.setName("Test device");
device.setType("Test device type");
config.setDataMapping(Map.of(
"name", "originatorName",
"type", "originatorType",
"label", "originatorLabel"));
config.setIgnoreNullStrings(true);
config.setFetchTo(FetchTo.DATA);
node.config = config;
node.fetchTo = FetchTo.DATA;
var msgMetaData = new TbMsgMetaData();
var msgData = "{\"temp\":42,\"humidity\":77}";
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID);
when(deviceServiceMock.findDeviceById(eq(DUMMY_TENANT_ID), eq(device.getId()))).thenReturn(device);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42,\"humidity\":77,\"originatorName\":\"Test device\",\"originatorType\":\"Test device type\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msgMetaData);
}
@Test
public void givenValidMsgAndFetchToMetaData_whenOnMsg_thenShouldTellSuccessAndFetchToMetaData() throws TbNodeException, ExecutionException, InterruptedException {
// GIVEN
var device = new Device();
device.setId(DUMMY_DEVICE_ORIGINATOR);
device.setName("Test device");
device.setType("Test device type");
config.setDataMapping(Map.of(
"name", "originatorName",
"type", "originatorType",
"label", "originatorLabel"));
config.setIgnoreNullStrings(true);
config.setFetchTo(FetchTo.METADATA);
node.config = config;
node.fetchTo = FetchTo.METADATA;
var msgMetaData = new TbMsgMetaData(Map.of(
"testKey1", "testValue1",
"testKey2", "123"));
var msgData = "[\"value1\",\"value2\"]";
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID);
when(deviceServiceMock.findDeviceById(eq(DUMMY_TENANT_ID), eq(device.getId()))).thenReturn(device);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
"testKey1", "testValue1",
"testKey2", "123",
"originatorName", "Test device",
"originatorType", "Test device type"
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void givenNullEntityFieldsAndIgnoreNullStringsFalse_whenOnMsg_thenShouldTellSuccessAndFetchNullField() throws TbNodeException, ExecutionException, InterruptedException {
// GIVEN
var device = new Device();
device.setId(DUMMY_DEVICE_ORIGINATOR);
device.setName("Test device");
device.setType("Test device type");
config.setDataMapping(Map.of(
"name", "originatorName",
"type", "originatorType",
"label", "originatorLabel"));
config.setIgnoreNullStrings(false);
config.setFetchTo(FetchTo.METADATA);
node.config = config;
node.fetchTo = FetchTo.METADATA;
var msgMetaData = new TbMsgMetaData(Map.of(
"testKey1", "testValue1",
"testKey2", "123"));
var msgData = "[\"value1\",\"value2\"]";
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(ctxMock.getTenantId()).thenReturn(DUMMY_TENANT_ID);
when(deviceServiceMock.findDeviceById(eq(DUMMY_TENANT_ID), eq(device.getId()))).thenReturn(device);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
"testKey1", "testValue1",
"testKey2", "123",
"originatorName", "Test device",
"originatorType", "Test device type",
"originatorLabel", "null"
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void givenEmptyFieldsMapping_whenInit_thenException() {
// GIVEN
config.setDataMapping(Collections.emptyMap());
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("At least one mapping entry should be specified!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void givenUnsupportedEntityType_whenOnMsg_thenShouldTellFailureWithSameMsg() throws TbNodeException, ExecutionException, InterruptedException {
// GIVEN
config.setDataMapping(Map.of(
"name", "originatorName",
"type", "originatorType",
"label", "originatorLabel"));
config.setIgnoreNullStrings(false);
config.setFetchTo(FetchTo.METADATA);
node.config = config;
node.fetchTo = FetchTo.METADATA;
var msgMetaData = new TbMsgMetaData(Map.of(
"testKey1", "testValue1",
"testKey2", "123"));
var msgData = "[\"value1\",\"value2\"]";
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", new DashboardId(UUID.randomUUID()), msgMetaData, msgData);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellFailure(actualMessageCaptor.capture(), any());
verify(ctxMock, never()).tellSuccess(any());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msgMetaData);
}
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbGetOriginatorFieldsConfiguration().defaultConfiguration();
var node = new TbGetOriginatorFieldsNode();
String oldConfig = "{\"fieldsMapping\":{\"name\":\"originatorName\",\"type\":\"originatorType\"},\"ignoreNullStrings\":false}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()));
}
}

633
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetRelatedAttributeNodeTest.java

@ -15,150 +15,617 @@
*/
package org.thingsboard.rule.engine.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
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.data.RelationsQuery;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.relation.EntitySearchDirection;
import org.thingsboard.server.common.data.relation.RelationEntityTypeFilter;
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.session.SessionMsgType;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.HashMap;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.UUID;
import java.util.concurrent.Callable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
@RunWith(MockitoJUnitRunner.class)
public class TbGetRelatedAttributeNodeTest extends AbstractAttributeNodeTest {
User user = new User();
Asset asset = new Asset();
Device device = new Device();
@ExtendWith(MockitoExtension.class)
public class TbGetRelatedAttributeNodeTest {
private static final EntityId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() {
@Override
public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void execute(@NotNull Runnable command) {
command.run();
}
};
@Mock
private TbContext ctxMock;
@Mock
private AttributesService attributesServiceMock;
@Mock
private RelationService relationService;
private TimeseriesService timeseriesServiceMock;
@Mock
private RelationService relationServiceMock;
@Mock
private DeviceService deviceServiceMock;
private TbGetRelatedAttributeNode node;
private TbGetRelatedDataNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private EntityRelation entityRelation;
private TbMsg msg;
@Before
public void initDataForTests() throws TbNodeException {
init(new TbGetRelatedAttributeNode());
@BeforeEach
public void setUp() {
node = new TbGetRelatedAttributeNode();
config = new TbGetRelatedDataNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
entityRelation = new EntityRelation();
entityRelation.setTo(customerId);
entityRelation.setType(EntityRelation.CONTAINS_TYPE);
when(ctx.getRelationService()).thenReturn(relationService);
}
user.setCustomerId(customerId);
user.setId(new UserId(UUID.randomUUID()));
entityRelation.setFrom(user.getId());
@Test
public void givenConfigWithNullFetchTo_whenInit_thenException() {
// GIVEN
config.setFetchTo(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
asset.setCustomerId(customerId);
asset.setId(new AssetId(UUID.randomUUID()));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
device.setCustomerId(customerId);
device.setId(new DeviceId(UUID.randomUUID()));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
verify(ctxMock, never()).tellSuccess(any());
}
@Override
protected TbEntityGetAttrNode getEmptyNode() {
return new TbGetRelatedAttributeNode();
@Test
public void givenConfigWithNullDataToFetch_whenInit_thenException() {
// GIVEN
config.setDataToFetch(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("DataToFetch property cannot be null! Supported values are: " + Arrays.toString(DataToFetch.values()));
verify(ctxMock, never()).tellSuccess(any());
}
@Override
TbGetEntityAttrNodeConfiguration getTbNodeConfig() {
return getConfig(false);
@Test
public void givenDefaultConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
var nodeConfig = (TbGetRelatedDataNodeConfiguration) node.config;
assertThat(nodeConfig).isEqualTo(config);
assertThat(nodeConfig.getDataMapping()).isEqualTo(Map.of("serialNumber", "sn"));
assertThat(nodeConfig.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES);
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA);
var relationsQuery = new RelationsQuery();
var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setDirection(EntitySearchDirection.FROM);
relationsQuery.setMaxLevel(1);
relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter));
assertThat(nodeConfig.getRelationsQuery()).isEqualTo(relationsQuery);
}
@Override
TbGetEntityAttrNodeConfiguration getTbNodeConfigForTelemetry() {
return getConfig(true);
@Test
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
config.setDataMapping(Map.of(
"sourceAttr1", "targetKey1",
"sourceAttr2", "targetKey2",
"sourceAttr3", "targetKey3"));
config.setDataToFetch(DataToFetch.LATEST_TELEMETRY);
config.setFetchTo(FetchTo.DATA);
var relationsQuery = new RelationsQuery();
var relationEntityTypeFilter = new RelationEntityTypeFilter(EntityRelation.CONTAINS_TYPE, Collections.emptyList());
relationsQuery.setDirection(EntitySearchDirection.FROM);
relationsQuery.setMaxLevel(1);
relationsQuery.setFilters(Collections.singletonList(relationEntityTypeFilter));
config.setRelationsQuery(relationsQuery);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
var nodeConfig = (TbGetRelatedDataNodeConfiguration) node.config;
assertThat(nodeConfig).isEqualTo(config);
assertThat(nodeConfig.getDataMapping()).isEqualTo(Map.of(
"sourceAttr1", "targetKey1",
"sourceAttr2", "targetKey2",
"sourceAttr3", "targetKey3"
));
assertThat(nodeConfig.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY);
assertThat(node.fetchTo).isEqualTo(FetchTo.DATA);
assertThat(nodeConfig.getRelationsQuery()).isEqualTo(relationsQuery);
}
private TbGetEntityAttrNodeConfiguration getConfig(boolean isTelemetry) {
TbGetRelatedAttrNodeConfiguration config = new TbGetRelatedAttrNodeConfiguration();
config = config.defaultConfiguration();
Map<String, String> conf = new HashMap<>();
conf.put(keyAttrConf, valueAttrConf);
config.setAttrMapping(conf);
config.setTelemetry(isTelemetry);
return config;
@Test
public void givenEmptyAttributesMapping_whenInit_thenException() {
// GIVEN
var expectedExceptionMessage = "At least one mapping entry should be specified!";
config.setDataMapping(Collections.emptyMap());
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo(expectedExceptionMessage);
verify(ctxMock, never()).tellSuccess(any());
}
@Override
EntityId getEntityId() {
return customerId;
@Test
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() {
// GIVEN
node.fetchTo = FetchTo.DATA;
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]");
// WHEN
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void errorThrownIfCannotLoadAttributes() {
entityRelation.setFrom(user.getId());
entityRelation.setTo(customerId);
when(relationService.findByQuery(any(), any())).thenReturn(Futures.immediateFuture(List.of(entityRelation)));
errorThrownIfCannotLoadAttributes(user);
public void givenDidNotFindEntity_whenOnMsg_thenShouldTellFailure() {
// GIVEN
prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, DUMMY_DEVICE_ORIGINATOR);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
doReturn(Futures.immediateFuture(null)).when(relationServiceMock).findByQuery(eq(TENANT_ID), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
var actualExceptionCaptor = ArgumentCaptor.forClass(Throwable.class);
verify(ctxMock, never()).tellSuccess(any());
verify(ctxMock, times(1))
.tellFailure(actualMessageCaptor.capture(), actualExceptionCaptor.capture());
var actualMessage = actualMessageCaptor.getValue();
var actualException = actualExceptionCaptor.getValue();
var expectedExceptionMessage = "Failed to find related entity to message originator using relation query specified in the configuration!";
assertEquals(msg, actualMessage);
assertEquals(expectedExceptionMessage, actualException.getMessage());
assertInstanceOf(NoSuchElementException.class, actualException);
}
@Test
public void errorThrownIfCannotLoadAttributesAsync() {
entityRelation.setFrom(user.getId());
entityRelation.setTo(customerId);
when(relationService.findByQuery(any(), any())).thenReturn(Futures.immediateFuture(List.of(entityRelation)));
errorThrownIfCannotLoadAttributesAsync(user);
public void givenFetchAttributesToData_whenOnMsg_thenShouldFetchAttributesToData() {
// GIVEN
var customer = new Customer(new CustomerId(UUID.randomUUID()));
var user = new User(new UserId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, customer.getId());
entityRelation.setFrom(customer.getId());
entityRelation.setTo(user.getId());
entityRelation.setType(EntityRelation.CONTAINS_TYPE);
List<AttributeKvEntry> attributes = List.of(
new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey2", "sourceValue2"), 2L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey3", "sourceValue3"), 3L)
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any());
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(user.getId()), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributes));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
"\"humidity\":77," +
"\"messageBodyPattern1\":\"targetKey2\"," +
"\"messageBodyPattern2\":\"sourceKey3\"," +
"\"targetKey1\":\"sourceValue1\"," +
"\"targetKey2\":\"sourceValue2\"," +
"\"targetKey3\":\"sourceValue3\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void failedChainUsedIfCustomerCannotBeFound() {
entityRelation.setFrom(customerId);
entityRelation.setTo(null);
when(relationService.findByQuery(any(), any())).thenReturn(Futures.immediateFuture(List.of(entityRelation)));
failedChainUsedIfCustomerCannotBeFound(user);
public void givenFetchAttributesToMetaData_whenOnMsg_thenShouldFetchAttributesToMetaData() {
// GIVEN
var firstCustomer = new Customer(new CustomerId(UUID.randomUUID()));
var secondCustomer = new Customer(new CustomerId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, firstCustomer.getId());
entityRelation.setFrom(firstCustomer.getId());
entityRelation.setTo(secondCustomer.getId());
entityRelation.setType(EntityRelation.MANAGES_TYPE);
List<AttributeKvEntry> attributes = List.of(
new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey2", "sourceValue2"), 2L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey3", "sourceValue3"), 3L)
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any());
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(secondCustomer.getId()), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributes));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
"metaDataPattern1", "sourceKey2",
"metaDataPattern2", "targetKey3",
"targetKey1", "sourceValue1",
"targetKey2", "sourceValue2",
"targetKey3", "sourceValue3"
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void customerAttributeAddedInMetadata() {
entityRelation.setFrom(customerId);
entityRelation.setTo(customerId);
when(relationService.findByQuery(any(), any())).thenReturn(Futures.immediateFuture(List.of(entityRelation)));
entityAttributeAddedInMetadata(customerId, "CUSTOMER");
public void givenFetchTelemetryToData_whenOnMsg_thenShouldFetchTelemetryToData() {
// GIVEN
var dashboard = new Dashboard(new DashboardId(UUID.randomUUID()));
var entityView = new EntityView(new EntityViewId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, dashboard.getId());
entityRelation.setFrom(dashboard.getId());
entityRelation.setTo(entityView.getId());
entityRelation.setType(EntityRelation.CONTAINS_TYPE);
List<TsKvEntry> timeseries = List.of(
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey2", "sourceValue2")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey3", "sourceValue3"))
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any());
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(entityView.getId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseries));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
"\"humidity\":77," +
"\"messageBodyPattern1\":\"targetKey2\"," +
"\"messageBodyPattern2\":\"sourceKey3\"," +
"\"targetKey1\":\"sourceValue1\"," +
"\"targetKey2\":\"sourceValue2\"," +
"\"targetKey3\":\"sourceValue3\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void usersCustomerAttributesFetched() {
entityRelation.setFrom(user.getId());
entityRelation.setTo(customerId);
when(relationService.findByQuery(any(), any())).thenReturn(Futures.immediateFuture(List.of(entityRelation)));
usersCustomerAttributesFetched(user);
public void givenFetchTelemetryToMetaData_whenOnMsg_thenShouldFetchTelemetryToMetaData() {
// GIVEN
var tenant = new Tenant(new TenantId(UUID.randomUUID()));
var device = new Device(new DeviceId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, tenant.getId());
entityRelation.setFrom(tenant.getId());
entityRelation.setTo(device.getId());
entityRelation.setType(EntityRelation.CONTAINS_TYPE);
List<TsKvEntry> timeseries = List.of(
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey2", "sourceValue2")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey3", "sourceValue3"))
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(tenant.getId());
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(tenant.getId()), any());
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(tenant.getId()), eq(device.getId()), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseries));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
"metaDataPattern1", "sourceKey2",
"metaDataPattern2", "targetKey3",
"targetKey1", "sourceValue1",
"targetKey2", "sourceValue2",
"targetKey3", "sourceValue3"
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void assetsCustomerAttributesFetched() {
public void givenFetchFieldsToData_whenOnMsg_thenShouldFetchFieldsToData() {
// GIVEN
var device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
device.setName("Device Name");
var asset = new Asset(new AssetId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.DATA, DataToFetch.FIELDS, asset.getId());
entityRelation.setFrom(asset.getId());
entityRelation.setTo(customerId);
when(relationService.findByQuery(any(), any())).thenReturn(Futures.immediateFuture(List.of(entityRelation)));
assetsCustomerAttributesFetched(asset);
entityRelation.setTo(device.getId());
entityRelation.setType(EntityRelation.CONTAINS_TYPE);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any());
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(deviceServiceMock.findDeviceById(eq(TENANT_ID), eq(device.getId()))).thenReturn(device);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern\":\"relatedEntityId\"," +
"\"relatedEntityId\":\"" + device.getId().getId() + "\",\"relatedEntityName\":\"" + device.getName() + "\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void deviceCustomerAttributesFetched() {
entityRelation.setFrom(device.getId());
entityRelation.setTo(customerId);
when(relationService.findByQuery(any(), any())).thenReturn(Futures.immediateFuture(List.of(entityRelation)));
deviceCustomerAttributesFetched(device);
public void givenFetchFieldsToMetadata_whenOnMsg_thenShouldFetchFieldsToMetadata() {
// GIVEN
var device = new Device();
device.setId(new DeviceId(UUID.randomUUID()));
device.setName("Device Name");
var asset = new Asset(new AssetId(UUID.randomUUID()));
prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.FIELDS, asset.getId());
entityRelation.setFrom(asset.getId());
entityRelation.setTo(device.getId());
entityRelation.setType(EntityRelation.CONTAINS_TYPE);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getRelationService()).thenReturn(relationServiceMock);
doReturn(Futures.immediateFuture(List.of(entityRelation))).when(relationServiceMock).findByQuery(eq(TENANT_ID), any());
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(deviceServiceMock.findDeviceById(eq(TENANT_ID), eq(device.getId()))).thenReturn(device);
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetadata = new TbMsgMetaData(Map.of(
"metaDataPattern", "relatedEntityName",
"relatedEntityId", device.getId().getId().toString(),
"relatedEntityName", device.getName()
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetadata);
}
@Test
public void deviceCustomerTelemetryFetched() throws TbNodeException {
entityRelation.setFrom(device.getId());
entityRelation.setTo(customerId);
when(relationService.findByQuery(any(), any())).thenReturn(Futures.immediateFuture(List.of(entityRelation)));
deviceCustomerTelemetryFetched(device);
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbGetRelatedDataNodeConfiguration().defaultConfiguration();
var node = new TbGetRelatedAttributeNode();
String oldConfig = "{\"attrMapping\":{\"serialNumber\":\"sn\"}," +
"\"relationsQuery\":{\"direction\":\"FROM\",\"maxLevel\":1," +
"\"filters\":[{\"relationType\":\"Contains\",\"entityTypes\":[]}]," +
"\"fetchLastLevelOnly\":false}," +
"\"telemetry\":false}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()));
}
private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) {
config.setDataToFetch(dataToFetch);
config.setFetchTo(fetchTo);
node.config = config;
node.fetchTo = fetchTo;
var msgMetaData = new TbMsgMetaData();
String msgData;
if (dataToFetch.equals(DataToFetch.FIELDS)) {
config.setDataMapping(Map.of(
"id", "$[messageBodyPattern]",
"name", "${metaDataPattern}"));
msgMetaData.putValue("metaDataPattern", "relatedEntityName");
msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern\":\"relatedEntityId\"}";
} else {
config.setDataMapping(Map.of(
"sourceKey1", "targetKey1",
"${metaDataPattern1}", "$[messageBodyPattern1]",
"$[messageBodyPattern2]", "${metaDataPattern2}"));
msgMetaData.putValue("metaDataPattern1", "sourceKey2");
msgMetaData.putValue("metaDataPattern2", "targetKey3");
msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}";
}
msg = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), originator, msgMetaData, msgData);
}
@RequiredArgsConstructor
private static class ListMatcher<T> implements ArgumentMatcher<List<T>> {
private final List<T> expectedList;
@Override
public boolean matches(List<T> actualList) {
if (actualList == expectedList) {
return true;
}
if (actualList.size() != expectedList.size()) {
return false;
}
return actualList.containsAll(expectedList);
}
}
}

422
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantAttributeNodeTest.java

@ -15,94 +15,422 @@
*/
package org.thingsboard.rule.engine.metadata;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.junit.MockitoJUnitRunner;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeConfiguration;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.StringDataEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
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.dao.attributes.AttributesService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Callable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.server.common.data.DataConstants.SERVER_SCOPE;
@RunWith(MockitoJUnitRunner.class)
public class TbGetTenantAttributeNodeTest extends AbstractAttributeNodeTest {
@ExtendWith(MockitoExtension.class)
public class TbGetTenantAttributeNodeTest {
User user = new User();
Asset asset = new Asset();
Device device = new Device();
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
private static final TenantId TENANT_ID = new TenantId(UUID.randomUUID());
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() {
@Override
public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Before
public void initDataForTests() throws TbNodeException {
init(new TbGetTenantAttributeNode());
@Override
public void execute(@NotNull Runnable command) {
command.run();
}
};
@Mock
private TbContext ctxMock;
@Mock
private AttributesService attributesServiceMock;
@Mock
private TimeseriesService timeseriesServiceMock;
private TbGetTenantAttributeNode node;
private TbGetEntityDataNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private TbMsg msg;
user.setTenantId(tenantId);
user.setId(new UserId(UUID.randomUUID()));
@BeforeEach
public void setUp() {
node = new TbGetTenantAttributeNode();
config = new TbGetEntityDataNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
}
asset.setTenantId(tenantId);
asset.setId(new AssetId(UUID.randomUUID()));
@Test
public void givenConfigWithNullFetchTo_whenInit_thenException() {
// GIVEN
config.setFetchTo(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
device.setTenantId(tenantId);
device.setId(new DeviceId(UUID.randomUUID()));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
when(ctx.getTenantId()).thenReturn(tenantId);
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
verify(ctxMock, never()).tellSuccess(any());
}
@Override
protected TbEntityGetAttrNode getEmptyNode() {
return new TbGetTenantAttributeNode();
@Test
public void givenConfigWithNullDataToFetch_whenInit_thenException() {
// GIVEN
config.setDataToFetch(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("DataToFetch property has invalid value: null. Only ATTRIBUTES and LATEST_TELEMETRY values supported!");
verify(ctxMock, never()).tellSuccess(any());
}
@Override
EntityId getEntityId() {
return tenantId;
@Test
public void givenConfigWithUnsupportedDataToFetch_whenInit_thenException() {
// GIVEN
config.setDataToFetch(DataToFetch.FIELDS);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("DataToFetch property has invalid value: FIELDS. Only ATTRIBUTES and LATEST_TELEMETRY values supported!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void errorThrownIfCannotLoadAttributes() {
errorThrownIfCannotLoadAttributes(user);
public void givenDefaultConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
assertThat(node.config).isEqualTo(config);
assertThat(config.getDataMapping()).isEqualTo(Map.of("alarmThreshold", "threshold"));
assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.ATTRIBUTES);
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA);
}
@Test
public void errorThrownIfCannotLoadAttributesAsync() {
errorThrownIfCannotLoadAttributesAsync(user);
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
config.setDataMapping(Map.of(
"sourceAttr1", "targetKey1",
"sourceAttr2", "targetKey2",
"sourceAttr3", "targetKey3"));
config.setDataToFetch(DataToFetch.LATEST_TELEMETRY);
config.setFetchTo(FetchTo.DATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
assertThat(node.config).isEqualTo(config);
assertThat(config.getDataMapping()).isEqualTo(Map.of(
"sourceAttr1", "targetKey1",
"sourceAttr2", "targetKey2",
"sourceAttr3", "targetKey3"));
assertThat(config.getDataToFetch()).isEqualTo(DataToFetch.LATEST_TELEMETRY);
assertThat(node.fetchTo).isEqualTo(FetchTo.DATA);
}
@Test
public void failedChainUsedIfTenantIdFromCtxCannotBeFound() {
when(ctx.getTenantId()).thenReturn(null);
failedChainUsedIfCustomerCannotBeFound(user);
public void givenEmptyAttributesMapping_whenInit_thenException() {
// GIVEN
var expectedExceptionMessage = "At least one mapping entry should be specified!";
config.setDataMapping(Collections.emptyMap());
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo(expectedExceptionMessage);
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void customerAttributeAddedInMetadata() {
entityAttributeAddedInMetadata(tenantId, "TENANT");
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() {
// GIVEN
node.fetchTo = FetchTo.DATA;
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]");
// WHEN
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void usersCustomerAttributesFetched() {
usersCustomerAttributesFetched(user);
public void givenFetchAttributesToData_whenOnMsg_thenShouldFetchAttributesToData() {
// GIVEN
var deviceId = new DeviceId(UUID.randomUUID());
prepareMsgAndConfig(FetchTo.DATA, DataToFetch.ATTRIBUTES, deviceId);
List<AttributeKvEntry> attributesList = List.of(
new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey2", "sourceValue2"), 2L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey3", "sourceValue3"), 3L)
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
"\"humidity\":77," +
"\"messageBodyPattern1\":\"targetKey2\"," +
"\"messageBodyPattern2\":\"sourceKey3\"," +
"\"targetKey1\":\"sourceValue1\"," +
"\"targetKey2\":\"sourceValue2\"," +
"\"targetKey3\":\"sourceValue3\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void assetsCustomerAttributesFetched() {
assetsCustomerAttributesFetched(asset);
public void givenFetchAttributesToMetaData_whenOnMsg_thenShouldFetchAttributesToMetaData() {
// GIVEN
prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.ATTRIBUTES, TENANT_ID);
List<AttributeKvEntry> attributesList = List.of(
new BaseAttributeKvEntry(new StringDataEntry("sourceKey1", "sourceValue1"), 1L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey2", "sourceValue2"), 2L),
new BaseAttributeKvEntry(new StringDataEntry("sourceKey3", "sourceValue3"), 3L)
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getAttributesService()).thenReturn(attributesServiceMock);
when(attributesServiceMock.find(eq(TENANT_ID), eq(TENANT_ID), eq(SERVER_SCOPE), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(attributesList));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
"metaDataPattern1", "sourceKey2",
"metaDataPattern2", "targetKey3",
"targetKey1", "sourceValue1",
"targetKey2", "sourceValue2",
"targetKey3", "sourceValue3"
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void deviceCustomerAttributesFetched() {
deviceCustomerAttributesFetched(device);
public void givenFetchTelemetryToData_whenOnMsg_thenShouldFetchTelemetryToData() {
// GIVEN
var customerId = new CustomerId(UUID.randomUUID());
prepareMsgAndConfig(FetchTo.DATA, DataToFetch.LATEST_TELEMETRY, customerId);
List<TsKvEntry> timeseries = List.of(
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey2", "sourceValue2")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey3", "sourceValue3"))
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(TENANT_ID), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseries));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"temp\":42," +
"\"humidity\":77," +
"\"messageBodyPattern1\":\"targetKey2\"," +
"\"messageBodyPattern2\":\"sourceKey3\"," +
"\"targetKey1\":\"sourceValue1\"," +
"\"targetKey2\":\"sourceValue2\"," +
"\"targetKey3\":\"sourceValue3\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void deviceCustomerTelemetryFetched() throws TbNodeException {
deviceCustomerTelemetryFetched(device);
public void givenFetchTelemetryToMetaData_whenOnMsg_thenShouldFetchTelemetryToMetaData() {
// GIVEN
var ruleChainId = new RuleChainId(UUID.randomUUID());
prepareMsgAndConfig(FetchTo.METADATA, DataToFetch.LATEST_TELEMETRY, ruleChainId);
List<TsKvEntry> timeseries = List.of(
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey1", "sourceValue1")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey2", "sourceValue2")),
new BasicTsKvEntry(1L, new StringDataEntry("sourceKey3", "sourceValue3"))
);
var expectedPatternProcessedKeysList = List.of("sourceKey1", "sourceKey2", "sourceKey3");
when(ctxMock.getTenantId()).thenReturn(TENANT_ID);
when(ctxMock.getTimeseriesService()).thenReturn(timeseriesServiceMock);
when(timeseriesServiceMock.findLatest(eq(TENANT_ID), eq(TENANT_ID), argThat(new ListMatcher<>(expectedPatternProcessedKeysList))))
.thenReturn(Futures.immediateFuture(timeseries));
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(Map.of(
"metaDataPattern1", "sourceKey2",
"metaDataPattern2", "targetKey3",
"targetKey1", "sourceValue1",
"targetKey2", "sourceValue2",
"targetKey3", "sourceValue3"
));
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbGetEntityDataNodeConfiguration().defaultConfiguration();
var node = new TbGetTenantAttributeNode();
String oldConfig = "{\"attrMapping\":{\"alarmThreshold\":\"threshold\"},\"telemetry\":false}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()));
}
private void prepareMsgAndConfig(FetchTo fetchTo, DataToFetch dataToFetch, EntityId originator) {
config.setDataMapping(Map.of(
"sourceKey1", "targetKey1",
"${metaDataPattern1}", "$[messageBodyPattern1]",
"$[messageBodyPattern2]", "${metaDataPattern2}"));
config.setDataToFetch(dataToFetch);
config.setFetchTo(fetchTo);
node.config = config;
node.fetchTo = fetchTo;
var msgMetaData = new TbMsgMetaData();
msgMetaData.putValue("metaDataPattern1", "sourceKey2");
msgMetaData.putValue("metaDataPattern2", "targetKey3");
var msgData = "{\"temp\":42,\"humidity\":77,\"messageBodyPattern1\":\"targetKey2\",\"messageBodyPattern2\":\"sourceKey3\"}";
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", originator, msgMetaData, msgData);
}
@RequiredArgsConstructor
private static class ListMatcher<T> implements ArgumentMatcher<List<T>> {
private final List<T> expectedList;
@Override
public boolean matches(List<T> actualList) {
if (actualList == expectedList) {
return true;
}
if (actualList.size() != expectedList.size()) {
return false;
}
return actualList.containsAll(expectedList);
}
}
}

298
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTenantDetailsNodeTest.java

@ -0,0 +1,298 @@
/**
* 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.metadata;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
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.ContactBasedEntityDetails;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
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.dao.tenant.TenantService;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class TbGetTenantDetailsNodeTest {
private static final DeviceId DUMMY_DEVICE_ORIGINATOR = new DeviceId(UUID.randomUUID());
@Mock
private TbContext ctxMock;
@Mock
private TenantService tenantServiceMock;
private TbGetTenantDetailsNode node;
private TbGetTenantDetailsNodeConfiguration config;
private TbNodeConfiguration nodeConfiguration;
private TbMsg msg;
private Tenant tenant;
@BeforeEach
public void setUp() {
node = new TbGetTenantDetailsNode();
config = new TbGetTenantDetailsNodeConfiguration().defaultConfiguration();
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
tenant = new Tenant();
tenant.setId(new TenantId(UUID.randomUUID()));
tenant.setTitle("Tenant title");
tenant.setCountry("Tenant country");
tenant.setCity("Tenant city");
tenant.setState("Tenant state");
tenant.setZip("123456");
tenant.setAddress("Tenant address 1");
tenant.setAddress2("Tenant address 2");
tenant.setPhone("+123456789");
tenant.setEmail("email@tenant.com");
tenant.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":\"Tenant description\"}"));
}
@Test
public void givenConfigWithNullFetchTo_whenInit_thenException() {
// GIVEN
config.setDetailsList(List.of(ContactBasedEntityDetails.ID));
config.setFetchTo(null);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
var exception = assertThrows(TbNodeException.class, () -> node.init(ctxMock, nodeConfiguration));
// THEN
assertThat(exception.getMessage()).isEqualTo("FetchTo cannot be null!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void givenDefaultConfig_whenInit_thenOK() {
// THEN
assertThat(config.getDetailsList()).isEqualTo(Collections.emptyList());
assertThat(config.getFetchTo()).isEqualTo(FetchTo.DATA);
}
@Test
public void givenCustomConfig_whenInit_thenOK() throws TbNodeException {
// GIVEN
config.setDetailsList(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE));
config.setFetchTo(FetchTo.METADATA);
nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
// WHEN
node.init(ctxMock, nodeConfiguration);
// THEN
assertThat(node.config).isEqualTo(config);
assertThat(config.getDetailsList()).isEqualTo(List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.PHONE));
assertThat(config.getFetchTo()).isEqualTo(FetchTo.METADATA);
assertThat(node.fetchTo).isEqualTo(FetchTo.METADATA);
}
@Test
public void givenMsgDataIsNotAnJsonObjectAndFetchToData_whenOnMsg_thenException() {
// GIVEN
node.fetchTo = FetchTo.DATA;
msg = TbMsg.newMsg("SOME_MESSAGE_TYPE", DUMMY_DEVICE_ORIGINATOR, new TbMsgMetaData(), "[]");
// WHEN
var exception = assertThrows(IllegalArgumentException.class, () -> node.onMsg(ctxMock, msg));
// THEN
assertThat(exception.getMessage()).isEqualTo("Message body is not an object!");
verify(ctxMock, never()).tellSuccess(any());
}
@Test
public void givenAllEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchAllToData() {
// GIVEN
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.values()));
mockFindTenant();
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"," +
"\"tenant_id\":\"" + tenant.getId() + "\"," +
"\"tenant_title\":\"" + tenant.getTitle() + "\"," +
"\"tenant_country\":\"" + tenant.getCountry() + "\"," +
"\"tenant_city\":\"" + tenant.getCity() + "\"," +
"\"tenant_state\":\"" + tenant.getState() + "\"," +
"\"tenant_zip\":\"" + tenant.getZip() + "\"," +
"\"tenant_address\":\"" + tenant.getAddress() + "\"," +
"\"tenant_address2\":\"" + tenant.getAddress2() + "\"," +
"\"tenant_phone\":\"" + tenant.getPhone() + "\"," +
"\"tenant_email\":\"" + tenant.getEmail() + "\"," +
"\"tenant_additionalInfo\":\"" + tenant.getAdditionalInfo().get("description").asText() + "\"}";
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(expectedMsgData);
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenSomeEntityDetailsAndFetchToMetadata_whenOnMsg_thenShouldTellSuccessAndFetchSomeToMetaData() {
// GIVEN
prepareMsgAndConfig(FetchTo.METADATA, List.of(ContactBasedEntityDetails.ID, ContactBasedEntityDetails.TITLE, ContactBasedEntityDetails.PHONE));
mockFindTenant();
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
var expectedMsgMetaData = new TbMsgMetaData(msg.getMetaData().getData());
expectedMsgMetaData.putValue("tenant_id", tenant.getId().getId().toString());
expectedMsgMetaData.putValue("tenant_title", tenant.getTitle());
expectedMsgMetaData.putValue("tenant_phone", tenant.getPhone());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(expectedMsgMetaData);
}
@Test
public void givenNotPresentEntityDetailsAndFetchToData_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() {
// GIVEN
tenant.setZip(null);
tenant.setAddress(null);
tenant.setAddress2(null);
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2));
mockFindTenant();
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenDidNotFindTenant_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() {
// GIVEN
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ZIP, ContactBasedEntityDetails.ADDRESS, ContactBasedEntityDetails.ADDRESS2));
when(ctxMock.getTenantId()).thenReturn(tenant.getId());
when(ctxMock.getTenantService()).thenReturn(tenantServiceMock);
when(tenantServiceMock.findTenantByIdAsync(eq(tenant.getId()), eq(tenant.getId()))).thenReturn(Futures.immediateFuture(null));
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenNullDescriptionAndAddInfoEntityDetails_whenOnMsg_thenShouldTellSuccessAndFetchNothingToData() {
// GIVEN
tenant.setAdditionalInfo(JacksonUtil.toJsonNode("{\"someProperty\":\"someValue\",\"description\":null}"));
prepareMsgAndConfig(FetchTo.DATA, List.of(ContactBasedEntityDetails.ADDITIONAL_INFO));
mockFindTenant();
// WHEN
node.onMsg(ctxMock, msg);
// THEN
var actualMessageCaptor = ArgumentCaptor.forClass(TbMsg.class);
verify(ctxMock, times(1)).tellSuccess(actualMessageCaptor.capture());
verify(ctxMock, never()).tellFailure(any(), any());
assertThat(actualMessageCaptor.getValue().getData()).isEqualTo(msg.getData());
assertThat(actualMessageCaptor.getValue().getMetaData()).isEqualTo(msg.getMetaData());
}
@Test
public void givenOldConfig_whenUpgrade_thenShouldReturnTrueResultWithNewConfig() throws Exception {
var defaultConfig = new TbGetTenantDetailsNodeConfiguration().defaultConfiguration();
var node = new TbGetTenantDetailsNode();
String oldConfig = "{\"detailsList\":[],\"addToMetadata\":false}";
JsonNode configJson = JacksonUtil.toJsonNode(oldConfig);
TbPair<Boolean, JsonNode> upgrade = node.upgrade(0, configJson);
Assertions.assertTrue(upgrade.getFirst());
Assertions.assertEquals(defaultConfig, JacksonUtil.treeToValue(upgrade.getSecond(), defaultConfig.getClass()));
}
private void prepareMsgAndConfig(FetchTo fetchTo, List<ContactBasedEntityDetails> detailsList) {
config.setDetailsList(detailsList);
config.setFetchTo(fetchTo);
node.config = config;
node.fetchTo = fetchTo;
var msgMetaData = new TbMsgMetaData();
msgMetaData.putValue("metaKey1", "metaValue1");
msgMetaData.putValue("metaKey2", "metaValue2");
var msgData = "{\"dataKey1\":123,\"dataKey2\":\"dataValue2\"}";
msg = TbMsg.newMsg("POST_TELEMETRY_REQUEST", DUMMY_DEVICE_ORIGINATOR, msgMetaData, msgData);
}
private void mockFindTenant() {
when(ctxMock.getTenantId()).thenReturn(tenant.getId());
when(ctxMock.getTenantService()).thenReturn(tenantServiceMock);
when(tenantServiceMock.findTenantByIdAsync(eq(tenant.getId()), eq(tenant.getId()))).thenReturn(Futures.immediateFuture(tenant));
}
}

12
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/transform/TbChangeOriginatorNodeTest.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.asset.AssetService;
import java.util.NoSuchElementException;
import java.util.concurrent.Callable;
import static org.junit.Assert.assertEquals;
@ -48,11 +49,12 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.thingsboard.rule.engine.api.TbRelationTypes.FAILURE;
@RunWith(MockitoJUnitRunner.class)
public class TbChangeOriginatorNodeTest {
private static final String CUSTOMER_SOURCE = "CUSTOMER";
private TbChangeOriginatorNode node;
@Mock
@ -152,13 +154,17 @@ public class TbChangeOriginatorNodeTest {
when(ctx.getAssetService()).thenReturn(assetService);
when(assetService.findAssetByIdAsync(any(), eq(assetId))).thenReturn(Futures.immediateFuture(null));
ArgumentCaptor<NoSuchElementException> exceptionCaptor = ArgumentCaptor.forClass(NoSuchElementException.class);
node.onMsg(ctx, msg);
verify(ctx).tellNext(same(msg), same(FAILURE));
verify(ctx).tellFailure(same(msg), exceptionCaptor.capture());
assertEquals("Failed to find new originator!", exceptionCaptor.getValue().getMessage());
}
public void init() throws TbNodeException {
TbChangeOriginatorNodeConfiguration config = new TbChangeOriginatorNodeConfiguration();
config.setOriginatorSource(TbChangeOriginatorNode.CUSTOMER_SOURCE);
config.setOriginatorSource(CUSTOMER_SOURCE);
TbNodeConfiguration nodeConfiguration = new TbNodeConfiguration(JacksonUtil.valueToTree(config));
when(ctx.getDbCallbackExecutor()).thenReturn(dbExecutor);

170
rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/util/EntitiesCustomerIdAsyncLoaderTest.java

@ -0,0 +1,170 @@
/**
* 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.util;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.TbContext;
import org.thingsboard.rule.engine.api.TbNodeException;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.user.UserService;
import java.util.EnumSet;
import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
public class EntitiesCustomerIdAsyncLoaderTest {
private static final EnumSet<EntityType> SUPPORTED_ENTITY_TYPES = EnumSet.of(
EntityType.CUSTOMER,
EntityType.USER,
EntityType.ASSET,
EntityType.DEVICE
);
private static final ListeningExecutor DB_EXECUTOR = new ListeningExecutor() {
@Override
public <T> ListenableFuture<T> executeAsync(Callable<T> task) {
try {
return Futures.immediateFuture(task.call());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Override
public void execute(@NotNull Runnable command) {
command.run();
}
};
@Mock
private TbContext ctxMock;
@Mock
private UserService userServiceMock;
@Mock
private AssetService assetServiceMock;
@Mock
private DeviceService deviceServiceMock;
@Test
public void givenCustomerEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var customer = new Customer(new CustomerId(UUID.randomUUID()));
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, customer.getId()).get();
// THEN
assertEquals(customer.getId(), actualCustomerId);
}
@Test
public void givenUserEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var user = new User(new UserId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
user.setCustomerId(expectedCustomerId);
when(ctxMock.getUserService()).thenReturn(userServiceMock);
doReturn(Futures.immediateFuture(user)).when(userServiceMock).findUserByIdAsync(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, user.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenAssetEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var asset = new Asset(new AssetId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
asset.setCustomerId(expectedCustomerId);
when(ctxMock.getAssetService()).thenReturn(assetServiceMock);
doReturn(Futures.immediateFuture(asset)).when(assetServiceMock).findAssetByIdAsync(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, asset.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenDeviceEntityType_whenFindEntityIdAsync_thenOK() throws ExecutionException, InterruptedException {
// GIVEN
var device = new Device(new DeviceId(UUID.randomUUID()));
var expectedCustomerId = new CustomerId(UUID.randomUUID());
device.setCustomerId(expectedCustomerId);
when(ctxMock.getDeviceService()).thenReturn(deviceServiceMock);
doReturn(device).when(deviceServiceMock).findDeviceById(any(), any());
when(ctxMock.getDbCallbackExecutor()).thenReturn(DB_EXECUTOR);
// WHEN
var actualCustomerId = EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, device.getId()).get();
// THEN
assertEquals(expectedCustomerId, actualCustomerId);
}
@Test
public void givenUnsupportedEntityTypes_whenFindEntityIdAsync_thenException() {
for (var entityType : EntityType.values()) {
if (!SUPPORTED_ENTITY_TYPES.contains(entityType)) {
var entityId = EntityIdFactory.getByTypeAndUuid(entityType, UUID.randomUUID());
var expectedExceptionMsg = "org.thingsboard.rule.engine.api.TbNodeException: Unexpected originator EntityType: " + entityType;
var exception = assertThrows(ExecutionException.class,
() -> EntitiesCustomerIdAsyncLoader.findEntityIdAsync(ctxMock, entityId).get());
assertInstanceOf(TbNodeException.class, exception.getCause());
assertEquals(expectedExceptionMsg, exception.getMessage());
}
}
}
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save