Browse Source

Merge branch 'develop/3.5' into replace_ObjectMapper_usages_with_JacksonUtil

# Conflicts:
#	application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
pull/8373/head
AndriiD 3 years ago
parent
commit
73833a666b
  1. 77
      application/src/main/data/json/system/widget_bundles/home_page_widgets.json
  2. 19
      application/src/main/data/upgrade/3.4.4/schema_update.sql
  3. 7
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  4. 114
      application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
  5. 1
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  6. 137
      application/src/main/java/org/thingsboard/server/controller/AlarmController.java
  7. 6
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  8. 15
      application/src/main/java/org/thingsboard/server/controller/NotificationController.java
  9. 31
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java
  11. 1
      application/src/main/java/org/thingsboard/server/service/component/AnnotationComponentDiscoveryService.java
  12. 51
      application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java
  13. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  14. 58
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  15. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java
  16. 7
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java
  17. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
  18. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java
  19. 5
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  20. 1
      application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java
  21. 6
      application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java
  22. 2
      application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java
  23. 11
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java
  24. 11
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java
  25. 8
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java
  26. 6
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java
  27. 15
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java
  28. 73
      application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java
  29. 2
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  30. 6
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
  31. 3
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  32. 23
      application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java
  33. 4
      application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java
  34. 5
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java
  35. 46
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java
  36. 4
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/DashboardImportService.java
  37. 6
      application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java
  38. 44
      application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java
  39. 13
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java
  40. 2
      application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java
  41. 15
      application/src/main/resources/thingsboard.yml
  42. 19
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java
  43. 72
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
  44. 48
      application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java
  45. 6
      application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java
  46. 31
      application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java
  47. 7
      application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java
  48. 8
      application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java
  49. 198
      application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java
  50. 25
      application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java
  51. 13
      application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java
  52. 55
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  53. 36
      application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java
  54. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java
  55. 4
      common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java
  56. 4
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  57. 6
      common/data/src/main/java/org/thingsboard/server/common/data/User.java
  58. 39
      common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java
  59. 4
      common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java
  60. 19
      common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java
  61. 4
      common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java
  62. 31
      common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantNotFoundException.java
  63. 3
      common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java
  64. 5
      common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java
  65. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java
  66. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java
  67. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java
  68. 3
      common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntityActionNotificationInfo.java
  69. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java
  70. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java
  71. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java
  72. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java
  73. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java
  74. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java
  75. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java
  76. 6
      common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java
  77. 24
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java
  78. 29
      common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentClusteringMode.java
  79. 10
      common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentDescriptor.java
  80. 5
      common/data/src/main/java/org/thingsboard/server/common/data/rule/RuleNode.java
  81. 8
      common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java
  82. 5
      common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java
  83. 47
      common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntityActionTrigger.java
  84. 27
      common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java
  85. 3
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  86. 54
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java
  87. 33
      common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java
  88. 2
      common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java
  89. 1
      common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java
  90. 27
      common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java
  91. 3
      common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java
  92. 40
      common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.java
  93. 83
      common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java
  94. 62
      common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java
  95. 74
      common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java
  96. 100
      common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java
  97. 2
      common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java
  98. 5
      dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java
  99. 11
      dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java
  100. 8
      dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java

77
application/src/main/data/json/system/widget_bundles/home_page_widgets.json

File diff suppressed because one or more lines are too long

19
application/src/main/data/upgrade/3.4.4/schema_update.sql

@ -47,11 +47,15 @@ UPDATE alarm SET acknowledged = true, cleared = false WHERE status = 'ACTIVE_ACK
UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK'; UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK';
UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK'; UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK';
-- Drop index by 'status' column and replace with new one that has only active alarms; -- Drop index by 'status' column and replace with new indexes that has only active alarms;
DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active; DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active;
CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active
ON alarm USING btree (originator_id, type) WHERE cleared = false; ON alarm USING btree (originator_id, type) WHERE cleared = false;
DROP INDEX IF EXISTS idx_alarm_tenant_alarm_type_active;
CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_active
ON alarm USING btree (tenant_id, type) WHERE cleared = false;
-- Cover index by alarm type to optimize propagated alarm queries; -- Cover index by alarm type to optimize propagated alarm queries;
DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id; DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id;
CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm
@ -610,3 +614,16 @@ END
$$; $$;
-- TTL DROP PARTITIONS FUNCTIONS UPDATE END -- TTL DROP PARTITIONS FUNCTIONS UPDATE END
-- RULE NODE SINGLETON MODE SUPPORT
ALTER TABLE rule_node ADD COLUMN IF NOT EXISTS singleton_mode bool DEFAULT false;
UPDATE rule_node SET singleton_mode = true WHERE type IN ('org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode', 'org.thingsboard.rule.engine.mqtt.TbMqttNode');
ALTER TABLE component_descriptor ADD COLUMN IF NOT EXISTS clustering_mode varchar(255) DEFAULT 'ENABLED';
UPDATE component_descriptor SET clustering_mode = 'USER_PREFERENCE' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.TbMqttNode';
UPDATE component_descriptor SET clustering_mode = 'SINGLETON' WHERE clazz = 'org.thingsboard.rule.engine.mqtt.azure.TbAzureIotHubNode';

7
application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java

@ -87,9 +87,10 @@ import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService; import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService; import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.queue.discovery.DiscoveryService;
import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider; import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.component.ComponentDiscoveryService; import org.thingsboard.server.service.component.ComponentDiscoveryService;
@ -177,6 +178,10 @@ public class ActorSystemContext {
@Setter @Setter
private ComponentDiscoveryService componentService; private ComponentDiscoveryService componentService;
@Autowired
@Getter
private DiscoveryService discoveryService;
@Autowired @Autowired
@Getter @Getter
private DataDecodingEncodingService encodingService; private DataDecodingEncodingService encodingService;

114
application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java

@ -31,7 +31,10 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg; import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.RuleNodeException; import org.thingsboard.server.common.msg.queue.RuleNodeException;
import org.thingsboard.server.common.msg.queue.RuleNodeInfo; import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.gen.transport.TransportProtos;
/** /**
* @author Andrew Shvayka * @author Andrew Shvayka
@ -39,11 +42,10 @@ import org.thingsboard.server.common.stats.TbApiUsageReportClient;
public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> { public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNodeId> {
private final String ruleChainName; private final String ruleChainName;
private final TbActorRef self;
private final TbApiUsageReportClient apiUsageClient; private final TbApiUsageReportClient apiUsageClient;
private final DefaultTbContext defaultCtx;
private RuleNode ruleNode; private RuleNode ruleNode;
private TbNode tbNode; private TbNode tbNode;
private DefaultTbContext defaultCtx;
private RuleNodeInfo info; private RuleNodeInfo info;
RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext RuleNodeActorMessageProcessor(TenantId tenantId, String ruleChainName, RuleNodeId ruleNodeId, ActorSystemContext systemContext
@ -51,7 +53,6 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
super(systemContext, tenantId, ruleNodeId); super(systemContext, tenantId, ruleNodeId);
this.apiUsageClient = systemContext.getApiUsageClient(); this.apiUsageClient = systemContext.getApiUsageClient();
this.ruleChainName = ruleChainName; this.ruleChainName = ruleChainName;
this.self = self;
this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); this.ruleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode)); this.defaultCtx = new DefaultTbContext(systemContext, ruleChainName, new RuleNodeCtx(tenantId, parent, self, ruleNode));
this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown"); this.info = new RuleNodeInfo(ruleNodeId, ruleChainName, ruleNode != null ? ruleNode.getName() : "Unknown");
@ -59,29 +60,36 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
@Override @Override
public void start(TbActorCtx context) throws Exception { public void start(TbActorCtx context) throws Exception {
tbNode = initComponent(ruleNode); if (isMyNodePartition()) {
if (tbNode != null) { tbNode = initComponent(ruleNode);
state = ComponentLifecycleState.ACTIVE; if (tbNode != null) {
state = ComponentLifecycleState.ACTIVE;
}
} }
} }
@Override @Override
public void onUpdate(TbActorCtx context) throws Exception { public void onUpdate(TbActorCtx context) throws Exception {
RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId); if (isMyNodePartition()) {
this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown"); RuleNode newRuleNode = systemContext.getRuleChainService().findRuleNodeById(tenantId, entityId);
boolean restartRequired = state != ComponentLifecycleState.ACTIVE || this.info = new RuleNodeInfo(entityId, ruleChainName, newRuleNode != null ? newRuleNode.getName() : "Unknown");
!(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration())); boolean restartRequired = state != ComponentLifecycleState.ACTIVE ||
this.ruleNode = newRuleNode; !(ruleNode.getType().equals(newRuleNode.getType()) && ruleNode.getConfiguration().equals(newRuleNode.getConfiguration()));
this.defaultCtx.updateSelf(newRuleNode); this.ruleNode = newRuleNode;
if (restartRequired) { this.defaultCtx.updateSelf(newRuleNode);
if (tbNode != null) { if (restartRequired) {
tbNode.destroy(); if (tbNode != null) {
} tbNode.destroy();
try { }
start(context); try {
} catch (Exception e) { start(context);
throw new TbRuleNodeUpdateException("Failed to update rule node", e); } catch (Exception e) {
throw new TbRuleNodeUpdateException("Failed to update rule node", e);
}
} }
} else if (tbNode != null) {
stop(null);
tbNode = null;
} }
} }
@ -94,9 +102,16 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
@Override @Override
public void onPartitionChangeMsg(PartitionChangeMsg msg) { public void onPartitionChangeMsg(PartitionChangeMsg msg) throws Exception {
if (tbNode != null) { if (tbNode != null) {
tbNode.onPartitionChangeMsg(defaultCtx, msg); if (!isMyNodePartition()) {
stop(null);
tbNode = null;
} else {
tbNode.onPartitionChangeMsg(defaultCtx, msg);
}
} else if (isMyNodePartition()) {
start(null);
} }
} }
@ -121,23 +136,27 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
} }
void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception { void onRuleChainToRuleNodeMsg(RuleChainToRuleNodeMsg msg) throws Exception {
msg.getMsg().getCallback().onProcessingStart(info); if (!isMyNodePartition()) {
checkComponentStateActive(msg.getMsg()); putToNodePartition(msg.getMsg());
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
msg.getCtx().tellFailure(msg.getMsg(), e);
}
} else { } else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode)); msg.getMsg().getCallback().onProcessingStart(info);
checkComponentStateActive(msg.getMsg());
TbMsg tbMsg = msg.getMsg();
int ruleNodeCount = tbMsg.getAndIncrementRuleNodeCounter();
int maxRuleNodeExecutionsPerMessage = getTenantProfileConfiguration().getMaxRuleNodeExecsPerMessage();
if (maxRuleNodeExecutionsPerMessage == 0 || ruleNodeCount < maxRuleNodeExecutionsPerMessage) {
apiUsageClient.report(tenantId, tbMsg.getCustomerId(), ApiUsageRecordKey.RE_EXEC_COUNT);
if (ruleNode.isDebugMode()) {
systemContext.persistDebugInput(tenantId, entityId, msg.getMsg(), msg.getFromRelationType());
}
try {
tbNode.onMsg(msg.getCtx(), msg.getMsg());
} catch (Exception e) {
msg.getCtx().tellFailure(msg.getMsg(), e);
}
} else {
tbMsg.getCallback().onFailure(new RuleNodeException("Message is processed by more then " + maxRuleNodeExecutionsPerMessage + " rule nodes!", ruleChainName, ruleNode));
}
} }
} }
@ -160,4 +179,23 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor<RuleNod
protected RuleNodeException getInactiveException() { protected RuleNodeException getInactiveException() {
return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode); return new RuleNodeException("Rule Node is not active! Failed to initialize.", ruleChainName, ruleNode);
} }
private boolean isMyNodePartition() {
return !ruleNode.isSingletonMode()
|| systemContext.getDiscoveryService().isMonolith()
|| defaultCtx.isLocalEntity(ruleNode.getId());
}
//Message will return after processing. See RuleChainActorMessageProcessor.pushToTarget.
private void putToNodePartition(TbMsg source) {
TbMsg tbMsg = TbMsg.newMsg(source, source.getQueueName(), source.getRuleChainId(), entityId);
TopicPartitionInfo tpi = systemContext.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, ruleNode.getId());
TransportProtos.ToRuleEngineMsg toQueueMsg = TransportProtos.ToRuleEngineMsg.newBuilder()
.setTenantIdMSB(tenantId.getId().getMostSignificantBits())
.setTenantIdLSB(tenantId.getId().getLeastSignificantBits())
.setTbMsg(TbMsg.toByteString(tbMsg))
.build();
systemContext.getClusterService().pushMsgToRuleEngine(tpi, tbMsg.getId(), toQueueMsg, null);
defaultCtx.ack(source);
}
} }

1
application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java

@ -96,6 +96,7 @@ public class TenantActor extends RuleChainManagerActor {
log.info("[{}] Skip init of the rule chains due to API limits", tenantId); log.info("[{}] Skip init of the rule chains due to API limits", tenantId);
} }
} catch (Exception e) { } catch (Exception e) {
log.info("Failed to check ApiUsage \"ReExecEnabled\"!!!", e);
cantFindTenant = true; cantFindTenant = true;
} }
} }

137
application/src/main/java/org/thingsboard/server/controller/AlarmController.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -52,6 +53,10 @@ import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource; import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
@ -85,9 +90,15 @@ public class AlarmController extends BaseController {
private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " + private static final String ALARM_SECURITY_CHECK = "If the user has the authority of 'Tenant Administrator', the server checks that the originator of alarm is owned by the same tenant. " +
"If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. "; "If the user has the authority of 'Customer User', the server checks that the originator of alarm belongs to the customer. ";
private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value"; private static final String ALARM_QUERY_SEARCH_STATUS_DESCRIPTION = "A string value representing one of the AlarmSearchStatus enumeration value";
private static final String ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSearchStatus enumeration value";
private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK";
private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value";
private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK";
private static final String ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing one of the AlarmSeverity enumeration value";
private static final String ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES = "CRITICAL, MAJOR, MINOR, WARNING, INDETERMINATE";
private static final String ALARM_QUERY_TYPE_ARRAY_DESCRIPTION = "A list of string values separated by comma ',' representing alarm types";
private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status"; private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status";
private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'.";
@ -331,6 +342,132 @@ public class AlarmController extends BaseController {
} }
} }
@ApiOperation(value = "Get Alarms (getAlarmsV2)",
notes = "Returns a page of alarms for the selected entity. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarm/{entityType}/{entityId}", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAlarmsV2(
@ApiParam(value = ENTITY_TYPE_PARAM_DESCRIPTION, required = true, defaultValue = "DEVICE")
@PathVariable(ENTITY_TYPE) String strEntityType,
@ApiParam(value = ENTITY_ID_PARAM_DESCRIPTION, required = true)
@PathVariable(ENTITY_ID) String strEntityId,
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] statusList,
@ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] severityList,
@ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION)
@RequestParam(required = false) String[] typeList,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException {
checkParameter("EntityId", strEntityId);
checkParameter("EntityType", strEntityType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strEntityType, strEntityId);
checkEntityId(entityId, Operation.READ);
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) {
for (String strStatus : statusList) {
if (!StringUtils.isEmpty(strStatus)) {
alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus));
}
}
}
List<AlarmSeverity> alarmSeverityList = new ArrayList<>();
if (severityList != null) {
for (String strSeverity : severityList) {
if (!StringUtils.isEmpty(strSeverity)) {
alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity));
}
}
}
List<String> alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList();
UserId assigneeUserId = null;
if (assigneeId != null) {
assigneeUserId = new UserId(UUID.fromString(assigneeId));
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(entityId, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
}
@ApiOperation(value = "Get All Alarms (getAllAlarmsV2)",
notes = "Returns a page of alarms that belongs to the current user owner. " +
"If the user has the authority of 'Tenant Administrator', the server returns alarms that belongs to the tenant of current user. " +
"If the user has the authority of 'Customer User', the server returns alarms that belongs to the customer of current user. " +
PAGE_DATA_PARAMETERS + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/v2/alarms", method = RequestMethod.GET)
@ResponseBody
public PageData<AlarmInfo> getAllAlarmsV2(
@ApiParam(value = ALARM_QUERY_SEARCH_STATUS_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] statusList,
@ApiParam(value = ALARM_QUERY_SEVERITY_ARRAY_DESCRIPTION, allowableValues = ALARM_QUERY_SEVERITY_ALLOWABLE_VALUES)
@RequestParam(required = false) String[] severityList,
@ApiParam(value = ALARM_QUERY_TYPE_ARRAY_DESCRIPTION)
@RequestParam(required = false) String[] typeList,
@ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION)
@RequestParam(required = false) String assigneeId,
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ALARM_QUERY_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ALARM_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder,
@ApiParam(value = ALARM_QUERY_START_TIME_DESCRIPTION)
@RequestParam(required = false) Long startTime,
@ApiParam(value = ALARM_QUERY_END_TIME_DESCRIPTION)
@RequestParam(required = false) Long endTime
) throws ThingsboardException, ExecutionException, InterruptedException {
List<AlarmSearchStatus> alarmStatusList = new ArrayList<>();
if (statusList != null) {
for (String strStatus : statusList) {
if (!StringUtils.isEmpty(strStatus)) {
alarmStatusList.add(AlarmSearchStatus.valueOf(strStatus));
}
}
}
List<AlarmSeverity> alarmSeverityList = new ArrayList<>();
if (severityList != null) {
for (String strSeverity : severityList) {
if (!StringUtils.isEmpty(strSeverity)) {
alarmSeverityList.add(AlarmSeverity.valueOf(strSeverity));
}
}
}
List<String> alarmTypeList = typeList != null ? Arrays.asList(typeList) : Collections.emptyList();
UserId assigneeUserId = null;
if (assigneeId != null) {
assigneeUserId = new UserId(UUID.fromString(assigneeId));
}
TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime);
if (getCurrentUser().isCustomerUser()) {
return checkNotNull(alarmService.findCustomerAlarmsV2(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
} else {
return checkNotNull(alarmService.findAlarmsV2(getCurrentUser().getTenantId(), new AlarmQueryV2(null, pageLink, alarmTypeList, alarmStatusList, alarmSeverityList, assigneeUserId)).get());
}
}
@ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)", @ApiOperation(value = "Get Highest Alarm Severity (getHighestAlarmSeverity)",
notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " + notes = "Search the alarms by originator ('entityType' and entityId') and optional 'status' or 'searchStatus' filters and returns the highest AlarmSeverity(CRITICAL, MAJOR, MINOR, WARNING or INDETERMINATE). " +
"Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH "Specifying both parameters 'searchStatus' and 'status' at the same time will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH

6
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -794,8 +794,10 @@ public abstract class BaseController {
protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) { protected <E extends HasName & HasId<? extends EntityId>> void logEntityAction(SecurityUser user, EntityType entityType, E entity, E savedEntity, ActionType actionType, Exception e) {
EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType); EntityId entityId = savedEntity != null ? savedEntity.getId() : emptyId(entityType);
entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity, if (!user.isSystemAdmin()) {
user.getCustomerId(), actionType, e); entityActionService.logEntityAction(user, entityId, savedEntity != null ? savedEntity : entity,
user.getCustomerId(), actionType, e);
}
} }
protected <E extends HasName & HasId<? extends EntityId>> E doSaveAndLog(EntityType entityType, E entity, BiFunction<TenantId, E, E> savingFunction) throws Exception { protected <E extends HasName & HasId<? extends EntityId>> E doSaveAndLog(EntityType entityType, E entity, BiFunction<TenantId, E, E> savingFunction) throws Exception {

15
application/src/main/java/org/thingsboard/server/controller/NotificationController.java

@ -47,11 +47,13 @@ import org.thingsboard.server.common.data.notification.targets.NotificationRecip
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetType; import org.thingsboard.server.common.data.notification.targets.NotificationTargetType;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.slack.SlackConversation;
import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.slack.SlackNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate; import org.thingsboard.server.common.data.notification.template.DeliveryMethodNotificationTemplate;
import org.thingsboard.server.common.data.notification.template.NotificationTemplate; import org.thingsboard.server.common.data.notification.template.NotificationTemplate;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.notification.NotificationService; import org.thingsboard.server.dao.notification.NotificationService;
import org.thingsboard.server.dao.notification.NotificationSettingsService; import org.thingsboard.server.dao.notification.NotificationSettingsService;
@ -230,7 +232,8 @@ public class NotificationController extends BaseController {
NotificationTargetType targetType = target.getConfiguration().getType(); NotificationTargetType targetType = target.getConfiguration().getType();
if (targetType == NotificationTargetType.PLATFORM_USERS) { if (targetType == NotificationTargetType.PLATFORM_USERS) {
PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(), PageData<User> recipients = notificationTargetService.findRecipientsForNotificationTargetConfig(user.getTenantId(),
(PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize)); (PlatformUsersNotificationTargetConfig) target.getConfiguration(), new PageLink(recipientsPreviewSize, 0, null,
new SortOrder("createdTime", SortOrder.Direction.DESC)));
recipientsCount = (int) recipients.getTotalElements(); recipientsCount = (int) recipients.getTotalElements();
recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList()); recipientsPart = recipients.getData().stream().map(r -> (NotificationRecipient) r).collect(Collectors.toList());
} else { } else {
@ -240,7 +243,15 @@ public class NotificationController extends BaseController {
firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null); firstRecipient.putIfAbsent(targetType, !recipientsPart.isEmpty() ? recipientsPart.get(0) : null);
for (NotificationRecipient recipient : recipientsPart) { for (NotificationRecipient recipient : recipientsPart) {
if (recipientsPreview.size() < recipientsPreviewSize) { if (recipientsPreview.size() < recipientsPreviewSize) {
recipientsPreview.add(recipient.getTitle()); String title = recipient.getTitle();
if (recipient instanceof SlackConversation) {
title = ((SlackConversation) recipient).getPointer() + title;
} else if (recipient instanceof User) {
if (!title.equals(recipient.getEmail())) {
title += " (" + recipient.getEmail() + ")";
}
}
recipientsPreview.add(title);
} else { } else {
break; break;
} }

31
application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java

@ -40,7 +40,10 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -52,6 +55,7 @@ import java.util.stream.Collectors;
public class EntityActionService { public class EntityActionService {
private final TbClusterService tbClusterService; private final TbClusterService tbClusterService;
private final AuditLogService auditLogService; private final AuditLogService auditLogService;
private final NotificationRuleProcessor notificationRuleProcessor;
public void pushEntityActionToRuleEngine(EntityId entityId, HasName entity, TenantId tenantId, CustomerId customerId, public void pushEntityActionToRuleEngine(EntityId entityId, HasName entity, TenantId tenantId, CustomerId customerId,
ActionType actionType, User user, Object... additionalInfo) { ActionType actionType, User user, Object... additionalInfo) {
@ -84,11 +88,11 @@ public class EntityActionService {
case ALARM_CLEAR: case ALARM_CLEAR:
msgType = DataConstants.ALARM_CLEAR; msgType = DataConstants.ALARM_CLEAR;
break; break;
case ALARM_ASSIGN: case ALARM_ASSIGNED:
msgType = DataConstants.ALARM_ASSIGN; msgType = DataConstants.ALARM_ASSIGNED;
break; break;
case ALARM_UNASSIGN: case ALARM_UNASSIGNED:
msgType = DataConstants.ALARM_UNASSIGN; msgType = DataConstants.ALARM_UNASSIGNED;
break; break;
case ALARM_DELETE: case ALARM_DELETE:
msgType = DataConstants.ALARM_DELETE; msgType = DataConstants.ALARM_DELETE;
@ -231,11 +235,28 @@ public class EntityActionService {
} }
} }
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode)); TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, JacksonUtil.toString(entityNode));
if (tenantId == null || tenantId.isNullUid()) { if (tenantId == null || tenantId.isNullUid()) {
if (entity instanceof HasTenantId) { if (entity instanceof HasTenantId) {
tenantId = ((HasTenantId) entity).getTenantId(); tenantId = ((HasTenantId) entity).getTenantId();
} }
} }
if (tenantId != null && !tenantId.isSysTenantId()) {
if (actionType == ActionType.ADDED) {
notificationRuleProcessor.process(EntitiesLimitTrigger.builder()
.tenantId(tenantId)
.entityType(entityId.getEntityType())
.build());
}
notificationRuleProcessor.process(EntityActionTrigger.builder()
.tenantId(tenantId)
.entityId(entityId)
.entity(entity)
.actionType(actionType)
.user(user)
.build());
}
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null); tbClusterService.pushMsgToRuleEngine(tenantId, entityId, tbMsg, null);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e); log.warn("[{}] Failed to push entity action to rule engine: {}", entityId, actionType, e);
@ -244,7 +265,7 @@ public class EntityActionService {
} }
public <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId, public <E extends HasName, I extends EntityId> void logEntityAction(User user, I entityId, E entity, CustomerId customerId,
ActionType actionType, Exception e, Object... additionalInfo) { ActionType actionType, Exception e, Object... additionalInfo) {
if (customerId == null || customerId.isNullUid()) { if (customerId == null || customerId.isNullUid()) {
customerId = user.getCustomerId(); customerId = user.getCustomerId();
} }

2
application/src/main/java/org/thingsboard/server/service/apiusage/DefaultTbApiUsageStateService.java

@ -48,7 +48,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageDataIterable; import org.thingsboard.server.common.data.page.PageDataIterable;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.TenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;

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

@ -153,6 +153,7 @@ public class AnnotationComponentDiscoveryService implements ComponentDiscoverySe
RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class); RuleNode ruleNodeAnnotation = clazz.getAnnotation(RuleNode.class);
scannedComponent.setName(ruleNodeAnnotation.name()); scannedComponent.setName(ruleNodeAnnotation.name());
scannedComponent.setScope(ruleNodeAnnotation.scope()); scannedComponent.setScope(ruleNodeAnnotation.scope());
scannedComponent.setClusteringMode(ruleNodeAnnotation.clusteringMode());
NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation); NodeDefinition nodeDefinition = prepareNodeDefinition(ruleNodeAnnotation);
ObjectNode configurationDescriptor = JacksonUtil.newObjectNode(); ObjectNode configurationDescriptor = JacksonUtil.newObjectNode();
JsonNode node = JacksonUtil.valueToTree(nodeDefinition); JsonNode node = JacksonUtil.valueToTree(nodeDefinition);

51
application/src/main/java/org/thingsboard/server/service/device/ClaimDevicesServiceImpl.java

@ -88,35 +88,32 @@ public class ClaimDevicesServiceImpl implements ClaimDevicesService {
@Override @Override
public ListenableFuture<Void> registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs) { public ListenableFuture<Void> registerClaimingInfo(TenantId tenantId, DeviceId deviceId, String secretKey, long durationMs) {
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); Device device = deviceService.findDeviceById(tenantId, deviceId);
return Futures.transformAsync(deviceFuture, device -> { Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE);
Cache cache = cacheManager.getCache(CLAIM_DEVICES_CACHE); List<Object> key = constructCacheKey(device.getId());
List<Object> key = constructCacheKey(device.getId()); if (isAllowedClaimingByDefault) {
if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
if (isAllowedClaimingByDefault) { persistInCache(secretKey, durationMs, cache, key);
if (device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) { return Futures.immediateFuture(null);
persistInCache(secretKey, durationMs, cache, key); }
return Futures.immediateFuture(null); log.warn("The device [{}] has been already claimed!", device.getName());
return Futures.immediateFailedFuture(new IllegalArgumentException());
} else {
ListenableFuture<List<AttributeKvEntry>> claimingAllowedFuture = attributesService.find(tenantId, device.getId(),
DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME));
return Futures.transform(claimingAllowedFuture, list -> {
if (list != null && !list.isEmpty()) {
Optional<Boolean> claimingAllowedOptional = list.get(0).getBooleanValue();
if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get()
&& device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
persistInCache(secretKey, durationMs, cache, key);
return null;
}
} }
log.warn("The device [{}] has been already claimed!", device.getName()); log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName());
throw new IllegalArgumentException(); throw new IllegalArgumentException();
} else { }, MoreExecutors.directExecutor());
ListenableFuture<List<AttributeKvEntry>> claimingAllowedFuture = attributesService.find(tenantId, device.getId(), }
DataConstants.SERVER_SCOPE, Collections.singletonList(CLAIM_ATTRIBUTE_NAME));
return Futures.transform(claimingAllowedFuture, list -> {
if (list != null && !list.isEmpty()) {
Optional<Boolean> claimingAllowedOptional = list.get(0).getBooleanValue();
if (claimingAllowedOptional.isPresent() && claimingAllowedOptional.get()
&& device.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
persistInCache(secretKey, durationMs, cache, key);
return null;
}
}
log.warn("Failed to find claimingAllowed attribute for device or it is already claimed![{}]", device.getName());
throw new IllegalArgumentException();
}, MoreExecutors.directExecutor());
}
}, MoreExecutors.directExecutor());
} }
private ListenableFuture<ClaimDataInfo> getClaimData(Cache cache, Device device) { private ListenableFuture<ClaimDataInfo> getClaimData(Cache cache, Device device) {

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java

@ -288,7 +288,7 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
if (session != null) { if (session != null) {
boolean success = false; boolean success = false;
if (session.isConnected()) { if (session.isConnected()) {
session.startSyncProcess(tenantId, edgeId, true); session.startSyncProcess(true);
success = true; success = true;
} }
clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success)); clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success));

58
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java

@ -148,7 +148,7 @@ public final class EdgeGrpcSession implements Closeable {
if (requestMsg.getSyncRequestMsg().hasFullSync()) { if (requestMsg.getSyncRequestMsg().hasFullSync()) {
fullSync = requestMsg.getSyncRequestMsg().getFullSync(); fullSync = requestMsg.getSyncRequestMsg().getFullSync();
} }
startSyncProcess(edge.getTenantId(), edge.getId(), fullSync); startSyncProcess(fullSync);
} else { } else {
syncCompleted = true; syncCompleted = true;
} }
@ -192,10 +192,10 @@ public final class EdgeGrpcSession implements Closeable {
}; };
} }
public void startSyncProcess(TenantId tenantId, EdgeId edgeId, boolean fullSync) { public void startSyncProcess(boolean fullSync) {
log.trace("[{}][{}] Staring edge sync process", tenantId, edgeId); log.trace("[{}][{}][{}] Staring edge sync process", edge.getTenantId(), edge.getId(), this.sessionId);
syncCompleted = false; syncCompleted = false;
interruptGeneralProcessingOnSync(tenantId, edgeId); interruptGeneralProcessingOnSync();
doSync(new EdgeSyncCursor(ctx, edge, fullSync)); doSync(new EdgeSyncCursor(ctx, edge, fullSync));
} }
@ -221,9 +221,9 @@ public final class EdgeGrpcSession implements Closeable {
.setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build()) .setSyncCompletedMsg(SyncCompletedMsg.newBuilder().build())
.build(); .build();
Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<Void>() { Futures.addCallback(sendDownlinkMsgsPack(Collections.singletonList(syncCompleteDownlinkMsg)), new FutureCallback<>() {
@Override @Override
public void onSuccess(Void result) { public void onSuccess(Boolean isInterrupted) {
syncCompleted = true; syncCompleted = true;
ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId()); ctx.getClusterService().onEdgeEventUpdate(edge.getTenantId(), edge.getId());
} }
@ -272,7 +272,7 @@ public final class EdgeGrpcSession implements Closeable {
} }
if (sessionState.getPendingMsgsMap().isEmpty()) { if (sessionState.getPendingMsgsMap().isEmpty()) {
log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey()); log.debug("[{}] Pending msgs map is empty. Stopping current iteration", edge.getRoutingKey());
stopCurrentSendDownlinkMsgsTask(null); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e); log.error("[{}] Can't process downlink response message [{}]", this.sessionId, msg, e);
@ -367,14 +367,19 @@ public final class EdgeGrpcSession implements Closeable {
if (isConnected() && !pageData.getData().isEmpty()) { if (isConnected() && !pageData.getData().isEmpty()) {
log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size()); log.trace("[{}] [{}] event(s) are going to be processed.", this.sessionId, pageData.getData().size());
List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData()); List<DownlinkMsg> downlinkMsgsPack = convertToDownlinkMsgsPack(pageData.getData());
Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<Void>() { Futures.addCallback(sendDownlinkMsgsPack(downlinkMsgsPack), new FutureCallback<>() {
@Override @Override
public void onSuccess(@Nullable Void tmp) { public void onSuccess(@Nullable Boolean isInterrupted) {
if (isConnected() && pageData.hasNext()) { if (Boolean.TRUE.equals(isInterrupted)) {
processEdgeEvents(fetcher, pageLink.nextPageLink(), result); log.debug("[{}][{}][{}] Send downlink messages task was interrupted", edge.getTenantId(), edge.getId(), sessionId);
result.set(null);
} else { } else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId(); if (isConnected() && pageData.hasNext()) {
result.set(ifOffset); processEdgeEvents(fetcher, pageLink.nextPageLink(), result);
} else {
UUID ifOffset = pageData.getData().get(pageData.getData().size() - 1).getUuidId();
result.set(ifOffset);
}
} }
} }
@ -394,7 +399,7 @@ public final class EdgeGrpcSession implements Closeable {
} }
} }
private ListenableFuture<Void> sendDownlinkMsgsPack(List<DownlinkMsg> downlinkMsgsPack) { private ListenableFuture<Boolean> sendDownlinkMsgsPack(List<DownlinkMsg> downlinkMsgsPack) {
interruptPreviousSendDownlinkMsgsTask(); interruptPreviousSendDownlinkMsgsTask();
sessionState.setSendDownlinkMsgsFuture(SettableFuture.create()); sessionState.setSendDownlinkMsgsFuture(SettableFuture.create());
@ -433,14 +438,14 @@ public final class EdgeGrpcSession implements Closeable {
} else { } else {
log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}", log.warn("[{}] Failed to deliver the batch after {} attempts. Next messages are going to be discarded {}",
this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy); this.sessionId, MAX_DOWNLINK_ATTEMPTS, copy);
stopCurrentSendDownlinkMsgsTask(null); stopCurrentSendDownlinkMsgsTask(false);
} }
} else { } else {
stopCurrentSendDownlinkMsgsTask(null); stopCurrentSendDownlinkMsgsTask(false);
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to send downlink msgs. Error msg {}", this.sessionId, e.getMessage(), e); log.warn("[{}] Failed to send downlink msgs. Error msg {}", this.sessionId, e.getMessage(), e);
stopCurrentSendDownlinkMsgsTask(e); stopCurrentSendDownlinkMsgsTask(true);
} }
}; };
@ -688,23 +693,18 @@ public final class EdgeGrpcSession implements Closeable {
} }
private void interruptPreviousSendDownlinkMsgsTask() { private void interruptPreviousSendDownlinkMsgsTask() {
String msg = String.format("[%s] Previous send downlink future was not properly completed, stopping it now!", this.sessionId); log.debug("[{}][{}][{}] Previous send downlink future was not properly completed, stopping it now!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); stopCurrentSendDownlinkMsgsTask(true);
} }
private void interruptGeneralProcessingOnSync(TenantId tenantId, EdgeId edgeId) { private void interruptGeneralProcessingOnSync() {
String msg = String.format("[%s][%s] Sync process started. General processing interrupted!", tenantId, edgeId); log.debug("[{}][{}][{}] Sync process started. General processing interrupted!", edge.getTenantId(), edge.getId(), this.sessionId);
stopCurrentSendDownlinkMsgsTask(new RuntimeException(msg)); stopCurrentSendDownlinkMsgsTask(true);
} }
public void stopCurrentSendDownlinkMsgsTask(Exception e) { public void stopCurrentSendDownlinkMsgsTask(Boolean isInterrupted) {
if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) { if (sessionState.getSendDownlinkMsgsFuture() != null && !sessionState.getSendDownlinkMsgsFuture().isDone()) {
if (e != null) { sessionState.getSendDownlinkMsgsFuture().set(isInterrupted);
log.debug(e.getMessage());
sessionState.getSendDownlinkMsgsFuture().setException(e);
} else {
sessionState.getSendDownlinkMsgsFuture().set(null);
}
} }
if (sessionState.getScheduledSendDownlinkTask() != null) { if (sessionState.getScheduledSendDownlinkTask() != null) {
sessionState.getScheduledSendDownlinkTask().cancel(true); sessionState.getScheduledSendDownlinkTask().cancel(true);

2
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSessionState.java

@ -28,6 +28,6 @@ import java.util.concurrent.ScheduledFuture;
public class EdgeSessionState { public class EdgeSessionState {
private final Map<Integer, DownlinkMsg> pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>()); private final Map<Integer, DownlinkMsg> pendingMsgsMap = Collections.synchronizedMap(new LinkedHashMap<>());
private SettableFuture<Void> sendDownlinkMsgsFuture; private SettableFuture<Boolean> sendDownlinkMsgsFuture;
private ScheduledFuture<?> scheduledSendDownlinkTask; private ScheduledFuture<?> scheduledSendDownlinkTask;
} }

7
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/device/BaseDeviceProcessor.java

@ -16,7 +16,6 @@
package org.thingsboard.server.service.edge.rpc.processor.device; package org.thingsboard.server.service.edge.rpc.processor.device;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -108,8 +107,8 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
public ListenableFuture<Void> processDeviceCredentialsMsg(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) { public ListenableFuture<Void> processDeviceCredentialsMsg(TenantId tenantId, DeviceCredentialsUpdateMsg deviceCredentialsUpdateMsg) {
log.debug("[{}] Executing processDeviceCredentialsMsg, deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentialsUpdateMsg); log.debug("[{}] Executing processDeviceCredentialsMsg, deviceCredentialsUpdateMsg [{}]", tenantId, deviceCredentialsUpdateMsg);
DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB())); DeviceId deviceId = new DeviceId(new UUID(deviceCredentialsUpdateMsg.getDeviceIdMSB(), deviceCredentialsUpdateMsg.getDeviceIdLSB()));
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(tenantId, deviceId); return dbCallbackExecutorService.submit(() -> {
return Futures.transform(deviceFuture, device -> { Device device = deviceService.findDeviceById(tenantId, deviceId);
if (device != null) { if (device != null) {
log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]", log.debug("Updating device credentials for device [{}]. New device credentials Id [{}], value [{}]",
device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue()); device.getName(), deviceCredentialsUpdateMsg.getCredentialsId(), deviceCredentialsUpdateMsg.getCredentialsValue());
@ -129,6 +128,6 @@ public abstract class BaseDeviceProcessor extends BaseEdgeProcessor {
log.warn("Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", deviceId, deviceCredentialsUpdateMsg); log.warn("Can't find device by id [{}], deviceCredentialsUpdateMsg [{}]", deviceId, deviceCredentialsUpdateMsg);
} }
return null; return null;
}, dbCallbackExecutorService); });
} }
} }

8
application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java

@ -309,10 +309,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
return EdgeEventActionType.ALARM_ACK; return EdgeEventActionType.ALARM_ACK;
case ALARM_CLEAR: case ALARM_CLEAR:
return EdgeEventActionType.ALARM_CLEAR; return EdgeEventActionType.ALARM_CLEAR;
case ALARM_ASSIGN: case ALARM_ASSIGNED:
return EdgeEventActionType.ALARM_ASSIGN; return EdgeEventActionType.ALARM_ASSIGNED;
case ALARM_UNASSIGN: case ALARM_UNASSIGNED:
return EdgeEventActionType.ALARM_UNASSIGN; return EdgeEventActionType.ALARM_UNASSIGNED;
case DELETED: case DELETED:
return EdgeEventActionType.DELETED; return EdgeEventActionType.DELETED;
case RELATION_ADD_OR_UPDATE: case RELATION_ADD_OR_UPDATE:

4
application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java

@ -175,7 +175,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
} catch (ThingsboardException e) { } catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e); log.error("Failed to save alarm comment", e);
} }
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGN, user); notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGNED, user);
} else { } else {
throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }
@ -203,7 +203,7 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb
} catch (ThingsboardException e) { } catch (ThingsboardException e) {
log.error("Failed to save alarm comment", e); log.error("Failed to save alarm comment", e);
} }
notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGN, user); notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGNED, user);
} else { } else {
throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} }

5
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -364,6 +364,11 @@ public class DefaultMailService implements MailService {
mailSender.testConnection(); mailSender.testConnection();
} }
@Override
public boolean isConfigured(TenantId tenantId) {
return mailSender != null;
}
private String toEnabledValueLabel(ApiFeature apiFeature) { private String toEnabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) { switch (apiFeature) {
case DB: case DB:

1
application/src/main/java/org/thingsboard/server/service/notification/NotificationProcessingContext.java

@ -128,6 +128,7 @@ public class NotificationProcessingContext {
private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) { private Map<String, String> createTemplateContextForRecipient(NotificationRecipient recipient) {
return Map.of( return Map.of(
"recipientTitle", recipient.getTitle(),
"recipientEmail", Strings.nullToEmpty(recipient.getEmail()), "recipientEmail", Strings.nullToEmpty(recipient.getEmail()),
"recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()), "recipientFirstName", Strings.nullToEmpty(recipient.getFirstName()),
"recipientLastName", Strings.nullToEmpty(recipient.getLastName()) "recipientLastName", Strings.nullToEmpty(recipient.getLastName())

6
application/src/main/java/org/thingsboard/server/service/notification/channels/EmailNotificationChannel.java

@ -49,10 +49,8 @@ public class EmailNotificationChannel implements NotificationChannel<User, Email
@Override @Override
public void check(TenantId tenantId) throws Exception { public void check(TenantId tenantId) throws Exception {
try { if (!mailService.isConfigured(tenantId)) {
mailService.testConnection(tenantId); throw new RuntimeException("Mail server is not configured");
} catch (Exception e) {
throw new RuntimeException("Mail server is not available");
} }
} }

2
application/src/main/java/org/thingsboard/server/service/notification/rule/DefaultNotificationRuleProcessor.java

@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger; import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;

11
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmAssignmentTriggerProcessor.java

@ -32,19 +32,20 @@ import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigg
import java.util.Set; import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> { public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmAssignmentNotificationRuleTriggerConfig> {
@Override @Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(RuleEngineMsgTrigger trigger, AlarmAssignmentNotificationRuleTriggerConfig triggerConfig) {
Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? Action.ASSIGNED : Action.UNASSIGNED; Action action = trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? Action.ASSIGNED : Action.UNASSIGNED;
if (!triggerConfig.getNotifyOn().contains(action)) { if (!triggerConfig.getNotifyOn().contains(action)) {
return false; return false;
} }
Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class); Alarm alarm = JacksonUtil.fromString(trigger.getMsg().getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) && emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
} }
@ -53,7 +54,7 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio
AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class); AlarmInfo alarmInfo = JacksonUtil.fromString(trigger.getMsg().getData(), AlarmInfo.class);
AlarmAssignee assignee = alarmInfo.getAssignee(); AlarmAssignee assignee = alarmInfo.getAssignee();
return AlarmAssignmentNotificationInfo.builder() return AlarmAssignmentNotificationInfo.builder()
.action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGN) ? "assigned" : "unassigned") .action(trigger.getMsg().getType().equals(DataConstants.ALARM_ASSIGNED) ? "assigned" : "unassigned")
.assigneeFirstName(assignee != null ? assignee.getFirstName() : null) .assigneeFirstName(assignee != null ? assignee.getFirstName() : null)
.assigneeLastName(assignee != null ? assignee.getLastName() : null) .assigneeLastName(assignee != null ? assignee.getLastName() : null)
.assigneeEmail(assignee != null ? assignee.getEmail() : null) .assigneeEmail(assignee != null ? assignee.getEmail() : null)
@ -78,7 +79,7 @@ public class AlarmAssignmentTriggerProcessor implements RuleEngineMsgNotificatio
@Override @Override
public Set<String> getSupportedMsgTypes() { public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ALARM_ASSIGN, DataConstants.ALARM_UNASSIGN); return Set.of(DataConstants.ALARM_ASSIGNED, DataConstants.ALARM_UNASSIGNED);
} }
} }

11
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmCommentTriggerProcessor.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigg
import java.util.Set; import java.util.Set;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> { public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<AlarmCommentNotificationRuleTriggerConfig> {
@ -53,8 +54,8 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
} }
} }
Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class); Alarm alarm = JacksonUtil.fromString(msg.getData(), Alarm.class);
return (isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType())) && return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType()) &&
(isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity())) && emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity()) &&
(isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm)); (isEmpty(triggerConfig.getAlarmStatuses()) || AlarmStatusFilter.from(triggerConfig.getAlarmStatuses()).matches(alarm));
} }
@ -66,9 +67,9 @@ public class AlarmCommentTriggerProcessor implements RuleEngineMsgNotificationRu
return AlarmCommentNotificationInfo.builder() return AlarmCommentNotificationInfo.builder()
.comment(comment.getComment().get("text").asText()) .comment(comment.getComment().get("text").asText())
.action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated") .action(msg.getType().equals(DataConstants.COMMENT_CREATED) ? "added" : "updated")
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail")) .userEmail(msg.getMetaData().getValue("userEmail"))
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName")) .userFirstName(msg.getMetaData().getValue("userFirstName"))
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName")) .userLastName(msg.getMetaData().getValue("userLastName"))
.alarmId(alarmInfo.getUuidId()) .alarmId(alarmInfo.getUuidId())
.alarmType(alarmInfo.getType()) .alarmType(alarmInfo.getType())
.alarmOriginator(alarmInfo.getOriginator()) .alarmOriginator(alarmInfo.getOriginator())

8
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/AlarmTriggerProcessor.java

@ -17,6 +17,7 @@ package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo; import org.thingsboard.server.common.data.notification.info.AlarmNotificationInfo;
@ -25,11 +26,10 @@ import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotific
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.ClearRule;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger; import org.thingsboard.server.common.msg.notification.trigger.AlarmTrigger;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> { public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<AlarmTrigger, AlarmNotificationRuleTriggerConfig> {
@ -85,11 +85,11 @@ public class AlarmTriggerProcessor implements NotificationRuleTriggerProcessor<A
} }
private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { private boolean severityMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
return isEmpty(triggerConfig.getAlarmSeverities()) || triggerConfig.getAlarmSeverities().contains(alarm.getSeverity()); return emptyOrContains(triggerConfig.getAlarmSeverities(), alarm.getSeverity());
} }
private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) { private boolean typeMatches(Alarm alarm, AlarmNotificationRuleTriggerConfig triggerConfig) {
return isEmpty(triggerConfig.getAlarmTypes()) || triggerConfig.getAlarmTypes().contains(alarm.getType()); return emptyOrContains(triggerConfig.getAlarmTypes(), alarm.getType());
} }
@Override @Override

6
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/ApiUsageLimitTriggerProcessor.java

@ -24,7 +24,7 @@ import org.thingsboard.server.common.data.notification.rule.trigger.Notification
import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.ApiUsageLimitTrigger;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import static org.apache.commons.collections.CollectionUtils.isEmpty; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@ -34,8 +34,8 @@ public class ApiUsageLimitTriggerProcessor implements NotificationRuleTriggerPro
@Override @Override
public boolean matchesFilter(ApiUsageLimitTrigger trigger, ApiUsageLimitNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(ApiUsageLimitTrigger trigger, ApiUsageLimitNotificationRuleTriggerConfig triggerConfig) {
return (isEmpty(triggerConfig.getApiFeatures()) || triggerConfig.getApiFeatures().contains(trigger.getState().getApiFeature())) && return emptyOrContains(triggerConfig.getApiFeatures(), trigger.getState().getApiFeature()) &&
(isEmpty(triggerConfig.getNotifyOn()) || triggerConfig.getNotifyOn().contains(trigger.getStatus())); emptyOrContains(triggerConfig.getNotifyOn(), trigger.getStatus());
} }
@Override @Override

15
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntitiesLimitTriggerProcessor.java

@ -21,7 +21,10 @@ import org.thingsboard.server.common.data.notification.info.EntitiesLimitNotific
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger; import org.thingsboard.server.common.msg.notification.trigger.EntitiesLimitTrigger;
import org.thingsboard.server.dao.entity.EntityCountService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import static org.apache.commons.collections.CollectionUtils.isNotEmpty; import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
@ -30,6 +33,8 @@ import static org.apache.commons.collections.CollectionUtils.isNotEmpty;
@RequiredArgsConstructor @RequiredArgsConstructor
public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor<EntitiesLimitTrigger, EntitiesLimitNotificationRuleTriggerConfig> { public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerProcessor<EntitiesLimitTrigger, EntitiesLimitNotificationRuleTriggerConfig> {
private final EntityCountService entityCountService;
private final TbTenantProfileCache tenantProfileCache;
private final TenantService tenantService; private final TenantService tenantService;
@Override @Override
@ -37,7 +42,15 @@ public class EntitiesLimitTriggerProcessor implements NotificationRuleTriggerPro
if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) { if (isNotEmpty(triggerConfig.getEntityTypes()) && !triggerConfig.getEntityTypes().contains(trigger.getEntityType())) {
return false; return false;
} }
return (int) (trigger.getLimit() * triggerConfig.getThreshold()) == trigger.getCurrentCount(); // strict comparing not to send notification on each new entity DefaultTenantProfileConfiguration profileConfiguration = tenantProfileCache.get(trigger.getTenantId()).getDefaultProfileConfiguration();
long limit = profileConfiguration.getEntitiesLimit(trigger.getEntityType());
if (limit <= 0) {
return false;
}
long currentCount = entityCountService.countByTenantIdAndEntityType(trigger.getTenantId(), trigger.getEntityType());
trigger.setLimit(limit);
trigger.setCurrentCount(currentCount);
return (int) (limit * triggerConfig.getThreshold()) == currentCount; // strict comparing not to send notification on each new entity
} }
@Override @Override

73
application/src/main/java/org/thingsboard/server/service/notification/rule/trigger/EntityActionTriggerProcessor.java

@ -16,78 +16,47 @@
package org.thingsboard.server.service.notification.rule.trigger; package org.thingsboard.server.service.notification.rule.trigger;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.HasCustomerId;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo; import org.thingsboard.server.common.data.notification.info.EntityActionNotificationInfo;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo; import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.notification.trigger.EntityActionTrigger;
import org.thingsboard.server.common.msg.notification.trigger.RuleEngineMsgTrigger;
import java.util.Optional; import static org.thingsboard.server.common.data.util.CollectionsUtil.emptyOrContains;
import java.util.Set;
import java.util.UUID;
import static org.apache.commons.collections.CollectionUtils.isEmpty;
@Service @Service
public class EntityActionTriggerProcessor implements RuleEngineMsgNotificationRuleTriggerProcessor<EntityActionNotificationRuleTriggerConfig> { public class EntityActionTriggerProcessor implements NotificationRuleTriggerProcessor<EntityActionTrigger, EntityActionNotificationRuleTriggerConfig> {
@Override @Override
public boolean matchesFilter(RuleEngineMsgTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) { public boolean matchesFilter(EntityActionTrigger trigger, EntityActionNotificationRuleTriggerConfig triggerConfig) {
String msgType = trigger.getMsg().getType(); return ((trigger.getActionType() == ActionType.ADDED && triggerConfig.isCreated())
if (msgType.equals(DataConstants.ENTITY_CREATED)) { || (trigger.getActionType() == ActionType.UPDATED && triggerConfig.isUpdated())
if (!triggerConfig.isCreated()) { || (trigger.getActionType() == ActionType.DELETED && triggerConfig.isDeleted()))
return false; && emptyOrContains(triggerConfig.getEntityTypes(), trigger.getEntityId().getEntityType());
}
} else if (msgType.equals(DataConstants.ENTITY_UPDATED)) {
if (!triggerConfig.isUpdated()) {
return false;
}
} else if (msgType.equals(DataConstants.ENTITY_DELETED)) {
if (!triggerConfig.isDeleted()) {
return false;
}
} else {
return false;
}
return isEmpty(triggerConfig.getEntityTypes()) || triggerConfig.getEntityTypes().contains(getEntityType(trigger.getMsg()));
} }
@Override @Override
public RuleOriginatedNotificationInfo constructNotificationInfo(RuleEngineMsgTrigger trigger) { public RuleOriginatedNotificationInfo constructNotificationInfo(EntityActionTrigger trigger) {
TbMsg msg = trigger.getMsg();
String msgType = msg.getType();
ActionType actionType = msgType.equals(DataConstants.ENTITY_CREATED) ? ActionType.ADDED :
msgType.equals(DataConstants.ENTITY_UPDATED) ? ActionType.UPDATED :
msgType.equals(DataConstants.ENTITY_DELETED) ? ActionType.DELETED : null;
return EntityActionNotificationInfo.builder() return EntityActionNotificationInfo.builder()
.entityId(msg.getOriginator()) .entityId(trigger.getEntityId())
.entityName(msg.getMetaData().getValue("entityName")) .entityName(trigger.getEntity().getName())
.actionType(actionType) .actionType(trigger.getActionType())
.userId(UUID.fromString(msg.getMetaData().getValue("userId"))) .userId(trigger.getUser().getUuidId())
.userEmail(trigger.getMsg().getMetaData().getValue("userEmail")) .userTitle(trigger.getUser().getTitle())
.userFirstName(trigger.getMsg().getMetaData().getValue("userFirstName")) .userEmail(trigger.getUser().getEmail())
.userLastName(trigger.getMsg().getMetaData().getValue("userLastName")) .userFirstName(trigger.getUser().getFirstName())
.entityCustomerId(msg.getCustomerId()) .userLastName(trigger.getUser().getLastName())
.entityCustomerId(trigger.getEntity() instanceof HasCustomerId ?
((HasCustomerId) trigger.getEntity()).getCustomerId() :
trigger.getUser().getCustomerId())
.build(); .build();
} }
private static EntityType getEntityType(TbMsg msg) {
return Optional.ofNullable(msg.getMetaData().getValue("entityType"))
.map(EntityType::valueOf).orElse(null);
}
@Override @Override
public NotificationRuleTriggerType getTriggerType() { public NotificationRuleTriggerType getTriggerType() {
return NotificationRuleTriggerType.ENTITY_ACTION; return NotificationRuleTriggerType.ENTITY_ACTION;
} }
@Override
public Set<String> getSupportedMsgTypes() {
return Set.of(DataConstants.ENTITY_CREATED, DataConstants.ENTITY_UPDATED, DataConstants.ENTITY_DELETED);
}
} }

2
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java

@ -35,7 +35,7 @@ import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.rpc.RpcError; import org.thingsboard.server.common.data.rpc.RpcError;
import org.thingsboard.server.common.msg.MsgType; import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg; import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TbCallback;

6
application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java

@ -18,13 +18,13 @@ package org.thingsboard.server.service.queue;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile; import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService; import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.TenantRoutingInfo; import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService; import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
@Slf4j @Slf4j
@Service @Service
@ -46,7 +46,7 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService
if (tenantProfile != null) { if (tenantProfile != null) {
return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbRuleEngine()); return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbRuleEngine());
} else { } else {
throw new RuntimeException("Tenant not found!"); throw new TenantNotFoundException(tenantId);
} }
} }
} }

3
application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java

@ -264,8 +264,7 @@ public class AccessValidator {
if (currentUser.isSystemAdmin()) { if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION)); callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
} else { } else {
ListenableFuture<Device> deviceFuture = deviceService.findDeviceByIdAsync(currentUser.getTenantId(), new DeviceId(entityId.getId())); Futures.addCallback(Futures.immediateFuture(deviceService.findDeviceById(currentUser.getTenantId(), new DeviceId(entityId.getId()))), getCallback(callback, device -> {
Futures.addCallback(deviceFuture, getCallback(callback, device -> {
if (device == null) { if (device == null) {
return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND); return ValidationResult.entityNotFound(DEVICE_WITH_REQUESTED_ID_NOT_FOUND);
} else { } else {

23
application/src/main/java/org/thingsboard/server/service/slack/DefaultSlackService.java

@ -28,7 +28,6 @@ import com.slack.api.methods.response.conversations.ConversationsListResponse;
import com.slack.api.methods.response.users.UsersListResponse; import com.slack.api.methods.response.users.UsersListResponse;
import com.slack.api.model.ConversationType; import com.slack.api.model.ConversationType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.slack.SlackService; import org.thingsboard.rule.engine.api.slack.SlackService;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -44,8 +43,6 @@ import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class DefaultSlackService implements SlackService { public class DefaultSlackService implements SlackService {
@ -81,15 +78,11 @@ public class DefaultSlackService implements SlackService {
.filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot()) .filter(user -> !user.isDeleted() && !user.isStranger() && !user.isBot())
.map(user -> { .map(user -> {
SlackConversation conversation = new SlackConversation(); SlackConversation conversation = new SlackConversation();
conversation.setType(conversationType);
conversation.setId(user.getId()); conversation.setId(user.getId());
conversation.setShortName(user.getName()); conversation.setName(user.getName());
conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName()); conversation.setWholeName(user.getProfile() != null ? user.getProfile().getRealNameNormalized() : user.getRealName());
conversation.setEmail(user.getProfile() != null ? user.getProfile().getEmail() : null); conversation.setEmail(user.getProfile() != null ? user.getProfile().getEmail() : null);
String title = "@" + conversation.getShortName();
if (isNotEmpty(conversation.getWholeName()) && !conversation.getWholeName().equals(conversation.getShortName())) {
title += " (" + conversation.getWholeName() + ")";
}
conversation.setTitle(title);
return conversation; return conversation;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -107,10 +100,10 @@ public class DefaultSlackService implements SlackService {
.filter(channel -> !channel.isArchived()) .filter(channel -> !channel.isArchived())
.map(channel -> { .map(channel -> {
SlackConversation conversation = new SlackConversation(); SlackConversation conversation = new SlackConversation();
conversation.setType(conversationType);
conversation.setId(channel.getId()); conversation.setId(channel.getId());
conversation.setShortName(channel.getName()); conversation.setName(channel.getName());
conversation.setWholeName(channel.getNameNormalized()); conversation.setWholeName(channel.getNameNormalized());
conversation.setTitle("#" + channel.getName());
return conversation; return conversation;
}) })
.collect(Collectors.toList()); .collect(Collectors.toList());
@ -118,14 +111,6 @@ public class DefaultSlackService implements SlackService {
}); });
} }
@Override
public SlackConversation findConversation(TenantId tenantId, String token, SlackConversationType conversationType, String namePattern) {
List<SlackConversation> conversations = listConversations(tenantId, token, conversationType);
return conversations.stream()
.filter(conversation -> StringUtils.containsIgnoreCase(conversation.getTitle(), namePattern))
.findFirst().orElse(null);
}
@Override @Override
public String getToken(TenantId tenantId) { public String getToken(TenantId tenantId) {
NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId); NotificationSettings settings = notificationSettingsService.findNotificationSettings(tenantId);

4
application/src/main/java/org/thingsboard/server/service/state/DefaultDeviceStateService.java

@ -40,6 +40,7 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceIdInfo; import org.thingsboard.server.common.data.DeviceIdInfo;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -453,6 +454,9 @@ public class DefaultDeviceStateService extends AbstractPartitionBasedService<Dev
updateInactivityStateIfExpired(ts, deviceId, stateData); updateInactivityStateIfExpired(ts, deviceId, stateData);
} catch (Exception e) { } catch (Exception e) {
log.warn("[{}] Failed to update inactivity state", deviceId, e); log.warn("[{}] Failed to update inactivity state", deviceId, e);
if (e instanceof TenantNotFoundException) {
continue;
}
} }
Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(), Pair<AtomicInteger, AtomicInteger> tenantDevicesActivity = devicesActivity.computeIfAbsent(stateData.getTenantId(),
tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger())); tenantId -> Pair.of(new AtomicInteger(), new AtomicInteger()));

5
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/csv/AbstractBulkImportService.java

@ -113,8 +113,9 @@ public abstract class AbstractBulkImportService<E extends HasId<? extends Entity
ImportedEntityInfo<E> importedEntityInfo = saveEntity(entityData.getFields(), user); ImportedEntityInfo<E> importedEntityInfo = saveEntity(entityData.getFields(), user);
E entity = importedEntityInfo.getEntity(); E entity = importedEntityInfo.getEntity();
saveKvs(user, entity, entityData.getKvs()); if (request.getMapping().getUpdate() || !importedEntityInfo.isUpdated()) {
saveKvs(user, entity, entityData.getKvs());
}
return importedEntityInfo; return importedEntityInfo;
}, },
importedEntityInfo -> { importedEntityInfo -> {

46
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/BaseEntityImportService.java

@ -23,7 +23,6 @@ import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable; import org.checkerframework.checker.nullness.qual.Nullable;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -87,7 +86,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
@Autowired @Autowired
protected TbNotificationEntityService entityNotificationService; protected TbNotificationEntityService entityNotificationService;
@Transactional(rollbackFor = Exception.class)
@Override @Override
public EntityImportResult<E> importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException { public EntityImportResult<E> importEntity(EntitiesImportCtx ctx, D exportData) throws ThingsboardException {
EntityImportResult<E> importResult = new EntityImportResult<>(); EntityImportResult<E> importResult = new EntityImportResult<>();
@ -336,27 +334,37 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty(); if (externalUuid.equals(EntityId.NULL_UUID)) return Optional.empty();
for (EntityType entityType : EntityType.values()) { for (EntityType entityType : EntityType.values()) {
Optional<EntityId> externalIdOpt = buildEntityId(entityType, externalUuid); Optional<EntityId> externalId = buildEntityId(entityType, externalUuid);
if (!externalIdOpt.isPresent()) { if (externalId.isEmpty()) {
continue; continue;
} }
EntityId internalId = ctx.getInternalId(externalIdOpt.get()); EntityId internalId = ctx.getInternalId(externalId.get());
if (internalId != null) { if (internalId != null) {
return Optional.of(internalId); return Optional.of(internalId);
} }
} }
if (fetchAllUUIDs) { if (fetchAllUUIDs) {
for (EntityType entityType : hints) { Set<EntityType> processLast = Set.of(EntityType.TENANT);
Optional<EntityId> internalId = lookupInDb(externalUuid, entityType); List<EntityType> entityTypes = new ArrayList<>(hints);
if (internalId.isPresent()) return internalId;
}
for (EntityType entityType : EntityType.values()) { for (EntityType entityType : EntityType.values()) {
if (hints.contains(entityType)) { if (!hints.contains(entityType) && !processLast.contains(entityType)) {
entityTypes.add(entityType);
}
}
entityTypes.addAll(processLast);
for (EntityType entityType : entityTypes) {
Optional<EntityId> externalId = buildEntityId(entityType, externalUuid);
if (externalId.isEmpty() || ctx.isNotFound(externalId.get())) {
continue; continue;
} }
Optional<EntityId> internalId = lookupInDb(externalUuid, entityType); EntityId internalId = getInternalId(externalId.get(), false);
if (internalId.isPresent()) return internalId; if (internalId != null) {
return Optional.of(internalId);
} else {
ctx.registerNotFound(externalId.get());
}
} }
} }
@ -364,20 +372,6 @@ public abstract class BaseEntityImportService<I extends EntityId, E extends Expo
return Optional.empty(); return Optional.empty();
} }
private Optional<EntityId> lookupInDb(UUID externalUuid, EntityType entityType) {
Optional<EntityId> externalIdOpt = buildEntityId(entityType, externalUuid);
if (externalIdOpt.isEmpty() || ctx.isNotFound(externalIdOpt.get())) {
return Optional.empty();
}
EntityId internalId = getInternalId(externalIdOpt.get(), false);
if (internalId != null) {
return Optional.of(internalId);
} else {
ctx.registerNotFound(externalIdOpt.get());
}
return Optional.empty();
}
private Optional<EntityId> buildEntityId(EntityType entityType, UUID externalUuid) { private Optional<EntityId> buildEntityId(EntityType entityType, UUID externalUuid) {
try { try {
return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid)); return Optional.of(EntityIdFactory.getByTypeAndUuid(entityType, externalUuid));

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

@ -65,10 +65,10 @@ public class DashboardImportService extends BaseEntityImportService<DashboardId,
@Override @Override
protected Dashboard prepare(EntitiesImportCtx ctx, Dashboard dashboard, Dashboard old, EntityExportData<Dashboard> exportData, IdProvider idProvider) { protected Dashboard prepare(EntitiesImportCtx ctx, Dashboard dashboard, Dashboard old, EntityExportData<Dashboard> exportData, IdProvider idProvider) {
for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) { for (JsonNode entityAlias : dashboard.getEntityAliasesConfig()) {
replaceIdsRecursively(ctx, idProvider, entityAlias, Collections.emptySet(), HINTS); replaceIdsRecursively(ctx, idProvider, entityAlias, Set.of("id"), HINTS);
} }
for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) { for (JsonNode widgetConfig : dashboard.getWidgetsConfig()) {
replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Collections.singleton("id"), HINTS); replaceIdsRecursively(ctx, idProvider, JacksonUtil.getSafely(widgetConfig, "config", "actions"), Set.of("id"), HINTS);
} }
return dashboard; return dashboard;
} }

6
application/src/main/java/org/thingsboard/server/service/sync/vc/DefaultEntitiesVersionControlService.java

@ -44,7 +44,6 @@ import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.common.data.sync.ie.EntityExportData; import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.common.data.sync.ie.EntityExportSettings; import org.thingsboard.server.common.data.sync.ie.EntityExportSettings;
import org.thingsboard.server.common.data.sync.ie.EntityImportResult; import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
@ -69,6 +68,7 @@ import org.thingsboard.server.common.data.sync.vc.request.load.EntityTypeVersion
import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest; import org.thingsboard.server.common.data.sync.vc.request.load.SingleEntityVersionLoadRequest;
import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadConfig;
import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest; import org.thingsboard.server.common.data.sync.vc.request.load.VersionLoadRequest;
import org.thingsboard.server.common.data.util.ThrowingRunnable;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.edge.EdgeService; import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException; import org.thingsboard.server.dao.exception.DeviceCredentialsValidationException;
@ -259,7 +259,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
return gitServiceQueue.listEntitiesAtVersion(tenantId, versionId); return gitServiceQueue.listEntitiesAtVersion(tenantId, versionId);
} }
@SuppressWarnings({"UnstableApiUsage", "rawtypes"}) @SuppressWarnings({"rawtypes"})
@Override @Override
public UUID loadEntitiesVersion(User user, VersionLoadRequest request) throws Exception { public UUID loadEntitiesVersion(User user, VersionLoadRequest request) throws Exception {
EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId()); EntitiesImportCtx ctx = new EntitiesImportCtx(UUID.randomUUID(), user, request.getVersionId());
@ -402,7 +402,7 @@ public class DefaultEntitiesVersionControlService implements EntitiesVersionCont
ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>()) ctx.getImportedEntities().computeIfAbsent(entityType, t -> new HashSet<>())
.add(importResult.getSavedEntity().getId()); .add(importResult.getSavedEntity().getId());
} }
log.debug("Imported {} pack for tenant {}", entityType, ctx.getTenantId()); log.debug("Imported {} pack ({}) for tenant {}", entityType, entityDataList.size(), ctx.getTenantId());
offset += limit; offset += limit;
} while (entityDataList.size() == limit); } while (entityDataList.size() == limit);
} }

44
application/src/main/java/org/thingsboard/server/service/system/DefaultSystemInfoService.java

@ -15,7 +15,6 @@
*/ */
package org.thingsboard.server.service.system; package org.thingsboard.server.service.system;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
import com.google.protobuf.ProtocolStringList; import com.google.protobuf.ProtocolStringList;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@ -23,10 +22,11 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.FeaturesInfo; import org.thingsboard.server.common.data.FeaturesInfo;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.SystemInfo; import org.thingsboard.server.common.data.SystemInfo;
import org.thingsboard.server.common.data.SystemInfoData; import org.thingsboard.server.common.data.SystemInfoData;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -38,7 +38,6 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.dao.oauth2.OAuth2Service; import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo; import org.thingsboard.server.gen.transport.TransportProtos.ServiceInfo;
import org.thingsboard.server.queue.discovery.DiscoveryService; import org.thingsboard.server.queue.discovery.DiscoveryService;
@ -59,10 +58,10 @@ import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.thingsboard.common.util.SystemUtil.getCpuCount;
import static org.thingsboard.common.util.SystemUtil.getCpuUsage; import static org.thingsboard.common.util.SystemUtil.getCpuUsage;
import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
import static org.thingsboard.common.util.SystemUtil.getDiscSpaceUsage; import static org.thingsboard.common.util.SystemUtil.getDiscSpaceUsage;
import static org.thingsboard.common.util.SystemUtil.getCpuCount; import static org.thingsboard.common.util.SystemUtil.getMemoryUsage;
import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace; import static org.thingsboard.common.util.SystemUtil.getTotalDiscSpace;
import static org.thingsboard.common.util.SystemUtil.getTotalMemory; import static org.thingsboard.common.util.SystemUtil.getTotalMemory;
@ -90,6 +89,8 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
private final TbApiUsageStateClient apiUsageStateClient; private final TbApiUsageStateClient apiUsageStateClient;
private final AdminSettingsService adminSettingsService; private final AdminSettingsService adminSettingsService;
private final OAuth2Service oAuth2Service; private final OAuth2Service oAuth2Service;
private final MailService mailService;
private final SmsService smsService;
private volatile ScheduledExecutorService scheduler; private volatile ScheduledExecutorService scheduler;
@Override @Override
@ -135,18 +136,37 @@ public class DefaultSystemInfoService extends TbApplicationEventListener<Partiti
public FeaturesInfo getFeaturesInfo() { public FeaturesInfo getFeaturesInfo() {
FeaturesInfo featuresInfo = new FeaturesInfo(); FeaturesInfo featuresInfo = new FeaturesInfo();
featuresInfo.setEmailEnabled(isEmailEnabled()); featuresInfo.setEmailEnabled(isEmailEnabled());
featuresInfo.setSmsEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "sms") != null); featuresInfo.setSmsEnabled(smsService.isConfigured(TenantId.SYS_TENANT_ID));
featuresInfo.setOauthEnabled(oAuth2Service.findOAuth2Info().isEnabled()); featuresInfo.setOauthEnabled(oAuth2Service.findOAuth2Info().isEnabled());
featuresInfo.setTwoFaEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings") != null); featuresInfo.setTwoFaEnabled(isTwoFaEnabled());
featuresInfo.setNotificationEnabled(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications") != null); featuresInfo.setNotificationEnabled(isSlackEnabled());
return featuresInfo; return featuresInfo;
} }
private boolean isEmailEnabled() { private boolean isEmailEnabled() {
AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail"); try {
if (mailSettings != null) { mailService.testConnection(TenantId.SYS_TENANT_ID);
JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom"); return true;
return StringUtils.isNotEmpty(mailFrom.asText()) && !"ThingsBoard <sysadmin@localhost.localdomain>".equalsIgnoreCase(mailFrom.asText()); } catch (Exception e) {
return false;
}
}
private boolean isTwoFaEnabled() {
AdminSettings twoFaSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "twoFaSettings");
if (twoFaSettings != null) {
var providers = twoFaSettings.getJsonValue().get("providers");
if (providers != null) {
return providers.size() > 0;
}
}
return false;
}
private boolean isSlackEnabled() {
AdminSettings notifications = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "notifications");
if (notifications != null) {
return notifications.getJsonValue().get("deliveryMethodsConfigs").has("SLACK");
} }
return false; return false;
} }

13
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java

@ -33,6 +33,7 @@ import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -51,7 +52,7 @@ import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.common.stats.TbApiUsageReportClient; import org.thingsboard.server.common.stats.TbApiUsageReportClient;
import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult;
import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService; import org.thingsboard.server.service.entitiy.alarm.TbAlarmCommentService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils; import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
@ -189,6 +190,16 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService
return alarmService.findCustomerAlarms(tenantId, customerId, query); return alarmService.findCustomerAlarms(tenantId, customerId, query);
} }
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return alarmService.findAlarmsV2(tenantId, query);
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return alarmService.findCustomerAlarmsV2(tenantId, customerId, query);
}
@Override @Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) {
return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId); return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId);

2
application/src/main/java/org/thingsboard/server/service/update/DefaultUpdateService.java

@ -29,7 +29,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.UpdateMessage; import org.thingsboard.server.common.data.UpdateMessage;
import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger; import org.thingsboard.server.common.msg.notification.trigger.NewPlatformVersionTrigger;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor; import org.thingsboard.server.queue.notification.NotificationRuleProcessor;
import org.thingsboard.server.queue.util.AfterStartUp; import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.queue.util.TbCoreComponent;

15
application/src/main/resources/thingsboard.yml

@ -259,13 +259,11 @@ sql:
batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY_MS:100}" batch_max_delay: "${SQL_ATTRIBUTES_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_ATTRIBUTES_BATCH_STATS_PRINT_MS:10000}" stats_print_interval_ms: "${SQL_ATTRIBUTES_BATCH_STATS_PRINT_MS:10000}"
batch_threads: "${SQL_ATTRIBUTES_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution batch_threads: "${SQL_ATTRIBUTES_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution
noxss_validation_enabled: "${SQL_ATTRIBUTES_NOXSS_VALIDATION_ENABLED:true}"
ts: ts:
batch_size: "${SQL_TS_BATCH_SIZE:10000}" batch_size: "${SQL_TS_BATCH_SIZE:10000}"
batch_max_delay: "${SQL_TS_BATCH_MAX_DELAY_MS:100}" batch_max_delay: "${SQL_TS_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_TS_BATCH_STATS_PRINT_MS:10000}" stats_print_interval_ms: "${SQL_TS_BATCH_STATS_PRINT_MS:10000}"
batch_threads: "${SQL_TS_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution batch_threads: "${SQL_TS_BATCH_THREADS:3}" # batch thread count have to be a prime number like 3 or 5 to gain perfect hash distribution
noxss_validation_enabled: "${SQL_TS_NOXSS_VALIDATION_ENABLED:true}"
ts_latest: ts_latest:
batch_size: "${SQL_TS_LATEST_BATCH_SIZE:10000}" batch_size: "${SQL_TS_LATEST_BATCH_SIZE:10000}"
batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}" batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}"
@ -1020,6 +1018,8 @@ queue:
max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}" max_poll_records: "${TB_QUEUE_KAFKA_MAX_POLL_RECORDS:8192}"
max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}" max_partition_fetch_bytes: "${TB_QUEUE_KAFKA_MAX_PARTITION_FETCH_BYTES:16777216}"
fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}" fetch_max_bytes: "${TB_QUEUE_KAFKA_FETCH_MAX_BYTES:134217728}"
request.timeout.ms: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms
session.timeout.ms: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms
use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}" use_confluent_cloud: "${TB_QUEUE_KAFKA_USE_CONFLUENT_CLOUD:false}"
confluent: confluent:
ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}" ssl.algorithm: "${TB_QUEUE_KAFKA_CONFLUENT_SSL_ALGORITHM:https}"
@ -1038,11 +1038,12 @@ queue:
# tb_rule_engine.sq: # tb_rule_engine.sq:
# - key: max.poll.records # - key: max.poll.records
# value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}" # value: "${TB_QUEUE_KAFKA_SQ_MAX_POLL_RECORDS:1024}"
other: # In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside other-inline: "${TB_QUEUE_KAFKA_OTHER_PROPERTIES:}" # In this section you can specify custom parameters (semicolon separated) for Kafka consumer/producer/admin # Example "metrics.recording.level:INFO;metrics.sample.window.ms:30000"
- key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms other: # DEPRECATED. In this section you can specify custom parameters for Kafka consumer/producer and expose the env variables to configure outside
value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds) # - key: "request.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/producer-configs.html#producerconfigs_request.timeout.ms
- key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms # value: "${TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS:30000}" # (30 seconds)
value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds) # - key: "session.timeout.ms" # refer to https://docs.confluent.io/platform/current/installation/configuration/consumer-configs.html#consumerconfigs_session.timeout.ms
# value: "${TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS:10000}" # (10 seconds)
topic-properties: topic-properties:
rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"
core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}" core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1}"

19
application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java

@ -29,7 +29,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
@ -180,7 +179,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
alarm = updatedAlarm; alarm = updatedAlarm;
alarm.setAssigneeId(null); alarm.setAssigneeId(null);
@ -192,7 +191,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class);
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED);
} }
@Test @Test
@ -414,7 +413,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
} }
@Test @Test
@ -445,7 +444,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
logout(); logout();
@ -462,7 +461,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGNED);
} }
@Test @Test
@ -480,7 +479,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
beforeAssignmentTs = System.currentTimeMillis(); beforeAssignmentTs = System.currentTimeMillis();
Thread.sleep(2); Thread.sleep(2);
@ -491,7 +490,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGNED);
} }
@Test @Test
@ -509,7 +508,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGNED);
logout(); logout();
loginCustomerUser(); loginCustomerUser();
@ -525,7 +524,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis());
testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(),
tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGN); tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGNED);
} }
@Test @Test

72
application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import com.datastax.oss.driver.api.core.uuid.Uuids; import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.ListeningExecutorService;
@ -33,11 +34,13 @@ import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.OtaPackageInfo; import org.thingsboard.server.common.data.OtaPackageInfo;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest; import org.thingsboard.server.common.data.SaveOtaPackageInfoRequest;
@ -68,6 +71,8 @@ import org.thingsboard.server.service.gateway_device.GatewayNotificationsService
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
@ -1392,6 +1397,73 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
Assert.assertEquals(savedCredentials, updatedCredentials); Assert.assertEquals(savedCredentials, updatedCredentials);
} }
@Test
public void testBulkImportDeviceWithUpdateFalse() throws Exception {
String deviceName = "firstDevice";
String attributeValue = "testValue";
BulkImportRequest request = new BulkImportRequest();
request.setFile(String.format("NAME,TYPE,DATA\n%s,%s,%s", deviceName, "thermostat", attributeValue));
BulkImportRequest.Mapping mapping = new BulkImportRequest.Mapping();
BulkImportRequest.ColumnMapping name = new BulkImportRequest.ColumnMapping();
name.setType(BulkImportColumnType.NAME);
BulkImportRequest.ColumnMapping type = new BulkImportRequest.ColumnMapping();
type.setType(BulkImportColumnType.TYPE);
BulkImportRequest.ColumnMapping attribute = new BulkImportRequest.ColumnMapping();
attribute.setType(BulkImportColumnType.SERVER_ATTRIBUTE);
attribute.setKey("DATA");
List<BulkImportRequest.ColumnMapping> columns = new ArrayList<>();
columns.add(name);
columns.add(type);
columns.add(attribute);
mapping.setColumns(columns);
mapping.setDelimiter(',');
mapping.setUpdate(true);
mapping.setHeader(true);
request.setMapping(mapping);
//import device
doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {});
Device savedDevice = doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
//check server attribute value
List<Map<String, Object>> values = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId() +
"/values/attributes/SERVER_SCOPE", new TypeReference<>() {
});
Map<String, Object> serverAttribute = values.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get();
Assert.assertEquals(attributeValue, serverAttribute.get("value"));
//update server attribute value
String newAttributeValue = "testValue2";
JsonNode content = JacksonUtil.toJsonNode("{\"DATA\": \"" + newAttributeValue + "\"}");
doPost("/api/plugins/telemetry/" + EntityType.DEVICE.name() + "/" + savedDevice.getUuidId() + "/SERVER_SCOPE", content)
.andExpect(status().isOk());
//reimport devices
String deviceName2 = "secondDevice";
String attributeValue2 = "testValue3";
request.setFile(String.format("NAME,TYPE,DATA\n%s,%s,%s\n%s,%s,%s", deviceName, "thermostat", attributeValue,
deviceName2, "thermostat", attributeValue2));
mapping.setUpdate(false);
doPostWithTypedResponse("/api/device/bulk_import", request, new TypeReference<>() {});
Device savedDevice2 = doGet("/api/tenant/devices?deviceName=" + deviceName2, Device.class);
//check attribute value was not changed after reimport
List<Map<String, Object>> values2 = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice.getId() +
"/values/attributes/SERVER_SCOPE", new TypeReference<>() {
});
Map<String, Object> retrievedServerAttribute2 = values2.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get();
Assert.assertEquals(newAttributeValue, retrievedServerAttribute2.get("value"));
//check attribute for second device
List<Map<String, Object>> values3 = doGetAsyncTyped("/api/plugins/telemetry/DEVICE/" + savedDevice2.getId() +
"/values/attributes/SERVER_SCOPE", new TypeReference<>() {
});
Map<String, Object> retrievedServerAttribute3 = values3.stream().filter(att -> att.get("key").equals("DATA")).findFirst().get();
Assert.assertEquals(attributeValue2, retrievedServerAttribute3.get("value"));
}
private Device createDevice(String name) { private Device createDevice(String name) {
Device device = new Device(); Device device = new Device();
device.setName(name); device.setName(name);

48
application/src/test/java/org/thingsboard/server/controller/BaseHomePageApiTest.java

@ -21,8 +21,12 @@ import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageState; import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Customer;
@ -53,6 +57,7 @@ import org.thingsboard.server.common.data.query.TsValue;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.stats.TbApiUsageStateClient; import org.thingsboard.server.common.stats.TbApiUsageStateClient;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache; import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountCmd;
import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate; import org.thingsboard.server.service.ws.telemetry.cmd.v2.EntityCountUpdate;
@ -76,6 +81,15 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
@Autowired @Autowired
private TbTenantProfileCache tenantProfileCache; private TbTenantProfileCache tenantProfileCache;
@Autowired
private AdminSettingsService adminSettingsService;
@MockBean
private MailService mailService;
@MockBean
private SmsService smsService;
//For system administrator //For system administrator
@Test @Test
public void testTenantsCountWsCmd() throws Exception { public void testTenantsCountWsCmd() throws Exception {
@ -266,6 +280,19 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
@Test @Test
public void testGetFeaturesInfo() throws Exception { public void testGetFeaturesInfo() throws Exception {
String mail = "test@thingsboard.org";
Mockito.doAnswer(invocation -> {
AdminSettings mailSettings = adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "mail");
JsonNode mailFrom = mailSettings.getJsonValue().get("mailFrom");
if (!mailFrom.asText().equals(mail)) {
throw new Exception();
}
return null;
}).when(mailService).testConnection(TenantId.SYS_TENANT_ID);
Mockito.when(smsService.isConfigured(TenantId.SYS_TENANT_ID))
.then(a -> adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, "sms") != null);
loginSysAdmin(); loginSysAdmin();
FeaturesInfo featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); FeaturesInfo featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
@ -279,7 +306,7 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
AdminSettings mailSettings = doGet("/api/admin/settings/mail", AdminSettings.class); AdminSettings mailSettings = doGet("/api/admin/settings/mail", AdminSettings.class);
JsonNode jsonValue = mailSettings.getJsonValue(); JsonNode jsonValue = mailSettings.getJsonValue();
((ObjectNode) jsonValue).put("mailFrom", "test@thingsboard.org"); ((ObjectNode) jsonValue).put("mailFrom", mail);
mailSettings.setJsonValue(jsonValue); mailSettings.setJsonValue(jsonValue);
doPost("/api/admin/settings", mailSettings).andExpect(status().isOk()); doPost("/api/admin/settings", mailSettings).andExpect(status().isOk());
@ -305,7 +332,12 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
AdminSettings twoFaSettingsSettings = new AdminSettings(); AdminSettings twoFaSettingsSettings = new AdminSettings();
twoFaSettingsSettings.setKey("twoFaSettings"); twoFaSettingsSettings.setKey("twoFaSettings");
twoFaSettingsSettings.setJsonValue(JacksonUtil.newObjectNode());
var twoFaSettings = JacksonUtil.newObjectNode();
var providers = JacksonUtil.newArrayNode();
providers.add(JacksonUtil.newObjectNode());
twoFaSettings.set("providers", providers);
twoFaSettingsSettings.setJsonValue(twoFaSettings);
doPost("/api/admin/settings", twoFaSettingsSettings).andExpect(status().isOk()); doPost("/api/admin/settings", twoFaSettingsSettings).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
@ -317,7 +349,13 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
AdminSettings notificationsSettings = new AdminSettings(); AdminSettings notificationsSettings = new AdminSettings();
notificationsSettings.setKey("notifications"); notificationsSettings.setKey("notifications");
notificationsSettings.setJsonValue(JacksonUtil.newObjectNode());
var notificationSettings = JacksonUtil.newObjectNode();
var deliveryMethodsConfigs = JacksonUtil.newObjectNode();
deliveryMethodsConfigs.set("SLACK", JacksonUtil.newObjectNode());
notificationSettings.set("deliveryMethodsConfigs", deliveryMethodsConfigs);
notificationsSettings.setJsonValue(notificationSettings);
doPost("/api/admin/settings", notificationsSettings).andExpect(status().isOk()); doPost("/api/admin/settings", notificationsSettings).andExpect(status().isOk());
featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class); featuresInfo = doGet("/api/admin/featuresInfo", FeaturesInfo.class);
@ -338,6 +376,10 @@ public abstract class BaseHomePageApiTest extends AbstractControllerTest {
Assert.assertTrue(featuresInfo.isTwoFaEnabled()); Assert.assertTrue(featuresInfo.isTwoFaEnabled());
Assert.assertTrue(featuresInfo.isNotificationEnabled()); Assert.assertTrue(featuresInfo.isNotificationEnabled());
Assert.assertTrue(featuresInfo.isOauthEnabled()); Assert.assertTrue(featuresInfo.isOauthEnabled());
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "notifications");
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "twoFaSettings");
adminSettingsService.deleteAdminSettingsByTenantIdAndKey(TenantId.SYS_TENANT_ID, "sms");
} }
@Test @Test

6
application/src/test/java/org/thingsboard/server/controller/BaseTelemetryControllerTest.java

@ -16,6 +16,7 @@
package org.thingsboard.server.controller; package org.thingsboard.server.controller;
import org.junit.Test; import org.junit.Test;
import org.stringtemplate.v4.ST;
import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest; import org.thingsboard.server.common.data.SaveDeviceWithCredentialsRequest;
import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentials;
@ -32,10 +33,7 @@ public abstract class BaseTelemetryControllerTest extends AbstractControllerTest
String correctRequestBody = "{\"data\": \"value\"}"; String correctRequestBody = "{\"data\": \"value\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", correctRequestBody, String.class, status().isOk()); doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", correctRequestBody, String.class, status().isOk());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", correctRequestBody, String.class, status().isOk()); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", correctRequestBody, String.class, status().isOk());
String invalidRequestBody = "{\"data\": \"<object data=\\\"data:text/html,<script>alert(document)</script>\\\"></object>\"}"; String invalidRequestBody = "{\"<object data=\\\"data:text/html,<script>alert(document)</script>\\\"></object>\": \"data\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
invalidRequestBody = "{\"<object data=\\\"data:text/html,<script>alert(document)</script>\\\"></object>\": \"data\"}";
doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest()); doPostAsync("/api/plugins/telemetry/" + device.getId() + "/SHARED_SCOPE", invalidRequestBody, String.class, status().isBadRequest());
doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest()); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId() + "/timeseries/smth", invalidRequestBody, String.class, status().isBadRequest());
} }

31
application/src/test/java/org/thingsboard/server/service/notification/AbstractNotificationApiTest.java

@ -35,6 +35,10 @@ import org.thingsboard.server.common.data.notification.NotificationRequestConfig
import org.thingsboard.server.common.data.notification.NotificationRequestInfo; import org.thingsboard.server.common.data.notification.NotificationRequestInfo;
import org.thingsboard.server.common.data.notification.NotificationRequestStats; import org.thingsboard.server.common.data.notification.NotificationRequestStats;
import org.thingsboard.server.common.data.notification.NotificationType; import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.rule.DefaultNotificationRuleRecipientsConfig;
import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.settings.NotificationSettings; import org.thingsboard.server.common.data.notification.settings.NotificationSettings;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig; import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
@ -52,6 +56,7 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.DaoUtil;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -195,6 +200,32 @@ public abstract class AbstractNotificationApiTest extends AbstractControllerTest
new PageLink(limit, 0), unreadOnly).getData(); new PageLink(limit, 0), unreadOnly).getData();
} }
protected NotificationRule createNotificationRule(NotificationRuleTriggerConfig triggerConfig, String subject, String text, NotificationTargetId... targets) {
NotificationTemplate template = createNotificationTemplate(NotificationType.valueOf(triggerConfig.getTriggerType().toString()), subject, text, NotificationDeliveryMethod.WEB);
NotificationRule rule = new NotificationRule();
rule.setName(triggerConfig.getTriggerType() + " [" + Arrays.toString(targets) + "]");
rule.setTemplateId(template.getId());
rule.setTriggerType(triggerConfig.getTriggerType());
rule.setTriggerConfig(triggerConfig);
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig();
recipientsConfig.setTriggerType(triggerConfig.getTriggerType());
recipientsConfig.setTargets(DaoUtil.toUUIDs(List.of(targets)));
rule.setRecipientsConfig(recipientsConfig);
return saveNotificationRule(rule);
}
protected NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}
protected PageData<NotificationRuleInfo> findNotificationRules() throws Exception {
PageLink pageLink = new PageLink(10);
return doGetTypedWithPageLink("/api/notification/rules?", new TypeReference<PageData<NotificationRuleInfo>>() {}, pageLink);
}
@Override @Override
protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException { protected NotificationApiWsClient buildAndConnectWebSocketClient() throws URISyntaxException, InterruptedException {
NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort, token); NotificationApiWsClient wsClient = new NotificationApiWsClient(WS_URL + wsPort, token);

7
application/src/test/java/org/thingsboard/server/service/notification/MockNotificationSettingsService.java

@ -19,17 +19,14 @@ import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService; import org.thingsboard.server.dao.notification.DefaultNotificationSettingsService;
import org.thingsboard.server.dao.notification.NotificationRuleService;
import org.thingsboard.server.dao.notification.NotificationTargetService;
import org.thingsboard.server.dao.notification.NotificationTemplateService;
import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.settings.AdminSettingsService;
@Service @Service
@Primary @Primary
public class MockNotificationSettingsService extends DefaultNotificationSettingsService { public class MockNotificationSettingsService extends DefaultNotificationSettingsService {
public MockNotificationSettingsService(AdminSettingsService adminSettingsService, NotificationTargetService notificationTargetService, NotificationTemplateService notificationTemplateService, NotificationRuleService notificationRuleService) { public MockNotificationSettingsService(AdminSettingsService adminSettingsService) {
super(adminSettingsService, notificationTargetService, notificationTemplateService, notificationRuleService); super(adminSettingsService, null, null);
} }
@Override @Override

8
application/src/test/java/org/thingsboard/server/service/notification/NotificationApiTest.java

@ -362,13 +362,14 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
SlackNotificationTargetConfig slackTargetConfig = new SlackNotificationTargetConfig(); SlackNotificationTargetConfig slackTargetConfig = new SlackNotificationTargetConfig();
slackTargetConfig.setConversationType(SlackConversationType.DIRECT); slackTargetConfig.setConversationType(SlackConversationType.DIRECT);
SlackConversation slackConversation = new SlackConversation(); SlackConversation slackConversation = new SlackConversation();
slackConversation.setType(SlackConversationType.DIRECT);
slackConversation.setId("U1234567"); slackConversation.setId("U1234567");
slackConversation.setTitle("@jdoe (John Doe)"); slackConversation.setName("jdoe");
slackConversation.setWholeName("John Doe"); slackConversation.setWholeName("John Doe");
slackTargetConfig.setConversation(slackConversation); slackTargetConfig.setConversation(slackConversation);
slackTarget.setConfiguration(slackTargetConfig); slackTarget.setConfiguration(slackTargetConfig);
slackTarget = saveNotificationTarget(slackTarget); slackTarget = saveNotificationTarget(slackTarget);
recipients.add(slackConversation.getTitle()); recipients.add("@" + slackConversation.getWholeName());
NotificationTemplate notificationTemplate = new NotificationTemplate(); NotificationTemplate notificationTemplate = new NotificationTemplate();
notificationTemplate.setNotificationType(NotificationType.GENERAL); notificationTemplate.setNotificationType(NotificationType.GENERAL);
@ -500,8 +501,9 @@ public class NotificationApiTest extends AbstractNotificationApiTest {
notificationTarget.setName(conversationName + " in Slack"); notificationTarget.setName(conversationName + " in Slack");
SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig(); SlackNotificationTargetConfig targetConfig = new SlackNotificationTargetConfig();
targetConfig.setConversation(SlackConversation.builder() targetConfig.setConversation(SlackConversation.builder()
.type(SlackConversationType.DIRECT)
.id(conversationId) .id(conversationId)
.title(conversationName) .name(conversationName)
.build()); .build());
notificationTarget.setConfiguration(targetConfig); notificationTarget.setConfiguration(targetConfig);
notificationTarget = saveNotificationTarget(notificationTarget); notificationTarget = saveNotificationTarget(notificationTarget);

198
application/src/test/java/org/thingsboard/server/service/notification/NotificationRuleApiTest.java

@ -15,11 +15,11 @@
*/ */
package org.thingsboard.server.service.notification; package org.thingsboard.server.service.notification;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.BooleanNode;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.function.ThrowingRunnable;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.data.util.Pair; import org.springframework.data.util.Pair;
@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmCondition;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter;
import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilterKey;
@ -54,6 +55,7 @@ import org.thingsboard.server.common.data.notification.rule.NotificationRule;
import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo; import org.thingsboard.server.common.data.notification.rule.NotificationRuleInfo;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction; import org.thingsboard.server.common.data.notification.rule.trigger.AlarmNotificationRuleTriggerConfig.AlarmAction;
import org.thingsboard.server.common.data.notification.rule.trigger.EntitiesLimitNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig; import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType; import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
@ -63,14 +65,18 @@ import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.query.BooleanFilterPredicate; import org.thingsboard.server.common.data.query.BooleanFilterPredicate;
import org.thingsboard.server.common.data.query.EntityKeyValueType; import org.thingsboard.server.common.data.query.EntityKeyValueType;
import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.data.query.FilterPredicateValue;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.security.Authority; import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.dao.notification.NotificationRequestService; import org.thingsboard.server.dao.notification.NotificationRequestService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.tenant.TenantProfileService; import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.service.apiusage.limits.LimitedApi; import org.thingsboard.server.service.apiusage.limits.LimitedApi;
import org.thingsboard.server.service.apiusage.limits.RateLimitService; import org.thingsboard.server.service.apiusage.limits.RateLimitService;
import org.thingsboard.server.service.entitiy.tenant.profile.TbTenantProfileService;
import org.thingsboard.server.service.telemetry.AlarmSubscriptionService; import org.thingsboard.server.service.telemetry.AlarmSubscriptionService;
import java.util.ArrayList; import java.util.ArrayList;
@ -80,7 +86,10 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.offset; import static org.assertj.core.api.Assertions.offset;
@ -98,7 +107,11 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private TenantProfileService tenantProfileService; private TenantProfileService tenantProfileService;
@Autowired @Autowired
private TbTenantProfileService tbTenantProfileService;
@Autowired
private RateLimitService rateLimitService; private RateLimitService rateLimitService;
@Autowired
private RuleChainService ruleChainService;
@Before @Before
public void beforeEach() throws Exception { public void beforeEach() throws Exception {
@ -107,56 +120,33 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
@Test @Test
public void testNotificationRuleProcessing_entityActionTrigger() throws Exception { public void testNotificationRuleProcessing_entityActionTrigger() throws Exception {
String notificationSubject = "${actionType}: ${entityType} [${entityId}]";
String notificationText = "User: ${userEmail}";
NotificationTemplate notificationTemplate = createNotificationTemplate(NotificationType.GENERAL, notificationSubject, notificationText, NotificationDeliveryMethod.WEB);
NotificationRule notificationRule = new NotificationRule();
notificationRule.setName("Web notification when any device is created, updated or deleted");
notificationRule.setTemplateId(notificationTemplate.getId());
notificationRule.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION);
EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig(); EntityActionNotificationRuleTriggerConfig triggerConfig = new EntityActionNotificationRuleTriggerConfig();
triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE)); triggerConfig.setEntityTypes(Set.of(EntityType.DEVICE));
triggerConfig.setCreated(true); triggerConfig.setCreated(true);
triggerConfig.setUpdated(true); triggerConfig.setUpdated(true);
triggerConfig.setDeleted(true); triggerConfig.setDeleted(true);
createNotificationRule(triggerConfig, "${actionType}: ${entityType} [${entityId}]",
"User: ${userEmail}", createNotificationTarget(tenantAdminUserId).getId());
Device device = checkNotificationAfter(() -> {
return createDevice("DEVICE!!!", "default", "12345");
}, (notification, newDevice) -> {
assertThat(notification.getSubject()).isEqualTo("added: Device [" + newDevice.getId() + "]");
assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL);
});
DefaultNotificationRuleRecipientsConfig recipientsConfig = new DefaultNotificationRuleRecipientsConfig(); checkNotificationAfter(() -> {
recipientsConfig.setTriggerType(NotificationRuleTriggerType.ENTITY_ACTION); device.setName("Updated name");
recipientsConfig.setTargets(List.of(createNotificationTarget(tenantAdminUserId).getUuidId())); doPost("/api/device", device, Device.class);
}, notification -> {
notificationRule.setTriggerConfig(triggerConfig); assertThat(notification.getSubject()).isEqualTo("updated: Device [" + device.getId() + "]");
notificationRule.setRecipientsConfig(recipientsConfig); });
notificationRule = saveNotificationRule(notificationRule);
getWsClient().subscribeForUnreadNotifications(10).waitForReply(true);
getWsClient().registerWaitForUpdate();
Device device = createDevice("DEVICE!!!", "default", "12345");
getWsClient().waitForUpdate(true);
Notification notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("added: Device [" + device.getId() + "]");
assertThat(notification.getText()).isEqualTo("User: " + TENANT_ADMIN_EMAIL);
getWsClient().registerWaitForUpdate();
device.setName("Updated name");
device = doPost("/api/device", device, Device.class);
getWsClient().waitForUpdate(true);
notification = getWsClient().getLastDataUpdate().getUpdate();
assertThat(notification.getSubject()).isEqualTo("updated: Device [" + device.getId() + "]");
getWsClient().registerWaitForUpdate();
doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
getWsClient().waitForUpdate(true);
notification = getWsClient().getLastDataUpdate().getUpdate(); checkNotificationAfter(() -> {
assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]"); doDelete("/api/device/" + device.getId()).andExpect(status().isOk());
}, notification -> {
assertThat(notification.getSubject()).isEqualTo("deleted: Device [" + device.getId() + "]");
});
} }
@Test @Test
@ -215,7 +205,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
clients.forEach((expectedDelay, wsClient) -> { clients.forEach((expectedDelay, wsClient) -> {
Notification notification = wsClient.getLastDataUpdate().getUpdate(); Notification notification = wsClient.getLastDataUpdate().getUpdate();
double actualDelay = (double) (notification.getCreatedTime() - ts) / 1000; double actualDelay = (double) (notification.getCreatedTime() - ts) / 1000;
assertThat(actualDelay).isCloseTo(expectedDelay, offset(0.5)); assertThat(actualDelay).isCloseTo(expectedDelay, offset(2.0));
assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " + assertThat(notification.getSubject()).isEqualTo("Alarm type: " + alarmType + ", status: " + AlarmStatus.ACTIVE_UNACK + ", " +
"severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId()); "severity: " + AlarmSeverity.CRITICAL.toString().toLowerCase() + ", deviceId: " + device.getId());
@ -312,6 +302,79 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty(); assertThat(findNotificationRequests(EntityType.ALARM).getData()).filteredOn(NotificationRequest::isScheduled).isEmpty();
} }
@Test
public void testNotificationRuleProcessing_entitiesLimit() throws Exception {
TenantProfile tenantProfile = tenantProfileService.findDefaultTenantProfile(TenantId.SYS_TENANT_ID);
TenantProfileData profileData = tenantProfile.getProfileData();
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration();
int limit = 5;
profileConfiguration.setMaxDevices(limit);
profileConfiguration.setMaxAssets(limit);
profileConfiguration.setMaxCustomers(limit);
profileConfiguration.setMaxUsers(limit);
profileConfiguration.setMaxDashboards(limit);
profileConfiguration.setMaxRuleChains(limit);
tenantProfile.setProfileData(profileData);
tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null);
EntitiesLimitNotificationRuleTriggerConfig triggerConfig = EntitiesLimitNotificationRuleTriggerConfig.builder()
.entityTypes(null).threshold(0.8f)
.build();
loginSysAdmin();
NotificationRule rule = createNotificationRule(triggerConfig, "${entityType}s limit will be reached soon",
"${entityType}s usage: ${currentCount}/${limit} (${percents}%)", createNotificationTarget(tenantAdminUserId).getId());
int threshold = (int) (limit * 0.8);
loginTenantAdmin();
checkNotificationAfter(() -> {
for (int i = 1; i <= threshold; i++) {
createDevice(i + "", i + "");
}
}, notification -> {
assertThat(notification.getText()).isEqualTo("Devices usage: " + threshold + "/" + limit + " (80%)");
});
checkNotificationAfter(() -> {
for (int i = 1; i <= threshold; i++) {
Asset asset = new Asset();
asset.setType("Test");
asset.setName(i + "");
doPost("/api/asset", asset);
}
}, notification -> {
assertThat(notification.getText()).isEqualTo("Assets usage: " + threshold + "/" + limit + " (80%)");
});
checkNotificationAfter(() -> {
long present = ruleChainService.countByTenantId(tenantId);
for (int i = 1; i <= threshold - present; i++) {
RuleChain ruleChain = new RuleChain();
ruleChain.setName(i + "");
ruleChain.setRoot(false);
ruleChain.setDebugMode(false);
ruleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
RuleChainMetaData metaData = new RuleChainMetaData();
metaData.setRuleChainId(ruleChain.getId());
metaData.setNodes(List.of());
doPost("/api/ruleChain/metadata", metaData);
}
}, notification -> {
assertThat(notification.getText()).isEqualTo("Rule chains usage: " + threshold + "/" + limit + " (80%)");
});
triggerConfig.setThreshold(1.0f);
rule.setTriggerConfig(triggerConfig);
loginSysAdmin();
saveNotificationRule(rule);
loginTenantAdmin();
checkNotificationAfter(() -> {
createDevice(limit + "", limit + "");
}, notification -> {
assertThat(notification.getText()).isEqualTo("Devices usage: " + limit + "/" + limit + " (100%)");
});
}
@Test @Test
public void testNotificationRuleInfo() throws Exception { public void testNotificationRuleInfo() throws Exception {
NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL}; NotificationDeliveryMethod[] deliveryMethods = {NotificationDeliveryMethod.WEB, NotificationDeliveryMethod.EMAIL};
@ -345,9 +408,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration(); DefaultTenantProfileConfiguration profileConfiguration = (DefaultTenantProfileConfiguration) profileData.getConfiguration();
profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(notificationRequestsLimit + ":300"); profileConfiguration.setTenantNotificationRequestsPerRuleRateLimit(notificationRequestsLimit + ":300");
tenantProfile.setProfileData(profileData); tenantProfile.setProfileData(profileData);
loginSysAdmin(); tbTenantProfileService.save(TenantId.SYS_TENANT_ID, tenantProfile, null);
doPost("/api/tenantProfile", tenantProfile).andExpect(status().isOk());
loginTenantAdmin();
NotificationRule rule = new NotificationRule(); NotificationRule rule = new NotificationRule();
rule.setName("Device created"); rule.setName("Device created");
@ -370,7 +431,7 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
String name = "device " + i; String name = "device " + i;
createDevice(name, name); createDevice(name, name);
} }
await().atMost(5, TimeUnit.SECONDS) await().atMost(30, TimeUnit.SECONDS)
.untilAsserted(() -> { .untilAsserted(() -> {
assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit); assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit);
}); });
@ -386,6 +447,32 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit); assertThat(getMyNotifications(false, 100)).size().isEqualTo(notificationRequestsLimit);
} }
private <R> R checkNotificationAfter(Callable<R> action, BiConsumer<Notification, R> check) throws Exception {
if (getWsClient().getLastDataUpdate() == null) {
getWsClient().subscribeForUnreadNotifications(10).waitForReply(true);
}
getWsClient().registerWaitForUpdate();
R result = action.call();
getWsClient().waitForUpdate(true);
check.accept(getWsClient().getLastDataUpdate().getUpdate(), result);
return result;
}
private void checkNotificationAfter(ThrowingRunnable action, Consumer<Notification> check) throws Exception {
checkNotificationAfter(() -> {
try {
action.run();
return null;
} catch (Throwable e) {
throw new Exception(e);
}
}, (notification, r) -> check.accept(notification));
}
private PageData<NotificationRequestInfo> findNotificationRequests(EntityType originatorType) {
return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(tenantId, originatorType, new PageLink(100));
}
private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) { private DeviceProfile createDeviceProfileWithAlarmRules(String alarmType) {
DeviceProfile deviceProfile = createDeviceProfile("For notification rule test"); DeviceProfile deviceProfile = createDeviceProfile("For notification rule test");
deviceProfile.setTenantId(tenantId); deviceProfile.setTenantId(tenantId);
@ -421,17 +508,4 @@ public class NotificationRuleApiTest extends AbstractNotificationApiTest {
return deviceProfile; return deviceProfile;
} }
private NotificationRule saveNotificationRule(NotificationRule notificationRule) {
return doPost("/api/notification/rule", notificationRule, NotificationRule.class);
}
private PageData<NotificationRuleInfo> findNotificationRules() throws Exception {
PageLink pageLink = new PageLink(10);
return doGetTypedWithPageLink("/api/notification/rules?", new TypeReference<PageData<NotificationRuleInfo>>() {}, pageLink);
}
private PageData<NotificationRequestInfo> findNotificationRequests(EntityType originatorType) {
return notificationRequestService.findNotificationRequestsInfosByTenantIdAndOriginatorType(tenantId, originatorType, new PageLink(100));
}
} }

25
application/src/test/java/org/thingsboard/server/service/notification/NotificationTargetApiTest.java

@ -23,6 +23,8 @@ import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.ResultMatcher;
import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.NotificationDeliveryMethod;
import org.thingsboard.server.common.data.notification.rule.trigger.EntityActionNotificationRuleTriggerConfig;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget; import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.AllUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter; import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
@ -30,7 +32,6 @@ import org.thingsboard.server.common.data.notification.targets.platform.Platform
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter; import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.notification.NotificationTargetDao; import org.thingsboard.server.dao.notification.NotificationTargetDao;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
@ -41,7 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
public class NotificationTargetApiTest extends AbstractControllerTest { public class NotificationTargetApiTest extends AbstractNotificationApiTest {
@Autowired @Autowired
private NotificationTargetDao notificationTargetDao; private NotificationTargetDao notificationTargetDao;
@ -148,6 +149,26 @@ public class NotificationTargetApiTest extends AbstractControllerTest {
assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty(); assertThat(notificationTargetDao.findByTenantIdAndPageLink(differentTenantId, new PageLink(10)).getData()).isEmpty();
} }
@Test
public void whenDeletingTargetUsedByRule_thenReturnError() throws Exception {
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
createNotificationRule(new EntityActionNotificationRuleTriggerConfig(), "Test", "Test", target.getId());
String error = getErrorMessage(doDelete("/api/notification/target/" + target.getId())
.andExpect(status().isBadRequest()));
assertThat(error).containsIgnoringCase("used in notification rule");
}
@Test
public void whenDeletingTargetUsedByScheduledNotificationRequest_thenReturnError() throws Exception {
NotificationTarget target = createNotificationTarget(tenantAdminUserId);
submitNotificationRequest(target.getId(), "Test", 100, NotificationDeliveryMethod.WEB);
String error = getErrorMessage(doDelete("/api/notification/target/" + target.getId())
.andExpect(status().isBadRequest()));
assertThat(error).containsIgnoringCase("referenced by scheduled notification request");
}
private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception { private String saveAndGetError(NotificationTarget notificationTarget, ResultMatcher statusMatcher) throws Exception {
return getErrorMessage(save(notificationTarget, statusMatcher)); return getErrorMessage(save(notificationTarget, statusMatcher));
} }

13
application/src/test/java/org/thingsboard/server/service/stats/DevicesStatisticsTest.java

@ -28,6 +28,7 @@ import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest; import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.state.DeviceStateService;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -35,17 +36,14 @@ import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await; import static org.awaitility.Awaitility.await;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@DaoSqlTest @DaoSqlTest
@TestPropertySource(properties = { @TestPropertySource(properties = {
"usage.stats.report.enabled=true", "usage.stats.report.enabled=true",
"transport.http.enabled=true",
"usage.stats.report.interval=2", "usage.stats.report.interval=2",
"usage.stats.gauge_report_interval=1", "usage.stats.gauge_report_interval=1",
"state.defaultStateCheckIntervalInSec=3", "state.defaultStateCheckIntervalInSec=3",
"state.defaultInactivityTimeoutInSec=10" "state.defaultInactivityTimeoutInSec=10"
}) })
public class DevicesStatisticsTest extends AbstractControllerTest { public class DevicesStatisticsTest extends AbstractControllerTest {
@ -53,6 +51,8 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
private TbApiUsageStateService apiUsageStateService; private TbApiUsageStateService apiUsageStateService;
@Autowired @Autowired
private TimeseriesService timeseriesService; private TimeseriesService timeseriesService;
@Autowired
private DeviceStateService deviceStateService;
private ApiUsageStateId apiUsageStateId; private ApiUsageStateId apiUsageStateId;
@ -86,7 +86,7 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
}); });
for (Device device : activeDevices) { for (Device device : activeDevices) {
postTelemetry(device.getName(), "{\"dp\":1}"); deviceStateService.onDeviceActivity(tenantId, device.getId(), System.currentTimeMillis());
} }
await().atMost(40, TimeUnit.SECONDS) await().atMost(40, TimeUnit.SECONDS)
@ -102,9 +102,4 @@ public class DevicesStatisticsTest extends AbstractControllerTest {
.get().stream().findFirst().flatMap(KvEntry::getLongValue).orElse(null); .get().stream().findFirst().flatMap(KvEntry::getLongValue).orElse(null);
} }
@SneakyThrows
private void postTelemetry(String accessToken, String json) {
doPost("/api/v1/" + accessToken + "/telemetry", json, new String[0]).andExpect(status().isOk());
}
} }

55
application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java

@ -68,7 +68,6 @@ import org.thingsboard.server.service.ota.OtaPackageStateService;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
@ -254,21 +253,25 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1"); Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2"); Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2");
Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1"); Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1");
DeviceProfile existingDeviceProfile = createDeviceProfile(tenantId2, null, null, "Existing");
String aliasId = "23c4185d-1497-9457-30b2-6d91e69a5b2c";
String unknownUuid = "ea0dc8b0-3d85-11ed-9200-77fc04fa14fa";
String entityAliases = "{\n" + String entityAliases = "{\n" +
"\t\"23c4185d-1497-9457-30b2-6d91e69a5b2c\": {\n" + "\"" + aliasId + "\": {\n" +
"\t\t\"alias\": \"assets\",\n" + "\"alias\": \"assets\",\n" +
"\t\t\"filter\": {\n" + "\"filter\": {\n" +
"\t\t\t\"entityList\": [\n" + "\"entityList\": [\n" +
"\t\t\t\t\"" + asset1.getId().toString() + "\",\n" + "\"" + asset1.getId().toString() + "\",\n" +
"\t\t\t\t\"" + asset2.getId().toString() + "\"\n" + "\"" + asset2.getId().toString() + "\",\n" +
"\t\t\t],\n" + "\"" + tenantId1.getId().toString() + "\",\n" +
"\t\t\t\"entityType\": \"ASSET\",\n" + "\"" + existingDeviceProfile.getId().toString() + "\",\n" +
"\t\t\t\"resolveMultiple\": true,\n" + "\"" + unknownUuid + "\"\n" +
"\t\t\t\"type\": \"entityList\"\n" + "],\n" +
"\t\t},\n" + "\"resolveMultiple\": true\n" +
"\t\t\"id\": \"23c4185d-1497-9457-30b2-6d91e69a5b2c\"\n" + "},\n" +
"\t}\n" + "\"id\": \"" + aliasId + "\"\n" +
"}\n" +
"}"; "}";
ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode(); ObjectNode dashboardConfiguration = JacksonUtil.newObjectNode();
dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases)); dashboardConfiguration.set("entityAliases", JacksonUtil.toJsonNode(entityAliases));
@ -287,11 +290,23 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity(); Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity();
Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity(); Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity();
Set<String> entityAliasEntitiesIds = Streams.stream(importedDashboard.getConfiguration() Map.Entry<String, JsonNode> entityAlias = importedDashboard.getConfiguration().get("entityAliases").fields().next();
.get("entityAliases").elements().next().get("filter").get("entityList").elements()) assertThat(entityAlias.getKey()).isEqualTo(aliasId);
.map(JsonNode::asText).collect(Collectors.toSet()); assertThat(entityAlias.getValue().get("id").asText()).isEqualTo(aliasId);
assertThat(entityAliasEntitiesIds).doesNotContain(asset1.getId().toString(), asset2.getId().toString());
assertThat(entityAliasEntitiesIds).contains(importedAsset1.getId().toString(), importedAsset2.getId().toString()); List<String> aliasEntitiesIds = Streams.stream(entityAlias.getValue().get("filter").get("entityList").elements())
.map(JsonNode::asText).collect(Collectors.toList());
assertThat(aliasEntitiesIds).size().isEqualTo(5);
assertThat(aliasEntitiesIds).element(0).as("external asset 1 was replaced with imported one")
.isEqualTo(importedAsset1.getId().toString());
assertThat(aliasEntitiesIds).element(1).as("external asset 2 was replaced with imported one")
.isEqualTo(importedAsset2.getId().toString());
assertThat(aliasEntitiesIds).element(2).as("external tenant id was replaced with new tenant id")
.isEqualTo(tenantId2.toString());
assertThat(aliasEntitiesIds).element(3).as("existing device profile id was left as is")
.isEqualTo(existingDeviceProfile.getId().toString());
assertThat(aliasEntitiesIds).element(4).as("unresolved uuid was replaced with tenant id")
.isEqualTo(tenantId2.toString());
} }
@ -469,7 +484,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1"); Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1");
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(), Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId()) ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId())
.map(entityId -> { .map(entityId -> {
try { try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder() return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()

36
application/src/test/java/org/thingsboard/server/system/RestTemplateConvertersTest.java

@ -0,0 +1,36 @@
/**
* 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.system;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;
@Slf4j
public class RestTemplateConvertersTest {
@Test
public void testJacksonXmlConverter() {
ClassLoader classLoader = RestTemplate.class.getClassLoader();
boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
Assertions.assertFalse(jackson2XmlPresent, "XmlMapper must not be present in classpath, please, exclude \"jackson-dataformat-xml\" dependency!");
//If this xml mapper will be present in classpath then we will get "Unsupported Media Type" in RestTemplate
}
}

5
common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
@ -105,6 +106,10 @@ public interface AlarmService extends EntityDaoService {
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); ListenableFuture<PageData<AlarmInfo>> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId); AlarmStatus alarmStatus, String assigneeId);

4
common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java

@ -74,8 +74,8 @@ public class DataConstants {
public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED"; public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED";
public static final String ALARM_ACK = "ALARM_ACK"; public static final String ALARM_ACK = "ALARM_ACK";
public static final String ALARM_CLEAR = "ALARM_CLEAR"; public static final String ALARM_CLEAR = "ALARM_CLEAR";
public static final String ALARM_ASSIGN = "ALARM_ASSIGN"; public static final String ALARM_ASSIGNED = "ALARM_ASSIGNED";
public static final String ALARM_UNASSIGN = "ALARM_UNASSIGN"; public static final String ALARM_UNASSIGNED = "ALARM_UNASSIGNED";
public static final String ALARM_DELETE = "ALARM_DELETE"; public static final String ALARM_DELETE = "ALARM_DELETE";
public static final String COMMENT_CREATED = "COMMENT_CREATED"; public static final String COMMENT_CREATED = "COMMENT_CREATED";
public static final String COMMENT_UPDATED = "COMMENT_UPDATED"; public static final String COMMENT_UPDATED = "COMMENT_UPDATED";

4
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -46,6 +46,10 @@ public class StringUtils {
return source != null && !source.isEmpty() && !source.trim().isEmpty(); return source != null && !source.isEmpty() && !source.trim().isEmpty();
} }
public static String notBlankOrDefault(String src, String def) {
return isNotBlank(src) ? src : def;
}
public static String removeStart(final String str, final String remove) { public static String removeStart(final String str, final String remove) {
if (isEmpty(str) || isEmpty(remove)) { if (isEmpty(str) || isEmpty(remove)) {
return str; return str;

6
common/data/src/main/java/org/thingsboard/server/common/data/User.java

@ -74,7 +74,7 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
@ApiModelProperty(position = 1, value = "JSON object with the User Id. " + @ApiModelProperty(position = 1, value = "JSON object with the User Id. " +
"Specify this field to update the device. " + "Specify this field to update the device. " +
"Referencing non-existing User Id will cause error. " + "Referencing non-existing User Id will cause error. " +
"Omit this field to create new customer." ) "Omit this field to create new customer.")
@Override @Override
public UserId getId() { public UserId getId() {
return super.getId(); return super.getId();
@ -169,6 +169,10 @@ public class User extends SearchTextBasedWithAdditionalInfo<UserId> implements H
@JsonIgnore @JsonIgnore
public String getTitle() { public String getTitle() {
return getTitle(email, firstName, lastName);
}
public static String getTitle(String email, String firstName, String lastName) {
String title = ""; String title = "";
if (isNotEmpty(firstName)) { if (isNotEmpty(firstName)) {
title += firstName; title += firstName;

39
common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQueryV2.java

@ -0,0 +1,39 @@
/**
* 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.common.data.alarm;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.TimePageLink;
import java.util.List;
@Data
@Builder
@AllArgsConstructor
public class AlarmQueryV2 {
private EntityId affectedEntityId;
private TimePageLink pageLink;
private List<String> typeList;
private List<AlarmSearchStatus> statusList;
private List<AlarmSeverity> severityList;
private UserId assigneeId;
}

4
common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java

@ -40,8 +40,8 @@ public enum ActionType {
ALARM_ACK(false), ALARM_ACK(false),
ALARM_CLEAR(false), ALARM_CLEAR(false),
ALARM_DELETE(false), ALARM_DELETE(false),
ALARM_ASSIGN(false), ALARM_ASSIGNED(false),
ALARM_UNASSIGN(false), ALARM_UNASSIGNED(false),
LOGIN(false), LOGIN(false),
LOGOUT(false), LOGOUT(false),
LOCKOUT(false), LOCKOUT(false),

19
common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java

@ -17,8 +17,10 @@ package org.thingsboard.server.common.data.device.profile;
import lombok.Data; import lombok.Data;
import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.validation.NoXss; import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Objects;
import java.util.Set; import java.util.Set;
@Data @Data
@ -42,12 +44,19 @@ public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTra
} }
public TransportPayloadTypeConfiguration getTransportPayloadTypeConfiguration() { public TransportPayloadTypeConfiguration getTransportPayloadTypeConfiguration() {
if (transportPayloadTypeConfiguration != null) { return Objects.requireNonNullElseGet(transportPayloadTypeConfiguration, JsonTransportPayloadConfiguration::new);
return transportPayloadTypeConfiguration;
} else {
return new JsonTransportPayloadConfiguration();
}
} }
public String getDeviceTelemetryTopic() {
return StringUtils.notBlankOrDefault(deviceTelemetryTopic, MqttTopics.DEVICE_TELEMETRY_TOPIC);
}
public String getDeviceAttributesTopic() {
return StringUtils.notBlankOrDefault(deviceAttributesTopic, MqttTopics.DEVICE_ATTRIBUTES_TOPIC);
}
public String getDeviceAttributesSubscribeTopic() {
return StringUtils.notBlankOrDefault(deviceAttributesSubscribeTopic, MqttTopics.DEVICE_ATTRIBUTES_TOPIC);
}
} }

4
common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java

@ -31,8 +31,8 @@ public enum EdgeEventActionType {
RPC_CALL, RPC_CALL,
ALARM_ACK, ALARM_ACK,
ALARM_CLEAR, ALARM_CLEAR,
ALARM_ASSIGN, ALARM_ASSIGNED,
ALARM_UNASSIGN, ALARM_UNASSIGNED,
ASSIGNED_TO_EDGE, ASSIGNED_TO_EDGE,
UNASSIGNED_FROM_EDGE, UNASSIGNED_FROM_EDGE,
CREDENTIALS_REQUEST, CREDENTIALS_REQUEST,

31
common/data/src/main/java/org/thingsboard/server/common/data/exception/TenantNotFoundException.java

@ -0,0 +1,31 @@
/**
* 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.common.data.exception;
import lombok.Getter;
import org.thingsboard.server.common.data.id.TenantId;
public class TenantNotFoundException extends RuntimeException {
@Getter
private final TenantId tenantId;
public TenantNotFoundException(TenantId tenantId) {
super("Tenant with id " + tenantId + " not found");
this.tenantId = tenantId;
}
}

3
common/data/src/main/java/org/thingsboard/server/common/data/kv/JsonDataEntry.java

@ -15,14 +15,11 @@
*/ */
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
public class JsonDataEntry extends BasicKvEntry { public class JsonDataEntry extends BasicKvEntry {
@NoXss
private final String value; private final String value;
public JsonDataEntry(String key, String value) { public JsonDataEntry(String key, String value) {

5
common/data/src/main/java/org/thingsboard/server/common/data/kv/StringDataEntry.java

@ -15,8 +15,6 @@
*/ */
package org.thingsboard.server.common.data.kv; package org.thingsboard.server.common.data.kv;
import org.thingsboard.server.common.data.validation.NoXss;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
@ -24,7 +22,6 @@ public class StringDataEntry extends BasicKvEntry {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@NoXss
private final String value; private final String value;
public StringDataEntry(String key, String value) { public StringDataEntry(String key, String value) {
@ -68,7 +65,7 @@ public class StringDataEntry extends BasicKvEntry {
public String toString() { public String toString() {
return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString(); return "StringDataEntry{" + "value='" + value + '\'' + "} " + super.toString();
} }
@Override @Override
public String getValueAsString() { public String getValueAsString() {
return value; return value;

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/NotificationType.java

@ -27,6 +27,6 @@ public enum NotificationType {
NEW_PLATFORM_VERSION, NEW_PLATFORM_VERSION,
ENTITIES_LIMIT, ENTITIES_LIMIT,
API_USAGE_LIMIT, API_USAGE_LIMIT,
RULE_ENGINE RULE_NODE
} }

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmAssignmentNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -59,10 +60,12 @@ public class AlarmAssignmentNotificationInfo implements RuleOriginatedNotificati
public Map<String, String> getTemplateData() { public Map<String, String> getTemplateData() {
return mapOf( return mapOf(
"action", action, "action", action,
"assigneeTitle", User.getTitle(assigneeEmail, assigneeFirstName, assigneeLastName),
"assigneeFirstName", assigneeFirstName, "assigneeFirstName", assigneeFirstName,
"assigneeLastName", assigneeLastName, "assigneeLastName", assigneeLastName,
"assigneeEmail", assigneeEmail, "assigneeEmail", assigneeEmail,
"assigneeId", assigneeId != null ? assigneeId.toString() : null, "assigneeId", assigneeId != null ? assigneeId.toString() : null,
"userTitle", User.getTitle(userEmail, userFirstName, userLastName),
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName, "userLastName", userLastName,

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/AlarmCommentNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.alarm.AlarmStatus;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
@ -55,6 +56,7 @@ public class AlarmCommentNotificationInfo implements RuleOriginatedNotificationI
return mapOf( return mapOf(
"comment", comment, "comment", comment,
"action", action, "action", action,
"userTitle", User.getTitle(userEmail, userFirstName, userLastName),
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName, "userLastName", userLastName,

3
common/data/src/main/java/org/thingsboard/server/common/data/notification/info/EntityActionNotificationInfo.java

@ -19,6 +19,7 @@ import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
@ -40,6 +41,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
private CustomerId entityCustomerId; private CustomerId entityCustomerId;
private UUID userId; private UUID userId;
private String userTitle;
private String userEmail; private String userEmail;
private String userFirstName; private String userFirstName;
private String userLastName; private String userLastName;
@ -52,6 +54,7 @@ public class EntityActionNotificationInfo implements RuleOriginatedNotificationI
"entityName", entityName, "entityName", entityName,
"actionType", actionType.name().toLowerCase(), "actionType", actionType.name().toLowerCase(),
"userId", userId.toString(), "userId", userId.toString(),
"userTitle", userTitle,
"userEmail", userEmail, "userEmail", userEmail,
"userFirstName", userFirstName, "userFirstName", userFirstName,
"userLastName", userLastName "userLastName", userLastName

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmAssignmentNotificationRuleTriggerConfig.java

@ -15,7 +15,10 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@ -23,6 +26,9 @@ import javax.validation.constraints.NotEmpty;
import java.util.Set; import java.util.Set;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class AlarmAssignmentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<String> alarmTypes; private Set<String> alarmTypes;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmCommentNotificationRuleTriggerConfig.java

@ -15,13 +15,19 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import java.util.Set; import java.util.Set;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class AlarmCommentNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<String> alarmTypes; private Set<String> alarmTypes;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/AlarmNotificationRuleTriggerConfig.java

@ -15,7 +15,10 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
@ -24,6 +27,9 @@ import java.io.Serializable;
import java.util.Set; import java.util.Set;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class AlarmNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<String> alarmTypes; private Set<String> alarmTypes;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/ApiUsageLimitNotificationRuleTriggerConfig.java

@ -15,13 +15,19 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.ApiFeature; import org.thingsboard.server.common.data.ApiFeature;
import org.thingsboard.server.common.data.ApiUsageStateValue; import org.thingsboard.server.common.data.ApiUsageStateValue;
import java.util.Set; import java.util.Set;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class ApiUsageLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class ApiUsageLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<ApiFeature> apiFeatures; private Set<ApiFeature> apiFeatures;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/DeviceActivityNotificationRuleTriggerConfig.java

@ -15,13 +15,19 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class DeviceActivityNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class DeviceActivityNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<UUID> devices; private Set<UUID> devices;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntitiesLimitNotificationRuleTriggerConfig.java

@ -15,13 +15,19 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import javax.validation.constraints.Max; import javax.validation.constraints.Max;
import java.util.Set; import java.util.Set;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class EntitiesLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class EntitiesLimitNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<EntityType> entityTypes; private Set<EntityType> entityTypes;

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/EntityActionNotificationRuleTriggerConfig.java

@ -15,12 +15,18 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.EntityType;
import java.util.Set; import java.util.Set;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class EntityActionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class EntityActionNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<EntityType> entityTypes; // maybe add name filter ? private Set<EntityType> entityTypes; // maybe add name filter ?

6
common/data/src/main/java/org/thingsboard/server/common/data/notification/rule/trigger/RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig.java

@ -15,13 +15,19 @@
*/ */
package org.thingsboard.server.common.data.notification.rule.trigger; package org.thingsboard.server.common.data.notification.rule.trigger;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent; import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
@Data @Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig { public class RuleEngineComponentLifecycleEventNotificationRuleTriggerConfig implements NotificationRuleTriggerConfig {
private Set<UUID> ruleChains; // if empty - all rule chains private Set<UUID> ruleChains; // if empty - all rule chains

24
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/slack/SlackConversation.java

@ -16,6 +16,7 @@
package org.thingsboard.server.common.data.notification.targets.slack; package org.thingsboard.server.common.data.notification.targets.slack;
import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;
@ -24,6 +25,7 @@ import org.apache.commons.lang3.StringUtils;
import org.thingsboard.server.common.data.notification.targets.NotificationRecipient; import org.thingsboard.server.common.data.notification.targets.NotificationRecipient;
import javax.validation.constraints.NotEmpty; import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import static org.apache.commons.lang3.StringUtils.isEmpty; import static org.apache.commons.lang3.StringUtils.isEmpty;
@ -31,23 +33,34 @@ import static org.apache.commons.lang3.StringUtils.isEmpty;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class SlackConversation implements NotificationRecipient { public class SlackConversation implements NotificationRecipient {
@NotNull
private SlackConversationType type;
@NotEmpty @NotEmpty
private String id; private String id;
@NotEmpty @NotEmpty
private String title; private String name;
private String shortName;
private String wholeName; private String wholeName;
private String email; private String email;
@Override
public String getTitle() {
if (type == SlackConversationType.DIRECT) {
return StringUtils.defaultIfEmpty(wholeName, name);
} else {
return name;
}
}
@JsonIgnore @JsonIgnore
@Override @Override
public String getFirstName() { public String getFirstName() {
String firstName = StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[0] : wholeName; String firstName = StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[0] : wholeName;
if (isEmpty(firstName)) { if (isEmpty(firstName)) {
firstName = shortName; firstName = name;
} }
return firstName; return firstName;
} }
@ -58,4 +71,9 @@ public class SlackConversation implements NotificationRecipient {
return StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[1] : null; return StringUtils.contains(wholeName, " ") ? wholeName.split(" ")[1] : null;
} }
@JsonIgnore
public String getPointer() {
return type == SlackConversationType.DIRECT ? "@" : "#";
}
} }

29
common/data/src/main/java/org/thingsboard/server/common/data/plugin/ComponentClusteringMode.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.server.common.data.plugin;
/**
* The main idea to use this - it's adding the ability to start rule nodes in singleton mode in cluster setup
* (singleton rule node will start in only one Rule Engine instance)
* USER_PREFERENCE - user has ability to configure clustering mode (enable/disable singleton mode in rule node config)
* ENABLE - user doesn't have ability to configure clustering mode (singleton mode is always FALSE in rule node config)
* SINGLETON - user doesn't have ability to configure clustering mode (singleton mode is always TRUE in rule node config)
*/
public enum ComponentClusteringMode {
USER_PREFERENCE,
ENABLED,
SINGLETON
}

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

@ -36,15 +36,17 @@ public class ComponentDescriptor extends SearchTextBased<ComponentDescriptorId>
@Getter @Setter private ComponentType type; @Getter @Setter private ComponentType type;
@ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT") @ApiModelProperty(position = 4, value = "Scope of the Rule Node. Always set to 'TENANT', since no rule chains on the 'SYSTEM' level yet.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "TENANT", example = "TENANT")
@Getter @Setter private ComponentScope scope; @Getter @Setter private ComponentScope scope;
@ApiModelProperty(position = 5, value = "Clustering mode of the RuleNode. This mode represents the ability to start Rule Node in multiple microservices.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, allowableValues = "USER_PREFERENCE, ENABLED, SINGLETON", example = "ENABLED")
@Getter @Setter private ComponentClusteringMode clusteringMode;
@Length(fieldName = "name") @Length(fieldName = "name")
@ApiModelProperty(position = 5, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "Custom Rule Node") @ApiModelProperty(position = 6, value = "Name of the Rule Node. Taken from the @RuleNode annotation.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "Custom Rule Node")
@Getter @Setter private String name; @Getter @Setter private String name;
@ApiModelProperty(position = 6, value = "Full name of the Java class that implements the Rule Engine Node interface.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "com.mycompany.CustomRuleNode") @ApiModelProperty(position = 7, value = "Full name of the Java class that implements the Rule Engine Node interface.", accessMode = ApiModelProperty.AccessMode.READ_ONLY, example = "com.mycompany.CustomRuleNode")
@Getter @Setter private String clazz; @Getter @Setter private String clazz;
@ApiModelProperty(position = 7, value = "Complex JSON object that represents the Rule Node configuration.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 8, value = "Complex JSON object that represents the Rule Node configuration.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private transient JsonNode configurationDescriptor; @Getter @Setter private transient JsonNode configurationDescriptor;
@Length(fieldName = "actions") @Length(fieldName = "actions")
@ApiModelProperty(position = 8, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @ApiModelProperty(position = 9, value = "Rule Node Actions. Deprecated. Always null.", accessMode = ApiModelProperty.AccessMode.READ_ONLY)
@Getter @Setter private String actions; @Getter @Setter private String actions;
public ComponentDescriptor() { public ComponentDescriptor() {

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

@ -48,7 +48,9 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
private String name; private String name;
@ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false") @ApiModelProperty(position = 6, value = "Enable/disable debug. ", example = "false")
private boolean debugMode; private boolean debugMode;
@ApiModelProperty(position = 7, value = "JSON with the rule node configuration. Structure depends on the rule node implementation.", dataType = "com.fasterxml.jackson.databind.JsonNode") @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")
private transient JsonNode configuration; private transient JsonNode configuration;
@JsonIgnore @JsonIgnore
private byte[] configurationBytes; private byte[] configurationBytes;
@ -69,6 +71,7 @@ public class RuleNode extends SearchTextBasedWithAdditionalInfo<RuleNodeId> impl
this.type = ruleNode.getType(); this.type = ruleNode.getType();
this.name = ruleNode.getName(); this.name = ruleNode.getName();
this.debugMode = ruleNode.isDebugMode(); this.debugMode = ruleNode.isDebugMode();
this.singletonMode = ruleNode.isSingletonMode();
this.setConfiguration(ruleNode.getConfiguration()); this.setConfiguration(ruleNode.getConfiguration());
this.externalId = ruleNode.getExternalId(); this.externalId = ruleNode.getExternalId();
} }

8
common/data/src/main/java/org/thingsboard/server/common/data/util/CollectionsUtil.java

@ -15,8 +15,6 @@
*/ */
package org.thingsboard.server.common.data.util; package org.thingsboard.server.common.data.util;
import com.google.common.collect.ImmutableMap;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
@ -63,8 +61,12 @@ public class CollectionsUtil {
return map; return map;
} }
public static <K, V> Map<K, V> unmodifiableMapOf(Object... kvs) { public static <K, V> Map<K, V> unmodifiableMapOf(Object... kvs) {
return Collections.unmodifiableMap(mapOf(kvs)); return Collections.unmodifiableMap(mapOf(kvs));
} }
public static <V> boolean emptyOrContains(Collection<V> collection, V element) {
return isEmpty(collection) || collection.contains(element);
}
} }

5
common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntitiesLimitTrigger.java

@ -28,8 +28,9 @@ public class EntitiesLimitTrigger implements NotificationRuleTrigger {
private final TenantId tenantId; private final TenantId tenantId;
private final EntityType entityType; private final EntityType entityType;
private final long currentCount;
private final long limit; private long limit;
private long currentCount;
@Override @Override
public NotificationRuleTriggerType getType() { public NotificationRuleTriggerType getType() {

47
common/message/src/main/java/org/thingsboard/server/common/msg/notification/trigger/EntityActionTrigger.java

@ -0,0 +1,47 @@
/**
* 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.common.msg.notification.trigger;
import lombok.Builder;
import lombok.Data;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.rule.trigger.NotificationRuleTriggerType;
@Data
@Builder
public class EntityActionTrigger implements NotificationRuleTrigger {
private final TenantId tenantId;
private final EntityId entityId;
private final HasName entity;
private final ActionType actionType;
private final User user;
@Override
public NotificationRuleTriggerType getType() {
return NotificationRuleTriggerType.ENTITY_ACTION;
}
@Override
public EntityId getOriginatorEntityId() {
return entityId;
}
}

27
common/queue/src/main/java/org/thingsboard/server/queue/azure/servicebus/TbServiceBusQueueConfigs.java

@ -19,10 +19,9 @@ import lombok.Getter;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.queue.util.PropertyUtils;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Component @Component
@ -55,24 +54,12 @@ public class TbServiceBusQueueConfigs {
@PostConstruct @PostConstruct
private void init() { private void init() {
coreConfigs = getConfigs(coreProperties); coreConfigs = PropertyUtils.getProps(coreProperties);
ruleEngineConfigs = getConfigs(ruleEngineProperties); ruleEngineConfigs = PropertyUtils.getProps(ruleEngineProperties);
transportApiConfigs = getConfigs(transportApiProperties); transportApiConfigs = PropertyUtils.getProps(transportApiProperties);
notificationsConfigs = getConfigs(notificationsProperties); notificationsConfigs = PropertyUtils.getProps(notificationsProperties);
jsExecutorConfigs = getConfigs(jsExecutorProperties); jsExecutorConfigs = PropertyUtils.getProps(jsExecutorProperties);
vcConfigs = getConfigs(vcProperties); vcConfigs = PropertyUtils.getProps(vcProperties);
} }
private Map<String, String> getConfigs(String properties) {
Map<String, String> configs = new HashMap<>();
if (StringUtils.isNotEmpty(properties)) {
for (String property : properties.split(";")) {
int delimiterPosition = property.indexOf(":");
String key = property.substring(0, delimiterPosition);
String value = property.substring(delimiterPosition + 1);
configs.put(key, value);
}
}
return configs;
}
} }

3
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
@ -368,7 +369,7 @@ public class HashPartitionService implements PartitionService {
} }
} }
if (routingInfo == null) { if (routingInfo == null) {
throw new RuntimeException("Tenant not found!"); throw new TenantNotFoundException(tenantId);
} }
switch (serviceType) { switch (serviceType) {
case TB_RULE_ENGINE: case TB_RULE_ENGINE:

54
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaSettings.java

@ -32,6 +32,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.TbProperty; import org.thingsboard.server.common.data.TbProperty;
import org.thingsboard.server.queue.util.PropertyUtils;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -53,34 +54,34 @@ public class TbKafkaSettings {
@Value("${queue.kafka.ssl.enabled:false}") @Value("${queue.kafka.ssl.enabled:false}")
private boolean sslEnabled; private boolean sslEnabled;
@Value("${queue.kafka.ssl.truststore.location}") @Value("${queue.kafka.ssl.truststore.location:}")
private String sslTruststoreLocation; private String sslTruststoreLocation;
@Value("${queue.kafka.ssl.truststore.password}") @Value("${queue.kafka.ssl.truststore.password:}")
private String sslTruststorePassword; private String sslTruststorePassword;
@Value("${queue.kafka.ssl.keystore.location}") @Value("${queue.kafka.ssl.keystore.location:}")
private String sslKeystoreLocation; private String sslKeystoreLocation;
@Value("${queue.kafka.ssl.keystore.password}") @Value("${queue.kafka.ssl.keystore.password:}")
private String sslKeystorePassword; private String sslKeystorePassword;
@Value("${queue.kafka.ssl.key.password}") @Value("${queue.kafka.ssl.key.password:}")
private String sslKeyPassword; private String sslKeyPassword;
@Value("${queue.kafka.acks}") @Value("${queue.kafka.acks:all}")
private String acks; private String acks;
@Value("${queue.kafka.retries}") @Value("${queue.kafka.retries:1}")
private int retries; private int retries;
@Value("${queue.kafka.compression.type:none}") @Value("${queue.kafka.compression.type:none}")
private String compressionType; private String compressionType;
@Value("${queue.kafka.batch.size}") @Value("${queue.kafka.batch.size:16384}")
private int batchSize; private int batchSize;
@Value("${queue.kafka.linger.ms}") @Value("${queue.kafka.linger.ms:1}")
private long lingerMs; private long lingerMs;
@Value("${queue.kafka.max.request.size:1048576}") @Value("${queue.kafka.max.request.size:1048576}")
@ -89,10 +90,10 @@ public class TbKafkaSettings {
@Value("${queue.kafka.max.in.flight.requests.per.connection:5}") @Value("${queue.kafka.max.in.flight.requests.per.connection:5}")
private int maxInFlightRequestsPerConnection; private int maxInFlightRequestsPerConnection;
@Value("${queue.kafka.buffer.memory}") @Value("${queue.kafka.buffer.memory:33554432}")
private long bufferMemory; private long bufferMemory;
@Value("${queue.kafka.replication_factor}") @Value("${queue.kafka.replication_factor:1}")
@Getter @Getter
private short replicationFactor; private short replicationFactor;
@ -108,21 +109,31 @@ public class TbKafkaSettings {
@Value("${queue.kafka.fetch_max_bytes:134217728}") @Value("${queue.kafka.fetch_max_bytes:134217728}")
private int fetchMaxBytes; private int fetchMaxBytes;
@Value("${queue.kafka.request.timeout.ms:30000}")
private int requestTimeoutMs;
@Value("${queue.kafka.session.timeout.ms:10000}")
private int sessionTimeoutMs;
@Value("${queue.kafka.use_confluent_cloud:false}") @Value("${queue.kafka.use_confluent_cloud:false}")
private boolean useConfluent; private boolean useConfluent;
@Value("${queue.kafka.confluent.ssl.algorithm}") @Value("${queue.kafka.confluent.ssl.algorithm:}")
private String sslAlgorithm; private String sslAlgorithm;
@Value("${queue.kafka.confluent.sasl.mechanism}") @Value("${queue.kafka.confluent.sasl.mechanism:}")
private String saslMechanism; private String saslMechanism;
@Value("${queue.kafka.confluent.sasl.config}") @Value("${queue.kafka.confluent.sasl.config:}")
private String saslConfig; private String saslConfig;
@Value("${queue.kafka.confluent.security.protocol}") @Value("${queue.kafka.confluent.security.protocol:}")
private String securityProtocol; private String securityProtocol;
@Value("${queue.kafka.other-inline:}")
private String otherInline;
@Deprecated
@Setter @Setter
private List<TbProperty> other; private List<TbProperty> other;
@ -134,8 +145,6 @@ public class TbKafkaSettings {
props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers); props.put(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG, servers);
props.put(AdminClientConfig.RETRIES_CONFIG, retries); props.put(AdminClientConfig.RETRIES_CONFIG, retries);
configureSSL(props);
return props; return props;
} }
@ -147,8 +156,6 @@ public class TbKafkaSettings {
props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes); props.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, fetchMaxBytes);
props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs); props.put(ConsumerConfig.MAX_POLL_INTERVAL_MS_CONFIG, maxPollIntervalMs);
configureSSL(props);
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class);
@ -174,7 +181,7 @@ public class TbKafkaSettings {
return props; return props;
} }
private Properties toProps() { Properties toProps() {
Properties props = new Properties(); Properties props = new Properties();
if (useConfluent) { if (useConfluent) {
@ -184,6 +191,11 @@ public class TbKafkaSettings {
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol); props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, securityProtocol);
} }
props.put(CommonClientConfigs.REQUEST_TIMEOUT_MS_CONFIG, requestTimeoutMs);
props.put(CommonClientConfigs.SESSION_TIMEOUT_MS_CONFIG, sessionTimeoutMs);
props.putAll(PropertyUtils.getProps(otherInline));
if (other != null) { if (other != null) {
other.forEach(kv -> props.put(kv.getKey(), kv.getValue())); other.forEach(kv -> props.put(kv.getKey(), kv.getValue()));
} }
@ -193,7 +205,7 @@ public class TbKafkaSettings {
return props; return props;
} }
private void configureSSL(Properties props) { void configureSSL(Properties props) {
if (sslEnabled) { if (sslEnabled) {
props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL"); props.put(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG, "SSL");
props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocation); props.put(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG, sslTruststoreLocation);

33
common/queue/src/main/java/org/thingsboard/server/queue/kafka/TbKafkaTopicConfigs.java

@ -19,10 +19,9 @@ import lombok.Getter;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.queue.util.PropertyUtils;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Component @Component
@ -66,29 +65,17 @@ public class TbKafkaTopicConfigs {
@PostConstruct @PostConstruct
private void init() { private void init() {
coreConfigs = getConfigs(coreProperties); coreConfigs = PropertyUtils.getProps(coreProperties);
ruleEngineConfigs = getConfigs(ruleEngineProperties); ruleEngineConfigs = PropertyUtils.getProps(ruleEngineProperties);
transportApiRequestConfigs = getConfigs(transportApiProperties); transportApiRequestConfigs = PropertyUtils.getProps(transportApiProperties);
transportApiResponseConfigs = getConfigs(transportApiProperties); transportApiResponseConfigs = PropertyUtils.getProps(transportApiProperties);
transportApiResponseConfigs.put(NUM_PARTITIONS_SETTING, "1"); transportApiResponseConfigs.put(NUM_PARTITIONS_SETTING, "1");
notificationsConfigs = getConfigs(notificationsProperties); notificationsConfigs = PropertyUtils.getProps(notificationsProperties);
jsExecutorRequestConfigs = getConfigs(jsExecutorProperties); jsExecutorRequestConfigs = PropertyUtils.getProps(jsExecutorProperties);
jsExecutorResponseConfigs = getConfigs(jsExecutorProperties); jsExecutorResponseConfigs = PropertyUtils.getProps(jsExecutorProperties);
jsExecutorResponseConfigs.put(NUM_PARTITIONS_SETTING, "1"); jsExecutorResponseConfigs.put(NUM_PARTITIONS_SETTING, "1");
fwUpdatesConfigs = getConfigs(fwUpdatesProperties); fwUpdatesConfigs = PropertyUtils.getProps(fwUpdatesProperties);
vcConfigs = getConfigs(vcProperties); vcConfigs = PropertyUtils.getProps(vcProperties);
} }
private Map<String, String> getConfigs(String properties) {
Map<String, String> configs = new HashMap<>();
if (StringUtils.isNotEmpty(properties)) {
for (String property : properties.split(";")) {
int delimiterPosition = property.indexOf(":");
String key = property.substring(0, delimiterPosition);
String value = property.substring(delimiterPosition + 1);
configs.put(key, value);
}
}
return configs;
}
} }

2
common/message/src/main/java/org/thingsboard/server/common/msg/notification/NotificationRuleProcessor.java → common/queue/src/main/java/org/thingsboard/server/queue/notification/NotificationRuleProcessor.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.thingsboard.server.common.msg.notification; package org.thingsboard.server.queue.notification;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;

1
common/queue/src/main/java/org/thingsboard/server/queue/notification/RemoteNotificationRuleProcessor.java

@ -20,7 +20,6 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.thingsboard.server.common.msg.notification.NotificationRuleProcessor;
import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger; import org.thingsboard.server.common.msg.notification.trigger.NotificationRuleTrigger;
import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;

27
common/queue/src/main/java/org/thingsboard/server/queue/pubsub/TbPubSubSubscriptionSettings.java

@ -19,10 +19,9 @@ import lombok.Getter;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.queue.util.PropertyUtils;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
@Component @Component
@ -56,24 +55,12 @@ public class TbPubSubSubscriptionSettings {
@PostConstruct @PostConstruct
private void init() { private void init() {
coreSettings = getSettings(coreProperties); coreSettings = PropertyUtils.getProps(coreProperties);
ruleEngineSettings = getSettings(ruleEngineProperties); ruleEngineSettings = PropertyUtils.getProps(ruleEngineProperties);
transportApiSettings = getSettings(transportApiProperties); transportApiSettings = PropertyUtils.getProps(transportApiProperties);
notificationsSettings = getSettings(notificationsProperties); notificationsSettings = PropertyUtils.getProps(notificationsProperties);
jsExecutorSettings = getSettings(jsExecutorProperties); jsExecutorSettings = PropertyUtils.getProps(jsExecutorProperties);
vcSettings = getSettings(vcProperties); vcSettings = PropertyUtils.getProps(vcProperties);
} }
private Map<String, String> getSettings(String properties) {
Map<String, String> configs = new HashMap<>();
if (StringUtils.isNotEmpty(properties)) {
for (String property : properties.split(";")) {
int delimiterPosition = property.indexOf(":");
String key = property.substring(0, delimiterPosition);
String value = property.substring(delimiterPosition + 1);
configs.put(key, value);
}
}
return configs;
}
} }

3
common/queue/src/main/java/org/thingsboard/server/queue/usagestats/DefaultTbApiUsageReportClient.java

@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.ApiUsageRecordKey; import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.exception.TenantNotFoundException;
import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantId;
@ -122,6 +123,8 @@ public class DefaultTbApiUsageReportClient implements TbApiUsageReportClient {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId()) TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, parent.getTenantId(), parent.getId())
.newByTopic(msgProducer.getDefaultTopic()); .newByTopic(msgProducer.getDefaultTopic());
msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null); msgProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), statsMsg.build()), null);
} catch (TenantNotFoundException e) {
log.debug("Couldn't report usage stats for non-existing tenant: {}", e.getTenantId());
} catch (Exception e) { } catch (Exception e) {
log.warn("Failed to report usage stats for tenant {}", parent.getTenantId(), e); log.warn("Failed to report usage stats for tenant {}", parent.getTenantId(), e);
} }

40
common/queue/src/main/java/org/thingsboard/server/queue/util/PropertyUtils.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.queue.util;
import org.thingsboard.server.common.data.StringUtils;
import java.util.HashMap;
import java.util.Map;
public class PropertyUtils {
public static Map<String, String> getProps(String properties) {
Map<String, String> configs = new HashMap<>();
if (StringUtils.isNotEmpty(properties)) {
for (String property : properties.split(";")) {
if (StringUtils.isNotEmpty(property)) {
int delimiterPosition = property.indexOf(":");
String key = property.substring(0, delimiterPosition);
String value = property.substring(delimiterPosition + 1);
configs.put(key, value);
}
}
}
return configs;
}
}

83
common/queue/src/test/java/org/thingsboard/server/queue/kafka/TbKafkaSettingsTest.java

@ -0,0 +1,83 @@
/**
* 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.queue.kafka;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import java.util.Properties;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.spy;
@SpringBootTest(classes = TbKafkaSettings.class)
@TestPropertySource(properties = {
"queue.type=kafka",
"queue.kafka.bootstrap.servers=localhost:9092",
"queue.kafka.other-inline=metrics.recording.level:INFO;metrics.sample.window.ms:30000",
})
class TbKafkaSettingsTest {
@Autowired
TbKafkaSettings settings;
@BeforeEach
void beforeEach() {
settings = spy(settings); //SpyBean is not aware on @ConditionalOnProperty, that is why the traditional spy in use
}
@Test
void givenToProps_whenConfigureSSL_thenVerifyOnce() {
Properties props = settings.toProps();
assertThat(props).as("TB_QUEUE_KAFKA_REQUEST_TIMEOUT_MS").containsEntry("request.timeout.ms", 30000);
assertThat(props).as("TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS").containsEntry("session.timeout.ms", 10000);
//other-inline
assertThat(props).as("metrics.recording.level").containsEntry("metrics.recording.level", "INFO");
assertThat(props).as("TB_QUEUE_KAFKA_SESSION_TIMEOUT_MS").containsEntry("metrics.sample.window.ms", "30000");
Mockito.verify(settings).toProps();
Mockito.verify(settings).configureSSL(any());
}
@Test
void givenToAdminProps_whenConfigureSSL_thenVerifyOnce() {
settings.toAdminProps();
Mockito.verify(settings).toProps();
Mockito.verify(settings).configureSSL(any());
}
@Test
void givenToConsumerProps_whenConfigureSSL_thenVerifyOnce() {
settings.toConsumerProps("main");
Mockito.verify(settings).toProps();
Mockito.verify(settings).configureSSL(any());
}
@Test
void givenTotoProducerProps_whenConfigureSSL_thenVerifyOnce() {
settings.toProducerProps();
Mockito.verify(settings).toProps();
Mockito.verify(settings).configureSSL(any());
}
}

62
common/queue/src/test/java/org/thingsboard/server/queue/util/PropertyUtilsTest.java

@ -0,0 +1,62 @@
/**
* 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.queue.util;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
class PropertyUtilsTest {
@Test
void givenNullOrEmpty_whenGetConfig_thenEmptyMap() {
assertThat(PropertyUtils.getProps(null)).as("null property").isEmpty();
assertThat(PropertyUtils.getProps("")).as("empty property").isEmpty();
assertThat(PropertyUtils.getProps(";")).as("ends with ;").isEmpty();
}
@Test
void givenKafkaOtherProperties_whenGetConfig_thenReturnMappedValues() {
assertThat(PropertyUtils.getProps("metrics.recording.level:INFO;metrics.sample.window.ms:30000"))
.as("two pairs")
.isEqualTo(Map.of(
"metrics.recording.level", "INFO",
"metrics.sample.window.ms", "30000"
));
assertThat(PropertyUtils.getProps("metrics.recording.level:INFO;metrics.sample.window.ms:30000" + ";"))
.as("two pairs ends with ;")
.isEqualTo(Map.of(
"metrics.recording.level", "INFO",
"metrics.sample.window.ms", "30000"
));
}
@Test
void givenKafkaTopicProperties_whenGetConfig_thenReturnMappedValues() {
assertThat(PropertyUtils.getProps("retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1;min.insync.replicas:1"))
.isEqualTo(Map.of(
"retention.ms", "604800000",
"segment.bytes", "26214400",
"retention.bytes", "1048576000",
"partitions", "1",
"min.insync.replicas", "1"
));
}
}

74
common/script/script-api/src/main/java/org/thingsboard/script/api/tbel/TbUtils.java

@ -18,7 +18,9 @@ package org.thingsboard.script.api.tbel;
import org.mvel2.ExecutionContext; import org.mvel2.ExecutionContext;
import org.mvel2.ParserConfiguration; import org.mvel2.ParserConfiguration;
import org.mvel2.execution.ExecutionArrayList; import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.execution.ExecutionHashMap;
import org.mvel2.util.MethodStub; import org.mvel2.util.MethodStub;
import org.thingsboard.server.common.data.StringUtils;
import java.io.IOException; import java.io.IOException;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
@ -27,8 +29,12 @@ import java.math.RoundingMode;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder; import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64; import java.util.Base64;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Set;
public class TbUtils { public class TbUtils {
@ -89,6 +95,14 @@ public class TbUtils {
byte[].class))); byte[].class)));
parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex", parserConfig.addImport("bytesToHex", new MethodStub(TbUtils.class.getMethod("bytesToHex",
ExecutionArrayList.class))); ExecutionArrayList.class)));
parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap",
ExecutionContext.class, Map.class)));
parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap",
ExecutionContext.class, Map.class, boolean.class)));
parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap",
ExecutionContext.class, Map.class, List.class)));
parserConfig.addImport("toFlatMap", new MethodStub(TbUtils.class.getMethod("toFlatMap",
ExecutionContext.class, Map.class, List.class, boolean.class)));
} }
public static String btoa(String input) { public static String btoa(String input) {
@ -225,7 +239,7 @@ public class TbUtils {
} }
ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx); ExecutionArrayList<Byte> data = new ExecutionArrayList<>(ctx);
for (int i = 0; i < len; i += 2) { for (int i = 0; i < len; i += 2) {
data.add((byte)((Character.digit(hex.charAt(i), 16) << 4) data.add((byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16))); + Character.digit(hex.charAt(i + 1), 16)));
} }
return data; return data;
@ -316,4 +330,62 @@ public class TbUtils {
} }
return value; return value;
} }
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json) {
return toFlatMap(ctx, json, new ArrayList<>(), true);
}
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json, boolean pathInKey) {
return toFlatMap(ctx, json, new ArrayList<>(), pathInKey);
}
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json, List<String> excludeList) {
return toFlatMap(ctx, json, excludeList, true);
}
public static ExecutionHashMap<String, Object> toFlatMap(ExecutionContext ctx, Map<String, Object> json, List<String> excludeList, boolean pathInKey) {
ExecutionHashMap<String, Object> map = new ExecutionHashMap<>(16, ctx);
parseRecursive(json, map, excludeList, "", pathInKey);
return map;
}
private static void parseRecursive(Object json, Map<String, Object> map, List<String> excludeList, String path, boolean pathInKey) {
if (json instanceof Map.Entry) {
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) json;
if (StringUtils.isNotBlank(path)) {
path += ".";
}
if (excludeList.contains(entry.getKey())) {
return;
}
path += entry.getKey();
json = entry.getValue();
}
if (json instanceof Set || json instanceof List) {
String arrayPath = path + ".";
Object[] collection = ((Collection<?>) json).toArray();
for (int index = 0; index < collection.length; index++) {
parseRecursive(collection[index], map, excludeList, arrayPath + index, pathInKey);
}
} else if (json instanceof Map) {
Map<?, ?> node = (Map<?, ?>) json;
for (Map.Entry<?, ?> entry : node.entrySet()) {
parseRecursive(entry, map, excludeList, path, pathInKey);
}
} else {
if (pathInKey) {
map.put(path, json);
} else {
String key = path.substring(path.lastIndexOf('.') + 1);
if (StringUtils.isNumeric(key)) {
int pos = path.length();
for (int i = 0; i < 2; i++) {
pos = path.lastIndexOf('.', pos - 1);
}
key = path.substring(pos + 1);
}
map.put(key, json);
}
}
}
} }

100
common/script/script-api/src/test/java/org/thingsboard/script/api/tbel/TbUtilsTest.java

@ -15,15 +15,50 @@
*/ */
package org.thingsboard.script.api.tbel; package org.thingsboard.script.api.tbel;
import lombok.extern.slf4j.Slf4j;
import org.junit.After;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.mvel2.ExecutionContext;
import org.mvel2.ParserContext;
import org.mvel2.SandboxedParserConfiguration;
import org.mvel2.execution.ExecutionArrayList;
import org.mvel2.execution.ExecutionHashMap;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar;
import java.util.List; import java.util.List;
import java.util.Random;
@Slf4j
public class TbUtilsTest { public class TbUtilsTest {
private ExecutionContext ctx;
@Before
public void before() {
SandboxedParserConfiguration parserConfig = ParserContext.enableSandboxedMode();
parserConfig.addImport("JSON", TbJson.class);
parserConfig.registerDataType("Date", TbDate.class, date -> 8L);
parserConfig.registerDataType("Random", Random.class, date -> 8L);
parserConfig.registerDataType("Calendar", Calendar.class, date -> 8L);
try {
TbUtils.register(parserConfig);
} catch (Exception e) {
log.error("Cannot register functions", e);
}
ctx = new ExecutionContext(parserConfig);
Assert.assertNotNull(ctx);
}
@After
public void after() {
ctx.stop();
}
@Test @Test
public void parseHexToInt() { public void parseHexToInt() {
Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB")); Assert.assertEquals(0xAB, TbUtils.parseHexToInt("AB"));
@ -87,6 +122,71 @@ public class TbUtilsTest {
Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, false)); Assert.assertEquals(expected, TbUtils.parseBytesToInt(data, 0, 3, false));
} }
@Test
public void toFlatMap() {
ExecutionHashMap<String, Object> inputMap = new ExecutionHashMap<>(16, ctx);
inputMap.put("name", "Alice");
inputMap.put("age", 30);
inputMap.put("devices", new ExecutionArrayList<>(List.of(
new ExecutionHashMap<>(16, ctx) {{
put("id", "dev001");
put("type", "sensor");
}},
new ExecutionHashMap<>(16, ctx) {{
put("id", "dev002");
put("type", "actuator");
}}
), ctx));
inputMap.put("settings", new ExecutionHashMap<>(16, ctx) {{
put("notifications", true);
put("timezone", "UTC-5");
put("params", new ExecutionHashMap<>(16, ctx) {{
put("param1", "value1");
put("param2", "value2");
put("param3", new ExecutionHashMap<>(16, ctx) {{
put("subParam1", "value1");
put("subParam2", "value2");
}});
}});
}});
ExecutionArrayList<String> excludeList = new ExecutionArrayList<>(ctx);
excludeList.addAll(List.of("age", "id", "param1", "subParam2"));
ExecutionHashMap<String, Object> expectedMapWithPath = new ExecutionHashMap<>(16, ctx);
expectedMapWithPath.put("name", "Alice");
expectedMapWithPath.put("devices.0.type", "sensor");
expectedMapWithPath.put("devices.1.type", "actuator");
expectedMapWithPath.put("settings.notifications", true);
expectedMapWithPath.put("settings.timezone", "UTC-5");
expectedMapWithPath.put("settings.params.param2", "value2");
expectedMapWithPath.put("settings.params.param3.subParam1", "value1");
ExecutionHashMap<String, Object> actualMapWithPaths = TbUtils.toFlatMap(ctx, inputMap, excludeList, true);
Assert.assertEquals(expectedMapWithPath, actualMapWithPaths);
ExecutionHashMap<String, Object> expectedMapWithoutPaths = new ExecutionHashMap<>(16, ctx);
expectedMapWithoutPaths.put("timezone", "UTC-5");
expectedMapWithoutPaths.put("name", "Alice");
expectedMapWithoutPaths.put("id", "dev002");
expectedMapWithoutPaths.put("subParam2", "value2");
expectedMapWithoutPaths.put("type", "actuator");
expectedMapWithoutPaths.put("subParam1", "value1");
expectedMapWithoutPaths.put("param1", "value1");
expectedMapWithoutPaths.put("notifications", true);
expectedMapWithoutPaths.put("age", 30);
expectedMapWithoutPaths.put("param2", "value2");
ExecutionHashMap<String, Object> actualMapWithoutPaths = TbUtils.toFlatMap(ctx, inputMap, false);
Assert.assertEquals(expectedMapWithoutPaths, actualMapWithoutPaths);
}
private static String keyToValue(String key, String extraSymbol) {
return key + "Value" + (extraSymbol == null ? "" : extraSymbol);
}
private static List<Byte> toList(byte[] data) { private static List<Byte> toList(byte[] data) {
List<Byte> result = new ArrayList<>(data.length); List<Byte> result = new ArrayList<>(data.length);
for (Byte b : data) { for (Byte b : data) {

2
common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java

@ -221,7 +221,7 @@ public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler<S
private ListenableFuture<SparkplugDeviceSessionContext> onDeviceConnectProto(SparkplugTopic topic) throws ThingsboardException { private ListenableFuture<SparkplugDeviceSessionContext> onDeviceConnectProto(SparkplugTopic topic) throws ThingsboardException {
try { try {
String deviceType = this.gateway.getDeviceType() + "-node"; String deviceType = this.gateway.getDeviceType() + " device";
return onDeviceConnect(topic.getNodeDeviceName(), deviceType); return onDeviceConnect(topic.getNodeDeviceName(), deviceType);
} catch (RuntimeException e) { } catch (RuntimeException e) {
log.error("Failed Sparkplug Device connect proto!", e); log.error("Failed Sparkplug Device connect proto!", e);

5
dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java

@ -22,6 +22,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmSeverity;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
@ -66,6 +67,10 @@ public interface AlarmDao extends Dao<Alarm> {
PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); PageData<AlarmInfo> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query);
PageData<AlarmInfo> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query);
PageData<AlarmInfo> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query);
PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds); PageData<AlarmData> findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection<EntityId> orderedEntityIds);
Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId); Set<AlarmSeverity> findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId);

11
dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.alarm.AlarmApiCallResult;
import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest;
import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmModificationRequest;
import org.thingsboard.server.common.data.alarm.AlarmQueryV2;
import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.alarm.AlarmStatusFilter;
import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest;
import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmQuery;
@ -338,6 +339,16 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ
return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query)); return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query));
} }
@Override
public ListenableFuture<PageData<AlarmInfo>> findAlarmsV2(TenantId tenantId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findAlarmsV2(tenantId, query));
}
@Override
public ListenableFuture<PageData<AlarmInfo>> findCustomerAlarmsV2(TenantId tenantId, CustomerId customerId, AlarmQueryV2 query) {
return Futures.immediateFuture(alarmDao.findCustomerAlarmsV2(tenantId, customerId, query));
}
@Override @Override
public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus,
AlarmStatus alarmStatus, String assigneeId) { AlarmStatus alarmStatus, String assigneeId) {

8
dao/src/main/java/org/thingsboard/server/dao/attributes/AttributeUtils.java

@ -30,12 +30,12 @@ public class AttributeUtils {
Validator.validateString(scope, "Incorrect scope " + scope); Validator.validateString(scope, "Incorrect scope " + scope);
} }
public static void validate(List<AttributeKvEntry> kvEntries, boolean validateNoxss) { public static void validate(List<AttributeKvEntry> kvEntries) {
kvEntries.forEach(kv -> validate(kv, validateNoxss)); kvEntries.forEach(AttributeUtils::validate);
} }
public static void validate(AttributeKvEntry kvEntry, boolean validateNoxss) { public static void validate(AttributeKvEntry kvEntry) {
KvUtils.validate(kvEntry, validateNoxss); KvUtils.validate(kvEntry);
if (kvEntry.getDataType() == null) { if (kvEntry.getDataType() == null) {
throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null"); throw new IncorrectParameterException("Incorrect kvEntry. Data type can't be null");
} else { } else {

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

Loading…
Cancel
Save