Browse Source

Merge branch 'master' into bulk-import-fix

pull/7250/head
dlandiak 4 years ago
parent
commit
e479f0c9e6
  1. 1
      application/pom.xml
  2. 74
      application/src/main/data/upgrade/3.4.1/schema_update.sql
  3. 21
      application/src/main/data/upgrade/3.4.1/schema_update_after.sql
  4. 45
      application/src/main/data/upgrade/3.4.1/schema_update_before.sql
  5. 10
      application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
  6. 10
      application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
  7. 108
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  8. 11
      application/src/main/java/org/thingsboard/server/actors/tenant/TenantActor.java
  9. 16
      application/src/main/java/org/thingsboard/server/controller/AssetController.java
  10. 227
      application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java
  11. 30
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  12. 10
      application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java
  13. 2
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  14. 20
      application/src/main/java/org/thingsboard/server/controller/EdgeController.java
  15. 4
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
  16. 37
      application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java
  17. 11
      application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java
  18. 1
      application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java
  19. 8
      application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java
  20. 147
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcService.java
  21. 8
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java
  22. 11
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java
  23. 2
      application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeSyncCursor.java
  24. 4
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java
  25. 67
      application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java
  26. 47
      application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java
  27. 63
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java
  28. 20
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java
  29. 62
      application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java
  30. 50
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java
  31. 3
      application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java
  32. 110
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java
  33. 26
      application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java
  34. 70
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  35. 27
      application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
  36. 162
      application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java
  37. 33
      application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java
  38. 76
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
  39. 28
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
  40. 4
      application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
  41. 21
      application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
  42. 116
      application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java
  43. 10
      application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java
  44. 2
      application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java
  45. 19
      application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java
  46. 193
      application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java
  47. 4
      application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java
  48. 27
      application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
  49. 5
      application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java
  50. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
  51. 1
      application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
  52. 247
      application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java
  53. 29
      application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java
  54. 25
      application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java
  55. 2
      application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java
  56. 7
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java
  57. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java
  58. 43
      application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java
  59. 1
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java
  60. 80
      application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java
  61. 54
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
  62. 2
      application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java
  63. 29
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java
  64. 32
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java
  65. 29
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java
  66. 21
      application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java
  67. 61
      application/src/main/java/org/thingsboard/server/service/ttl/AuditLogsCleanUpService.java
  68. 20
      application/src/main/resources/thingsboard.yml
  69. 10
      application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java
  70. 50
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  71. 20
      application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java
  72. 45
      application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java
  73. 463
      application/src/test/java/org/thingsboard/server/controller/BaseAssetProfileControllerTest.java
  74. 68
      application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java
  75. 20
      application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java
  76. 20
      application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java
  77. 28
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
  78. 20
      application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java
  79. 28
      application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java
  80. 20
      application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
  81. 19
      application/src/test/java/org/thingsboard/server/controller/BaseRuleChainControllerTest.java
  82. 19
      application/src/test/java/org/thingsboard/server/controller/BaseUserControllerTest.java
  83. 23
      application/src/test/java/org/thingsboard/server/controller/sql/AssetProfileControllerSqlTest.java
  84. 44
      application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java
  85. 6
      application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java
  86. 96
      application/src/test/java/org/thingsboard/server/service/script/LocalJsInvokeServiceTest.java
  87. 2
      application/src/test/java/org/thingsboard/server/service/script/MockJsInvokeService.java
  88. 219
      application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java
  89. 24
      application/src/test/java/org/thingsboard/server/service/sync/ie/BaseExportImportServiceTest.java
  90. 97
      application/src/test/java/org/thingsboard/server/service/sync/ie/ExportImportServiceSqlTest.java
  91. 5
      application/src/test/resources/application-test.properties
  92. 6
      common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java
  93. 17
      common/cluster-api/src/main/proto/jsinvoke.proto
  94. 2
      common/cluster-api/src/main/proto/queue.proto
  95. 17
      common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java
  96. 53
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java
  97. 7
      common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java
  98. 2
      common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java
  99. 5
      common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java
  100. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/timeseries/TimeseriesService.java

1
application/pom.xml

@ -286,7 +286,6 @@
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>

74
application/src/main/data/upgrade/3.4.1/schema_update.sql

@ -0,0 +1,74 @@
--
-- Copyright © 2016-2022 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.
--
DO
$$
DECLARE table_partition RECORD;
BEGIN
-- in case of running the upgrade script a second time:
IF NOT (SELECT exists(SELECT FROM pg_tables WHERE tablename = 'old_audit_log')) THEN
ALTER TABLE audit_log RENAME TO old_audit_log;
ALTER INDEX IF EXISTS idx_audit_log_tenant_id_and_created_time RENAME TO idx_old_audit_log_tenant_id_and_created_time;
FOR table_partition IN SELECT tablename AS name, split_part(tablename, '_', 3) AS partition_ts
FROM pg_tables WHERE tablename LIKE 'audit_log_%'
LOOP
EXECUTE format('ALTER TABLE %s RENAME TO old_audit_log_%s', table_partition.name, table_partition.partition_ts);
END LOOP;
ELSE
RAISE NOTICE 'Table old_audit_log already exists, leaving as is';
END IF;
END;
$$;
CREATE TABLE IF NOT EXISTS audit_log (
id uuid NOT NULL,
created_time bigint NOT NULL,
tenant_id uuid,
customer_id uuid,
entity_id uuid,
entity_type varchar(255),
entity_name varchar(255),
user_id uuid,
user_name varchar(255),
action_type varchar(255),
action_data varchar(1000000),
action_status varchar(255),
action_failure_details varchar(1000000)
) PARTITION BY RANGE (created_time);
CREATE INDEX IF NOT EXISTS idx_audit_log_tenant_id_and_created_time ON audit_log(tenant_id, created_time DESC);
CREATE OR REPLACE PROCEDURE migrate_audit_logs(IN start_time_ms BIGINT, IN end_time_ms BIGINT, IN partition_size_ms BIGINT)
LANGUAGE plpgsql AS
$$
DECLARE
p RECORD;
partition_end_ts BIGINT;
BEGIN
FOR p IN SELECT DISTINCT (created_time - created_time % partition_size_ms) AS partition_ts FROM old_audit_log
WHERE created_time >= start_time_ms AND created_time < end_time_ms
LOOP
partition_end_ts = p.partition_ts + partition_size_ms;
RAISE NOTICE '[audit_log] Partition to create : [%-%]', p.partition_ts, partition_end_ts;
EXECUTE format('CREATE TABLE IF NOT EXISTS audit_log_%s PARTITION OF audit_log ' ||
'FOR VALUES FROM ( %s ) TO ( %s )', p.partition_ts, p.partition_ts, partition_end_ts);
END LOOP;
INSERT INTO audit_log
SELECT * FROM old_audit_log
WHERE created_time >= start_time_ms AND created_time < end_time_ms;
END;
$$;

21
application/src/main/data/upgrade/3.4.1/schema_update_after.sql

@ -0,0 +1,21 @@
--
-- Copyright © 2016-2022 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.
--
DROP PROCEDURE IF EXISTS update_asset_profiles;
ALTER TABLE asset ALTER COLUMN asset_profile_id SET NOT NULL;
ALTER TABLE asset DROP CONSTRAINT IF EXISTS fk_asset_profile;
ALTER TABLE asset ADD CONSTRAINT fk_asset_profile FOREIGN KEY (asset_profile_id) REFERENCES asset_profile(id);

45
application/src/main/data/upgrade/3.4.1/schema_update_before.sql

@ -0,0 +1,45 @@
--
-- Copyright © 2016-2022 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.
--
CREATE TABLE IF NOT EXISTS asset_profile (
id uuid NOT NULL CONSTRAINT asset_profile_pkey PRIMARY KEY,
created_time bigint NOT NULL,
name varchar(255),
image varchar(1000000),
description varchar,
search_text varchar(255),
is_default boolean,
tenant_id uuid,
default_rule_chain_id uuid,
default_dashboard_id uuid,
default_queue_name varchar(255),
external_id uuid,
CONSTRAINT asset_profile_name_unq_key UNIQUE (tenant_id, name),
CONSTRAINT asset_profile_external_id_unq_key UNIQUE (tenant_id, external_id),
CONSTRAINT fk_default_rule_chain_asset_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id),
CONSTRAINT fk_default_dashboard_asset_profile FOREIGN KEY (default_dashboard_id) REFERENCES dashboard(id)
);
CREATE OR REPLACE PROCEDURE update_asset_profiles()
LANGUAGE plpgsql AS
$$
BEGIN
UPDATE asset as a SET asset_profile_id = p.id
FROM
(SELECT id, tenant_id, name from asset_profile) as p
WHERE a.asset_profile_id IS NULL AND p.tenant_id = a.tenant_id AND a.type = p.name;
END;
$$;

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

@ -56,6 +56,7 @@ import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.ClaimDevicesService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService;
@ -86,6 +87,7 @@ import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.executors.ExternalCallExecutorService;
import org.thingsboard.server.service.executors.SharedEventLoopGroupService;
import org.thingsboard.server.service.mail.MailExecutorService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.TbRpcService;
@ -174,6 +176,10 @@ public class ActorSystemContext {
@Getter
private DeviceService deviceService;
@Autowired
@Getter
private DeviceCredentialsService deviceCredentialsService;
@Autowired
@Getter
private TbTenantProfileCache tenantProfileCache;
@ -182,6 +188,10 @@ public class ActorSystemContext {
@Getter
private TbDeviceProfileCache deviceProfileCache;
@Autowired
@Getter
private TbAssetProfileCache assetProfileCache;
@Autowired
@Getter
private AssetService assetService;

10
application/src/main/java/org/thingsboard/server/actors/app/AppActor.java

@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.MsgType;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.aware.TenantAwareMsg;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
@ -105,7 +105,9 @@ public class AppActor extends ContextAwareActor {
onToDeviceActorMsg((TenantAwareMsg) msg, true);
break;
case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG:
onToTenantActorMsg((EdgeEventUpdateMsg) msg);
case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG:
case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG:
onToEdgeSessionMsg((EdgeSessionMsg) msg);
break;
case SESSION_TIMEOUT_MSG:
ctx.broadcastToChildrenByType(msg, EntityType.TENANT);
@ -193,7 +195,7 @@ public class AppActor extends ContextAwareActor {
() -> new TenantActor.ActorCreator(systemContext, tenantId));
}
private void onToTenantActorMsg(EdgeEventUpdateMsg msg) {
private void onToEdgeSessionMsg(EdgeSessionMsg msg) {
TbActorRef target = null;
if (ModelConstants.SYSTEM_TENANT.equals(msg.getTenantId())) {
log.warn("Message has system tenant id: {}", msg);
@ -203,7 +205,7 @@ public class AppActor extends ContextAwareActor {
if (target != null) {
target.tellWithHighPriority(msg);
} else {
log.debug("[{}] Invalid edge event update msg: {}", msg.getTenantId(), msg);
log.debug("[{}] Invalid edge session msg: {}", msg.getTenantId(), msg);
}
}

108
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -17,11 +17,15 @@ package org.thingsboard.server.actors.ruleChain;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import io.netty.channel.EventLoopGroup;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.common.util.ListeningExecutor;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.rule.engine.api.RuleEngineAlarmService;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
@ -38,10 +42,13 @@ import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasRuleEngineProfile;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
@ -49,6 +56,7 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNode;
@ -64,6 +72,7 @@ import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.edge.EdgeService;
@ -84,6 +93,7 @@ import org.thingsboard.server.queue.TbQueueMsgMetadata;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@ -326,38 +336,62 @@ class DefaultTbContext implements TbContext {
}
public TbMsg deviceCreatedMsg(Device device, RuleNodeId ruleNodeId) {
RuleChainId ruleChainId = null;
String queueName = null;
DeviceProfile deviceProfile = null;
if (device.getDeviceProfileId() != null) {
DeviceProfile deviceProfile = mainCtx.getDeviceProfileCache().find(device.getDeviceProfileId());
if (deviceProfile == null) {
log.warn("[{}] Device profile is null!", device.getDeviceProfileId());
} else {
ruleChainId = deviceProfile.getDefaultRuleChainId();
queueName = deviceProfile.getDefaultQueueName();
}
deviceProfile = mainCtx.getDeviceProfileCache().find(device.getDeviceProfileId());
}
return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, queueName, ruleChainId);
return entityActionMsg(device, device.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, deviceProfile);
}
public TbMsg assetCreatedMsg(Asset asset, RuleNodeId ruleNodeId) {
return entityActionMsg(asset, asset.getId(), ruleNodeId, DataConstants.ENTITY_CREATED);
AssetProfile assetProfile = null;
if (asset.getAssetProfileId() != null) {
assetProfile = mainCtx.getAssetProfileCache().find(asset.getAssetProfileId());
}
return entityActionMsg(asset, asset.getId(), ruleNodeId, DataConstants.ENTITY_CREATED, assetProfile);
}
public TbMsg alarmActionMsg(Alarm alarm, RuleNodeId ruleNodeId, String action) {
RuleChainId ruleChainId = null;
String queueName = null;
HasRuleEngineProfile profile = null;
if (EntityType.DEVICE.equals(alarm.getOriginator().getEntityType())) {
DeviceId deviceId = new DeviceId(alarm.getOriginator().getId());
DeviceProfile deviceProfile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId);
if (deviceProfile == null) {
log.warn("[{}] Device profile is null!", deviceId);
} else {
ruleChainId = deviceProfile.getDefaultRuleChainId();
queueName = deviceProfile.getDefaultQueueName();
}
profile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId);
} else if (EntityType.ASSET.equals(alarm.getOriginator().getEntityType())) {
AssetId assetId = new AssetId(alarm.getOriginator().getId());
profile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId);
}
return entityActionMsg(alarm, alarm.getOriginator(), ruleNodeId, action, profile);
}
public TbMsg attributesUpdatedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List<AttributeKvEntry> attributes) {
ObjectNode entityNode = JacksonUtil.newObjectNode();
if (attributes != null) {
attributes.forEach(attributeKvEntry -> JacksonUtil.addKvEntry(entityNode, attributeKvEntry));
}
return attributesActionMsg(originator, ruleNodeId, scope, DataConstants.ATTRIBUTES_UPDATED, JacksonUtil.toString(entityNode));
}
public TbMsg attributesDeletedActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, List<String> keys) {
ObjectNode entityNode = JacksonUtil.newObjectNode();
ArrayNode attrsArrayNode = entityNode.putArray("attributes");
if (keys != null) {
keys.forEach(attrsArrayNode::add);
}
return attributesActionMsg(originator, ruleNodeId, scope, DataConstants.ATTRIBUTES_DELETED, JacksonUtil.toString(entityNode));
}
private TbMsg attributesActionMsg(EntityId originator, RuleNodeId ruleNodeId, String scope, String action, String msgData) {
TbMsgMetaData tbMsgMetaData = getActionMetaData(ruleNodeId);
tbMsgMetaData.putValue("scope", scope);
HasRuleEngineProfile profile = null;
if (EntityType.DEVICE.equals(originator.getEntityType())) {
DeviceId deviceId = new DeviceId(originator.getId());
profile = mainCtx.getDeviceProfileCache().get(getTenantId(), deviceId);
} else if (EntityType.ASSET.equals(originator.getEntityType())) {
AssetId assetId = new AssetId(originator.getId());
profile = mainCtx.getAssetProfileCache().get(getTenantId(), assetId);
}
return entityActionMsg(alarm, alarm.getId(), ruleNodeId, action, queueName, ruleChainId);
return entityActionMsg(originator, tbMsgMetaData, msgData, action, profile);
}
@Override
@ -366,17 +400,27 @@ class DefaultTbContext implements TbContext {
}
public <E, I extends EntityId> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action) {
return entityActionMsg(entity, id, ruleNodeId, action, null, null);
return entityActionMsg(entity, id, ruleNodeId, action, null);
}
public <E, I extends EntityId> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, String queueName, RuleChainId ruleChainId) {
public <E, I extends EntityId, K extends HasRuleEngineProfile> TbMsg entityActionMsg(E entity, I id, RuleNodeId ruleNodeId, String action, K profile) {
try {
return TbMsg.newMsg(queueName, action, id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)), ruleChainId, null);
return entityActionMsg(id, getActionMetaData(ruleNodeId), mapper.writeValueAsString(mapper.valueToTree(entity)), action, profile);
} catch (JsonProcessingException | IllegalArgumentException e) {
throw new RuntimeException("Failed to process " + id.getEntityType().name().toLowerCase() + " " + action + " msg: " + e);
}
}
private <I extends EntityId, K extends HasRuleEngineProfile> TbMsg entityActionMsg(I id, TbMsgMetaData msgMetaData, String msgData, String action, K profile) {
String defaultQueueName = null;
RuleChainId defaultRuleChainId = null;
if (profile != null) {
defaultQueueName = profile.getDefaultQueueName();
defaultRuleChainId = profile.getDefaultRuleChainId();
}
return TbMsg.newMsg(defaultQueueName, action, id, msgMetaData, msgData, defaultRuleChainId, null);
}
@Override
public RuleNodeId getSelfId() {
return nodeCtx.getSelf().getId();
@ -478,6 +522,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getDeviceService();
}
@Override
public DeviceCredentialsService getDeviceCredentialsService() {
return mainCtx.getDeviceCredentialsService();
}
@Override
public TbClusterService getClusterService() {
return mainCtx.getClusterService();
@ -533,6 +582,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getDeviceProfileCache();
}
@Override
public RuleEngineAssetProfileCache getAssetProfileCache() {
return mainCtx.getAssetProfileCache();
}
@Override
public EdgeService getEdgeService() {
return mainCtx.getEdgeService();
@ -647,9 +701,15 @@ class DefaultTbContext implements TbContext {
mainCtx.getDeviceProfileCache().addListener(getTenantId(), getSelfId(), profileListener, deviceListener);
}
@Override
public void addAssetProfileListeners(Consumer<AssetProfile> profileListener, BiConsumer<AssetId, AssetProfile> assetListener) {
mainCtx.getAssetProfileCache().addListener(getTenantId(), getSelfId(), profileListener, assetListener);
}
@Override
public void removeListeners() {
mainCtx.getDeviceProfileCache().removeListener(getTenantId(), getSelfId());
mainCtx.getAssetProfileCache().removeListener(getTenantId(), getSelfId());
mainCtx.getTenantProfileCache().removeListener(getTenantId(), getSelfId());
}

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

@ -47,7 +47,7 @@ import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.aware.DeviceAwareMsg;
import org.thingsboard.server.common.msg.aware.RuleChainAwareMsg;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.PartitionChangeMsg;
import org.thingsboard.server.common.msg.queue.QueueToRuleEngineMsg;
@ -167,7 +167,9 @@ public class TenantActor extends RuleChainManagerActor {
onRuleChainMsg((RuleChainAwareMsg) msg);
break;
case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG:
onToEdgeSessionMsg((EdgeEventUpdateMsg) msg);
case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG:
case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG:
onToEdgeSessionMsg((EdgeSessionMsg) msg);
break;
default:
return false;
@ -271,9 +273,8 @@ public class TenantActor extends RuleChainManagerActor {
() -> new DeviceActorCreator(systemContext, tenantId, deviceId));
}
private void onToEdgeSessionMsg(EdgeEventUpdateMsg msg) {
log.trace("[{}] onToEdgeSessionMsg [{}]", msg.getTenantId(), msg);
systemContext.getEdgeRpcService().onEdgeEvent(tenantId, msg.getEdgeId());
private void onToEdgeSessionMsg(EdgeSessionMsg msg) {
systemContext.getEdgeRpcService().onToEdgeSessionMsg(tenantId, msg);
}
private ApiUsageState getApiUsageState() {

16
application/src/main/java/org/thingsboard/server/controller/AssetController.java

@ -41,18 +41,19 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.asset.AssetBulkImportService;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportRequest;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportResult;
import org.thingsboard.server.service.entitiy.asset.TbAssetService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
@ -65,6 +66,7 @@ import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_NAME_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_TYPE_DESCRIPTION;
@ -257,6 +259,8 @@ public class AssetController extends BaseController {
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String assetProfileId,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -268,6 +272,9 @@ public class AssetController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetInfosByTenantIdAndType(tenantId, type, pageLink));
} else if (assetProfileId != null && assetProfileId.length() > 0) {
AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId));
return checkNotNull(assetService.findAssetInfosByTenantIdAndAssetProfileId(tenantId, profileId, pageLink));
} else {
return checkNotNull(assetService.findAssetInfosByTenantId(tenantId, pageLink));
}
@ -345,6 +352,8 @@ public class AssetController extends BaseController {
@RequestParam int page,
@ApiParam(value = ASSET_TYPE_DESCRIPTION)
@RequestParam(required = false) String type,
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@RequestParam(required = false) String assetProfileId,
@ApiParam(value = ASSET_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_SORT_PROPERTY_ALLOWABLE_VALUES)
@ -359,6 +368,9 @@ public class AssetController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
} else if (assetProfileId != null && assetProfileId.length() > 0) {
AssetProfileId profileId = new AssetProfileId(toUUID(assetProfileId));
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(tenantId, customerId, profileId, pageLink));
} else {
return checkNotNull(assetService.findAssetInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
}

227
application/src/main/java/org/thingsboard/server/controller/AssetProfileController.java

@ -0,0 +1,227 @@
/**
* Copyright © 2016-2022 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.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.asset.AssetProfileInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.asset.profile.TbAssetProfileService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_INFO_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_ALLOWABLE_VALUES;
import static org.thingsboard.server.controller.ControllerConstants.SORT_ORDER_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.SORT_PROPERTY_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH;
import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LINK;
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class AssetProfileController extends BaseController {
private final TbAssetProfileService tbAssetProfileService;
@ApiOperation(value = "Get Asset Profile (getAssetProfileById)",
notes = "Fetch the Asset Profile object based on the provided Asset Profile Id. " +
"The server checks that the asset profile is owned by the same tenant. " + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfile/{assetProfileId}", method = RequestMethod.GET)
@ResponseBody
public AssetProfile getAssetProfileById(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
try {
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
return checkAssetProfileId(assetProfileId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Asset Profile Info (getAssetProfileInfoById)",
notes = "Fetch the Asset Profile Info object based on the provided Asset Profile Id. "
+ ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assetProfileInfo/{assetProfileId}", method = RequestMethod.GET)
@ResponseBody
public AssetProfileInfo getAssetProfileInfoById(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
try {
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
return new AssetProfileInfo(checkAssetProfileId(assetProfileId, Operation.READ));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Default Asset Profile (getDefaultAssetProfileInfo)",
notes = "Fetch the Default Asset Profile Info object. " +
ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assetProfileInfo/default", method = RequestMethod.GET)
@ResponseBody
public AssetProfileInfo getDefaultAssetProfileInfo() throws ThingsboardException {
try {
return checkNotNull(assetProfileService.findDefaultAssetProfileInfo(getTenantId()));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Create Or Update Asset Profile (saveAssetProfile)",
notes = "Create or update the Asset Profile. When creating asset profile, platform generates asset profile id as " + UUID_WIKI_LINK +
"The newly created asset profile id will be present in the response. " +
"Specify existing asset profile id to update the asset profile. " +
"Referencing non-existing asset profile Id will cause 'Not Found' error. " + NEW_LINE +
"Asset profile name is unique in the scope of tenant. Only one 'default' asset profile may exist in scope of tenant. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Asset Profile entity. " +
TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json",
consumes = "application/json")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfile", method = RequestMethod.POST)
@ResponseBody
public AssetProfile saveAssetProfile(
@ApiParam(value = "A JSON value representing the asset profile.")
@RequestBody AssetProfile assetProfile) throws Exception {
assetProfile.setTenantId(getTenantId());
checkEntity(assetProfile.getId(), assetProfile, Resource.ASSET_PROFILE);
return tbAssetProfileService.save(assetProfile, getCurrentUser());
}
@ApiOperation(value = "Delete asset profile (deleteAssetProfile)",
notes = "Deletes the asset profile. Referencing non-existing asset profile Id will cause an error. " +
"Can't delete the asset profile if it is referenced by existing assets." + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfile/{assetProfileId}", method = RequestMethod.DELETE)
@ResponseStatus(value = HttpStatus.OK)
public void deleteAssetProfile(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
AssetProfile assetProfile = checkAssetProfileId(assetProfileId, Operation.DELETE);
tbAssetProfileService.delete(assetProfile, getCurrentUser());
}
@ApiOperation(value = "Make Asset Profile Default (setDefaultAssetProfile)",
notes = "Marks asset profile as default within a tenant scope." + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfile/{assetProfileId}/default", method = RequestMethod.POST)
@ResponseBody
public AssetProfile setDefaultAssetProfile(
@ApiParam(value = ASSET_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(ASSET_PROFILE_ID) String strAssetProfileId) throws ThingsboardException {
checkParameter(ASSET_PROFILE_ID, strAssetProfileId);
AssetProfileId assetProfileId = new AssetProfileId(toUUID(strAssetProfileId));
AssetProfile assetProfile = checkAssetProfileId(assetProfileId, Operation.WRITE);
AssetProfile previousDefaultAssetProfile = assetProfileService.findDefaultAssetProfile(getTenantId());
return tbAssetProfileService.setDefaultAssetProfile(assetProfile, previousDefaultAssetProfile, getCurrentUser());
}
@ApiOperation(value = "Get Asset Profiles (getAssetProfiles)",
notes = "Returns a page of asset profile objects owned by tenant. " +
PAGE_DATA_PARAMETERS + TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/assetProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AssetProfile> getAssetProfiles(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(assetProfileService.findAssetProfiles(getTenantId(), pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
@ApiOperation(value = "Get Asset Profile infos (getAssetProfileInfos)",
notes = "Returns a page of asset profile info objects owned by tenant. " +
PAGE_DATA_PARAMETERS + ASSET_PROFILE_INFO_DESCRIPTION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = "application/json")
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/assetProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
@ResponseBody
public PageData<AssetProfileInfo> getAssetProfileInfos(
@ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true)
@RequestParam int pageSize,
@ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true)
@RequestParam int page,
@ApiParam(value = ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION)
@RequestParam(required = false) String textSearch,
@ApiParam(value = SORT_PROPERTY_DESCRIPTION, allowableValues = ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortProperty,
@ApiParam(value = SORT_ORDER_DESCRIPTION, allowableValues = SORT_ORDER_ALLOWABLE_VALUES)
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
try {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
return checkNotNull(assetProfileService.findAssetProfileInfos(getTenantId(), pageLink));
} catch (Exception e) {
throw handleException(e);
}
}
}

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

@ -57,6 +57,7 @@ import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
@ -65,6 +66,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AlarmId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -96,6 +98,7 @@ import org.thingsboard.server.common.data.rule.RuleChainType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.widget.WidgetTypeDetails;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
@ -132,6 +135,7 @@ import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.edge.rpc.EdgeRpcService;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.resource.TbResourceService;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -190,6 +194,9 @@ public abstract class BaseController {
@Autowired
protected AssetService assetService;
@Autowired
protected AssetProfileService assetProfileService;
@Autowired
protected AlarmSubscriptionService alarmService;
@ -265,14 +272,14 @@ public abstract class BaseController {
@Autowired
protected TbDeviceProfileCache deviceProfileCache;
@Autowired(required = false)
protected EdgeService edgeService;
@Autowired
protected TbAssetProfileCache assetProfileCache;
@Autowired(required = false)
protected EdgeNotificationService edgeNotificationService;
protected EdgeService edgeService;
@Autowired(required = false)
protected EdgeRpcService edgeGrpcService;
protected EdgeRpcService edgeRpcService;
@Autowired
protected TbNotificationEntityService notificationEntityService;
@ -548,6 +555,9 @@ public abstract class BaseController {
case ASSET:
checkAssetId(new AssetId(entityId.getId()), operation);
return;
case ASSET_PROFILE:
checkAssetProfileId(new AssetProfileId(entityId.getId()), operation);
return;
case DASHBOARD:
checkDashboardId(new DashboardId(entityId.getId()), operation);
return;
@ -667,6 +677,18 @@ public abstract class BaseController {
}
}
AssetProfile checkAssetProfileId(AssetProfileId assetProfileId, Operation operation) throws ThingsboardException {
try {
validateId(assetProfileId, "Incorrect assetProfileId " + assetProfileId);
AssetProfile assetProfile = assetProfileService.findAssetProfileById(getCurrentUser().getTenantId(), assetProfileId);
checkNotNull(assetProfile, "Asset profile with id [" + assetProfileId + "] is not found");
accessControlService.checkPermission(getCurrentUser(), Resource.ASSET_PROFILE, operation, assetProfileId, assetProfile);
return assetProfile;
} catch (Exception e) {
throw handleException(e, false);
}
}
Alarm checkAlarmId(AlarmId alarmId, Operation operation) throws ThingsboardException {
try {
validateId(alarmId, "Incorrect alarmId " + alarmId);

10
application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java

@ -35,6 +35,8 @@ public class ControllerConstants {
protected static final String DEVICE_ID_PARAM_DESCRIPTION = "A string value representing the device id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ENTITY_VIEW_ID_PARAM_DESCRIPTION = "A string value representing the entity view id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String DEVICE_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the device profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String ASSET_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the asset profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String TENANT_PROFILE_ID_PARAM_DESCRIPTION = "A string value representing the tenant profile id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String TENANT_ID_PARAM_DESCRIPTION = "A string value representing the tenant id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
protected static final String EDGE_ID_PARAM_DESCRIPTION = "A string value representing the edge id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'";
@ -75,6 +77,8 @@ public class ControllerConstants {
protected static final String TENANT_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the tenant profile name.";
protected static final String RULE_CHAIN_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the rule chain name.";
protected static final String DEVICE_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the device profile name.";
protected static final String ASSET_PROFILE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the asset profile name.";
protected static final String CUSTOMER_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the customer title.";
protected static final String EDGE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the edge name.";
protected static final String EVENT_TEXT_SEARCH_DESCRIPTION = "The value is not used in searching.";
@ -92,6 +96,8 @@ public class ControllerConstants {
protected static final String TENANT_PROFILE_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "id, name";
protected static final String TENANT_INFO_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, tenantProfileName, title, email, country, state, city, address, address2, zip, phone, email";
protected static final String DEVICE_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, transportType, description, isDefault";
protected static final String ASSET_PROFILE_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, description, isDefault";
protected static final String ASSET_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, name, type, label, customerTitle";
protected static final String ALARM_SORT_PROPERTY_ALLOWABLE_VALUES = "createdTime, startTs, endTs, type, ackTs, clearTs, severity, status";
protected static final String EVENT_SORT_PROPERTY_ALLOWABLE_VALUES = "ts, id";
@ -110,6 +116,8 @@ public class ControllerConstants {
protected static final String RELATION_INFO_DESCRIPTION = "Relation Info is an extension of the default Relation object that contains information about the 'from' and 'to' entity names. ";
protected static final String EDGE_INFO_DESCRIPTION = "Edge Info is an extension of the default Edge object that contains information about the assigned customer name. ";
protected static final String DEVICE_PROFILE_INFO_DESCRIPTION = "Device Profile Info is a lightweight object that includes main information about Device Profile excluding the heavyweight configuration object. ";
protected static final String ASSET_PROFILE_INFO_DESCRIPTION = "Asset Profile Info is a lightweight object that includes main information about Asset Profile. ";
protected static final String QUEUE_SERVICE_TYPE_DESCRIPTION = "Service type (implemented only for the TB-RULE-ENGINE)";
protected static final String QUEUE_SERVICE_TYPE_ALLOWABLE_VALUES = "TB-RULE-ENGINE, TB-CORE, TB-TRANSPORT, JS-EXECUTOR";
protected static final String QUEUE_QUEUE_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on the queue name.";
@ -1415,6 +1423,8 @@ public class ControllerConstants {
protected static final String DEVICE_PROFILE_ID = "deviceProfileId";
protected static final String ASSET_PROFILE_ID = "assetProfileId";
protected static final String MODEL_DESCRIPTION = "See the 'Model' tab for more details.";
protected static final String ENTITY_VIEW_DESCRIPTION = "Entity Views limit the degree of exposure of the Device or Asset telemetry and attributes to the Customers. " +
"Every Entity View references exactly one entity (device or asset) and defines telemetry and attribute keys that will be visible to the assigned Customer. " +

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

@ -191,7 +191,7 @@ public class DeviceProfileController extends BaseController {
"Specify existing device profile id to update the device profile. " +
"Referencing non-existing device profile Id will cause 'Not Found' error. " + NEW_LINE +
"Device profile name is unique in the scope of tenant. Only one 'default' device profile may exist in scope of tenant." + DEVICE_PROFILE_DATA +
"Remove 'id', 'tenantId' and optionally 'customerId' from the request body example (below) to create new Device Profile entity. " +
"Remove 'id', 'tenantId' from the request body example (below) to create new Device Profile entity. " +
TENANT_AUTHORITY_PARAGRAPH,
produces = "application/json",
consumes = "application/json")

20
application/src/main/java/org/thingsboard/server/controller/EdgeController.java

@ -22,6 +22,7 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.rule.engine.flow.TbRuleChainInputNode;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
@ -47,6 +49,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
@ -61,6 +65,7 @@ import org.thingsboard.server.service.security.permission.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
@ -529,24 +534,35 @@ public class EdgeController extends BaseController {
"All entities that are assigned to particular edge are going to be send to remote edge service." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/edge/sync/{edgeId}", method = RequestMethod.POST)
public void syncEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
public DeferredResult<ResponseEntity> syncEdge(@ApiParam(value = EDGE_ID_PARAM_DESCRIPTION, required = true)
@PathVariable("edgeId") String strEdgeId) throws ThingsboardException {
checkParameter("edgeId", strEdgeId);
try {
final DeferredResult<ResponseEntity> response = new DeferredResult<>();
if (isEdgesEnabled()) {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
edgeId = checkNotNull(edgeId);
SecurityUser user = getCurrentUser();
TenantId tenantId = user.getTenantId();
edgeGrpcService.startSyncProcess(tenantId, edgeId);
ToEdgeSyncRequest request = new ToEdgeSyncRequest(UUID.randomUUID(), tenantId, edgeId);
edgeRpcService.processSyncRequest(request, fromEdgeSyncResponse -> reply(response, fromEdgeSyncResponse));
} else {
throw new ThingsboardException("Edges support disabled", ThingsboardErrorCode.GENERAL);
}
return response;
} catch (Exception e) {
throw handleException(e);
}
}
private void reply(DeferredResult<ResponseEntity> response, FromEdgeSyncResponse fromEdgeSyncResponse) {
if (fromEdgeSyncResponse.isSuccess()) {
response.setResult(new ResponseEntity<>(HttpStatus.OK));
} else {
response.setErrorResult(new ThingsboardException("Edge is not connected", ThingsboardErrorCode.GENERAL));
}
}
@ApiOperation(value = "Find missing rule chains (findMissingToRelatedRuleChains)",
notes = "Returns list of rule chains ids that are not assigned to particular edge, but these rule chains are present in the already assigned rule chains to edge." + TENANT_AUTHORITY_PARAGRAPH)
@PreAuthorize("hasAuthority('TENANT_ADMIN')")

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

@ -225,6 +225,10 @@ public class ThingsboardInstallService {
log.info("Upgrading ThingsBoard from version 3.4.0 to 3.4.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.0");
dataUpdateService.updateData("3.4.0");
case "3.4.1":
log.info("Upgrading ThingsBoard from version 3.4.1 to 3.4.2 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.4.1");
dataUpdateService.updateData("3.4.1");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
break;

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

@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
@ -34,9 +35,8 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.DataType;
import org.thingsboard.server.common.data.kv.KvEntry;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
@ -113,6 +113,15 @@ public class EntityActionService {
case UNASSIGNED_FROM_EDGE:
msgType = DataConstants.ENTITY_UNASSIGNED_FROM_EDGE;
break;
case RELATION_ADD_OR_UPDATE:
msgType = DataConstants.RELATION_ADD_OR_UPDATE;
break;
case RELATION_DELETED:
msgType = DataConstants.RELATION_DELETED;
break;
case RELATIONS_DELETED:
msgType = DataConstants.RELATIONS_DELETED;
break;
}
if (!StringUtils.isEmpty(msgType)) {
try {
@ -170,7 +179,7 @@ public class EntityActionService {
metaData.putValue(DataConstants.SCOPE, scope);
if (attributes != null) {
for (AttributeKvEntry attr : attributes) {
addKvEntry(entityNode, attr);
JacksonUtil.addKvEntry(entityNode, attr);
}
}
} else if (actionType == ActionType.ATTRIBUTES_DELETED) {
@ -195,10 +204,12 @@ public class EntityActionService {
}
entityNode.put("startTs", extractParameter(Long.class, 1, additionalInfo));
entityNode.put("endTs", extractParameter(Long.class, 2, additionalInfo));
} else if (ActionType.RELATION_ADD_OR_UPDATE.equals(actionType) || ActionType.RELATION_DELETED.equals(actionType)) {
entityNode = json.valueToTree(extractParameter(EntityRelation.class, 0, additionalInfo));
}
}
TbMsg tbMsg = TbMsg.newMsg(msgType, entityId, customerId, metaData, TbMsgDataType.JSON, json.writeValueAsString(entityNode));
if (tenantId.isNullUid()) {
if (tenantId == null || tenantId.isNullUid()) {
if (entity instanceof HasTenantId) {
tenantId = ((HasTenantId) entity).getTenantId();
}
@ -246,27 +257,11 @@ public class EntityActionService {
element.put("ts", entry.getKey());
ObjectNode values = element.putObject("values");
for (TsKvEntry tsKvEntry : entry.getValue()) {
addKvEntry(values, tsKvEntry);
JacksonUtil.addKvEntry(values, tsKvEntry);
}
result.add(element);
}
}
}
private void addKvEntry(ObjectNode entityNode, KvEntry kvEntry) throws Exception {
if (kvEntry.getDataType() == DataType.BOOLEAN) {
kvEntry.getBooleanValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.DOUBLE) {
kvEntry.getDoubleValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.LONG) {
kvEntry.getLongValue().ifPresent(value -> entityNode.put(kvEntry.getKey(), value));
} else if (kvEntry.getDataType() == DataType.JSON) {
if (kvEntry.getJsonValue().isPresent()) {
entityNode.set(kvEntry.getKey(), json.readTree(kvEntry.getJsonValue().get()));
}
} else {
entityNode.put(kvEntry.getKey(), kvEntry.getValueAsString());
}
}
}

11
application/src/main/java/org/thingsboard/server/service/asset/AssetBulkImportService.java

@ -21,9 +21,12 @@ import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.sync.ie.importing.csv.BulkImportColumnType;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.asset.TbAssetService;
@ -39,6 +42,7 @@ import java.util.Optional;
public class AssetBulkImportService extends AbstractBulkImportService<Asset> {
private final AssetService assetService;
private final TbAssetService tbAssetService;
private final AssetProfileService assetProfileService;
@Override
protected void setEntityFields(Asset entity, Map<BulkImportColumnType, String> fields) {
@ -65,6 +69,13 @@ public class AssetBulkImportService extends AbstractBulkImportService<Asset> {
@Override
@SneakyThrows
protected Asset saveEntity(SecurityUser user, Asset entity, Map<BulkImportColumnType, String> fields) {
AssetProfile assetProfile;
if (StringUtils.isNotEmpty(entity.getType())) {
assetProfile = assetProfileService.findOrCreateAssetProfile(entity.getTenantId(), entity.getType());
} else {
assetProfile = assetProfileService.findDefaultAssetProfile(entity.getTenantId());
}
entity.setAssetProfileId(assetProfile.getId());
return tbAssetService.save(entity, user);
}

1
application/src/main/java/org/thingsboard/server/service/edge/DefaultEdgeNotificationService.java

@ -134,6 +134,7 @@ public class DefaultEdgeNotificationService implements EdgeNotificationService {
case ASSET:
case DEVICE:
case DEVICE_PROFILE:
case ASSET_PROFILE:
case ENTITY_VIEW:
case DASHBOARD:
case RULE_CHAIN:

8
application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java

@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.dashboard.DashboardService;
@ -38,6 +39,7 @@ import org.thingsboard.server.service.edge.rpc.EdgeEventStorageSettings;
import org.thingsboard.server.service.edge.rpc.processor.AdminSettingsEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.AlarmEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.AssetEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.AssetProfileEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.CustomerEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.DashboardEdgeProcessor;
import org.thingsboard.server.service.edge.rpc.processor.DeviceEdgeProcessor;
@ -83,6 +85,9 @@ public class EdgeContextComponent {
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private AssetProfileService assetProfileService;
@Autowired
private AttributesService attributesService;
@ -113,6 +118,9 @@ public class EdgeContextComponent {
@Autowired
private DeviceProfileEdgeProcessor deviceProfileProcessor;
@Autowired
private AssetProfileEdgeProcessor assetProfileProcessor;
@Autowired
private DeviceEdgeProcessor deviceProcessor;

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

@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.ResourceUtils;
import org.thingsboard.server.common.data.edge.Edge;
@ -34,6 +35,10 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BasicTsKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest;
import org.thingsboard.server.gen.edge.v1.EdgeRpcServiceGrpc;
import org.thingsboard.server.gen.edge.v1.RequestMsg;
import org.thingsboard.server.gen.edge.v1.ResponseMsg;
@ -59,6 +64,7 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
@Service
@Slf4j
@ -71,6 +77,8 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
private final Map<EdgeId, Boolean> sessionNewEvents = new HashMap<>();
private final ConcurrentMap<EdgeId, ScheduledFuture<?>> sessionEdgeEventChecks = new ConcurrentHashMap<>();
private final ConcurrentMap<UUID, Consumer<FromEdgeSyncResponse>> localSyncEdgeRequests = new ConcurrentHashMap<>();
@Value("${edges.rpc.port}")
private int rpcPort;
@Value("${edges.rpc.ssl.enabled}")
@ -98,12 +106,17 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
@Autowired
private TelemetrySubscriptionService tsSubService;
@Autowired
private TbClusterService clusterService;
private Server server;
private ScheduledExecutorService edgeEventProcessingExecutorService;
private ScheduledExecutorService sendDownlinkExecutorService;
private ScheduledExecutorService executorService;
@PostConstruct
public void init() {
log.info("Initializing Edge RPC service!");
@ -129,8 +142,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
log.error("Failed to start Edge RPC server!", e);
throw new RuntimeException("Failed to start Edge RPC server!");
}
this.edgeEventProcessingExecutorService = Executors.newScheduledThreadPool(schedulerPoolSize, ThingsBoardThreadFactory.forName("edge-scheduler"));
this.edgeEventProcessingExecutorService = Executors.newScheduledThreadPool(schedulerPoolSize, ThingsBoardThreadFactory.forName("edge-event-check-scheduler"));
this.sendDownlinkExecutorService = Executors.newScheduledThreadPool(sendSchedulerPoolSize, ThingsBoardThreadFactory.forName("edge-send-scheduler"));
this.executorService = Executors.newSingleThreadScheduledExecutor(ThingsBoardThreadFactory.forName("edge-service"));
log.info("Edge RPC service initialized!");
}
@ -153,6 +167,9 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
if (sendDownlinkExecutorService != null) {
sendDownlinkExecutorService.shutdownNow();
}
if (executorService != null) {
executorService.shutdownNow();
}
}
@Override
@ -160,47 +177,76 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
return new EdgeGrpcSession(ctx, outputStream, this::onEdgeConnect, this::onEdgeDisconnect, sendDownlinkExecutorService).getInputStream();
}
@Override
public void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg) {
executorService.execute(() -> {
switch (msg.getMsgType()) {
case EDGE_EVENT_UPDATE_TO_EDGE_SESSION_MSG:
EdgeEventUpdateMsg edgeEventUpdateMsg = (EdgeEventUpdateMsg) msg;
log.trace("[{}] onToEdgeSessionMsg [{}]", edgeEventUpdateMsg.getTenantId(), msg);
onEdgeEvent(tenantId, edgeEventUpdateMsg.getEdgeId());
break;
case EDGE_SYNC_REQUEST_TO_EDGE_SESSION_MSG:
ToEdgeSyncRequest toEdgeSyncRequest = (ToEdgeSyncRequest) msg;
log.trace("[{}] toEdgeSyncRequest [{}]", toEdgeSyncRequest.getTenantId(), msg);
startSyncProcess(tenantId, toEdgeSyncRequest.getEdgeId(), toEdgeSyncRequest.getId());
break;
case EDGE_SYNC_RESPONSE_FROM_EDGE_SESSION_MSG:
FromEdgeSyncResponse fromEdgeSyncResponse = (FromEdgeSyncResponse) msg;
log.trace("[{}] fromEdgeSyncResponse [{}]", fromEdgeSyncResponse.getTenantId(), msg);
processSyncResponse(fromEdgeSyncResponse);
break;
}
});
}
@Override
public void updateEdge(TenantId tenantId, Edge edge) {
EdgeGrpcSession session = sessions.get(edge.getId());
if (session != null && session.isConnected()) {
log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
session.onConfigurationUpdate(edge);
} else {
log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
}
executorService.execute(() -> {
EdgeGrpcSession session = sessions.get(edge.getId());
if (session != null && session.isConnected()) {
log.debug("[{}] Updating configuration for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
session.onConfigurationUpdate(edge);
} else {
log.debug("[{}] Session doesn't exist for edge [{}] [{}]", tenantId, edge.getName(), edge.getId());
}
});
}
@Override
public void deleteEdge(TenantId tenantId, EdgeId edgeId) {
executorService.execute(() -> {
EdgeGrpcSession session = sessions.get(edgeId);
if (session != null && session.isConnected()) {
log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId);
session.close();
sessions.remove(edgeId);
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
sessionNewEvents.remove(edgeId);
} finally {
newEventLock.unlock();
}
cancelScheduleEdgeEventsCheck(edgeId);
}
});
}
private void onEdgeEvent(TenantId tenantId, EdgeId edgeId) {
EdgeGrpcSession session = sessions.get(edgeId);
if (session != null && session.isConnected()) {
log.info("[{}] Closing and removing session for edge [{}]", tenantId, edgeId);
session.close();
sessions.remove(edgeId);
log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId.getId());
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
sessionNewEvents.remove(edgeId);
if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) {
log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId());
sessionNewEvents.put(edgeId, true);
}
} finally {
newEventLock.unlock();
}
cancelScheduleEdgeEventsCheck(edgeId);
}
}
@Override
public void onEdgeEvent(TenantId tenantId, EdgeId edgeId) {
log.trace("[{}] onEdgeEvent [{}]", tenantId, edgeId.getId());
final Lock newEventLock = sessionNewEventsLocks.computeIfAbsent(edgeId, id -> new ReentrantLock());
newEventLock.lock();
try {
if (Boolean.FALSE.equals(sessionNewEvents.get(edgeId))) {
log.trace("[{}] set session new events flag to true [{}]", tenantId, edgeId.getId());
sessionNewEvents.put(edgeId, true);
}
} finally {
newEventLock.unlock();
}
}
@ -220,14 +266,47 @@ public class EdgeGrpcService extends EdgeRpcServiceGrpc.EdgeRpcServiceImplBase i
scheduleEdgeEventsCheck(edgeGrpcSession);
}
@Override
public void startSyncProcess(TenantId tenantId, EdgeId edgeId) {
private void startSyncProcess(TenantId tenantId, EdgeId edgeId, UUID requestId) {
EdgeGrpcSession session = sessions.get(edgeId);
if (session != null && session.isConnected()) {
session.startSyncProcess(tenantId, edgeId);
if (session != null) {
boolean success = false;
if (session.isConnected()) {
session.startSyncProcess(tenantId, edgeId);
success = true;
}
clusterService.pushEdgeSyncResponseToCore(new FromEdgeSyncResponse(requestId, tenantId, edgeId, success));
}
}
@Override
public void processSyncRequest(ToEdgeSyncRequest request, Consumer<FromEdgeSyncResponse> responseConsumer) {
log.trace("[{}][{}] Processing sync edge request [{}]", request.getTenantId(), request.getId(), request.getEdgeId());
UUID requestId = request.getId();
localSyncEdgeRequests.put(requestId, responseConsumer);
clusterService.pushEdgeSyncRequestToCore(request);
scheduleSyncRequestTimeout(request, requestId);
}
private void scheduleSyncRequestTimeout(ToEdgeSyncRequest request, UUID requestId) {
log.trace("[{}] scheduling sync edge request", requestId);
executorService.schedule(() -> {
log.trace("[{}] checking if sync edge request is not processed...", requestId);
Consumer<FromEdgeSyncResponse> consumer = localSyncEdgeRequests.remove(requestId);
if (consumer != null) {
log.trace("[{}] timeout for processing sync edge request.", requestId);
consumer.accept(new FromEdgeSyncResponse(requestId, request.getTenantId(), request.getEdgeId(), false));
}
}, 20, TimeUnit.SECONDS);
}
private void processSyncResponse(FromEdgeSyncResponse response) {
log.trace("[{}] Received response from sync service: [{}]", response.getId(), response);
UUID requestId = response.getId();
Consumer<FromEdgeSyncResponse> consumer = localSyncEdgeRequests.remove(requestId);
if (consumer != null) {
consumer.accept(response);
} else {
log.error("[{}] Edge is not connected [{}]", tenantId, edgeId);
throw new RuntimeException("Edge is not connected");
log.trace("[{}] Unknown or stale sync response received [{}]", requestId, response);
}
}

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

@ -38,6 +38,7 @@ import org.thingsboard.server.common.data.kv.LongDataEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileAssetsRequestMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectRequestMsg;
import org.thingsboard.server.gen.edge.v1.ConnectResponseCode;
@ -513,6 +514,8 @@ public final class EdgeGrpcSession implements Closeable {
return ctx.getDeviceProcessor().processDeviceToEdge(edge, edgeEvent, msgType, action);
case DEVICE_PROFILE:
return ctx.getDeviceProfileProcessor().processDeviceProfileToEdge(edgeEvent, msgType, action);
case ASSET_PROFILE:
return ctx.getAssetProfileProcessor().processAssetProfileToEdge(edgeEvent, msgType, action);
case ASSET:
return ctx.getAssetProcessor().processAssetToEdge(edge, edgeEvent, msgType, action);
case ENTITY_VIEW:
@ -634,6 +637,11 @@ public final class EdgeGrpcSession implements Closeable {
result.add(ctx.getEdgeRequestsService().processDeviceProfileDevicesRequestMsg(edge.getTenantId(), edge, deviceProfileDevicesRequestMsg));
}
}
if (uplinkMsg.getAssetProfileAssetsRequestMsgCount() > 0) {
for (AssetProfileAssetsRequestMsg assetProfileAssetsRequestMsg : uplinkMsg.getAssetProfileAssetsRequestMsgList()) {
result.add(ctx.getEdgeRequestsService().processAssetProfileAssetsRequestMsg(edge.getTenantId(), edge, assetProfileAssetsRequestMsg));
}
}
if (uplinkMsg.getWidgetBundleTypesRequestMsgCount() > 0) {
for (WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg : uplinkMsg.getWidgetBundleTypesRequestMsgList()) {
result.add(ctx.getEdgeRequestsService().processWidgetBundleTypesRequestMsg(edge.getTenantId(), edge, widgetBundleTypesRequestMsg));

11
application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeRpcService.java

@ -18,14 +18,19 @@ package org.thingsboard.server.service.edge.rpc;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.edge.EdgeSessionMsg;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest;
import java.util.function.Consumer;
public interface EdgeRpcService {
void onToEdgeSessionMsg(TenantId tenantId, EdgeSessionMsg msg);
void updateEdge(TenantId tenantId, Edge edge);
void deleteEdge(TenantId tenantId, EdgeId edgeId);
void onEdgeEvent(TenantId tenantId, EdgeId edgeId);
void startSyncProcess(TenantId tenantId, EdgeId edgeId);
void processSyncRequest(ToEdgeSyncRequest request, Consumer<FromEdgeSyncResponse> responseConsumer);
}

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

@ -19,6 +19,7 @@ import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.service.edge.EdgeContextComponent;
import org.thingsboard.server.service.edge.rpc.fetch.AdminSettingsEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.AssetProfilesEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.AssetsEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.CustomerEdgeEventFetcher;
import org.thingsboard.server.service.edge.rpc.fetch.CustomerUsersEdgeEventFetcher;
@ -47,6 +48,7 @@ public class EdgeSyncCursor {
fetchers.add(new RuleChainsEdgeEventFetcher(ctx.getRuleChainService()));
fetchers.add(new AdminSettingsEdgeEventFetcher(ctx.getAdminSettingsService(), ctx.getFreemarkerConfig()));
fetchers.add(new DeviceProfilesEdgeEventFetcher(ctx.getDeviceProfileService()));
fetchers.add(new AssetProfilesEdgeEventFetcher(ctx.getAssetProfileService()));
fetchers.add(new TenantAdminUsersEdgeEventFetcher(ctx.getUserService()));
if (edge.getCustomerId() != null && !EntityId.NULL_UUID.equals(edge.getCustomerId().getId())) {
fetchers.add(new CustomerEdgeEventFetcher());

4
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetMsgConstructor.java

@ -42,6 +42,10 @@ public class AssetMsgConstructor {
builder.setCustomerIdMSB(customerId.getId().getMostSignificantBits());
builder.setCustomerIdLSB(customerId.getId().getLeastSignificantBits());
}
if (asset.getAssetProfileId() != null) {
builder.setAssetProfileIdMSB(asset.getAssetProfileId().getId().getMostSignificantBits());
builder.setAssetProfileIdLSB(asset.getAssetProfileId().getId().getLeastSignificantBits());
}
if (asset.getAdditionalInfo() != null) {
builder.setAdditionalInfo(JacksonUtil.toString(asset.getAdditionalInfo()));
}

67
application/src/main/java/org/thingsboard/server/service/edge/rpc/constructor/AssetProfileMsgConstructor.java

@ -0,0 +1,67 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.constructor;
import com.google.protobuf.ByteString;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import java.nio.charset.StandardCharsets;
@Component
@TbCoreComponent
public class AssetProfileMsgConstructor {
@Autowired
private DataDecodingEncodingService dataDecodingEncodingService;
public AssetProfileUpdateMsg constructAssetProfileUpdatedMsg(UpdateMsgType msgType, AssetProfile assetProfile) {
AssetProfileUpdateMsg.Builder builder = AssetProfileUpdateMsg.newBuilder()
.setMsgType(msgType)
.setIdMSB(assetProfile.getId().getId().getMostSignificantBits())
.setIdLSB(assetProfile.getId().getId().getLeastSignificantBits())
.setName(assetProfile.getName())
.setDefault(assetProfile.isDefault());
if (assetProfile.getDefaultRuleChainId() != null) {
builder.setDefaultRuleChainIdMSB(assetProfile.getDefaultRuleChainId().getId().getMostSignificantBits())
.setDefaultRuleChainIdLSB(assetProfile.getDefaultRuleChainId().getId().getLeastSignificantBits());
}
if (assetProfile.getDefaultQueueName() != null) {
builder.setDefaultQueueName(assetProfile.getDefaultQueueName());
}
if (assetProfile.getDescription() != null) {
builder.setDescription(assetProfile.getDescription());
}
if (assetProfile.getImage() != null) {
builder.setImage(ByteString.copyFrom(assetProfile.getImage().getBytes(StandardCharsets.UTF_8)));
}
return builder.build();
}
public AssetProfileUpdateMsg constructAssetProfileDeleteMsg(AssetProfileId assetProfileId) {
return AssetProfileUpdateMsg.newBuilder()
.setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE)
.setIdMSB(assetProfileId.getId().getMostSignificantBits())
.setIdLSB(assetProfileId.getId().getLeastSignificantBits()).build();
}
}

47
application/src/main/java/org/thingsboard/server/service/edge/rpc/fetch/AssetProfilesEdgeEventFetcher.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.fetch;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.asset.AssetProfileService;
@AllArgsConstructor
@Slf4j
public class AssetProfilesEdgeEventFetcher extends BasePageableEdgeEventFetcher<AssetProfile> {
private final AssetProfileService assetProfileService;
@Override
PageData<AssetProfile> fetchPageData(TenantId tenantId, Edge edge, PageLink pageLink) {
return assetProfileService.findAssetProfiles(tenantId, pageLink);
}
@Override
EdgeEvent constructEdgeEvent(TenantId tenantId, Edge edge, AssetProfile assetProfile) {
return EdgeUtils.constructEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET_PROFILE,
EdgeEventActionType.ADDED, assetProfile.getId(), null);
}
}

63
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/AssetProfileEdgeProcessor.java

@ -0,0 +1,63 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.edge.rpc.processor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.UpdateMsgType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@Slf4j
@TbCoreComponent
public class AssetProfileEdgeProcessor extends BaseEdgeProcessor {
public DownlinkMsg processAssetProfileToEdge(EdgeEvent edgeEvent, UpdateMsgType msgType, EdgeEventActionType action) {
AssetProfileId assetProfileId = new AssetProfileId(edgeEvent.getEntityId());
DownlinkMsg downlinkMsg = null;
switch (action) {
case ADDED:
case UPDATED:
AssetProfile assetProfile = assetProfileService.findAssetProfileById(edgeEvent.getTenantId(), assetProfileId);
if (assetProfile != null) {
AssetProfileUpdateMsg assetProfileUpdateMsg =
assetProfileMsgConstructor.constructAssetProfileUpdatedMsg(msgType, assetProfile);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAssetProfileUpdateMsg(assetProfileUpdateMsg)
.build();
}
break;
case DELETED:
AssetProfileUpdateMsg assetProfileUpdateMsg =
assetProfileMsgConstructor.constructAssetProfileDeleteMsg(assetProfileId);
downlinkMsg = DownlinkMsg.newBuilder()
.setDownlinkMsgId(EdgeUtils.nextPositiveInt())
.addAssetProfileUpdateMsg(assetProfileUpdateMsg)
.build();
break;
}
return downlinkMsg;
}
}

20
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/BaseEdgeProcessor.java

@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
@ -61,6 +62,7 @@ import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.service.edge.rpc.constructor.AdminSettingsMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AlarmMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AssetMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.AssetProfileMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.CustomerMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.DashboardMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.DeviceMsgConstructor;
@ -74,9 +76,12 @@ import org.thingsboard.server.service.edge.rpc.constructor.RuleChainMsgConstruct
import org.thingsboard.server.service.edge.rpc.constructor.UserMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.WidgetTypeMsgConstructor;
import org.thingsboard.server.service.edge.rpc.constructor.WidgetsBundleMsgConstructor;
import org.thingsboard.server.service.entitiy.TbNotificationEntityService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import java.util.ArrayList;
import java.util.List;
@ -88,6 +93,12 @@ public abstract class BaseEdgeProcessor {
protected static final int DEFAULT_PAGE_SIZE = 1000;
@Autowired
protected TelemetrySubscriptionService tsSubService;
@Autowired
protected TbNotificationEntityService notificationEntityService;
@Autowired
protected RuleChainService ruleChainService;
@ -100,6 +111,9 @@ public abstract class BaseEdgeProcessor {
@Autowired
protected TbDeviceProfileCache deviceProfileCache;
@Autowired
protected TbAssetProfileCache assetProfileCache;
@Autowired
protected DashboardService dashboardService;
@ -124,6 +138,9 @@ public abstract class BaseEdgeProcessor {
@Autowired
protected DeviceProfileService deviceProfileService;
@Autowired
protected AssetProfileService assetProfileService;
@Autowired
protected RelationService relationService;
@ -197,6 +214,9 @@ public abstract class BaseEdgeProcessor {
@Autowired
protected DeviceProfileMsgConstructor deviceProfileMsgConstructor;
@Autowired
protected AssetProfileMsgConstructor assetProfileMsgConstructor;
@Autowired
protected WidgetsBundleMsgConstructor widgetsBundleMsgConstructor;

62
application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/TelemetryEdgeProcessor.java

@ -35,6 +35,8 @@ import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetId;
@ -44,7 +46,6 @@ import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
@ -57,6 +58,7 @@ import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.session.SessionMsgType;
import org.thingsboard.server.common.transport.adaptor.JsonConverter;
import org.thingsboard.server.common.transport.util.JsonUtils;
import org.thingsboard.server.controller.BaseController;
import org.thingsboard.server.gen.edge.v1.AttributeDeleteMsg;
import org.thingsboard.server.gen.edge.v1.DownlinkMsg;
import org.thingsboard.server.gen.edge.v1.EntityDataProto;
@ -101,7 +103,7 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor {
}
if (entityData.hasAttributesUpdatedMsg()) {
metaData.putValue("scope", entityData.getPostAttributeScope());
result.add(processAttributesUpdate(tenantId, customerId, entityId, entityData.getAttributesUpdatedMsg(), metaData));
result.add(processAttributesUpdate(tenantId, entityId, entityData.getAttributesUpdatedMsg(), metaData));
}
if (entityData.hasPostTelemetryMsg()) {
result.add(processPostTelemetry(tenantId, customerId, entityId, entityData.getPostTelemetryMsg(), metaData));
@ -159,23 +161,26 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor {
}
private Pair<String, RuleChainId> getDefaultQueueNameAndRuleChainId(TenantId tenantId, EntityId entityId) {
RuleChainId ruleChainId = null;
String queueName = null;
if (EntityType.DEVICE.equals(entityId.getEntityType())) {
DeviceProfile deviceProfile = deviceProfileCache.get(tenantId, new DeviceId(entityId.getId()));
RuleChainId ruleChainId;
String queueName;
if (deviceProfile == null) {
log.warn("[{}] Device profile is null!", entityId);
ruleChainId = null;
queueName = null;
} else {
ruleChainId = deviceProfile.getDefaultRuleChainId();
queueName = deviceProfile.getDefaultQueueName();
}
return new ImmutablePair<>(queueName, ruleChainId);
} else {
return new ImmutablePair<>(null, null);
} else if (EntityType.ASSET.equals(entityId.getEntityType())) {
AssetProfile assetProfile = assetProfileCache.get(tenantId, new AssetId(entityId.getId()));
if (assetProfile == null) {
log.warn("[{}] Asset profile is null!", entityId);
} else {
ruleChainId = assetProfile.getDefaultRuleChainId();
queueName = assetProfile.getDefaultQueueName();
}
}
return new ImmutablePair<>(queueName, ruleChainId);
}
private ListenableFuture<Void> processPostTelemetry(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostTelemetryMsg msg, TbMsgMetaData metaData) {
@ -221,39 +226,36 @@ public class TelemetryEdgeProcessor extends BaseEdgeProcessor {
return futureToSet;
}
private ListenableFuture<Void> processAttributesUpdate(TenantId tenantId, CustomerId customerId, EntityId entityId, TransportProtos.PostAttributeMsg msg, TbMsgMetaData metaData) {
private ListenableFuture<Void> processAttributesUpdate(TenantId tenantId,
EntityId entityId,
TransportProtos.PostAttributeMsg msg,
TbMsgMetaData metaData) {
SettableFuture<Void> futureToSet = SettableFuture.create();
JsonObject json = JsonUtils.getJsonObject(msg.getKvList());
Set<AttributeKvEntry> attributes = JsonConverter.convertToAttributes(json);
ListenableFuture<List<String>> future = attributesService.save(tenantId, entityId, metaData.getValue("scope"), new ArrayList<>(attributes));
Futures.addCallback(future, new FutureCallback<>() {
List<AttributeKvEntry> attributes = new ArrayList<>(JsonConverter.convertToAttributes(json));
String scope = metaData.getValue("scope");
tsSubService.saveAndNotify(tenantId, entityId, scope, attributes, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable List<String> keys) {
var defaultQueueAndRuleChain = getDefaultQueueNameAndRuleChainId(tenantId, entityId);
TbMsg tbMsg = TbMsg.newMsg(defaultQueueAndRuleChain.getKey(), DataConstants.ATTRIBUTES_UPDATED, entityId, customerId, metaData, gson.toJson(json), defaultQueueAndRuleChain.getValue(), null);
tbClusterService.pushMsgToRuleEngine(tenantId, tbMsg.getOriginator(), tbMsg, new TbQueueCallback() {
@Override
public void onSuccess(TbQueueMsgMetadata metadata) {
futureToSet.set(null);
}
@Override
public void onFailure(Throwable t) {
log.error("Can't process attributes update [{}]", msg, t);
futureToSet.setException(t);
}
});
public void onSuccess(@Nullable Void tmp) {
logAttributesUpdated(tenantId, entityId, scope, attributes, null);
futureToSet.set(null);
}
@Override
public void onFailure(Throwable t) {
log.error("Can't process attributes update [{}]", msg, t);
logAttributesUpdated(tenantId, entityId, scope, attributes, t);
futureToSet.setException(t);
}
}, dbCallbackExecutorService);
});
return futureToSet;
}
private void logAttributesUpdated(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) {
notificationEntityService.logEntityAction(tenantId, entityId, ActionType.ATTRIBUTES_UPDATED, null,
BaseController.toException(e), scope, attributes);
}
private ListenableFuture<Void> processAttributeDeleteMsg(TenantId tenantId, EntityId entityId, AttributeDeleteMsg attributeDeleteMsg, String entityType) {
SettableFuture<Void> futureToSet = SettableFuture.create();
String scope = attributeDeleteMsg.getScope();

50
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/DefaultEdgeRequestsService.java

@ -33,10 +33,13 @@ import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EdgeUtils;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEvent;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
@ -57,6 +60,8 @@ import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.common.data.relation.RelationsSearchParameters;
import org.thingsboard.server.common.data.widget.WidgetType;
import org.thingsboard.server.common.data.widget.WidgetsBundle;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
@ -64,6 +69,7 @@ import org.thingsboard.server.dao.edge.EdgeEventService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
import org.thingsboard.server.gen.edge.v1.AssetProfileAssetsRequestMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileDevicesRequestMsg;
@ -104,6 +110,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
@Autowired
private DeviceService deviceService;
@Autowired
private AssetService assetService;
@Lazy
@Autowired
private TbEntityViewService entityViewService;
@ -111,6 +120,9 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private AssetProfileService assetProfileService;
@Autowired
private WidgetsBundleService widgetsBundleService;
@ -351,6 +363,44 @@ public class DefaultEdgeRequestsService implements EdgeRequestsService {
return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService);
}
@Override
public ListenableFuture<Void> processAssetProfileAssetsRequestMsg(TenantId tenantId, Edge edge, AssetProfileAssetsRequestMsg assetProfileAssetsRequestMsg) {
log.trace("[{}] processAssetProfileAssetsRequestMsg [{}][{}]", tenantId, edge.getName(), assetProfileAssetsRequestMsg);
if (assetProfileAssetsRequestMsg.getAssetProfileIdMSB() == 0 || assetProfileAssetsRequestMsg.getAssetProfileIdLSB() == 0) {
return Futures.immediateFuture(null);
}
AssetProfileId assetProfileId = new AssetProfileId(new UUID(assetProfileAssetsRequestMsg.getAssetProfileIdMSB(), assetProfileAssetsRequestMsg.getAssetProfileIdLSB()));
AssetProfile assetProfileById = assetProfileService.findAssetProfileById(tenantId, assetProfileId);
if (assetProfileById == null) {
return Futures.immediateFuture(null);
}
return syncAssets(tenantId, edge, assetProfileById.getName());
}
private ListenableFuture<Void> syncAssets(TenantId tenantId, Edge edge, String assetType) {
log.trace("[{}] syncAssets [{}][{}]", tenantId, edge.getName(), assetType);
List<ListenableFuture<Void>> futures = new ArrayList<>();
try {
PageLink pageLink = new PageLink(DEFAULT_PAGE_SIZE);
PageData<Asset> pageData;
do {
pageData = assetService.findAssetsByTenantIdAndEdgeIdAndType(tenantId, edge.getId(), assetType, pageLink);
if (pageData != null && pageData.getData() != null && !pageData.getData().isEmpty()) {
log.trace("[{}] [{}] asset(s) are going to be pushed to edge.", edge.getId(), pageData.getData().size());
for (Asset asset : pageData.getData()) {
futures.add(saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.ASSET, EdgeEventActionType.ADDED, asset.getId(), null));
}
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
}
}
} while (pageData != null && pageData.hasNext());
} catch (Exception e) {
log.error("Exception during loading edge asset(s) on sync!", e);
}
return Futures.transform(Futures.allAsList(futures), voids -> null, dbCallbackExecutorService);
}
@Override
public ListenableFuture<Void> processWidgetBundleTypesRequestMsg(TenantId tenantId, Edge edge,
WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg) {

3
application/src/main/java/org/thingsboard/server/service/edge/rpc/sync/EdgeRequestsService.java

@ -18,6 +18,7 @@ package org.thingsboard.server.service.edge.rpc.sync;
import com.google.common.util.concurrent.ListenableFuture;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.edge.v1.AssetProfileAssetsRequestMsg;
import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileDevicesRequestMsg;
@ -41,6 +42,8 @@ public interface EdgeRequestsService {
ListenableFuture<Void> processDeviceProfileDevicesRequestMsg(TenantId tenantId, Edge edge, DeviceProfileDevicesRequestMsg deviceProfileDevicesRequestMsg);
ListenableFuture<Void> processAssetProfileAssetsRequestMsg(TenantId tenantId, Edge edge, AssetProfileAssetsRequestMsg assetProfileAssetsRequestMsg);
ListenableFuture<Void> processWidgetBundleTypesRequestMsg(TenantId tenantId, Edge edge, WidgetBundleTypesRequestMsg widgetBundleTypesRequestMsg);
ListenableFuture<Void> processEntityViewsRequestMsg(TenantId tenantId, Edge edge, EntityViewsRequestMsg entityViewsRequestMsg);

110
application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/DefaultTbAssetProfileService.java

@ -0,0 +1,110 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.entitiy.asset.profile;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import static org.thingsboard.server.dao.asset.BaseAssetService.TB_SERVICE_QUEUE;
@Service
@TbCoreComponent
@AllArgsConstructor
@Slf4j
public class DefaultTbAssetProfileService extends AbstractTbEntityService implements TbAssetProfileService {
private final AssetProfileService assetProfileService;
@Override
public AssetProfile save(AssetProfile assetProfile, User user) throws Exception {
ActionType actionType = assetProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = assetProfile.getTenantId();
try {
if (TB_SERVICE_QUEUE.equals(assetProfile.getName())) {
throw new ThingsboardException("Unable to save asset profile with name " + TB_SERVICE_QUEUE, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
} else if (assetProfile.getId() != null) {
AssetProfile foundAssetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfile.getId());
if (foundAssetProfile != null && TB_SERVICE_QUEUE.equals(foundAssetProfile.getName())) {
throw new ThingsboardException("Updating asset profile with name " + TB_SERVICE_QUEUE + " is prohibited!", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
AssetProfile savedAssetProfile = checkNotNull(assetProfileService.saveAssetProfile(assetProfile));
autoCommit(user, savedAssetProfile.getId());
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedAssetProfile.getId(),
actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedAssetProfile.getId(),
savedAssetProfile, user, actionType, true, null);
return savedAssetProfile;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), assetProfile, actionType, user, e);
throw e;
}
}
@Override
public void delete(AssetProfile assetProfile, User user) {
AssetProfileId assetProfileId = assetProfile.getId();
TenantId tenantId = assetProfile.getTenantId();
try {
assetProfileService.deleteAssetProfile(tenantId, assetProfileId);
tbClusterService.broadcastEntityStateChangeEvent(tenantId, assetProfileId, ComponentLifecycleEvent.DELETED);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, assetProfileId, assetProfile,
user, ActionType.DELETED, true, null, assetProfileId.toString());
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), ActionType.DELETED,
user, e, assetProfileId.toString());
throw e;
}
}
@Override
public AssetProfile setDefaultAssetProfile(AssetProfile assetProfile, AssetProfile previousDefaultAssetProfile, User user) throws ThingsboardException {
TenantId tenantId = assetProfile.getTenantId();
AssetProfileId assetProfileId = assetProfile.getId();
try {
if (assetProfileService.setDefaultAssetProfile(tenantId, assetProfileId)) {
if (previousDefaultAssetProfile != null) {
previousDefaultAssetProfile = assetProfileService.findAssetProfileById(tenantId, previousDefaultAssetProfile.getId());
notificationEntityService.logEntityAction(tenantId, previousDefaultAssetProfile.getId(), previousDefaultAssetProfile,
ActionType.UPDATED, user);
}
assetProfile = assetProfileService.findAssetProfileById(tenantId, assetProfileId);
notificationEntityService.logEntityAction(tenantId, assetProfileId, assetProfile, ActionType.UPDATED, user);
}
return assetProfile;
} catch (Exception e) {
notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ASSET_PROFILE), ActionType.UPDATED,
user, e, assetProfileId.toString());
throw e;
}
}
}

26
application/src/main/java/org/thingsboard/server/service/entitiy/asset/profile/TbAssetProfileService.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.entitiy.asset.profile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
public interface TbAssetProfileService extends SimpleTbEntityService<AssetProfile> {
AssetProfile setDefaultAssetProfile(AssetProfile assetProfile, AssetProfile previousDefaultAssetProfile, User user) throws ThingsboardException;
}

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

@ -15,8 +15,6 @@
*/
package org.thingsboard.server.service.install;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -25,10 +23,7 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@ -37,8 +32,8 @@ import org.thingsboard.server.common.data.queue.ProcessingStrategyType;
import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.data.queue.SubmitStrategy;
import org.thingsboard.server.common.data.queue.SubmitStrategyType;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
@ -61,11 +56,8 @@ import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO;
import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;
@ -117,9 +109,15 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
@Autowired
private DeviceService deviceService;
@Autowired
private AssetService assetService;
@Autowired
private DeviceProfileService deviceProfileService;
@Autowired
private AssetProfileService assetProfileService;
@Autowired
private ApiUsageStateService apiUsageStateService;
@ -609,11 +607,63 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.error("Failed updating schema!!!", e);
}
break;
case "3.4.1":
try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
log.info("Updating schema ...");
runSchemaUpdateScript(conn, "3.4.1");
if (isOldSchema(conn, 3004001)) {
try {
conn.createStatement().execute("ALTER TABLE asset ADD COLUMN asset_profile_id uuid");
} catch (Exception e) {
}
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.1", "schema_update_before.sql");
loadSql(schemaUpdateFile, conn);
log.info("Creating default asset profiles...");
PageLink pageLink = new PageLink(100);
PageData<Tenant> pageData;
do {
pageData = tenantService.findTenants(pageLink);
for (Tenant tenant : pageData.getData()) {
List<EntitySubtype> assetTypes = assetService.findAssetTypesByTenantId(tenant.getId()).get();
try {
assetProfileService.createDefaultAssetProfile(tenant.getId());
} catch (Exception e) {
}
for (EntitySubtype assetType : assetTypes) {
try {
assetProfileService.findOrCreateAssetProfile(tenant.getId(), assetType.getType());
} catch (Exception e) {
}
}
}
pageLink = pageLink.nextPageLink();
} while (pageData.hasNext());
log.info("Updating asset profiles...");
conn.createStatement().execute("call update_asset_profiles()");
schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.4.1", "schema_update_after.sql");
loadSql(schemaUpdateFile, conn);
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3004002;");
}
log.info("Schema updated.");
} catch (Exception e) {
log.error("Failed updating schema!!!", e);
}
break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
}
private void runSchemaUpdateScript(Connection connection, String version) throws Exception {
Path schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", version, SCHEMA_UPDATE_SQL);
loadSql(schemaUpdateFile, connection);
}
private void loadSql(Path sqlFile, Connection conn) throws Exception {
String sql = new String(Files.readAllBytes(sqlFile), Charset.forName("UTF-8"));
Statement st = conn.createStatement();

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

@ -63,6 +63,7 @@ import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileQueueConfiguration;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.alarm.AlarmDao;
import org.thingsboard.server.dao.audit.AuditLogDao;
import org.thingsboard.server.dao.entity.EntityService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.event.EventService;
@ -138,6 +139,9 @@ public class DefaultDataUpdateService implements DataUpdateService {
@Autowired
private EventService eventService;
@Autowired
private AuditLogDao auditLogDao;
@Override
public void updateData(String fromVersion) throws Exception {
switch (fromVersion) {
@ -170,12 +174,22 @@ public class DefaultDataUpdateService implements DataUpdateService {
rateLimitsUpdater.updateEntities();
break;
case "3.4.0":
String skipEventsMigration = System.getenv("TB_SKIP_EVENTS_MIGRATION");
if (skipEventsMigration == null || skipEventsMigration.equalsIgnoreCase("false")) {
boolean skipEventsMigration = getEnv("TB_SKIP_EVENTS_MIGRATION", false);
if (!skipEventsMigration) {
log.info("Updating data from version 3.4.0 to 3.4.1 ...");
eventService.migrateEvents();
}
break;
case "3.4.1":
boolean skipAuditLogsMigration = getEnv("TB_SKIP_AUDIT_LOGS_MIGRATION", false);
if (!skipAuditLogsMigration) {
log.info("Updating data from version 3.4.1 to 3.4.2 ...");
log.info("Starting audit logs migration. Can be skipped with TB_SKIP_AUDIT_LOGS_MIGRATION env variable set to true");
auditLogDao.migrateAuditLogs();
} else {
log.info("Skipping audit logs migration");
}
break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
@ -645,4 +659,13 @@ public class DefaultDataUpdateService implements DataUpdateService {
return mainQueueConfiguration;
}
private boolean getEnv(String name, boolean defaultValue) {
String env = System.getenv(name);
if (env == null) {
return defaultValue;
} else {
return Boolean.parseBoolean(env);
}
}
}

162
application/src/main/java/org/thingsboard/server/service/profile/DefaultTbAssetProfileCache.java

@ -0,0 +1,162 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.profile;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
@Service
@Slf4j
public class DefaultTbAssetProfileCache implements TbAssetProfileCache {
private final Lock assetProfileFetchLock = new ReentrantLock();
private final AssetProfileService assetProfileService;
private final AssetService assetService;
private final ConcurrentMap<AssetProfileId, AssetProfile> assetProfilesMap = new ConcurrentHashMap<>();
private final ConcurrentMap<AssetId, AssetProfileId> assetsMap = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, ConcurrentMap<EntityId, Consumer<AssetProfile>>> profileListeners = new ConcurrentHashMap<>();
private final ConcurrentMap<TenantId, ConcurrentMap<EntityId, BiConsumer<AssetId, AssetProfile>>> assetProfileListeners = new ConcurrentHashMap<>();
public DefaultTbAssetProfileCache(AssetProfileService assetProfileService, AssetService assetService) {
this.assetProfileService = assetProfileService;
this.assetService = assetService;
}
@Override
public AssetProfile get(TenantId tenantId, AssetProfileId assetProfileId) {
AssetProfile profile = assetProfilesMap.get(assetProfileId);
if (profile == null) {
assetProfileFetchLock.lock();
try {
profile = assetProfilesMap.get(assetProfileId);
if (profile == null) {
profile = assetProfileService.findAssetProfileById(tenantId, assetProfileId);
if (profile != null) {
assetProfilesMap.put(assetProfileId, profile);
log.debug("[{}] Fetch asset profile into cache: {}", profile.getId(), profile);
}
}
} finally {
assetProfileFetchLock.unlock();
}
}
log.trace("[{}] Found asset profile in cache: {}", assetProfileId, profile);
return profile;
}
@Override
public AssetProfile get(TenantId tenantId, AssetId assetId) {
AssetProfileId profileId = assetsMap.get(assetId);
if (profileId == null) {
Asset asset = assetService.findAssetById(tenantId, assetId);
if (asset != null) {
profileId = asset.getAssetProfileId();
assetsMap.put(assetId, profileId);
} else {
return null;
}
}
return get(tenantId, profileId);
}
@Override
public void evict(TenantId tenantId, AssetProfileId profileId) {
AssetProfile oldProfile = assetProfilesMap.remove(profileId);
log.debug("[{}] evict asset profile from cache: {}", profileId, oldProfile);
AssetProfile newProfile = get(tenantId, profileId);
if (newProfile != null) {
notifyProfileListeners(newProfile);
}
}
@Override
public void evict(TenantId tenantId, AssetId assetId) {
AssetProfileId old = assetsMap.remove(assetId);
if (old != null) {
AssetProfile newProfile = get(tenantId, assetId);
if (newProfile == null || !old.equals(newProfile.getId())) {
notifyAssetListeners(tenantId, assetId, newProfile);
}
}
}
@Override
public void addListener(TenantId tenantId, EntityId listenerId,
Consumer<AssetProfile> profileListener,
BiConsumer<AssetId, AssetProfile> assetListener) {
if (profileListener != null) {
profileListeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, profileListener);
}
if (assetListener != null) {
assetProfileListeners.computeIfAbsent(tenantId, id -> new ConcurrentHashMap<>()).put(listenerId, assetListener);
}
}
@Override
public AssetProfile find(AssetProfileId assetProfileId) {
return assetProfileService.findAssetProfileById(TenantId.SYS_TENANT_ID, assetProfileId);
}
@Override
public AssetProfile findOrCreateAssetProfile(TenantId tenantId, String profileName) {
return assetProfileService.findOrCreateAssetProfile(tenantId, profileName);
}
@Override
public void removeListener(TenantId tenantId, EntityId listenerId) {
ConcurrentMap<EntityId, Consumer<AssetProfile>> tenantListeners = profileListeners.get(tenantId);
if (tenantListeners != null) {
tenantListeners.remove(listenerId);
}
ConcurrentMap<EntityId, BiConsumer<AssetId, AssetProfile>> assetListeners = assetProfileListeners.get(tenantId);
if (assetListeners != null) {
assetListeners.remove(listenerId);
}
}
private void notifyProfileListeners(AssetProfile profile) {
ConcurrentMap<EntityId, Consumer<AssetProfile>> tenantListeners = profileListeners.get(profile.getTenantId());
if (tenantListeners != null) {
tenantListeners.forEach((id, listener) -> listener.accept(profile));
}
}
private void notifyAssetListeners(TenantId tenantId, AssetId assetId, AssetProfile profile) {
if (profile != null) {
ConcurrentMap<EntityId, BiConsumer<AssetId, AssetProfile>> tenantListeners = assetProfileListeners.get(tenantId);
if (tenantListeners != null) {
tenantListeners.forEach((id, listener) -> listener.accept(assetId, profile));
}
}
}
}

33
application/src/main/java/org/thingsboard/server/service/profile/TbAssetProfileCache.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.profile;
import org.thingsboard.rule.engine.api.RuleEngineAssetProfileCache;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId;
public interface TbAssetProfileCache extends RuleEngineAssetProfileCache {
void evict(TenantId tenantId, AssetProfileId id);
void evict(TenantId tenantId, AssetId id);
AssetProfile find(AssetProfileId assetProfileId);
AssetProfile findOrCreateAssetProfile(TenantId tenantId, String assetType);
}

76
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java

@ -35,13 +35,15 @@ import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.TbResource;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.edge.EdgeEventType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.QueueId;
import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@ -49,11 +51,12 @@ import org.thingsboard.server.common.data.queue.Queue;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg;
import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.FromDeviceRPCResponseProto;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreMsg;
@ -68,11 +71,12 @@ import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.NotificationsTopicService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.gateway_device.GatewayNotificationsService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import java.util.HashSet;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
@ -110,6 +114,7 @@ public class DefaultTbClusterService implements TbClusterService {
private final NotificationsTopicService notificationsTopicService;
private final DataDecodingEncodingService encodingService;
private final TbDeviceProfileCache deviceProfileCache;
private final TbAssetProfileCache assetProfileCache;
private final GatewayNotificationsService gatewayNotificationsService;
@Override
@ -167,7 +172,7 @@ public class DefaultTbClusterService implements TbClusterService {
@Override
public void pushMsgToRuleEngine(TenantId tenantId, EntityId entityId, TbMsg tbMsg, TbQueueCallback callback) {
if (tenantId.isNullUid()) {
if (tenantId == null || tenantId.isNullUid()) {
if (entityId.getEntityType().equals(EntityType.TENANT)) {
tenantId = TenantId.fromUUID(entityId.getId());
} else {
@ -179,6 +184,10 @@ public class DefaultTbClusterService implements TbClusterService {
tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceId(entityId.getId())));
} else if (entityId.getEntityType().equals(EntityType.DEVICE_PROFILE)) {
tbMsg = transformMsg(tbMsg, deviceProfileCache.get(tenantId, new DeviceProfileId(entityId.getId())));
} else if (entityId.getEntityType().equals(EntityType.ASSET)) {
tbMsg = transformMsg(tbMsg, assetProfileCache.get(tenantId, new AssetId(entityId.getId())));
} else if (entityId.getEntityType().equals(EntityType.ASSET_PROFILE)) {
tbMsg = transformMsg(tbMsg, assetProfileCache.get(tenantId, new AssetProfileId(entityId.getId())));
}
}
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tbMsg.getQueueName(), tenantId, entityId);
@ -195,16 +204,30 @@ public class DefaultTbClusterService implements TbClusterService {
if (deviceProfile != null) {
RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId();
String targetQueueName = deviceProfile.getDefaultQueueName();
boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId());
boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName());
if (isRuleChainTransform && isQueueTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName);
} else if (isRuleChainTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId);
} else if (isQueueTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName);
}
tbMsg = transformMsg(tbMsg, targetRuleChainId, targetQueueName);
}
return tbMsg;
}
private TbMsg transformMsg(TbMsg tbMsg, AssetProfile assetProfile) {
if (assetProfile != null) {
RuleChainId targetRuleChainId = assetProfile.getDefaultRuleChainId();
String targetQueueName = assetProfile.getDefaultQueueName();
tbMsg = transformMsg(tbMsg, targetRuleChainId, targetQueueName);
}
return tbMsg;
}
private TbMsg transformMsg(TbMsg tbMsg, RuleChainId targetRuleChainId, String targetQueueName) {
boolean isRuleChainTransform = targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId());
boolean isQueueTransform = targetQueueName != null && !targetQueueName.equals(tbMsg.getQueueName());
if (isRuleChainTransform && isQueueTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId, targetQueueName);
} else if (isRuleChainTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId);
} else if (isQueueTransform) {
tbMsg = TbMsg.transformMsg(tbMsg, targetQueueName);
}
return tbMsg;
}
@ -351,12 +374,32 @@ public class DefaultTbClusterService implements TbClusterService {
log.trace("[{}] Processing edge {} event update ", tenantId, edgeId);
EdgeEventUpdateMsg msg = new EdgeEventUpdateMsg(tenantId, edgeId);
byte[] msgBytes = encodingService.encode(msg);
ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setEdgeEventUpdateMsg(ByteString.copyFrom(msgBytes)).build();
pushEdgeSyncMsgToCore(edgeId, toCoreMsg);
}
@Override
public void pushEdgeSyncRequestToCore(ToEdgeSyncRequest toEdgeSyncRequest) {
log.trace("[{}] Processing edge sync request {} ", toEdgeSyncRequest.getTenantId(), toEdgeSyncRequest);
byte[] msgBytes = encodingService.encode(toEdgeSyncRequest);
ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToEdgeSyncRequestMsg(ByteString.copyFrom(msgBytes)).build();
pushEdgeSyncMsgToCore(toEdgeSyncRequest.getEdgeId(), toCoreMsg);
}
@Override
public void pushEdgeSyncResponseToCore(FromEdgeSyncResponse fromEdgeSyncResponse) {
log.trace("[{}] Processing edge sync response {}", fromEdgeSyncResponse.getTenantId(), fromEdgeSyncResponse);
byte[] msgBytes = encodingService.encode(fromEdgeSyncResponse);
ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setFromEdgeSyncResponseMsg(ByteString.copyFrom(msgBytes)).build();
pushEdgeSyncMsgToCore(fromEdgeSyncResponse.getEdgeId(), toCoreMsg);
}
private void pushEdgeSyncMsgToCore(EdgeId edgeId, ToCoreNotificationMsg toCoreMsg) {
TbQueueProducer<TbProtoQueueMsg<ToCoreNotificationMsg>> toCoreNfProducer = producerProvider.getTbCoreNotificationsMsgProducer();
Set<String> tbCoreServices = partitionService.getAllServiceIds(ServiceType.TB_CORE);
for (String serviceId : tbCoreServices) {
TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, serviceId);
ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setEdgeEventUpdateMsg(ByteString.copyFrom(msgBytes)).build();
toCoreNfProducer.send(tpi, new TbProtoQueueMsg<>(msg.getEdgeId().getId(), toCoreMsg), null);
toCoreNfProducer.send(tpi, new TbProtoQueueMsg<>(edgeId.getId(), toCoreMsg), null);
toCoreNfs.incrementAndGet();
}
}
@ -369,6 +412,7 @@ public class DefaultTbClusterService implements TbClusterService {
if (entityType.equals(EntityType.TENANT)
|| entityType.equals(EntityType.TENANT_PROFILE)
|| entityType.equals(EntityType.DEVICE_PROFILE)
|| entityType.equals(EntityType.ASSET_PROFILE)
|| entityType.equals(EntityType.API_USAGE_STATE)
|| (entityType.equals(EntityType.DEVICE) && msg.getEvent() == ComponentLifecycleEvent.UPDATED)
|| entityType.equals(EntityType.ENTITY_VIEW)

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

@ -20,8 +20,6 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
@ -67,6 +65,7 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.IdMsgPair;
@ -76,7 +75,6 @@ import org.thingsboard.server.service.state.DeviceStateService;
import org.thingsboard.server.service.subscription.SubscriptionManagerService;
import org.thingsboard.server.service.subscription.TbLocalSubscriptionService;
import org.thingsboard.server.service.subscription.TbSubscriptionUtils;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.sync.vc.GitVersionControlQueueService;
import org.thingsboard.server.service.transport.msg.TransportToDeviceActorMsgWrapper;
@ -138,6 +136,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
TbCoreDeviceRpcService tbCoreDeviceRpcService,
StatsFactory statsFactory,
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbApiUsageStateService statsService,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
@ -145,7 +144,7 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
OtaPackageStateService firmwareStateService,
GitVersionControlQueueService vcQueueService,
PartitionService partitionService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer());
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbCoreQueueFactory.createToCoreNotificationsMsgConsumer());
this.mainConsumer = tbCoreQueueFactory.createToCoreMsgConsumer();
this.usageStatsConsumer = tbCoreQueueFactory.createToUsageStatsServiceMsgConsumer();
this.firmwareStatesConsumer = tbCoreQueueFactory.createToOtaPackageStateServiceMsgConsumer();
@ -317,13 +316,12 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
} else if (toCoreNotification.getComponentLifecycleMsg() != null && !toCoreNotification.getComponentLifecycleMsg().isEmpty()) {
handleComponentLifecycleMsg(id, toCoreNotification.getComponentLifecycleMsg());
callback.onSuccess();
} else if (toCoreNotification.getEdgeEventUpdateMsg() != null && !toCoreNotification.getEdgeEventUpdateMsg().isEmpty()) {
Optional<TbActorMsg> actorMsg = encodingService.decode(toCoreNotification.getEdgeEventUpdateMsg().toByteArray());
if (actorMsg.isPresent()) {
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
actorContext.tellWithHighPriority(actorMsg.get());
}
callback.onSuccess();
} else if (!toCoreNotification.getEdgeEventUpdateMsg().isEmpty()) {
forwardToAppActor(id, encodingService.decode(toCoreNotification.getEdgeEventUpdateMsg().toByteArray()), callback);
} else if (!toCoreNotification.getToEdgeSyncRequestMsg().isEmpty()) {
forwardToAppActor(id, encodingService.decode(toCoreNotification.getToEdgeSyncRequestMsg().toByteArray()), callback);
} else if (!toCoreNotification.getFromEdgeSyncResponseMsg().isEmpty()) {
forwardToAppActor(id, encodingService.decode(toCoreNotification.getFromEdgeSyncResponseMsg().toByteArray()), callback);
} else if (toCoreNotification.hasQueueUpdateMsg()) {
TransportProtos.QueueUpdateMsg queue = toCoreNotification.getQueueUpdateMsg();
partitionService.updateQueue(queue);
@ -552,6 +550,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService<ToCore
actorContext.tell(new TransportToDeviceActorMsgWrapper(toDeviceActorMsg, callback));
}
private void forwardToAppActor(UUID id, Optional<TbActorMsg> actorMsg, TbCallback callback) {
if (actorMsg.isPresent()) {
log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
actorContext.tell(actorMsg.get());
}
callback.onSuccess();
}
private void throwNotHandled(Object msg, TbCallback callback) {
log.warn("Message not handled: {}", msg);
callback.onFailure(new RuntimeException("Message not handled!"));

4
application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java

@ -51,6 +51,7 @@ import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.queue.processing.TbRuleEngineProcessingDecision;
@ -121,10 +122,11 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbRuleEngineDeviceRpcService tbDeviceRpcService,
StatsFactory statsFactory,
TbDeviceProfileCache deviceProfileCache,
TbAssetProfileCache assetProfileCache,
TbTenantProfileCache tenantProfileCache,
TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbServiceInfoProvider serviceInfoProvider, QueueService queueService) {
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
super(actorContext, encodingService, tenantProfileCache, deviceProfileCache, assetProfileCache, apiUsageStateService, partitionService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
this.statisticsService = statisticsService;
this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;
this.submitStrategyFactory = submitStrategyFactory;

21
application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java

@ -18,11 +18,11 @@ package org.thingsboard.server.service.queue.processing;
import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.actors.ActorSystemContext;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -33,16 +33,17 @@ import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TbCallback;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.queue.discovery.TbApplicationEventListener;
import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent;
import org.thingsboard.server.queue.util.AfterStartUp;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import org.thingsboard.server.service.profile.TbAssetProfileCache;
import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.service.queue.TbPackCallback;
import org.thingsboard.server.service.queue.TbPackProcessingContext;
@ -70,6 +71,7 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
protected final DataDecodingEncodingService encodingService;
protected final TbTenantProfileCache tenantProfileCache;
protected final TbDeviceProfileCache deviceProfileCache;
protected final TbAssetProfileCache assetProfileCache;
protected final TbApiUsageStateService apiUsageStateService;
protected final PartitionService partitionService;
@ -77,12 +79,13 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbTenantProfileCache tenantProfileCache, TbDeviceProfileCache deviceProfileCache,
TbApiUsageStateService apiUsageStateService, PartitionService partitionService,
TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer) {
TbAssetProfileCache assetProfileCache, TbApiUsageStateService apiUsageStateService,
PartitionService partitionService, TbQueueConsumer<TbProtoQueueMsg<N>> nfConsumer) {
this.actorContext = actorContext;
this.encodingService = encodingService;
this.tenantProfileCache = tenantProfileCache;
this.deviceProfileCache = deviceProfileCache;
this.assetProfileCache = assetProfileCache;
this.apiUsageStateService = apiUsageStateService;
this.partitionService = partitionService;
this.nfConsumer = nfConsumer;
@ -180,6 +183,10 @@ public abstract class AbstractConsumerService<N extends com.google.protobuf.Gene
deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
deviceProfileCache.evict(componentLifecycleMsg.getTenantId(), new DeviceId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.ASSET_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(componentLifecycleMsg.getTenantId(), new AssetProfileId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.ASSET.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
assetProfileCache.evict(componentLifecycleMsg.getTenantId(), new AssetId(componentLifecycleMsg.getEntityId().getId()));
} else if (EntityType.ENTITY_VIEW.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
actorContext.getTbEntityViewService().onComponentLifecycleMsg(componentLifecycleMsg);
} else if (EntityType.API_USAGE_STATE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {

116
application/src/main/java/org/thingsboard/server/service/script/AbstractJsInvokeService.java

@ -15,9 +15,14 @@
*/
package org.thingsboard.server.service.script;
import com.google.common.hash.Hashing;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.util.Pair;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.id.CustomerId;
@ -30,20 +35,33 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import static java.lang.String.format;
/**
* Created by ashvayka on 26.09.18.
*/
@Slf4j
@SuppressWarnings("UnstableApiUsage")
public abstract class AbstractJsInvokeService implements JsInvokeService {
private final TbApiUsageStateService apiUsageStateService;
private final TbApiUsageClient apiUsageClient;
protected ScheduledExecutorService timeoutExecutorService;
protected Map<UUID, String> scriptIdToNameMap = new ConcurrentHashMap<>();
protected Map<UUID, DisableListInfo> disabledFunctions = new ConcurrentHashMap<>();
protected final Map<UUID, Pair<String, String>> scriptIdToNameAndHashMap = new ConcurrentHashMap<>();
protected final Map<UUID, DisableListInfo> disabledFunctions = new ConcurrentHashMap<>();
@Getter
@Value("${js.max_total_args_size:100000}")
private long maxTotalArgsSize;
@Getter
@Value("${js.max_result_size:300000}")
private long maxResultSize;
@Getter
@Value("${js.max_script_body_size:50000}")
private long maxScriptBodySize;
protected AbstractJsInvokeService(TbApiUsageStateService apiUsageStateService, TbApiUsageClient apiUsageClient) {
this.apiUsageStateService = apiUsageStateService;
@ -65,44 +83,63 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
@Override
public ListenableFuture<UUID> eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames) {
if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) {
if (scriptBodySizeExceeded(scriptBody)) {
return error(format("Script body exceeds maximum allowed size of %s symbols", getMaxScriptBodySize()));
}
UUID scriptId = UUID.randomUUID();
String functionName = "invokeInternal_" + scriptId.toString().replace('-', '_');
String scriptHash = hash(tenantId, scriptBody);
String functionName = constructFunctionName(scriptId, scriptHash);
String jsScript = generateJsScript(scriptType, functionName, scriptBody, argNames);
return doEval(scriptId, functionName, jsScript);
return doEval(scriptId, scriptHash, functionName, jsScript);
} else {
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
return error("JS Execution is disabled due to API limits!");
}
}
protected String constructFunctionName(UUID scriptId, String scriptHash) {
return "invokeInternal_" + scriptId.toString().replace('-', '_');
}
@Override
public ListenableFuture<Object> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
public ListenableFuture<String> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
if (apiUsageStateService.getApiUsageState(tenantId).isJsExecEnabled()) {
String functionName = scriptIdToNameMap.get(scriptId);
if (functionName == null) {
return Futures.immediateFailedFuture(new RuntimeException("No compiled script found for scriptId: [" + scriptId + "]!"));
Pair<String, String> nameAndHash = scriptIdToNameAndHashMap.get(scriptId);
if (nameAndHash == null) {
return error("No compiled script found for scriptId: [" + scriptId + "]!");
}
String functionName = nameAndHash.getFirst();
String scriptHash = nameAndHash.getSecond();
if (!isDisabled(scriptId)) {
if (argsSizeExceeded(args)) {
return scriptExecutionError(scriptId, format("Script input arguments exceed maximum allowed total args size of %s symbols", getMaxTotalArgsSize()));
}
apiUsageClient.report(tenantId, customerId, ApiUsageRecordKey.JS_EXEC_COUNT, 1);
return doInvokeFunction(scriptId, functionName, args);
return Futures.transformAsync(doInvokeFunction(scriptId, scriptHash, functionName, args), output -> {
String result = output.toString();
if (resultSizeExceeded(result)) {
return scriptExecutionError(scriptId, format("Script invocation result exceeds maximum allowed size of %s symbols", getMaxResultSize()));
}
return Futures.immediateFuture(result);
}, MoreExecutors.directExecutor());
} else {
String message = "Script invocation is blocked due to maximum error count "
+ getMaxErrors() + ", scriptId " + scriptId + "!";
log.warn(message);
return Futures.immediateFailedFuture(new RuntimeException(message));
return error(message);
}
} else {
return Futures.immediateFailedFuture(new RuntimeException("JS Execution is disabled due to API limits!"));
return error("JS Execution is disabled due to API limits!");
}
}
@Override
public ListenableFuture<Void> release(UUID scriptId) {
String functionName = scriptIdToNameMap.get(scriptId);
if (functionName != null) {
Pair<String, String> nameAndHash = scriptIdToNameAndHashMap.get(scriptId);
if (nameAndHash != null) {
try {
scriptIdToNameMap.remove(scriptId);
scriptIdToNameAndHashMap.remove(scriptId);
disabledFunctions.remove(scriptId);
doRelease(scriptId, functionName);
doRelease(scriptId, nameAndHash.getSecond(), nameAndHash.getFirst());
} catch (Exception e) {
return Futures.immediateFailedFuture(e);
}
@ -110,16 +147,24 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
return Futures.immediateFuture(null);
}
protected abstract ListenableFuture<UUID> doEval(UUID scriptId, String functionName, String scriptBody);
protected abstract ListenableFuture<UUID> doEval(UUID scriptId, String scriptHash, String functionName, String scriptBody);
protected abstract ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args);
protected abstract ListenableFuture<Object> doInvokeFunction(UUID scriptId, String scriptHash, String functionName, Object[] args);
protected abstract void doRelease(UUID scriptId, String functionName) throws Exception;
protected abstract void doRelease(UUID scriptId, String scriptHash, String functionName) throws Exception;
protected abstract int getMaxErrors();
protected abstract long getMaxBlacklistDuration();
protected String hash(TenantId tenantId, String scriptBody) {
return Hashing.murmur3_128().newHasher()
.putLong(tenantId.getId().getMostSignificantBits())
.putLong(tenantId.getId().getLeastSignificantBits())
.putUnencodedChars(scriptBody)
.hash().toString();
}
protected void onScriptExecutionError(UUID scriptId, Throwable t, String scriptBody) {
DisableListInfo disableListInfo = disabledFunctions.computeIfAbsent(scriptId, key -> new DisableListInfo());
log.warn("Script has exception and will increment counter {} on disabledFunctions for id {}, exception {}, cause {}, scriptBody {}",
@ -127,6 +172,27 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
disableListInfo.incrementAndGet();
}
private boolean scriptBodySizeExceeded(String scriptBody) {
if (getMaxScriptBodySize() <= 0) return false;
return scriptBody.length() > getMaxScriptBodySize();
}
private boolean argsSizeExceeded(Object[] args) {
if (getMaxTotalArgsSize() <= 0) return false;
long totalArgsSize = 0;
for (Object arg : args) {
if (arg instanceof CharSequence) {
totalArgsSize += ((CharSequence) arg).length();
}
}
return totalArgsSize > getMaxTotalArgsSize();
}
private boolean resultSizeExceeded(String result) {
if (getMaxResultSize() <= 0) return false;
return result.length() > getMaxResultSize();
}
private String generateJsScript(JsScriptType scriptType, String functionName, String scriptBody, String... argNames) {
if (scriptType == JsScriptType.RULE_NODE_SCRIPT) {
return RuleNodeScriptFactory.generateRuleNodeScript(functionName, scriptBody, argNames);
@ -148,6 +214,16 @@ public abstract class AbstractJsInvokeService implements JsInvokeService {
}
}
private <T> ListenableFuture<T> error(String message) {
return Futures.immediateFailedFuture(new RuntimeException(message));
}
private <T> ListenableFuture<T> scriptExecutionError(UUID scriptId, String errorMsg) {
RuntimeException error = new RuntimeException(errorMsg);
onScriptExecutionError(scriptId, error, null);
return Futures.immediateFailedFuture(error);
}
private class DisableListInfo {
private final AtomicInteger counter;
private long expirationTime;

10
application/src/main/java/org/thingsboard/server/service/script/AbstractNashornJsInvokeService.java

@ -24,6 +24,7 @@ import delight.nashornsandbox.NashornSandboxes;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.util.Pair;
import org.springframework.scheduling.annotation.Scheduled;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
@ -38,7 +39,6 @@ import javax.script.ScriptException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
@ -121,7 +121,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
protected abstract long getMaxCpuTime();
@Override
protected ListenableFuture<UUID> doEval(UUID scriptId, String functionName, String jsScript) {
protected ListenableFuture<UUID> doEval(UUID scriptId, String scriptHash, String functionName, String jsScript) {
jsPushedMsgs.incrementAndGet();
ListenableFuture<UUID> result = jsExecutor.executeAsync(() -> {
try {
@ -135,7 +135,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
} finally {
evalLock.unlock();
}
scriptIdToNameMap.put(scriptId, functionName);
scriptIdToNameAndHashMap.put(scriptId, Pair.of(functionName, scriptHash));
return scriptId;
} catch (Exception e) {
log.debug("Failed to compile JS script: {}", e.getMessage(), e);
@ -150,7 +150,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
}
@Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) {
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String scriptHash, String functionName, Object[] args) {
jsPushedMsgs.incrementAndGet();
ListenableFuture<Object> result = jsExecutor.executeAsync(() -> {
try {
@ -174,7 +174,7 @@ public abstract class AbstractNashornJsInvokeService extends AbstractJsInvokeSer
return result;
}
protected void doRelease(UUID scriptId, String functionName) throws ScriptException {
protected void doRelease(UUID scriptId, String scriptHash, String functionName) throws ScriptException {
if (useJsSandbox()) {
sandbox.eval(functionName + " = undefined;");
} else {

2
application/src/main/java/org/thingsboard/server/service/script/JsInvokeService.java

@ -25,7 +25,7 @@ public interface JsInvokeService {
ListenableFuture<UUID> eval(TenantId tenantId, JsScriptType scriptType, String scriptBody, String... argNames);
ListenableFuture<Object> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args);
ListenableFuture<String> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args);
ListenableFuture<Void> release(UUID scriptId);

19
application/src/main/java/org/thingsboard/server/service/script/NashornJsInvokeService.java

@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.script;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -32,12 +33,15 @@ public class NashornJsInvokeService extends AbstractNashornJsInvokeService {
@Value("${js.local.use_js_sandbox}")
private boolean useJsSandbox;
@Getter
@Value("${js.local.monitor_thread_pool_size}")
private int monitorThreadPoolSize;
@Getter
@Value("${js.local.max_cpu_time}")
private long maxCpuTime;
@Getter
@Value("${js.local.max_errors}")
private int maxErrors;
@ -53,21 +57,6 @@ public class NashornJsInvokeService extends AbstractNashornJsInvokeService {
return useJsSandbox;
}
@Override
protected int getMonitorThreadPoolSize() {
return monitorThreadPoolSize;
}
@Override
protected long getMaxCpuTime() {
return maxCpuTime;
}
@Override
protected int getMaxErrors() {
return maxErrors;
}
@Override
protected long getMaxBlacklistDuration() {
return TimeUnit.SECONDS.toMillis(maxBlackListDurationSec);

193
application/src/main/java/org/thingsboard/server/service/script/RemoteJsInvokeService.java

@ -18,16 +18,19 @@ package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.data.util.Pair;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StopWatch;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.gen.js.JsInvokeProtos.JsInvokeErrorCode;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.queue.common.TbProtoJsQueueMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
@ -45,6 +48,8 @@ import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@Slf4j
@ConditionalOnExpression("'${js.evaluator:null}'=='remote' && ('${service.type:null}'=='monolith' || '${service.type:null}'=='tb-core' || '${service.type:null}'=='tb-rule-engine')")
@ -98,9 +103,10 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
}
@Autowired
private TbQueueRequestTemplate<TbProtoJsQueueMsg<JsInvokeProtos.RemoteJsRequest>, TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> requestTemplate;
protected TbQueueRequestTemplate<TbProtoJsQueueMsg<JsInvokeProtos.RemoteJsRequest>, TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> requestTemplate;
private Map<UUID, String> scriptIdToBodysMap = new ConcurrentHashMap<>();
protected final Map<String, String> scriptHashToBodysMap = new ConcurrentHashMap<>();
private final Lock scriptsLock = new ReentrantLock();
@PostConstruct
public void init() {
@ -117,10 +123,9 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
}
@Override
protected ListenableFuture<UUID> doEval(UUID scriptId, String functionName, String scriptBody) {
protected ListenableFuture<UUID> doEval(UUID scriptId, String scriptHash, String functionName, String scriptBody) {
JsInvokeProtos.JsCompileRequest jsRequest = JsInvokeProtos.JsCompileRequest.newBuilder()
.setScriptIdMSB(scriptId.getMostSignificantBits())
.setScriptIdLSB(scriptId.getLeastSignificantBits())
.setScriptHash(scriptHash)
.setFunctionName(functionName)
.setScriptBody(scriptBody).build();
@ -128,74 +133,100 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
.setCompileRequest(jsRequest)
.build();
log.trace("Post compile request for scriptId [{}]", scriptId);
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
if (maxEvalRequestsTimeout > 0) {
future = Futures.withTimeout(future, maxEvalRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
}
queuePushedMsgs.incrementAndGet();
Futures.addCallback(future, new FutureCallback<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>>() {
@Override
public void onSuccess(@Nullable TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse> result) {
queueEvalMsgs.incrementAndGet();
}
@Override
public void onFailure(Throwable t) {
if (t instanceof TimeoutException || (t.getCause() != null && t.getCause() instanceof TimeoutException)) {
queueTimeoutMsgs.incrementAndGet();
}
queueFailedMsgs.incrementAndGet();
}
}, callbackExecutor);
log.trace("Post compile request for scriptId [{}] (hash: {})", scriptId, scriptHash);
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = sendJsRequest(UUID.randomUUID(), jsRequestWrapper, maxEvalRequestsTimeout, queueEvalMsgs);
return Futures.transform(future, response -> {
JsInvokeProtos.JsCompileResponse compilationResult = response.getValue().getCompileResponse();
UUID compiledScriptId = new UUID(compilationResult.getScriptIdMSB(), compilationResult.getScriptIdLSB());
if (compilationResult.getSuccess()) {
scriptIdToNameMap.put(scriptId, functionName);
scriptIdToBodysMap.put(scriptId, scriptBody);
return compiledScriptId;
scriptsLock.lock();
try {
scriptIdToNameAndHashMap.put(scriptId, Pair.of(functionName, scriptHash));
scriptHashToBodysMap.put(scriptHash, scriptBody);
} finally {
scriptsLock.unlock();
}
return scriptId;
} else {
log.debug("[{}] Failed to compile script due to [{}]: {}", compiledScriptId, compilationResult.getErrorCode().name(), compilationResult.getErrorDetails());
log.debug("[{}] (hash: {}) Failed to compile script due to [{}]: {}", scriptId, compilationResult.getScriptHash(),
compilationResult.getErrorCode().name(), compilationResult.getErrorDetails());
throw new RuntimeException(compilationResult.getErrorDetails());
}
}, callbackExecutor);
}
@Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String functionName, Object[] args) {
log.trace("doInvokeFunction js-request for uuid {} with timeout {}ms", scriptId, maxRequestsTimeout);
final String scriptBody = scriptIdToBodysMap.get(scriptId);
if (scriptBody == null) {
return Futures.immediateFailedFuture(new RuntimeException("No script body found for scriptId: [" + scriptId + "]!"));
}
JsInvokeProtos.JsInvokeRequest.Builder jsRequestBuilder = JsInvokeProtos.JsInvokeRequest.newBuilder()
.setScriptIdMSB(scriptId.getMostSignificantBits())
.setScriptIdLSB(scriptId.getLeastSignificantBits())
.setFunctionName(functionName)
.setTimeout((int) maxExecRequestsTimeout)
.setScriptBody(scriptBody);
protected String constructFunctionName(UUID scriptId, String scriptHash) {
return "invokeInternal_" + scriptHash;
}
for (Object arg : args) {
jsRequestBuilder.addArgs(arg.toString());
@Override
protected ListenableFuture<Object> doInvokeFunction(UUID scriptId, String scriptHash, String functionName, Object[] args) {
log.trace("doInvokeFunction js-request for uuid {} with timeout {}ms", scriptHash, maxRequestsTimeout);
String scriptBody = scriptHashToBodysMap.get(scriptHash);
if (scriptBody == null) {
return Futures.immediateFailedFuture(new RuntimeException("No script body found for script hash [" + scriptHash + "] (script id: [" + scriptId + "])"));
}
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder()
.setInvokeRequest(jsRequestBuilder.build())
.build();
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = buildJsInvokeRequest(scriptHash, functionName, args, false, null);
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(UUID.randomUUID(), jsRequestWrapper));
UUID requestKey = UUID.randomUUID();
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = sendJsRequest(requestKey, jsRequestWrapper, maxRequestsTimeout, queueInvokeMsgs);
return Futures.transformAsync(future, response -> {
stopWatch.stop();
log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey());
JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse();
if (invokeResult.getSuccess()) {
return Futures.immediateFuture(invokeResult.getResult());
} else {
return handleInvokeError(requestKey, scriptId, scriptHash, invokeResult.getErrorCode(),
invokeResult.getErrorDetails(), functionName, args, scriptBody);
}
}, callbackExecutor);
}
private ListenableFuture<Object> handleInvokeError(UUID requestKey, UUID scriptId, String scriptHash,
JsInvokeErrorCode errorCode, String errorDetails,
String functionName, Object[] args, String scriptBody) {
log.debug("[{}] Failed to invoke function due to [{}]: {}", scriptId, errorCode.name(), errorDetails);
RuntimeException e = new RuntimeException(errorDetails);
if (JsInvokeErrorCode.TIMEOUT_ERROR.equals(errorCode)) {
onScriptExecutionError(scriptId, e, scriptBody);
queueTimeoutMsgs.incrementAndGet();
} else if (JsInvokeErrorCode.COMPILATION_ERROR.equals(errorCode)) {
onScriptExecutionError(scriptId, e, scriptBody);
} else if (JsInvokeErrorCode.NOT_FOUND_ERROR.equals(errorCode)) {
log.debug("[{}] Remote JS executor couldn't find the script", scriptId);
if (scriptBody != null) {
JsInvokeProtos.RemoteJsRequest invokeRequestWithScriptBody = buildJsInvokeRequest(scriptHash, functionName, args, true, scriptBody);
log.debug("[{}] Sending invoke request again with script body", scriptId);
return Futures.transformAsync(sendJsRequest(requestKey, invokeRequestWithScriptBody, maxRequestsTimeout, queueInvokeMsgs), r -> {
JsInvokeProtos.JsInvokeResponse result = r.getValue().getInvokeResponse();
if (result.getSuccess()) {
return Futures.immediateFuture(result.getResult());
} else {
return handleInvokeError(requestKey, scriptId, scriptHash, result.getErrorCode(),
result.getErrorDetails(), functionName, args, null);
}
}, MoreExecutors.directExecutor());
}
}
queueFailedMsgs.incrementAndGet();
return Futures.immediateFailedFuture(e);
}
private ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> sendJsRequest(UUID requestKey, JsInvokeProtos.RemoteJsRequest jsRequestWrapper,
long maxRequestsTimeout, AtomicInteger msgsCounter) {
ListenableFuture<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>> future = requestTemplate.send(new TbProtoJsQueueMsg<>(requestKey, jsRequestWrapper));
if (maxRequestsTimeout > 0) {
future = Futures.withTimeout(future, maxRequestsTimeout, TimeUnit.MILLISECONDS, timeoutExecutorService);
}
queuePushedMsgs.incrementAndGet();
Futures.addCallback(future, new FutureCallback<TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse>>() {
Futures.addCallback(future, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable TbProtoQueueMsg<JsInvokeProtos.RemoteJsResponse> result) {
queueInvokeMsgs.incrementAndGet();
msgsCounter.incrementAndGet();
}
@Override
@ -206,32 +237,32 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
queueFailedMsgs.incrementAndGet();
}
}, callbackExecutor);
return Futures.transform(future, response -> {
stopWatch.stop();
log.trace("doInvokeFunction js-response took {}ms for uuid {}", stopWatch.getTotalTimeMillis(), response.getKey());
JsInvokeProtos.JsInvokeResponse invokeResult = response.getValue().getInvokeResponse();
if (invokeResult.getSuccess()) {
return invokeResult.getResult();
} else {
final RuntimeException e = new RuntimeException(invokeResult.getErrorDetails());
if (JsInvokeProtos.JsInvokeErrorCode.TIMEOUT_ERROR.equals(invokeResult.getErrorCode())) {
onScriptExecutionError(scriptId, e, scriptBody);
queueTimeoutMsgs.incrementAndGet();
} else if (JsInvokeProtos.JsInvokeErrorCode.COMPILATION_ERROR.equals(invokeResult.getErrorCode())) {
onScriptExecutionError(scriptId, e, scriptBody);
}
queueFailedMsgs.incrementAndGet();
log.debug("[{}] Failed to invoke function due to [{}]: {}", scriptId, invokeResult.getErrorCode().name(), invokeResult.getErrorDetails());
throw e;
}
}, callbackExecutor);
return future;
}
private JsInvokeProtos.RemoteJsRequest buildJsInvokeRequest(String scriptHash, String functionName, Object[] args, boolean includeScriptBody, String scriptBody) {
JsInvokeProtos.JsInvokeRequest.Builder jsRequestBuilder = JsInvokeProtos.JsInvokeRequest.newBuilder()
.setScriptHash(scriptHash)
.setFunctionName(functionName)
.setTimeout((int) maxExecRequestsTimeout);
if (includeScriptBody) jsRequestBuilder.setScriptBody(scriptBody);
for (Object arg : args) {
jsRequestBuilder.addArgs(arg.toString());
}
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder()
.setInvokeRequest(jsRequestBuilder.build())
.build();
return jsRequestWrapper;
}
@Override
protected void doRelease(UUID scriptId, String functionName) throws Exception {
protected void doRelease(UUID scriptId, String scriptHash, String functionName) throws Exception {
if (scriptIdToNameAndHashMap.values().stream().map(Pair::getSecond).anyMatch(hash -> hash.equals(scriptHash))) {
return;
}
JsInvokeProtos.JsReleaseRequest jsRequest = JsInvokeProtos.JsReleaseRequest.newBuilder()
.setScriptIdMSB(scriptId.getMostSignificantBits())
.setScriptIdLSB(scriptId.getLeastSignificantBits())
.setScriptHash(scriptHash)
.setFunctionName(functionName).build();
JsInvokeProtos.RemoteJsRequest jsRequestWrapper = JsInvokeProtos.RemoteJsRequest.newBuilder()
@ -244,12 +275,18 @@ public class RemoteJsInvokeService extends AbstractJsInvokeService {
}
JsInvokeProtos.RemoteJsResponse response = future.get().getValue();
JsInvokeProtos.JsReleaseResponse compilationResult = response.getReleaseResponse();
UUID compiledScriptId = new UUID(compilationResult.getScriptIdMSB(), compilationResult.getScriptIdLSB());
if (compilationResult.getSuccess()) {
scriptIdToBodysMap.remove(scriptId);
JsInvokeProtos.JsReleaseResponse releaseResponse = response.getReleaseResponse();
if (releaseResponse.getSuccess()) {
scriptsLock.lock();
try {
if (scriptIdToNameAndHashMap.values().stream().map(Pair::getSecond).noneMatch(h -> h.equals(scriptHash))) {
scriptHashToBodysMap.remove(scriptHash);
}
} finally {
scriptsLock.unlock();
}
} else {
log.debug("[{}] Failed to release script due", compiledScriptId);
log.debug("[{}] Failed to release script", scriptHash);
}
}

4
application/src/main/java/org/thingsboard/server/service/script/RuleNodeJsScriptEngine.java

@ -210,11 +210,11 @@ public class RuleNodeJsScriptEngine implements org.thingsboard.rule.engine.api.S
return executeScriptAsync(msg.getCustomerId(), inArgs[0], inArgs[1], inArgs[2]);
}
ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, Object... args) {
ListenableFuture<JsonNode> executeScriptAsync(CustomerId customerId, String... args) {
return Futures.transformAsync(sandboxService.invokeFunction(tenantId, customerId, this.scriptId, args),
o -> {
try {
return Futures.immediateFuture(mapper.readTree(o.toString()));
return Futures.immediateFuture(mapper.readTree(o));
} catch (Exception e) {
if (e.getCause() instanceof ScriptException) {
return Futures.immediateFailedFuture(e.getCause());

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

@ -35,10 +35,12 @@ import org.thingsboard.server.common.data.TbResourceInfo;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.ApiUsageStateId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -58,6 +60,7 @@ import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.controller.HttpValidationCallback;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceProfileService;
@ -113,6 +116,9 @@ public class AccessValidator {
@Autowired
protected DeviceProfileService deviceProfileService;
@Autowired
protected AssetProfileService assetProfileService;
@Autowired
protected AssetService assetService;
@ -211,6 +217,9 @@ public class AccessValidator {
case ASSET:
validateAsset(currentUser, operation, entityId, callback);
return;
case ASSET_PROFILE:
validateAssetProfile(currentUser, operation, entityId, callback);
return;
case RULE_CHAIN:
validateRuleChain(currentUser, operation, entityId, callback);
return;
@ -304,6 +313,24 @@ public class AccessValidator {
}
}
private void validateAssetProfile(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
} else {
AssetProfile assetProfile = assetProfileService.findAssetProfileById(currentUser.getTenantId(), new AssetProfileId(entityId.getId()));
if (assetProfile == null) {
callback.onSuccess(ValidationResult.entityNotFound("Asset profile with requested id wasn't found!"));
} else {
try {
accessControlService.checkPermission(currentUser, Resource.ASSET_PROFILE, operation, entityId, assetProfile);
} catch (ThingsboardException e) {
callback.onSuccess(ValidationResult.accessDenied(e.getMessage()));
}
callback.onSuccess(ValidationResult.ok(assetProfile));
}
}
}
private void validateApiUsageState(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback<ValidationResult> callback) {
if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));

5
application/src/main/java/org/thingsboard/server/service/security/permission/CustomerUserPermissions.java

@ -42,7 +42,8 @@ public class CustomerUserPermissions extends AbstractPermissions {
put(Resource.WIDGET_TYPE, widgetsPermissionChecker);
put(Resource.EDGE, customerEntityPermissionChecker);
put(Resource.RPC, rpcPermissionChecker);
put(Resource.DEVICE_PROFILE, deviceProfilePermissionChecker);
put(Resource.DEVICE_PROFILE, profilePermissionChecker);
put(Resource.ASSET_PROFILE, profilePermissionChecker);
}
private static final PermissionChecker customerAlarmPermissionChecker = new PermissionChecker() {
@ -154,7 +155,7 @@ public class CustomerUserPermissions extends AbstractPermissions {
}
};
private static final PermissionChecker deviceProfilePermissionChecker = new PermissionChecker.GenericPermissionChecker(Operation.READ) {
private static final PermissionChecker profilePermissionChecker = new PermissionChecker.GenericPermissionChecker(Operation.READ) {
@Override
@SuppressWarnings("unchecked")

1
application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java

@ -36,6 +36,7 @@ public enum Resource {
OAUTH2_CONFIGURATION_TEMPLATE(),
TENANT_PROFILE(EntityType.TENANT_PROFILE),
DEVICE_PROFILE(EntityType.DEVICE_PROFILE),
ASSET_PROFILE(EntityType.ASSET_PROFILE),
API_USAGE_STATE(EntityType.API_USAGE_STATE),
TB_RESOURCE(EntityType.TB_RESOURCE),
OTA_PACKAGE(EntityType.OTA_PACKAGE),

1
application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java

@ -41,6 +41,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker);
put(Resource.WIDGET_TYPE, widgetsPermissionChecker);
put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker);
put(Resource.ASSET_PROFILE, tenantEntityPermissionChecker);
put(Resource.API_USAGE_STATE, tenantEntityPermissionChecker);
put(Resource.TB_RESOURCE, tbResourcePermissionChecker);
put(Resource.OTA_PACKAGE, tenantEntityPermissionChecker);

247
application/src/main/java/org/thingsboard/server/service/subscription/DefaultTbEntityDataSubscriptionService.java

@ -22,7 +22,6 @@ import com.google.common.util.concurrent.MoreExecutors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
@ -30,13 +29,13 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.CloseStatus;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.query.AlarmDataQuery;
import org.thingsboard.server.common.data.query.ComparisonTsValue;
import org.thingsboard.server.common.data.query.EntityData;
import org.thingsboard.server.common.data.query.EntityDataQuery;
import org.thingsboard.server.common.data.query.EntityKey;
@ -51,6 +50,9 @@ import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketService;
import org.thingsboard.server.service.telemetry.TelemetryWebSocketSessionRef;
import org.thingsboard.server.service.telemetry.cmd.v2.AggHistoryCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AggKey;
import org.thingsboard.server.service.telemetry.cmd.v2.AggTimeSeriesCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataCmd;
import org.thingsboard.server.service.telemetry.cmd.v2.AlarmDataUpdate;
import org.thingsboard.server.service.telemetry.cmd.v2.EntityCountCmd;
@ -68,12 +70,12 @@ import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -83,6 +85,7 @@ import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@SuppressWarnings("UnstableApiUsage")
@Slf4j
@TbCoreComponent
@Service
@ -131,6 +134,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
private int maxEntitiesPerAlarmSubscription;
@Value("${server.ws.dynamic_page_link.max_alarm_queries_per_refresh_interval:10}")
private int maxAlarmQueriesPerRefreshInterval;
@Value("${ui.dashboard.max_datapoints_limit:50000}")
private int maxDatapointLimit;
private ExecutorService wsCallBackExecutor;
private boolean tsInSqlDB;
@ -165,7 +170,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
TbEntityDataSubCtx ctx = getSubCtx(session.getSessionId(), cmd.getCmdId());
if (ctx != null) {
log.debug("[{}][{}] Updating existing subscriptions using: {}", session.getSessionId(), cmd.getCmdId(), cmd);
if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null || cmd.getHistoryCmd() != null) {
if (cmd.hasAnyCmd()) {
ctx.clearEntitySubscriptions();
}
} else {
@ -173,6 +178,8 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
ctx = createSubCtx(session, cmd);
}
ctx.setCurrentCmd(cmd);
// Fetch entity list using entity data query
if (cmd.getQuery() != null) {
if (ctx.getQuery() == null) {
log.debug("[{}][{}] Initializing data using query: {}", session.getSessionId(), cmd.getCmdId(), cmd.getQuery());
@ -204,43 +211,143 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
finalCtx.setRefreshTask(task);
}
}
ListenableFuture<TbEntityDataSubCtx> historyFuture;
if (cmd.getHistoryCmd() != null) {
log.trace("[{}][{}] Going to process history command: {}", session.getSessionId(), cmd.getCmdId(), cmd.getHistoryCmd());
try {
historyFuture = handleHistoryCmd(ctx, cmd.getHistoryCmd());
} catch (RuntimeException e) {
handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd);
return;
try {
List<ListenableFuture<?>> cmdFutures = new ArrayList<>();
if (cmd.getAggHistoryCmd() != null) {
cmdFutures.add(handleAggHistoryCmd(ctx, cmd.getAggHistoryCmd()));
}
} else {
historyFuture = Futures.immediateFuture(ctx);
if (cmd.getAggTsCmd() != null) {
cmdFutures.add(handleAggTsCmd(ctx, cmd.getAggTsCmd()));
}
if (cmd.getHistoryCmd() != null) {
cmdFutures.add(handleHistoryCmd(ctx, cmd.getHistoryCmd()));
}
if (cmdFutures.isEmpty()) {
handleRegularCommands(ctx, cmd);
} else {
TbEntityDataSubCtx finalCtx = ctx;
Futures.addCallback(Futures.allAsList(cmdFutures), new FutureCallback<>() {
@Override
public void onSuccess(@Nullable List<Object> result) {
handleRegularCommands(finalCtx, cmd);
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process command", finalCtx.getSessionId(), finalCtx.getCmdId());
}
}, wsCallBackExecutor);
}
} catch (RuntimeException e) {
handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd);
}
}
private void handleRegularCommands(TbEntityDataSubCtx ctx, EntityDataCmd cmd) {
try {
if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) {
if (cmd.getLatestCmd() != null) {
handleLatestCmd(ctx, cmd.getLatestCmd());
}
if (cmd.getTsCmd() != null) {
handleTimeSeriesCmd(ctx, cmd.getTsCmd());
}
} else {
checkAndSendInitialData(ctx);
}
} catch (RuntimeException e) {
handleWsCmdRuntimeException(ctx.getSessionId(), e, cmd);
}
Futures.addCallback(historyFuture, new FutureCallback<>() {
@Override
public void onSuccess(@Nullable TbEntityDataSubCtx theCtx) {
}
private void checkAndSendInitialData(@Nullable TbEntityDataSubCtx theCtx) {
if (!theCtx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription());
theCtx.sendWsMsg(update);
theCtx.setInitialDataSent(true);
}
}
private ListenableFuture<TbEntityDataSubCtx> handleAggHistoryCmd(TbEntityDataSubCtx ctx, AggHistoryCmd cmd) {
ConcurrentMap<Integer, ReadTsKvQueryInfo> queries = new ConcurrentHashMap<>();
for (AggKey key : cmd.getKeys()) {
if (key.getPreviousValueOnly() == null || !key.getPreviousValueOnly()) {
var query = new BaseReadTsKvQuery(key.getKey(), cmd.getStartTs(), cmd.getEndTs(), cmd.getEndTs() - cmd.getStartTs(), 1, key.getAgg());
queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, false));
}
if (key.getPreviousStartTs() != null && key.getPreviousEndTs() != null && key.getPreviousEndTs() >= key.getPreviousStartTs()) {
var query = new BaseReadTsKvQuery(key.getKey(), key.getPreviousStartTs(), key.getPreviousEndTs(), key.getPreviousEndTs() - key.getPreviousStartTs(), 1, key.getAgg());
queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, true));
}
}
return handleAggCmd(ctx, cmd.getKeys(), queries, cmd.getStartTs(), cmd.getEndTs(), false);
}
private ListenableFuture<TbEntityDataSubCtx> handleAggTsCmd(TbEntityDataSubCtx ctx, AggTimeSeriesCmd cmd) {
ConcurrentMap<Integer, ReadTsKvQueryInfo> queries = new ConcurrentHashMap<>();
for (AggKey key : cmd.getKeys()) {
var query = new BaseReadTsKvQuery(key.getKey(), cmd.getStartTs(), cmd.getStartTs() + cmd.getTimeWindow(), cmd.getTimeWindow(), 1, key.getAgg());
queries.put(query.getId(), new ReadTsKvQueryInfo(key, query, false));
}
return handleAggCmd(ctx, cmd.getKeys(), queries, cmd.getStartTs(), cmd.getStartTs() + cmd.getTimeWindow(), true);
}
private ListenableFuture<TbEntityDataSubCtx> handleAggCmd(TbEntityDataSubCtx ctx, List<AggKey> keys, ConcurrentMap<Integer, ReadTsKvQueryInfo> queries,
long startTs, long endTs, boolean subscribe) {
Map<EntityData, ListenableFuture<List<ReadTsKvQueryResult>>> fetchResultMap = new HashMap<>();
List<EntityData> entityDataList = ctx.getData().getData();
List<ReadTsKvQuery> queryList = queries.values().stream().map(ReadTsKvQueryInfo::getQuery).collect(Collectors.toList());
entityDataList.forEach(entityData -> fetchResultMap.put(entityData,
tsService.findAllByQueries(ctx.getTenantId(), entityData.getEntityId(), queryList)));
return Futures.transform(Futures.allAsList(fetchResultMap.values()), f -> {
// Map that holds last ts for each key for each entity.
Map<EntityData, Map<String, Long>> lastTsEntityMap = new HashMap<>();
fetchResultMap.forEach((entityData, future) -> {
try {
if (cmd.getLatestCmd() != null || cmd.getTsCmd() != null) {
if (cmd.getLatestCmd() != null) {
handleLatestCmd(theCtx, cmd.getLatestCmd());
}
if (cmd.getTsCmd() != null) {
handleTimeSeriesCmd(theCtx, cmd.getTsCmd());
Map<String, Long> lastTsMap = new HashMap<>();
lastTsEntityMap.put(entityData, lastTsMap);
List<ReadTsKvQueryResult> queryResults = future.get();
if (queryResults != null) {
for (ReadTsKvQueryResult queryResult : queryResults) {
ReadTsKvQueryInfo queryInfo = queries.get(queryResult.getQueryId());
ComparisonTsValue comparisonTsValue = entityData.getAggLatest().computeIfAbsent(queryInfo.getKey().getId(), agg -> new ComparisonTsValue());
if (queryInfo.isPrevious()) {
comparisonTsValue.setPrevious(queryResult.toTsValue(queryInfo.getQuery()));
} else {
comparisonTsValue.setCurrent(queryResult.toTsValue(queryInfo.getQuery()));
lastTsMap.put(queryInfo.getQuery().getKey(), queryResult.getLastEntryTs());
}
}
} else if (!theCtx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(theCtx.getCmdId(), theCtx.getData(), null, theCtx.getMaxEntitiesPerDataSubscription());
theCtx.sendWsMsg(update);
theCtx.setInitialDataSent(true);
}
} catch (RuntimeException e) {
handleWsCmdRuntimeException(theCtx.getSessionId(), e, cmd);
// Populate with empty values if no data found.
keys.forEach(key -> {
entityData.getAggLatest().putIfAbsent(key.getId(), new ComparisonTsValue(TsValue.EMPTY, TsValue.EMPTY));
});
} catch (InterruptedException | ExecutionException e) {
log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), e);
ctx.sendWsMsg(new EntityDataUpdate(ctx.getCmdId(), SubscriptionErrorCode.INTERNAL_ERROR.getCode(), "Failed to fetch historical data!"));
}
});
ctx.getWsLock().lock();
try {
EntityDataUpdate update;
if (!ctx.isInitialDataSent()) {
update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription());
ctx.setInitialDataSent(true);
} else {
update = new EntityDataUpdate(ctx.getCmdId(), null, entityDataList, ctx.getMaxEntitiesPerDataSubscription());
}
if (subscribe) {
ctx.createTimeSeriesSubscriptions(lastTsEntityMap, startTs, endTs, true);
}
ctx.sendWsMsg(update);
entityDataList.forEach(EntityData::clearTsAndAggData);
} finally {
ctx.getWsLock().unlock();
}
@Override
public void onFailure(Throwable t) {
log.warn("[{}][{}] Failed to process command", session.getSessionId(), cmd.getCmdId());
}
return ctx;
}, wsCallBackExecutor);
}
@ -416,36 +523,58 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
}
private ListenableFuture<TbEntityDataSubCtx> handleGetTsCmd(TbEntityDataSubCtx ctx, GetTsCmd cmd, boolean subscribe) {
Map<Integer, String> queriesKeys = new ConcurrentHashMap<>();
List<String> keys = cmd.getKeys();
List<ReadTsKvQuery> finalTsKvQueryList;
List<ReadTsKvQuery> tsKvQueryList = cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery(
key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg()
)).collect(Collectors.toList());
List<ReadTsKvQuery> tsKvQueryList = keys.stream().map(key -> {
var query = new BaseReadTsKvQuery(
key, cmd.getStartTs(), cmd.getEndTs(), cmd.getInterval(), getLimit(cmd.getLimit()), cmd.getAgg()
);
queriesKeys.put(query.getId(), query.getKey());
return query;
}).collect(Collectors.toList());
if (cmd.isFetchLatestPreviousPoint()) {
finalTsKvQueryList = new ArrayList<>(tsKvQueryList);
finalTsKvQueryList.addAll(cmd.getKeys().stream().map(key -> new BaseReadTsKvQuery(
key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg()
)).collect(Collectors.toList()));
finalTsKvQueryList.addAll(keys.stream().map(key -> {
var query = new BaseReadTsKvQuery(
key, cmd.getStartTs() - TimeUnit.DAYS.toMillis(365), cmd.getStartTs(), cmd.getInterval(), 1, cmd.getAgg());
queriesKeys.put(query.getId(), query.getKey());
return query;
}
).collect(Collectors.toList()));
} else {
finalTsKvQueryList = tsKvQueryList;
}
Map<EntityData, ListenableFuture<List<TsKvEntry>>> fetchResultMap = new HashMap<>();
ctx.getData().getData().forEach(entityData -> fetchResultMap.put(entityData,
tsService.findAll(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList)));
Map<EntityData, ListenableFuture<List<ReadTsKvQueryResult>>> fetchResultMap = new HashMap<>();
List<EntityData> entityDataList = ctx.getData().getData();
entityDataList.forEach(entityData -> fetchResultMap.put(entityData,
tsService.findAllByQueries(ctx.getTenantId(), entityData.getEntityId(), finalTsKvQueryList)));
return Futures.transform(Futures.allAsList(fetchResultMap.values()), f -> {
// Map that holds last ts for each key for each entity.
Map<EntityData, Map<String, Long>> lastTsEntityMap = new HashMap<>();
fetchResultMap.forEach((entityData, future) -> {
Map<String, List<TsValue>> keyData = new LinkedHashMap<>();
cmd.getKeys().forEach(key -> keyData.put(key, new ArrayList<>()));
try {
List<TsKvEntry> entityTsData = future.get();
if (entityTsData != null) {
entityTsData.forEach(entry -> keyData.get(entry.getKey()).add(new TsValue(entry.getTs(), entry.getValueAsString())));
Map<String, Long> lastTsMap = new HashMap<>();
lastTsEntityMap.put(entityData, lastTsMap);
List<ReadTsKvQueryResult> queryResults = future.get();
if (queryResults != null) {
for (ReadTsKvQueryResult queryResult : queryResults) {
String queryKey = queriesKeys.get(queryResult.getQueryId());
entityData.getTimeseries().put(queryKey, queryResult.toTsValues());
lastTsMap.put(queryKey, queryResult.getLastEntryTs());
}
}
keyData.forEach((k, v) -> entityData.getTimeseries().put(k, v.toArray(new TsValue[v.size()])));
// Populate with empty values if no data found.
keys.forEach(key -> {
if (!entityData.getTimeseries().containsKey(key)) {
entityData.getTimeseries().put(key, new TsValue[0]);
}
});
if (cmd.isFetchLatestPreviousPoint()) {
entityData.getTimeseries().values().forEach(dataArray -> {
Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs()));
});
entityData.getTimeseries().values().forEach(dataArray -> Arrays.sort(dataArray, (o1, o2) -> Long.compare(o2.getTs(), o1.getTs())));
}
} catch (InterruptedException | ExecutionException e) {
log.warn("[{}][{}][{}] Failed to fetch historical data", ctx.getSessionId(), ctx.getCmdId(), entityData.getEntityId(), e);
@ -459,13 +588,13 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription());
ctx.setInitialDataSent(true);
} else {
update = new EntityDataUpdate(ctx.getCmdId(), null, ctx.getData().getData(), ctx.getMaxEntitiesPerDataSubscription());
update = new EntityDataUpdate(ctx.getCmdId(), null, entityDataList, ctx.getMaxEntitiesPerDataSubscription());
}
if (subscribe) {
ctx.createTimeseriesSubscriptions(keys.stream().map(key -> new EntityKey(EntityKeyType.TIME_SERIES, key)).collect(Collectors.toList()), cmd.getStartTs(), cmd.getEndTs());
ctx.createTimeSeriesSubscriptions(lastTsEntityMap, cmd.getStartTs(), cmd.getEndTs());
}
ctx.sendWsMsg(update);
ctx.getData().getData().forEach(ed -> ed.getTimeseries().clear());
entityDataList.forEach(EntityData::clearTsAndAggData);
} finally {
ctx.getWsLock().unlock();
}
@ -533,11 +662,7 @@ public class DefaultTbEntityDataSubscriptionService implements TbEntityDataSubsc
ctx.getWsLock().lock();
try {
ctx.createLatestValuesSubscriptions(latestCmd.getKeys());
if (!ctx.isInitialDataSent()) {
EntityDataUpdate update = new EntityDataUpdate(ctx.getCmdId(), ctx.getData(), null, ctx.getMaxEntitiesPerDataSubscription());
ctx.sendWsMsg(update);
ctx.setInitialDataSent(true);
}
checkAndSendInitialData(ctx);
} finally {
ctx.getWsLock().unlock();
}

29
application/src/main/java/org/thingsboard/server/service/subscription/ReadTsKvQueryInfo.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.subscription;
import lombok.Data;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.service.telemetry.cmd.v2.AggKey;
@Data
public class ReadTsKvQueryInfo {
private final AggKey key;
private final ReadTsKvQuery query;
private final boolean previous;
}

25
application/src/main/java/org/thingsboard/server/service/subscription/TbAbstractDataSubCtx.java

@ -86,7 +86,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
} else {
oldDataMap = Collections.emptyMap();
}
Map<EntityId, EntityData> newDataMap = newData.getData().stream().collect(Collectors.toMap(EntityData::getEntityId, Function.identity(), (a,b)-> a));
Map<EntityId, EntityData> newDataMap = newData.getData().stream().collect(Collectors.toMap(EntityData::getEntityId, Function.identity(), (a, b) -> a));
if (oldDataMap.size() == newDataMap.size() && oldDataMap.keySet().equals(newDataMap.keySet())) {
log.trace("[{}][{}] No updates to entity data found", sessionRef.getSessionId(), cmdId);
} else {
@ -122,8 +122,17 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
createSubscriptions(keys, true, 0, 0);
}
public void createTimeseriesSubscriptions(List<EntityKey> keys, long startTs, long endTs) {
createSubscriptions(keys, false, startTs, endTs);
public void createTimeSeriesSubscriptions(Map<EntityData, Map<String, Long>> entityKeyStates, long startTs, long endTs) {
createTimeSeriesSubscriptions(entityKeyStates, startTs, endTs, false);
}
public void createTimeSeriesSubscriptions(Map<EntityData, Map<String, Long>> entityKeyStates, long startTs, long endTs, boolean resultToLatestValues) {
entityKeyStates.forEach((entityData, keyStates) -> {
int subIdx = sessionRef.getSessionSubIdSeq().incrementAndGet();
subToEntityIdMap.put(subIdx, entityData.getEntityId());
localSubscriptionService.addSubscription(
createTsSub(entityData, subIdx, false, startTs, endTs, keyStates, resultToLatestValues));
});
}
private void createSubscriptions(List<EntityKey> keys, boolean latestValues, long startTs, long endTs) {
@ -191,6 +200,14 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
keyStates.put(k, ts);
});
}
return createTsSub(entityData, subIdx, latestValues, startTs, endTs, keyStates);
}
private TbTimeseriesSubscription createTsSub(EntityData entityData, int subIdx, boolean latestValues, long startTs, long endTs, Map<String, Long> keyStates) {
return createTsSub(entityData, subIdx, latestValues, startTs, endTs, keyStates, latestValues);
}
private TbTimeseriesSubscription createTsSub(EntityData entityData, int subIdx, boolean latestValues, long startTs, long endTs, Map<String, Long> keyStates, boolean resultToLatestValues) {
log.trace("[{}][{}][{}] Creating time-series subscription for [{}] with keys: {}", serviceId, cmdId, subIdx, entityData.getEntityId(), keyStates);
return TbTimeseriesSubscription.builder()
.serviceId(serviceId)
@ -198,7 +215,7 @@ public abstract class TbAbstractDataSubCtx<T extends AbstractDataQuery<? extends
.subscriptionId(subIdx)
.tenantId(sessionRef.getSecurityCtx().getTenantId())
.entityId(entityData.getEntityId())
.updateConsumer((sessionId, subscriptionUpdate) -> sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, latestValues))
.updateConsumer((sessionId, subscriptionUpdate) -> sendWsMsg(sessionId, subscriptionUpdate, EntityKeyType.TIME_SERIES, resultToLatestValues))
.allKeys(false)
.keyStates(keyStates)
.latestValues(latestValues)

2
application/src/main/java/org/thingsboard/server/service/sync/ie/DefaultEntitiesExportImportService.java

@ -64,7 +64,7 @@ public class DefaultEntitiesExportImportService implements EntitiesExportImportS
private final TbNotificationEntityService entityNotificationService;
protected static final List<EntityType> SUPPORTED_ENTITY_TYPES = List.of(
EntityType.CUSTOMER, EntityType.ASSET, EntityType.RULE_CHAIN,
EntityType.CUSTOMER, EntityType.ASSET_PROFILE, EntityType.ASSET, EntityType.RULE_CHAIN,
EntityType.DASHBOARD, EntityType.DEVICE_PROFILE, EntityType.DEVICE,
EntityType.ENTITY_VIEW, EntityType.WIDGETS_BUNDLE
);

7
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/DefaultExportableEntitiesService.java

@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.Dao;
import org.thingsboard.server.dao.ExportableEntityDao;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
@ -183,7 +185,7 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi
@Autowired
private void setRemovers(CustomerService customerService, AssetService assetService, RuleChainService ruleChainService,
DashboardService dashboardService, DeviceProfileService deviceProfileService,
DeviceService deviceService, WidgetsBundleService widgetsBundleService) {
AssetProfileService assetProfileService, DeviceService deviceService, WidgetsBundleService widgetsBundleService) {
removers.put(EntityType.CUSTOMER, (tenantId, entityId) -> {
customerService.deleteCustomer(tenantId, (CustomerId) entityId);
});
@ -199,6 +201,9 @@ public class DefaultExportableEntitiesService implements ExportableEntitiesServi
removers.put(EntityType.DEVICE_PROFILE, (tenantId, entityId) -> {
deviceProfileService.deleteDeviceProfile(tenantId, (DeviceProfileId) entityId);
});
removers.put(EntityType.ASSET_PROFILE, (tenantId, entityId) -> {
assetProfileService.deleteAssetProfile(tenantId, (AssetProfileId) entityId);
});
removers.put(EntityType.DEVICE, (tenantId, entityId) -> {
deviceService.deleteDevice(tenantId, (DeviceId) entityId);
});

1
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetExportService.java

@ -32,6 +32,7 @@ public class AssetExportService extends BaseEntityExportService<AssetId, Asset,
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, Asset asset, EntityExportData<Asset> exportData) {
asset.setCustomerId(getExternalIdOrElseInternal(ctx, asset.getCustomerId()));
asset.setAssetProfileId(getExternalIdOrElseInternal(ctx, asset.getAssetProfileId()));
}
@Override

43
application/src/main/java/org/thingsboard/server/service/sync/ie/exporting/impl/AssetProfileExportService.java

@ -0,0 +1,43 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.ie.exporting.impl;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesExportCtx;
import java.util.Set;
@Service
@TbCoreComponent
public class AssetProfileExportService extends BaseEntityExportService<AssetProfileId, AssetProfile, EntityExportData<AssetProfile>> {
@Override
protected void setRelatedEntities(EntitiesExportCtx<?> ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData) {
assetProfile.setDefaultDashboardId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultDashboardId()));
assetProfile.setDefaultRuleChainId(getExternalIdOrElseInternal(ctx, assetProfile.getDefaultRuleChainId()));
}
@Override
public Set<EntityType> getSupportedEntityTypes() {
return Set.of(EntityType.ASSET_PROFILE);
}
}

1
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetImportService.java

@ -41,6 +41,7 @@ public class AssetImportService extends BaseEntityImportService<AssetId, Asset,
@Override
protected Asset prepare(EntitiesImportCtx ctx, Asset asset, Asset old, EntityExportData<Asset> exportData, IdProvider idProvider) {
asset.setAssetProfileId(idProvider.getInternalId(asset.getAssetProfileId()));
return asset;
}

80
application/src/main/java/org/thingsboard/server/service/sync/ie/importing/impl/AssetProfileImportService.java

@ -0,0 +1,80 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.sync.ie.importing.impl;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.sync.ie.EntityExportData;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.sync.vc.data.EntitiesImportCtx;
@Service
@TbCoreComponent
@RequiredArgsConstructor
public class AssetProfileImportService extends BaseEntityImportService<AssetProfileId, AssetProfile, EntityExportData<AssetProfile>> {
private final AssetProfileService assetProfileService;
@Override
protected void setOwner(TenantId tenantId, AssetProfile assetProfile, IdProvider idProvider) {
assetProfile.setTenantId(tenantId);
}
@Override
protected AssetProfile prepare(EntitiesImportCtx ctx, AssetProfile assetProfile, AssetProfile old, EntityExportData<AssetProfile> exportData, IdProvider idProvider) {
assetProfile.setDefaultRuleChainId(idProvider.getInternalId(assetProfile.getDefaultRuleChainId()));
assetProfile.setDefaultDashboardId(idProvider.getInternalId(assetProfile.getDefaultDashboardId()));
return assetProfile;
}
@Override
protected AssetProfile saveOrUpdate(EntitiesImportCtx ctx, AssetProfile assetProfile, EntityExportData<AssetProfile> exportData, IdProvider idProvider) {
return assetProfileService.saveAssetProfile(assetProfile);
}
@Override
protected void onEntitySaved(User user, AssetProfile savedAssetProfile, AssetProfile oldAssetProfile) throws ThingsboardException {
clusterService.broadcastEntityStateChangeEvent(user.getTenantId(), savedAssetProfile.getId(),
oldAssetProfile == null ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
entityNotificationService.notifyCreateOrUpdateOrDelete(savedAssetProfile.getTenantId(), null,
savedAssetProfile.getId(), savedAssetProfile, user, oldAssetProfile == null ? ActionType.ADDED : ActionType.UPDATED, true, null);
}
@Override
protected AssetProfile deepCopy(AssetProfile assetProfile) {
return new AssetProfile(assetProfile);
}
@Override
protected void cleanupForComparison(AssetProfile assetProfile) {
super.cleanupForComparison(assetProfile);
}
@Override
public EntityType getEntityType() {
return EntityType.ASSET_PROFILE;
}
}

54
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java

@ -19,6 +19,7 @@ import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -115,6 +116,13 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
super.shutdownExecutor();
}
@Override
public ListenableFuture<Void> saveAndNotify(TenantId tenantId, EntityId entityId, TsKvEntry ts) {
SettableFuture<Void> future = SettableFuture.create();
saveAndNotify(tenantId, entityId, Collections.singletonList(ts), new VoidFutureCallback(future));
return future;
}
@Override
public void saveAndNotify(TenantId tenantId, EntityId entityId, List<TsKvEntry> ts, FutureCallback<Void> callback) {
saveAndNotify(tenantId, null, entityId, ts, 0L, callback);
@ -332,6 +340,34 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
, System.currentTimeMillis())), callback);
}
@Override
public ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, long value) {
SettableFuture<Void> future = SettableFuture.create();
saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future));
return future;
}
@Override
public ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, String value) {
SettableFuture<Void> future = SettableFuture.create();
saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future));
return future;
}
@Override
public ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, double value) {
SettableFuture<Void> future = SettableFuture.create();
saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future));
return future;
}
@Override
public ListenableFuture<Void> saveAttrAndNotify(TenantId tenantId, EntityId entityId, String scope, String key, boolean value) {
SettableFuture<Void> future = SettableFuture.create();
saveAttrAndNotify(tenantId, entityId, scope, key, value, new VoidFutureCallback(future));
return future;
}
private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes, boolean notifyDevice) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (currentPartitions.contains(tpi)) {
@ -436,4 +472,22 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
}
}
private static class VoidFutureCallback implements FutureCallback<Void> {
private final SettableFuture<Void> future;
public VoidFutureCallback(SettableFuture<Void> future) {
this.future = future;
}
@Override
public void onSuccess(Void result) {
future.set(null);
}
@Override
public void onFailure(Throwable t) {
future.setException(t);
}
}
}

2
application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java

@ -716,7 +716,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi
"Cmd id is negative value!");
sendWsMsg(sessionRef, update);
return false;
} else if (cmd.getQuery() == null && cmd.getLatestCmd() == null && cmd.getHistoryCmd() == null && cmd.getTsCmd() == null) {
} else if (cmd.getQuery() == null && !cmd.hasAnyCmd()) {
TelemetrySubscriptionUpdate update = new TelemetrySubscriptionUpdate(cmd.getCmdId(), SubscriptionErrorCode.BAD_REQUEST,
"Query is empty!");
sendWsMsg(sessionRef, update);

29
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggHistoryCmd.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import lombok.Data;
import java.util.List;
@Data
public class AggHistoryCmd {
private List<AggKey> keys;
private long startTs;
private long endTs;
}

32
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggKey.java

@ -0,0 +1,32 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import lombok.Data;
import org.thingsboard.server.common.data.kv.Aggregation;
@Data
public class AggKey {
private int id;
private String key;
private Aggregation agg;
private Long previousStartTs;
private Long previousEndTs;
private Boolean previousValueOnly;
}

29
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/AggTimeSeriesCmd.java

@ -0,0 +1,29 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.telemetry.cmd.v2;
import lombok.Data;
import java.util.List;
@Data
public class AggTimeSeriesCmd {
private List<AggKey> keys;
private long startTs;
private long timeWindow;
}

21
application/src/main/java/org/thingsboard/server/service/telemetry/cmd/v2/EntityDataCmd.java

@ -16,6 +16,7 @@
package org.thingsboard.server.service.telemetry.cmd.v2;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import org.thingsboard.server.common.data.query.EntityDataQuery;
@ -30,17 +31,35 @@ public class EntityDataCmd extends DataCmd {
private final LatestValueCmd latestCmd;
@Getter
private final TimeSeriesCmd tsCmd;
@Getter
private final AggHistoryCmd aggHistoryCmd;
@Getter
private final AggTimeSeriesCmd aggTsCmd;
public EntityDataCmd(int cmdId, EntityDataQuery query, EntityHistoryCmd historyCmd, LatestValueCmd latestCmd, TimeSeriesCmd tsCmd) {
this(cmdId, query, historyCmd, latestCmd, tsCmd, null, null);
}
@JsonCreator
public EntityDataCmd(@JsonProperty("cmdId") int cmdId,
@JsonProperty("query") EntityDataQuery query,
@JsonProperty("historyCmd") EntityHistoryCmd historyCmd,
@JsonProperty("latestCmd") LatestValueCmd latestCmd,
@JsonProperty("tsCmd") TimeSeriesCmd tsCmd) {
@JsonProperty("tsCmd") TimeSeriesCmd tsCmd,
@JsonProperty("aggHistoryCmd") AggHistoryCmd aggHistoryCmd,
@JsonProperty("aggTsCmd") AggTimeSeriesCmd aggTsCmd) {
super(cmdId);
this.query = query;
this.historyCmd = historyCmd;
this.latestCmd = latestCmd;
this.tsCmd = tsCmd;
this.aggHistoryCmd = aggHistoryCmd;
this.aggTsCmd = aggTsCmd;
}
@JsonIgnore
public boolean hasAnyCmd() {
return historyCmd != null || latestCmd != null || tsCmd != null || aggHistoryCmd != null || aggTsCmd != null;
}
}

61
application/src/main/java/org/thingsboard/server/service/ttl/AuditLogsCleanUpService.java

@ -0,0 +1,61 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.ttl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.server.dao.audit.AuditLogDao;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.queue.discovery.PartitionService;
import java.util.concurrent.TimeUnit;
import static org.thingsboard.server.dao.model.ModelConstants.AUDIT_LOG_COLUMN_FAMILY_NAME;
@Service
@ConditionalOnExpression("${sql.ttl.audit_logs.enabled:true} && ${sql.ttl.audit_logs.ttl:0} > 0")
@Slf4j
public class AuditLogsCleanUpService extends AbstractCleanUpService {
private final AuditLogDao auditLogDao;
private final SqlPartitioningRepository partitioningRepository;
@Value("${sql.ttl.audit_logs.ttl:0}")
private long ttlInSec;
@Value("${sql.audit_logs.partition_size:168}")
private int partitionSizeInHours;
public AuditLogsCleanUpService(PartitionService partitionService, AuditLogDao auditLogDao, SqlPartitioningRepository partitioningRepository) {
super(partitionService);
this.auditLogDao = auditLogDao;
this.partitioningRepository = partitioningRepository;
}
@Scheduled(initialDelayString = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.audit_logs.checking_interval_ms})}",
fixedDelayString = "${sql.ttl.audit_logs.checking_interval_ms}")
public void cleanUp() {
long auditLogsExpTime = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttlInSec);
if (isSystemTenantPartitionMine()) {
auditLogDao.cleanUpAuditLogs(auditLogsExpTime);
} else {
partitioningRepository.cleanupPartitionsCache(AUDIT_LOG_COLUMN_FAMILY_NAME, auditLogsExpTime, TimeUnit.HOURS.toMillis(partitionSizeInHours));
}
}
}

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

@ -265,6 +265,8 @@ sql:
batch_size: "${SQL_EDGE_EVENTS_BATCH_SIZE:1000}"
batch_max_delay: "${SQL_EDGE_EVENTS_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_EDGE_EVENTS_BATCH_STATS_PRINT_MS:10000}"
audit_logs:
partition_size: "${SQL_AUDIT_LOGS_PARTITION_SIZE_HOURS:168}" # Default value - 1 week
# Specify whether to sort entities before batch update. Should be enabled for cluster mode to avoid deadlocks
batch_sort: "${SQL_BATCH_SORT:false}"
# Specify whether to remove null characters from strValue of attributes and timeseries before insert
@ -272,6 +274,8 @@ sql:
# Specify whether to log database queries and their parameters generated by entity query repository
log_queries: "${SQL_LOG_QUERIES:false}"
log_queries_threshold: "${SQL_LOG_QUERIES_THRESHOLD:5000}"
log_tenant_stats: "${SQL_LOG_TENANT_STATS:true}"
log_tenant_stats_interval_ms: "${SQL_LOG_TENANT_STATS_INTERVAL_MS:60000}"
postgres:
# Specify partitioning size for timestamp key-value storage. Example: DAYS, MONTHS, YEARS, INDEFINITE.
ts_key_value_partitioning: "${SQL_POSTGRES_TS_KV_PARTITIONING:MONTHS}"
@ -301,6 +305,10 @@ sql:
rpc:
enabled: "${SQL_TTL_RPC_ENABLED:true}"
checking_interval: "${SQL_RPC_TTL_CHECKING_INTERVAL:7200000}" # Number of milliseconds. The current value corresponds to two hours
audit_logs:
enabled: "${SQL_TTL_AUDIT_LOGS_ENABLED:true}"
ttl: "${SQL_TTL_AUDIT_LOGS_SECS:0}" # Disabled by default. Accuracy of the cleanup depends on the sql.audit_logs.partition_size
checking_interval_ms: "${SQL_TTL_AUDIT_LOGS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day
relations:
max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # //This value has to be reasonable small to prevent infinite recursion as early as possible
@ -406,6 +414,9 @@ cache:
deviceProfiles:
timeToLiveInMinutes: "${CACHE_SPECS_DEVICE_PROFILES_TTL:1440}"
maxSize: "${CACHE_SPECS_DEVICE_PROFILES_MAX_SIZE:10000}"
assetProfiles:
timeToLiveInMinutes: "${CACHE_SPECS_ASSET_PROFILES_TTL:1440}"
maxSize: "${CACHE_SPECS_ASSET_PROFILES_MAX_SIZE:10000}"
attributes:
timeToLiveInMinutes: "${CACHE_SPECS_ATTRIBUTES_TTL:1440}"
maxSize: "${CACHE_SPECS_ATTRIBUTES_MAX_SIZE:100000}"
@ -561,6 +572,7 @@ audit-log:
"alarm": "${AUDIT_LOG_MASK_ALARM:W}"
"entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}"
"device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}"
"asset_profile": "${AUDIT_LOG_MASK_ASSET_PROFILE:W}"
"edge": "${AUDIT_LOG_MASK_EDGE:W}"
"tb_resource": "${AUDIT_LOG_MASK_RESOURCE:W}"
"ota_package": "${AUDIT_LOG_MASK_OTA_PACKAGE:W}"
@ -589,6 +601,9 @@ state:
js:
evaluator: "${JS_EVALUATOR:local}" # local/remote
max_total_args_size: "${JS_MAX_TOTAL_ARGS_SIZE:100000}"
max_result_size: "${JS_MAX_RESULT_SIZE:300000}"
max_script_body_size: "${JS_MAX_SCRIPT_BODY_SIZE:50000}"
# Built-in JVM JavaScript environment properties
local:
# Use Sandboxed (secured) JVM JavaScript environment
@ -710,6 +725,8 @@ transport:
dtls:
# Enable/disable DTLS 1.2 support
enabled: "${COAP_DTLS_ENABLED:false}"
# RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000
retransmission_timeout: "${COAP_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}"
# CoAP DTLS bind address
bind_address: "${COAP_DTLS_BIND_ADDRESS:0.0.0.0}"
# CoAP DTLS bind port
@ -747,6 +764,9 @@ transport:
lwm2m:
# Enable/disable lvm2m transport protocol.
enabled: "${LWM2M_ENABLED:true}"
dtls:
# RFC7925_RETRANSMISSION_TIMEOUT_IN_MILLISECONDS = 9000
retransmission_timeout: "${LWM2M_DTLS_RETRANSMISSION_TIMEOUT_MS:9000}"
server:
id: "${LWM2M_SERVER_ID:123}"
bind_address: "${LWM2M_BIND_ADDRESS:0.0.0.0}"

10
application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java

@ -89,11 +89,9 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
argument -> argument.getClass().equals(UserId.class) : argument -> argument.equals(userId);
testLogEntityActionAdditionalInfo(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime,
extractMatcherAdditionalInfo(additionalInfo));
testPushMsgToRuleEngineNever(relation.getTo());
matcherOriginatorId = argument -> argument.equals(relation.getFrom());
testLogEntityActionAdditionalInfo(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId, userName, actionType, cntTime,
extractMatcherAdditionalInfo(additionalInfo));
testPushMsgToRuleEngineNever(relation.getFrom());
Mockito.reset(tbClusterService, auditLogService);
}
@ -111,7 +109,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
argument -> argument.getClass().equals(UserId.class) : argument -> argument.equals(userId);
testLogEntityActionAdditionalInfoAny(matcherEntityClassEquals, matcherOriginatorId, tenantId, matcherCustomerId, matcherUserId,
userName, actionType, cntTime * 2, 1);
testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, new Tenant(), cntTime);
testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, new Tenant(), cntTime * 3);
Mockito.reset(tbClusterService, auditLogService);
}
@ -141,11 +139,7 @@ public abstract class AbstractNotifyEntityTest extends AbstractWebTest {
testNotificationMsgToEdgeServiceNeverWithActionType(entityId, actionType);
testLogEntityAction(entity, originatorId, tenantId, customerId, userId, userName, actionType, cntTime, additionalInfo);
ArgumentMatcher<EntityId> matcherOriginatorId = argument -> argument.equals(originatorId);
if (ActionType.RELATIONS_DELETED.equals(actionType)) {
testPushMsgToRuleEngineNever(originatorId);
} else {
testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime);
}
testPushMsgToRuleEngineTime(matcherOriginatorId, tenantId, entity, cntTime);
Mockito.reset(tbClusterService, auditLogService);
}

50
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -35,7 +35,7 @@ import org.junit.Rule;
import org.junit.rules.TestRule;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.mockito.BDDMockito;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
@ -59,6 +59,7 @@ import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
@ -131,8 +132,9 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected static final String DIFFERENT_CUSTOMER_USER_EMAIL = "testdifferentcustomer@thingsboard.org";
private static final String DIFFERENT_CUSTOMER_USER_PASSWORD = "diffcustomer";
/** See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)}
* and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()}
/**
* See {@link org.springframework.test.web.servlet.DefaultMvcResult#getAsyncResult(long)}
* and {@link org.springframework.mock.web.MockAsyncContext#getTimeout()}
*/
private static final long DEFAULT_TIMEOUT = -1L;
@ -448,6 +450,15 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return deviceProfile;
}
protected AssetProfile createAssetProfile(String name) {
AssetProfile assetProfile = new AssetProfile();
assetProfile.setName(name);
assetProfile.setDescription(name + " Test");
assetProfile.setDefault(false);
assetProfile.setDefaultRuleChainId(null);
return assetProfile;
}
protected MqttDeviceProfileTransportConfiguration createMqttDeviceProfileTransportConfiguration(TransportPayloadTypeConfiguration transportPayloadTypeConfiguration, boolean sendAckOnValidationException) {
MqttDeviceProfileTransportConfiguration mqttDeviceProfileTransportConfiguration = new MqttDeviceProfileTransportConfiguration();
mqttDeviceProfileTransportConfiguration.setDeviceTelemetryTopic(MqttTopics.DEVICE_TELEMETRY_TOPIC);
@ -722,23 +733,18 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected <T> void testEntityDaoWithRelationsTransactionalException(Dao<T> dao, EntityId entityIdFrom, EntityId entityTo,
String urlDelete) throws Exception {
entityDaoRemoveByIdWithException (dao);
createEntityRelation(entityIdFrom, entityTo, "TEST_TRANSACTIONAL_TYPE");
assertThat(findRelationsByTo(entityTo)).hasSize(1);
doDelete(urlDelete)
.andExpect(status().isInternalServerError());
assertThat(findRelationsByTo(entityTo)).hasSize(1);
}
Mockito.doThrow(new ConstraintViolationException("mock message", new SQLException(), "MOCK_CONSTRAINT")).when(dao).removeById(any(), any());
try {
createEntityRelation(entityIdFrom, entityTo, "TEST_TRANSACTIONAL_TYPE");
assertThat(findRelationsByTo(entityTo)).hasSize(1);
protected <T> void entityDaoRemoveByIdWithException (Dao<T> dao) throws Exception {
BDDMockito.willThrow(new ConstraintViolationException("mock message", new SQLException(), "MOCK_CONSTRAINT"))
.given(dao).removeById(any(), any());
}
doDelete(urlDelete)
.andExpect(status().isInternalServerError());
protected <T> void afterTestEntityDaoRemoveByIdWithException (Dao<T> dao) throws Exception {
BDDMockito.willCallRealMethod().given(dao).removeById(any(), any());
assertThat(findRelationsByTo(entityTo)).hasSize(1);
} finally {
Mockito.reset(dao);
}
}
protected void createEntityRelation(EntityId entityIdFrom, EntityId entityIdTo, String typeRelation) throws Exception {
@ -751,9 +757,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
MvcResult mvcResult = doGet(url).andReturn();
switch (mvcResult.getResponse().getStatus()) {
case 200: return readResponse(mvcResult, new TypeReference<>() {});
case 404: return Collections.emptyList();
case 200:
return readResponse(mvcResult, new TypeReference<>() {
});
case 404:
return Collections.emptyList();
}
throw new AssertionError("Unexpected status " + mvcResult.getResponse().getStatus());
}
}

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

@ -22,8 +22,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
@ -43,16 +47,24 @@ import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Slf4j
@ContextConfiguration(classes = {BaseAlarmControllerTest.Config.class})
public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
public static final String TEST_ALARM_TYPE = "Test";
protected Device customerDevice;
@SpyBean
@Autowired
private AlarmDao alarmDao;
static class Config {
@Bean
@Primary
public AlarmDao alarmDao(AlarmDao alarmDao) {
return Mockito.mock(AlarmDao.class, AdditionalAnswers.delegatesTo(alarmDao));
}
}
@Before
public void setup() throws Exception {
loginTenantAdmin();
@ -72,8 +84,6 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest {
public void teardown() throws Exception {
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException (alarmDao);
deleteDifferentTenant();
}

45
application/src/test/java/org/thingsboard/server/controller/BaseAssetControllerTest.java

@ -21,8 +21,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityView;
@ -30,6 +34,7 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.id.AssetId;
@ -50,6 +55,7 @@ import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@ContextConfiguration(classes = {BaseAssetControllerTest.Config.class})
public abstract class BaseAssetControllerTest extends AbstractControllerTest {
private IdComparator<Asset> idComparator = new IdComparator<>();
@ -57,9 +63,17 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest {
private Tenant savedTenant;
private User tenantAdmin;
@SpyBean
@Autowired
private AssetDao assetDao;
static class Config {
@Bean
@Primary
public AssetDao assetDao(AssetDao assetDao) {
return Mockito.mock(AssetDao.class, AdditionalAnswers.delegatesTo(assetDao));
}
}
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -83,8 +97,6 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest {
public void afterTest() throws Exception {
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException (assetDao);
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}
@ -187,6 +199,20 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest {
deleteDifferentTenant();
}
@Test
public void testSaveAssetWithProfileFromDifferentTenant() throws Exception {
loginDifferentTenant();
AssetProfile differentProfile = createAssetProfile("Different profile");
differentProfile = doPost("/api/assetProfile", differentProfile, AssetProfile.class);
loginTenantAdmin();
Asset asset = new Asset();
asset.setName("My device");
asset.setAssetProfileId(differentProfile.getId());
doPost("/api/asset", asset).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Asset can`t be referencing to asset profile from different tenant!")));
}
@Test
public void testFindAssetById() throws Exception {
Asset asset = new Asset();
@ -310,13 +336,12 @@ public abstract class BaseAssetControllerTest extends AbstractControllerTest {
Mockito.reset(tbClusterService, auditLogService);
String msgError = "Asset type " + msgErrorShouldBeSpecified;
doPost("/api/asset", asset)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));
Asset savedAsset = doPost("/api/asset", asset, Asset.class);
Assert.assertEquals("default", savedAsset.getType());
testNotifyEntityEqualsOneTimeServiceNeverError(asset, savedTenant.getId(),
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
testNotifyEntityOneTimeMsgToEdgeServiceNever(savedAsset, savedAsset.getId(), savedAsset.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ADDED);
}
@Test

463
application/src/test/java/org/thingsboard/server/controller/BaseAssetProfileControllerTest.java

@ -0,0 +1,463 @@
/**
* Copyright © 2016-2022 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.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.asset.AssetProfileInfo;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleChain;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.asset.AssetProfileDao;
import org.thingsboard.server.dao.exception.DataValidationException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ContextConfiguration(classes = {BaseAssetProfileControllerTest.Config.class})
public abstract class BaseAssetProfileControllerTest extends AbstractControllerTest {
private IdComparator<AssetProfile> idComparator = new IdComparator<>();
private IdComparator<AssetProfileInfo> assetProfileInfoIdComparator = new IdComparator<>();
private Tenant savedTenant;
private User tenantAdmin;
@Autowired
private AssetProfileDao assetProfileDao;
static class Config {
@Bean
@Primary
public AssetProfileDao assetProfileDao(AssetProfileDao assetProfileDao) {
return Mockito.mock(AssetProfileDao.class, AdditionalAnswers.delegatesTo(assetProfileDao));
}
}
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
Tenant tenant = new Tenant();
tenant.setTitle("My tenant");
savedTenant = doPost("/api/tenant", tenant, Tenant.class);
Assert.assertNotNull(savedTenant);
tenantAdmin = new User();
tenantAdmin.setAuthority(Authority.TENANT_ADMIN);
tenantAdmin.setTenantId(savedTenant.getId());
tenantAdmin.setEmail("tenant2@thingsboard.org");
tenantAdmin.setFirstName("Joe");
tenantAdmin.setLastName("Downs");
tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}
@Test
public void testSaveAssetProfile() throws Exception {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile");
Mockito.reset(tbClusterService, auditLogService);
AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
Assert.assertNotNull(savedAssetProfile);
Assert.assertNotNull(savedAssetProfile.getId());
Assert.assertTrue(savedAssetProfile.getCreatedTime() > 0);
Assert.assertEquals(assetProfile.getName(), savedAssetProfile.getName());
Assert.assertEquals(assetProfile.getDescription(), savedAssetProfile.getDescription());
Assert.assertEquals(assetProfile.isDefault(), savedAssetProfile.isDefault());
Assert.assertEquals(assetProfile.getDefaultRuleChainId(), savedAssetProfile.getDefaultRuleChainId());
testNotifyEntityBroadcastEntityStateChangeEventOneTime(savedAssetProfile, savedAssetProfile.getId(), savedAssetProfile.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ADDED);
savedAssetProfile.setName("New asset profile");
doPost("/api/assetProfile", savedAssetProfile, AssetProfile.class);
AssetProfile foundAssetProfile = doGet("/api/assetProfile/" + savedAssetProfile.getId().getId().toString(), AssetProfile.class);
Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfile.getName());
testNotifyEntityBroadcastEntityStateChangeEventOneTime(foundAssetProfile, foundAssetProfile.getId(), foundAssetProfile.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.UPDATED);
}
@Test
public void saveAssetProfileWithViolationOfValidation() throws Exception {
String msgError = msgErrorFieldLength("name");
Mockito.reset(tbClusterService, auditLogService);
AssetProfile createAssetProfile = this.createAssetProfile(StringUtils.randomAlphabetic(300));
doPost("/api/assetProfile", createAssetProfile)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));
testNotifyEntityEqualsOneTimeServiceNeverError(createAssetProfile, savedTenant.getId(),
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
}
@Test
public void testFindAssetProfileById() throws Exception {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile");
AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
AssetProfile foundAssetProfile = doGet("/api/assetProfile/" + savedAssetProfile.getId().getId().toString(), AssetProfile.class);
Assert.assertNotNull(foundAssetProfile);
Assert.assertEquals(savedAssetProfile, foundAssetProfile);
}
@Test
public void whenGetAssetProfileById_thenPermissionsAreChecked() throws Exception {
AssetProfile assetProfile = createAssetProfile("Asset profile 1");
assetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
loginDifferentTenant();
doGet("/api/assetProfile/" + assetProfile.getId())
.andExpect(status().isForbidden())
.andExpect(statusReason(containsString(msgErrorPermission)));
}
@Test
public void testFindAssetProfileInfoById() throws Exception {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile");
AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
AssetProfileInfo foundAssetProfileInfo = doGet("/api/assetProfileInfo/" + savedAssetProfile.getId().getId().toString(), AssetProfileInfo.class);
Assert.assertNotNull(foundAssetProfileInfo);
Assert.assertEquals(savedAssetProfile.getId(), foundAssetProfileInfo.getId());
Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName());
Customer customer = new Customer();
customer.setTitle("Customer");
customer.setTenantId(savedTenant.getId());
Customer savedCustomer = doPost("/api/customer", customer, Customer.class);
User customerUser = new User();
customerUser.setAuthority(Authority.CUSTOMER_USER);
customerUser.setTenantId(savedTenant.getId());
customerUser.setCustomerId(savedCustomer.getId());
customerUser.setEmail("customer2@thingsboard.org");
createUserAndLogin(customerUser, "customer");
foundAssetProfileInfo = doGet("/api/assetProfileInfo/" + savedAssetProfile.getId().getId().toString(), AssetProfileInfo.class);
Assert.assertNotNull(foundAssetProfileInfo);
Assert.assertEquals(savedAssetProfile.getId(), foundAssetProfileInfo.getId());
Assert.assertEquals(savedAssetProfile.getName(), foundAssetProfileInfo.getName());
}
@Test
public void whenGetAssetProfileInfoById_thenPermissionsAreChecked() throws Exception {
AssetProfile assetProfile = createAssetProfile("Asset profile 1");
assetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
loginDifferentTenant();
doGet("/api/assetProfileInfo/" + assetProfile.getId())
.andExpect(status().isForbidden())
.andExpect(statusReason(containsString(msgErrorPermission)));
}
@Test
public void testFindDefaultAssetProfileInfo() throws Exception {
AssetProfileInfo foundDefaultAssetProfileInfo = doGet("/api/assetProfileInfo/default", AssetProfileInfo.class);
Assert.assertNotNull(foundDefaultAssetProfileInfo);
Assert.assertNotNull(foundDefaultAssetProfileInfo.getId());
Assert.assertNotNull(foundDefaultAssetProfileInfo.getName());
Assert.assertEquals("default", foundDefaultAssetProfileInfo.getName());
}
@Test
public void testSetDefaultAssetProfile() throws Exception {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile 1");
AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
Mockito.reset(tbClusterService, auditLogService);
AssetProfile defaultAssetProfile = doPost("/api/assetProfile/" + savedAssetProfile.getId().getId().toString() + "/default", AssetProfile.class);
Assert.assertNotNull(defaultAssetProfile);
AssetProfileInfo foundDefaultAssetProfile = doGet("/api/assetProfileInfo/default", AssetProfileInfo.class);
Assert.assertNotNull(foundDefaultAssetProfile);
Assert.assertEquals(savedAssetProfile.getName(), foundDefaultAssetProfile.getName());
Assert.assertEquals(savedAssetProfile.getId(), foundDefaultAssetProfile.getId());
testNotifyEntityOneTimeMsgToEdgeServiceNever(defaultAssetProfile, defaultAssetProfile.getId(), defaultAssetProfile.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.UPDATED);
}
@Test
public void testSaveAssetProfileWithEmptyName() throws Exception {
AssetProfile assetProfile = new AssetProfile();
Mockito.reset(tbClusterService, auditLogService);
String msgError = "Asset profile name " + msgErrorShouldBeSpecified;
doPost("/api/assetProfile", assetProfile)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));
testNotifyEntityEqualsOneTimeServiceNeverError(assetProfile, savedTenant.getId(),
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
}
@Test
public void testSaveAssetProfileWithSameName() throws Exception {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile");
doPost("/api/assetProfile", assetProfile).andExpect(status().isOk());
AssetProfile assetProfile2 = this.createAssetProfile("Asset Profile");
Mockito.reset(tbClusterService, auditLogService);
String msgError = "Asset profile with such name already exists";
doPost("/api/assetProfile", assetProfile2)
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString(msgError)));
testNotifyEntityEqualsOneTimeServiceNeverError(assetProfile, savedTenant.getId(),
tenantAdmin.getId(), tenantAdmin.getEmail(), ActionType.ADDED, new DataValidationException(msgError));
}
@Test
public void testDeleteAssetProfileWithExistingAsset() throws Exception {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile");
AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
Asset asset = new Asset();
asset.setName("Test asset");
asset.setAssetProfileId(savedAssetProfile.getId());
doPost("/api/asset", asset, Asset.class);
Mockito.reset(tbClusterService, auditLogService);
doDelete("/api/assetProfile/" + savedAssetProfile.getId().getId().toString())
.andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("The asset profile referenced by the assets cannot be deleted")));
testNotifyEntityNever(savedAssetProfile.getId(), savedAssetProfile);
}
@Test
public void testSaveAssetProfileWithRuleChainFromDifferentTenant() throws Exception {
loginDifferentTenant();
RuleChain ruleChain = new RuleChain();
ruleChain.setName("Different rule chain");
RuleChain savedRuleChain = doPost("/api/ruleChain", ruleChain, RuleChain.class);
loginTenantAdmin();
AssetProfile assetProfile = this.createAssetProfile("Asset Profile");
assetProfile.setDefaultRuleChainId(savedRuleChain.getId());
doPost("/api/assetProfile", assetProfile).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Can't assign rule chain from different tenant!")));
}
@Test
public void testSaveAssetProfileWithDashboardFromDifferentTenant() throws Exception {
loginDifferentTenant();
Dashboard dashboard = new Dashboard();
dashboard.setTitle("Different dashboard");
Dashboard savedDashboard = doPost("/api/dashboard", dashboard, Dashboard.class);
loginTenantAdmin();
AssetProfile assetProfile = this.createAssetProfile("Asset Profile");
assetProfile.setDefaultDashboardId(savedDashboard.getId());
doPost("/api/assetProfile", assetProfile).andExpect(status().isBadRequest())
.andExpect(statusReason(containsString("Can't assign dashboard from different tenant!")));
}
@Test
public void testDeleteAssetProfile() throws Exception {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile");
AssetProfile savedAssetProfile = doPost("/api/assetProfile", assetProfile, AssetProfile.class);
Mockito.reset(tbClusterService, auditLogService);
doDelete("/api/assetProfile/" + savedAssetProfile.getId().getId().toString())
.andExpect(status().isOk());
String savedAssetProfileIdStr = savedAssetProfile.getId().getId().toString();
testNotifyEntityBroadcastEntityStateChangeEventOneTime(savedAssetProfile, savedAssetProfile.getId(), savedAssetProfile.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.DELETED, savedAssetProfileIdStr);
doGet("/api/assetProfile/" + savedAssetProfile.getId().getId().toString())
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Asset profile", savedAssetProfileIdStr))));
}
@Test
public void testFindAssetProfiles() throws Exception {
List<AssetProfile> assetProfiles = new ArrayList<>();
PageLink pageLink = new PageLink(17);
PageData<AssetProfile> pageData = doGetTypedWithPageLink("/api/assetProfiles?",
new TypeReference<>() {
}, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getTotalElements());
assetProfiles.addAll(pageData.getData());
Mockito.reset(tbClusterService, auditLogService);
int cntEntity = 28;
for (int i = 0; i < cntEntity; i++) {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile" + i);
assetProfiles.add(doPost("/api/assetProfile", assetProfile, AssetProfile.class));
}
testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(new AssetProfile(), new AssetProfile(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ADDED, ActionType.ADDED, cntEntity, cntEntity, cntEntity);
Mockito.reset(tbClusterService, auditLogService);
List<AssetProfile> loadedAssetProfiles = new ArrayList<>();
pageLink = new PageLink(17);
do {
pageData = doGetTypedWithPageLink("/api/assetProfiles?",
new TypeReference<>() {
}, pageLink);
loadedAssetProfiles.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
}
} while (pageData.hasNext());
Collections.sort(assetProfiles, idComparator);
Collections.sort(loadedAssetProfiles, idComparator);
Assert.assertEquals(assetProfiles, loadedAssetProfiles);
for (AssetProfile assetProfile : loadedAssetProfiles) {
if (!assetProfile.isDefault()) {
doDelete("/api/assetProfile/" + assetProfile.getId().getId().toString())
.andExpect(status().isOk());
}
}
testNotifyManyEntityManyTimeMsgToEdgeServiceEntityEqAny(loadedAssetProfiles.get(0), loadedAssetProfiles.get(0),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.DELETED, ActionType.DELETED, cntEntity, cntEntity, cntEntity, loadedAssetProfiles.get(0).getId().getId().toString());
pageLink = new PageLink(17);
pageData = doGetTypedWithPageLink("/api/assetProfiles?",
new TypeReference<>() {
}, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getTotalElements());
}
@Test
public void testFindAssetProfileInfos() throws Exception {
List<AssetProfile> assetProfiles = new ArrayList<>();
PageLink pageLink = new PageLink(17);
PageData<AssetProfile> assetProfilePageData = doGetTypedWithPageLink("/api/assetProfiles?",
new TypeReference<PageData<AssetProfile>>() {
}, pageLink);
Assert.assertFalse(assetProfilePageData.hasNext());
Assert.assertEquals(1, assetProfilePageData.getTotalElements());
assetProfiles.addAll(assetProfilePageData.getData());
for (int i = 0; i < 28; i++) {
AssetProfile assetProfile = this.createAssetProfile("Asset Profile" + i);
assetProfiles.add(doPost("/api/assetProfile", assetProfile, AssetProfile.class));
}
List<AssetProfileInfo> loadedAssetProfileInfos = new ArrayList<>();
pageLink = new PageLink(17);
PageData<AssetProfileInfo> pageData;
do {
pageData = doGetTypedWithPageLink("/api/assetProfileInfos?",
new TypeReference<>() {
}, pageLink);
loadedAssetProfileInfos.addAll(pageData.getData());
if (pageData.hasNext()) {
pageLink = pageLink.nextPageLink();
}
} while (pageData.hasNext());
Collections.sort(assetProfiles, idComparator);
Collections.sort(loadedAssetProfileInfos, assetProfileInfoIdComparator);
List<AssetProfileInfo> assetProfileInfos = assetProfiles.stream().map(assetProfile -> new AssetProfileInfo(assetProfile.getId(),
assetProfile.getName(), assetProfile.getImage(), assetProfile.getDefaultDashboardId())).collect(Collectors.toList());
Assert.assertEquals(assetProfileInfos, loadedAssetProfileInfos);
for (AssetProfile assetProfile : assetProfiles) {
if (!assetProfile.isDefault()) {
doDelete("/api/assetProfile/" + assetProfile.getId().getId().toString())
.andExpect(status().isOk());
}
}
pageLink = new PageLink(17);
pageData = doGetTypedWithPageLink("/api/assetProfileInfos?",
new TypeReference<PageData<AssetProfileInfo>>() {
}, pageLink);
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(1, pageData.getTotalElements());
}
@Test
public void testDeleteAssetProfileWithDeleteRelationsOk() throws Exception {
AssetProfileId assetProfileId = savedAssetProfile("AssetProfile for Test WithRelationsOk").getId();
testEntityDaoWithRelationsOk(savedTenant.getId(), assetProfileId, "/api/assetProfile/" + assetProfileId);
}
@Test
public void testDeleteAssetProfileExceptionWithRelationsTransactional() throws Exception {
AssetProfileId assetProfileId = savedAssetProfile("AssetProfile for Test WithRelations Transactional Exception").getId();
testEntityDaoWithRelationsTransactionalException(assetProfileDao, savedTenant.getId(), assetProfileId, "/api/assetProfile/" + assetProfileId);
}
private AssetProfile savedAssetProfile(String name) {
AssetProfile assetProfile = createAssetProfile(name);
return doPost("/api/assetProfile", assetProfile, AssetProfile.class);
}
}

68
application/src/test/java/org/thingsboard/server/controller/BaseAuditLogControllerTest.java

@ -20,18 +20,33 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.dao.audit.AuditLogDao;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.sqlts.insert.sql.SqlPartitioningRepository;
import org.thingsboard.server.service.ttl.AuditLogsCleanUpService;
import java.time.LocalDate;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class BaseAuditLogControllerTest extends AbstractControllerTest {
@ -39,6 +54,18 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest
private Tenant savedTenant;
private User tenantAdmin;
@Autowired
private AuditLogDao auditLogDao;
@SpyBean
private SqlPartitioningRepository partitioningRepository;
@Autowired
private AuditLogsCleanUpService auditLogsCleanUpService;
@Value("#{${sql.audit_logs.partition_size} * 60 * 60 * 1000}")
private long partitionDurationInMs;
@Value("${sql.ttl.audit_logs.ttl}")
private long auditLogsTtlInSec;
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -145,4 +172,45 @@ public abstract class BaseAuditLogControllerTest extends AbstractControllerTest
Assert.assertEquals(179, loadedAuditLogs.size());
}
@Test
public void whenSavingNewAuditLog_thenCheckAndCreatePartitionIfNotExists() {
reset(partitioningRepository);
AuditLog auditLog = createAuditLog(ActionType.LOGIN, tenantAdminUserId);
verify(partitioningRepository).createPartitionIfNotExists(eq("audit_log"), eq(auditLog.getCreatedTime()), eq(partitionDurationInMs));
List<Long> partitions = partitioningRepository.fetchPartitions("audit_log");
assertThat(partitions).singleElement().satisfies(partitionStartTs -> {
assertThat(partitionStartTs).isEqualTo(partitioningRepository.calculatePartitionStartTime(auditLog.getCreatedTime(), partitionDurationInMs));
});
}
@Test
public void whenCleaningUpAuditLogsByTtl_thenDropOldPartitions() {
long oldAuditLogTs = LocalDate.of(2020, 10, 1).atStartOfDay().toInstant(ZoneOffset.UTC).toEpochMilli();
long partitionStartTs = partitioningRepository.calculatePartitionStartTime(oldAuditLogTs, partitionDurationInMs);
partitioningRepository.createPartitionIfNotExists("audit_log", oldAuditLogTs, partitionDurationInMs);
List<Long> partitions = partitioningRepository.fetchPartitions("audit_log");
assertThat(partitions).contains(partitionStartTs);
auditLogsCleanUpService.cleanUp();
partitions = partitioningRepository.fetchPartitions("audit_log");
assertThat(partitions).doesNotContain(partitionStartTs);
assertThat(partitions).allSatisfy(partitionsStart -> {
long partitionEndTs = partitionsStart + partitionDurationInMs;
assertThat(partitionEndTs).isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(auditLogsTtlInSec));
});
}
private AuditLog createAuditLog(ActionType actionType, EntityId entityId) {
AuditLog auditLog = new AuditLog();
auditLog.setTenantId(tenantId);
auditLog.setCustomerId(null);
auditLog.setUserId(tenantAdminUserId);
auditLog.setEntityId(entityId);
auditLog.setUserName(tenantAdmin.getEmail());
auditLog.setActionType(actionType);
return auditLogDao.save(tenantId, auditLog);
}
}

20
application/src/test/java/org/thingsboard/server/controller/BaseCustomerControllerTest.java

@ -24,8 +24,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
@ -48,6 +52,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ContextConfiguration(classes = {BaseCustomerControllerTest.Config.class})
public abstract class BaseCustomerControllerTest extends AbstractControllerTest {
static final TypeReference<PageData<Customer>> PAGE_DATA_CUSTOMER_TYPE_REFERENCE = new TypeReference<>() {
};
@ -57,9 +62,18 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest
private Tenant savedTenant;
private User tenantAdmin;
@SpyBean
@Autowired
private CustomerDao customerDao;
static class Config {
@Bean
@Primary
public CustomerDao customerDao(CustomerDao customerDao) {
return Mockito.mock(CustomerDao.class, AdditionalAnswers.delegatesTo(customerDao));
}
}
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
@ -87,8 +101,6 @@ public abstract class BaseCustomerControllerTest extends AbstractControllerTest
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException (customerDao);
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}

20
application/src/test/java/org/thingsboard/server/controller/BaseDashboardControllerTest.java

@ -21,8 +21,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.DashboardInfo;
@ -46,6 +50,7 @@ import java.util.List;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ContextConfiguration(classes = {BaseDashboardControllerTest.Config.class})
public abstract class BaseDashboardControllerTest extends AbstractControllerTest {
private IdComparator<DashboardInfo> idComparator = new IdComparator<>();
@ -53,9 +58,17 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest
private Tenant savedTenant;
private User tenantAdmin;
@SpyBean
@Autowired
private DashboardDao dashboardDao;
static class Config {
@Bean
@Primary
public DashboardDao dashboardDao(DashboardDao dashboardDao) {
return Mockito.mock(DashboardDao.class, AdditionalAnswers.delegatesTo(dashboardDao));
}
}
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -79,8 +92,6 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest
public void afterTest() throws Exception {
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException (dashboardDao);
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}
@ -489,4 +500,5 @@ public abstract class BaseDashboardControllerTest extends AbstractControllerTest
dashboard.setTitle(title);
return doPost("/api/dashboard", dashboard, Dashboard.class);
}
}

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

@ -25,8 +25,13 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.common.util.ThingsBoardExecutors;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
@ -69,8 +74,10 @@ import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE;
import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@ContextConfiguration(classes = {BaseDeviceControllerTest.Config.class})
public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
static final TypeReference<PageData<Device>> PAGE_DATA_DEVICE_TYPE_REF = new TypeReference<>() {};
static final TypeReference<PageData<Device>> PAGE_DATA_DEVICE_TYPE_REF = new TypeReference<>() {
};
ListeningExecutorService executor;
@ -83,9 +90,16 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
@SpyBean
private GatewayNotificationsService gatewayNotificationsService;
@SpyBean
@Autowired
private DeviceDao deviceDao;
static class Config {
@Bean
@Primary
public DeviceDao deviceDao(DeviceDao deviceDao) {
return Mockito.mock(DeviceDao.class, AdditionalAnswers.delegatesTo(deviceDao));
}
}
@Before
public void beforeTest() throws Exception {
@ -114,8 +128,6 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException (deviceDao);
doDelete("/api/tenant/" + savedTenant.getId().getId())
.andExpect(status().isOk());
}
@ -464,9 +476,9 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
String customerIdStr = savedDevice.getId().toString();
doPost("/api/customer/" + customerIdStr
+ "/device/" + savedDevice.getId().getId())
+ "/device/" + savedDevice.getId().getId())
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Customer", customerIdStr))));
.andExpect(statusReason(containsString(msgErrorNoFound("Customer", customerIdStr))));
testNotifyEntityNever(savedDevice.getId(), savedDevice);
testNotificationUpdateGatewayNever();
@ -657,7 +669,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
doPost("/api/device/credentials", deviceCredentials)
.andExpect(status().isNotFound())
.andExpect(statusReason(containsString(msgErrorNoFound("Device", deviceTimeBasedId.toString()))));
.andExpect(statusReason(containsString(msgErrorNoFound("Device", deviceTimeBasedId.toString()))));
testNotifyEntityNever(savedDevice.getId(), savedDevice);
testNotificationUpdateGatewayNever();
@ -1168,7 +1180,7 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
doPost("/api/edge/" + savedEdge.getId().getId()
+ "/device/" + savedDevice.getId().getId(), Device.class);
testNotifyEntityAllOneTime(savedDevice, savedDevice.getId(), savedDevice.getId(),
testNotifyEntityAllOneTime(savedDevice, savedDevice.getId(), savedDevice.getId(),
savedTenant.getId(), tenantAdmin.getCustomerId(), tenantAdmin.getId(), tenantAdmin.getEmail(),
ActionType.ASSIGNED_TO_EDGE,
savedDevice.getId().getId().toString(), savedEdge.getId().getId().toString(), savedEdge.getName());

20
application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java

@ -26,8 +26,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Dashboard;
import org.thingsboard.server.common.data.Device;
@ -69,6 +73,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
import static org.thingsboard.server.common.data.ota.OtaPackageType.SOFTWARE;
@ContextConfiguration(classes = {BaseDeviceProfileControllerTest.Config.class})
public abstract class BaseDeviceProfileControllerTest extends AbstractControllerTest {
private IdComparator<DeviceProfile> idComparator = new IdComparator<>();
@ -77,9 +82,17 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
private Tenant savedTenant;
private User tenantAdmin;
@SpyBean
@Autowired
private DeviceProfileDao deviceProfileDao;
static class Config {
@Bean
@Primary
public DeviceProfileDao deviceProfileDao(DeviceProfileDao deviceProfileDao) {
return Mockito.mock(DeviceProfileDao.class, AdditionalAnswers.delegatesTo(deviceProfileDao));
}
}
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -103,8 +116,6 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
public void afterTest() throws Exception {
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException (deviceProfileDao);
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}
@ -1149,4 +1160,5 @@ public abstract class BaseDeviceProfileControllerTest extends AbstractController
DeviceProfile deviceProfile = createDeviceProfile(name);
return doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
}
}

28
application/src/test/java/org/thingsboard/server/controller/BaseEdgeControllerTest.java

@ -21,8 +21,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
@ -44,6 +48,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.edge.imitator.EdgeImitator;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DeviceUpdateMsg;
@ -64,6 +69,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
@TestPropertySource(properties = {
"edges.enabled=true",
})
@ContextConfiguration(classes = {BaseEdgeControllerTest.Config.class})
public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
public static final String EDGE_HOST = "localhost";
@ -75,10 +81,18 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
private TenantId tenantId;
private User tenantAdmin;
@SpyBean
@Autowired
private EdgeDao edgeDao;
@Before
static class Config {
@Bean
@Primary
public EdgeDao edgeDao(EdgeDao edgeDao) {
return Mockito.mock(EdgeDao.class, AdditionalAnswers.delegatesTo(edgeDao));
}
}
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -102,8 +116,6 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
public void afterTest() throws Exception {
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException (edgeDao);
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}
@ -808,7 +820,7 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
EdgeImitator edgeImitator = new EdgeImitator(EDGE_HOST, EDGE_PORT, edge.getRoutingKey(), edge.getSecret());
edgeImitator.ignoreType(UserCredentialsUpdateMsg.class);
edgeImitator.expectMessageAmount(12);
edgeImitator.expectMessageAmount(14);
edgeImitator.connect();
assertThat(edgeImitator.waitForMessages()).as("await for messages on first connect").isTrue();
@ -816,17 +828,19 @@ public abstract class BaseEdgeControllerTest extends AbstractControllerTest {
assertThat(edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class)).as("one msg during sync process, another from edge creation").hasSize(2);
assertThat(edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class)).as("one msg during sync process for 'default' device profile").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(DeviceUpdateMsg.class)).as("one msg once device assigned to edge").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class)).as("two msgs during sync process for 'default' and 'test' asset profiles").hasSize(2);
assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("two msgs - one during sync process, and one more once asset assigned to edge").hasSize(2);
assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("one msg during sync process for tenant admin user").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update").hasSize(4);
edgeImitator.expectMessageAmount(9);
edgeImitator.expectMessageAmount(11);
doPost("/api/edge/sync/" + edge.getId());
assertThat(edgeImitator.waitForMessages()).as("await for messages after edge sync rest api call").isTrue();
assertThat(edgeImitator.findAllMessagesByType(QueueUpdateMsg.class)).as("queue msg").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(RuleChainUpdateMsg.class)).as("rule chain msg").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(DeviceProfileUpdateMsg.class)).as("device profile msg").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(AssetProfileUpdateMsg.class)).as("asset profile msg").hasSize(2);
assertThat(edgeImitator.findAllMessagesByType(AssetUpdateMsg.class)).as("asset update msg").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(UserUpdateMsg.class)).as("user update msg").hasSize(1);
assertThat(edgeImitator.findAllMessagesByType(AdminSettingsUpdateMsg.class)).as("admin setting update msg").hasSize(4);

20
application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java

@ -31,8 +31,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.ResultActions;
import org.thingsboard.common.util.ThingsBoardExecutors;
@ -84,6 +88,7 @@ import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID;
"js.evaluator=mock",
})
@Slf4j
@ContextConfiguration(classes = {BaseEntityViewControllerTest.Config.class})
public abstract class BaseEntityViewControllerTest extends AbstractControllerTest {
static final TypeReference<PageData<EntityView>> PAGE_DATA_ENTITY_VIEW_TYPE_REF = new TypeReference<>() {
};
@ -96,9 +101,17 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
List<ListenableFuture<ResultActions>> deleteFutures = new ArrayList<>();
ListeningExecutorService executor;
@SpyBean
@Autowired
private EntityViewDao entityViewDao;
static class Config {
@Bean
@Primary
public EntityViewDao entityViewDao(EntityViewDao entityViewDao) {
return Mockito.mock(EntityViewDao.class, AdditionalAnswers.delegatesTo(entityViewDao));
}
}
@Before
public void beforeTest() throws Exception {
executor = MoreExecutors.listeningDecorator(ThingsBoardExecutors.newWorkStealingPool(8, getClass()));
@ -120,9 +133,6 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
@After
public void afterTest() throws Exception {
afterTestEntityDaoRemoveByIdWithException (entityViewDao);
executor.shutdownNow();
}

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

@ -20,8 +20,12 @@ import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@ -43,6 +47,7 @@ import java.util.List;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ContextConfiguration(classes = {BaseRuleChainControllerTest.Config.class})
public abstract class BaseRuleChainControllerTest extends AbstractControllerTest {
private IdComparator<RuleChain> idComparator = new IdComparator<>();
@ -50,9 +55,17 @@ public abstract class BaseRuleChainControllerTest extends AbstractControllerTest
private Tenant savedTenant;
private User tenantAdmin;
@SpyBean
@Autowired
private RuleChainDao ruleChainDao;
static class Config {
@Bean
@Primary
public RuleChainDao ruleChainDao(RuleChainDao ruleChainDao) {
return Mockito.mock(RuleChainDao.class, AdditionalAnswers.delegatesTo(ruleChainDao));
}
}
@Before
public void beforeTest() throws Exception {
loginSysAdmin();
@ -76,8 +89,6 @@ public abstract class BaseRuleChainControllerTest extends AbstractControllerTest
public void afterTest() throws Exception {
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException(ruleChainDao);
doDelete("/api/tenant/" + savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}

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

@ -21,9 +21,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ContextConfiguration;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.Tenant;
@ -51,20 +55,27 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.dao.model.ModelConstants.SYSTEM_TENANT;
@ContextConfiguration(classes = {BaseUserControllerTest.Config.class})
public abstract class BaseUserControllerTest extends AbstractControllerTest {
private IdComparator<User> idComparator = new IdComparator<>();
private CustomerId customerNUULId = (CustomerId) createEntityId_NULL_UUID(new Customer());
@SpyBean
@Autowired
private UserDao userDao;
static class Config {
@Bean
@Primary
public UserDao userDao(UserDao userDao) {
return Mockito.mock(UserDao.class, AdditionalAnswers.delegatesTo(userDao));
}
}
@After
public void afterTest() throws Exception {
loginSysAdmin();
afterTestEntityDaoRemoveByIdWithException(userDao);
}
@Test

23
application/src/test/java/org/thingsboard/server/controller/sql/AssetProfileControllerSqlTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 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.controller.sql;
import org.thingsboard.server.controller.BaseAssetProfileControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class AssetProfileControllerSqlTest extends BaseAssetProfileControllerTest {
}

44
application/src/test/java/org/thingsboard/server/edge/BaseEdgeTest.java

@ -26,6 +26,7 @@ import com.google.protobuf.AbstractMessage;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.MessageLite;
import io.netty.handler.codec.mqtt.MqttQoS;
import org.awaitility.Awaitility;
import org.junit.After;
import org.junit.Assert;
@ -162,10 +163,11 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.util.DataDecodingEncodingService;
import org.thingsboard.server.transport.AbstractTransportIntegrationTest;
import org.thingsboard.server.transport.lwm2m.AbstractLwM2MIntegrationTest;
import org.thingsboard.server.transport.mqtt.MqttTestCallback;
import org.thingsboard.server.transport.mqtt.MqttTestClient;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -174,11 +176,13 @@ import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertEquals;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE;
@TestPropertySource(properties = {
"edges.enabled=true",
"transport.mqtt.enabled=true"
})
abstract public class BaseEdgeTest extends AbstractControllerTest {
@ -228,7 +232,7 @@ abstract public class BaseEdgeTest extends AbstractControllerTest {
installation();
edgeImitator = new EdgeImitator("localhost", 7070, edge.getRoutingKey(), edge.getSecret());
edgeImitator.expectMessageAmount(15);
edgeImitator.expectMessageAmount(17);
edgeImitator.connect();
requestEdgeRuleChainMetadata();
@ -2147,6 +2151,42 @@ abstract public class BaseEdgeTest extends AbstractControllerTest {
Assert.assertEquals(queueUpdateMsg.getIdLSB(), savedQueue.getUuidId().getLeastSignificantBits());
}
@Test
public void sendUpdateSharedAttributeToCloudAndValidateDeviceSubscription() throws Exception {
Device device = saveDeviceOnCloudAndVerifyDeliveryToEdge();
DeviceCredentials deviceCredentials = doGet("/api/device/" + device.getUuidId() + "/credentials", DeviceCredentials.class);
MqttTestClient client = new MqttTestClient();
client.connectAndWait(deviceCredentials.getCredentialsId());
MqttTestCallback onUpdateCallback = new MqttTestCallback();
client.setCallback(onUpdateCallback);
client.subscribeAndWait("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE);
edgeImitator.expectResponsesAmount(1);
JsonObject attributesData = new JsonObject();
String attrKey = "sharedAttrName";
String attrValue = "sharedAttrValue";
attributesData.addProperty(attrKey, attrValue);
UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder();
EntityDataProto.Builder entityDataBuilder = EntityDataProto.newBuilder();
entityDataBuilder.setEntityType(device.getId().getEntityType().name());
entityDataBuilder.setEntityIdMSB(device.getId().getId().getMostSignificantBits());
entityDataBuilder.setEntityIdLSB(device.getId().getId().getLeastSignificantBits());
entityDataBuilder.setAttributesUpdatedMsg(JsonConverter.convertToAttributesProto(attributesData));
entityDataBuilder.setPostAttributeScope(DataConstants.SHARED_SCOPE);
uplinkMsgBuilder.addEntityData(entityDataBuilder.build());
edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build());
Assert.assertTrue(edgeImitator.waitForResponses());
Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(5, TimeUnit.SECONDS));
assertEquals(JacksonUtil.OBJECT_MAPPER.createObjectNode().put(attrKey, attrValue),
JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes()));
}
// Utility methods
private Device saveDeviceOnCloudAndVerifyDeliveryToEdge() throws Exception {

6
application/src/test/java/org/thingsboard/server/edge/imitator/EdgeImitator.java

@ -28,6 +28,7 @@ import org.thingsboard.edge.rpc.EdgeGrpcClient;
import org.thingsboard.edge.rpc.EdgeRpcClient;
import org.thingsboard.server.gen.edge.v1.AdminSettingsUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AlarmUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetProfileUpdateMsg;
import org.thingsboard.server.gen.edge.v1.AssetUpdateMsg;
import org.thingsboard.server.gen.edge.v1.CustomerUpdateMsg;
import org.thingsboard.server.gen.edge.v1.DashboardUpdateMsg;
@ -196,6 +197,11 @@ public class EdgeImitator {
result.add(saveDownlinkMsg(deviceCredentialsUpdateMsg));
}
}
if (downlinkMsg.getAssetProfileUpdateMsgCount() > 0) {
for (AssetProfileUpdateMsg assetProfileUpdateMsg : downlinkMsg.getAssetProfileUpdateMsgList()) {
result.add(saveDownlinkMsg(assetProfileUpdateMsg));
}
}
if (downlinkMsg.getAssetUpdateMsgCount() > 0) {
for (AssetUpdateMsg assetUpdateMsg : downlinkMsg.getAssetUpdateMsgList()) {
result.add(saveDownlinkMsg(assetUpdateMsg));

96
application/src/test/java/org/thingsboard/server/service/script/LocalJsInvokeServiceTest.java

@ -0,0 +1,96 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.test.context.TestPropertySource;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@DaoSqlTest
@TestPropertySource(properties = {
"js.max_script_body_size=50",
"js.max_total_args_size=50",
"js.max_result_size=50",
"js.local.max_errors=2"
})
class LocalJsInvokeServiceTest extends AbstractControllerTest {
@Autowired
private NashornJsInvokeService jsInvokeService;
@Value("${js.local.max_errors}")
private int maxJsErrors;
@Test
void givenTooBigScriptForEval_thenReturnError() {
String hugeScript = "var a = 'qwertyqwertywertyqwabababer'; return {a: a};";
assertThatThrownBy(() -> {
evalScript(hugeScript);
}).hasMessageContaining("body exceeds maximum allowed size");
}
@Test
void givenTooBigScriptInputArgs_thenReturnErrorAndReportScriptExecutionError() throws Exception {
String script = "return { msg: msg };";
String hugeMsg = "{\"input\":\"123456781234349\"}";
UUID scriptId = evalScript(script);
for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> {
invokeScript(scriptId, hugeMsg);
}).hasMessageContaining("input arguments exceed maximum");
}
assertThatScriptIsBlocked(scriptId);
}
@Test
void whenScriptInvocationResultIsTooBig_thenReturnErrorAndReportScriptExecutionError() throws Exception {
String script = "var s = new Array(50).join('a'); return { s: s};";
UUID scriptId = evalScript(script);
for (int i = 0; i < maxJsErrors; i++) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
}).hasMessageContaining("result exceeds maximum allowed size");
}
assertThatScriptIsBlocked(scriptId);
}
private void assertThatScriptIsBlocked(UUID scriptId) {
assertThatThrownBy(() -> {
invokeScript(scriptId, "{}");
}).hasMessageContaining("invocation is blocked due to maximum error");
}
private UUID evalScript(String script) throws ExecutionException, InterruptedException {
return jsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, script).get();
}
private String invokeScript(UUID scriptId, String msg) throws ExecutionException, InterruptedException {
return jsInvokeService.invokeFunction(TenantId.SYS_TENANT_ID, null, scriptId, msg, "{}", "POST_TELEMETRY_REQUEST").get();
}
}

2
application/src/test/java/org/thingsboard/server/service/script/MockJsInvokeService.java

@ -37,7 +37,7 @@ public class MockJsInvokeService implements JsInvokeService {
}
@Override
public ListenableFuture<Object> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
public ListenableFuture<String> invokeFunction(TenantId tenantId, CustomerId customerId, UUID scriptId, Object... args) {
log.warn("invokeFunction {} {} {} {}", tenantId, customerId, scriptId, args);
return Futures.immediateFuture("{}");
}

219
application/src/test/java/org/thingsboard/server/service/script/RemoteJsInvokeServiceTest.java

@ -0,0 +1,219 @@
/**
* Copyright © 2016-2022 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.service.script;
import com.google.common.util.concurrent.Futures;
import org.apache.commons.lang3.StringUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.thingsboard.server.common.data.ApiUsageState;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.gen.js.JsInvokeProtos;
import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsRequest;
import org.thingsboard.server.gen.js.JsInvokeProtos.RemoteJsResponse;
import org.thingsboard.server.queue.TbQueueRequestTemplate;
import org.thingsboard.server.queue.common.TbProtoJsQueueMsg;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
class RemoteJsInvokeServiceTest {
private RemoteJsInvokeService remoteJsInvokeService;
private TbQueueRequestTemplate<TbProtoJsQueueMsg<RemoteJsRequest>, TbProtoQueueMsg<RemoteJsResponse>> jsRequestTemplate;
@BeforeEach
public void beforeEach() {
TbApiUsageStateService apiUsageStateService = mock(TbApiUsageStateService.class);
ApiUsageState apiUsageState = mock(ApiUsageState.class);
when(apiUsageState.isJsExecEnabled()).thenReturn(true);
when(apiUsageStateService.getApiUsageState(any())).thenReturn(apiUsageState);
TbApiUsageClient apiUsageClient = mock(TbApiUsageClient.class);
remoteJsInvokeService = new RemoteJsInvokeService(apiUsageStateService, apiUsageClient);
jsRequestTemplate = mock(TbQueueRequestTemplate.class);
remoteJsInvokeService.requestTemplate = jsRequestTemplate;
}
@AfterEach
public void afterEach() {
reset(jsRequestTemplate);
}
@Test
public void whenInvokingFunction_thenDoNotSendScriptBody() throws Exception {
mockJsEvalResponse();
String scriptBody = "return { a: 'b'};";
UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, scriptBody).get();
reset(jsRequestTemplate);
String expectedInvocationResult = "scriptInvocationResult";
doReturn(Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder()
.setInvokeResponse(JsInvokeProtos.JsInvokeResponse.newBuilder()
.setSuccess(true)
.setResult(expectedInvocationResult)
.build())
.build())))
.when(jsRequestTemplate).send(any());
ArgumentCaptor<TbProtoJsQueueMsg<RemoteJsRequest>> jsRequestCaptor = ArgumentCaptor.forClass(TbProtoJsQueueMsg.class);
Object invocationResult = remoteJsInvokeService.invokeFunction(TenantId.SYS_TENANT_ID, null, scriptId, "{}").get();
verify(jsRequestTemplate).send(jsRequestCaptor.capture());
JsInvokeProtos.JsInvokeRequest jsInvokeRequestMade = jsRequestCaptor.getValue().getValue().getInvokeRequest();
assertThat(jsInvokeRequestMade.getScriptBody()).isNullOrEmpty();
assertThat(jsInvokeRequestMade.getScriptHash()).isEqualTo(getScriptHash(scriptId));
assertThat(invocationResult).isEqualTo(expectedInvocationResult);
}
@Test
public void whenInvokingFunctionAndRemoteJsExecutorRemovedScript_thenHandleNotFoundErrorAndMakeInvokeRequestWithScriptBody() throws Exception {
mockJsEvalResponse();
String scriptBody = "return { a: 'b'};";
UUID scriptId = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, scriptBody).get();
reset(jsRequestTemplate);
doReturn(Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder()
.setInvokeResponse(JsInvokeProtos.JsInvokeResponse.newBuilder()
.setSuccess(false)
.setErrorCode(JsInvokeProtos.JsInvokeErrorCode.NOT_FOUND_ERROR)
.build())
.build())))
.when(jsRequestTemplate).send(argThat(jsQueueMsg -> {
return StringUtils.isEmpty(jsQueueMsg.getValue().getInvokeRequest().getScriptBody());
}));
String expectedInvocationResult = "invocationResult";
doReturn(Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder()
.setInvokeResponse(JsInvokeProtos.JsInvokeResponse.newBuilder()
.setSuccess(true)
.setResult(expectedInvocationResult)
.build())
.build())))
.when(jsRequestTemplate).send(argThat(jsQueueMsg -> {
return StringUtils.isNotEmpty(jsQueueMsg.getValue().getInvokeRequest().getScriptBody());
}));
ArgumentCaptor<TbProtoJsQueueMsg<RemoteJsRequest>> jsRequestsCaptor = ArgumentCaptor.forClass(TbProtoJsQueueMsg.class);
Object invocationResult = remoteJsInvokeService.invokeFunction(TenantId.SYS_TENANT_ID, null, scriptId, "{}").get();
verify(jsRequestTemplate, times(2)).send(jsRequestsCaptor.capture());
List<TbProtoJsQueueMsg<RemoteJsRequest>> jsInvokeRequestsMade = jsRequestsCaptor.getAllValues();
JsInvokeProtos.JsInvokeRequest firstRequestMade = jsInvokeRequestsMade.get(0).getValue().getInvokeRequest();
assertThat(firstRequestMade.getScriptBody()).isNullOrEmpty();
JsInvokeProtos.JsInvokeRequest secondRequestMade = jsInvokeRequestsMade.get(1).getValue().getInvokeRequest();
assertThat(secondRequestMade.getScriptBody()).contains(scriptBody);
assertThat(jsInvokeRequestsMade.stream().map(TbProtoQueueMsg::getKey).distinct().count()).as("partition keys are same")
.isOne();
assertThat(invocationResult).isEqualTo(expectedInvocationResult);
}
@Test
public void whenDoingEval_thenSaveScriptByHashOfTenantIdAndScriptBody() throws Exception {
mockJsEvalResponse();
TenantId tenantId1 = TenantId.fromUUID(UUID.randomUUID());
String scriptBody1 = "var msg = { temp: 42, humidity: 77 };\n" +
"var metadata = { data: 40 };\n" +
"var msgType = \"POST_TELEMETRY_REQUEST\";\n" +
"\n" +
"return { msg: msg, metadata: metadata, msgType: msgType };";
Set<String> scriptHashes = new HashSet<>();
String tenant1Script1Hash = null;
for (int i = 0; i < 3; i++) {
UUID scriptUuid = remoteJsInvokeService.eval(tenantId1, JsScriptType.RULE_NODE_SCRIPT, scriptBody1).get();
tenant1Script1Hash = getScriptHash(scriptUuid);
scriptHashes.add(tenant1Script1Hash);
}
assertThat(scriptHashes).as("Unique scripts ids").size().isOne();
TenantId tenantId2 = TenantId.fromUUID(UUID.randomUUID());
UUID scriptUuid = remoteJsInvokeService.eval(tenantId2, JsScriptType.RULE_NODE_SCRIPT, scriptBody1).get();
String tenant2Script1Id = getScriptHash(scriptUuid);
assertThat(tenant2Script1Id).isNotEqualTo(tenant1Script1Hash);
String scriptBody2 = scriptBody1 + ";;";
scriptUuid = remoteJsInvokeService.eval(tenantId2, JsScriptType.RULE_NODE_SCRIPT, scriptBody2).get();
String tenant2Script2Id = getScriptHash(scriptUuid);
assertThat(tenant2Script2Id).isNotEqualTo(tenant2Script1Id);
}
@Test
public void whenReleasingScript_thenCheckForHashUsages() throws Exception {
mockJsEvalResponse();
String scriptBody = "return { a: 'b'};";
UUID scriptId1 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, scriptBody).get();
UUID scriptId2 = remoteJsInvokeService.eval(TenantId.SYS_TENANT_ID, JsScriptType.RULE_NODE_SCRIPT, scriptBody).get();
String scriptHash = getScriptHash(scriptId1);
assertThat(scriptHash).isEqualTo(getScriptHash(scriptId2));
reset(jsRequestTemplate);
doReturn(Futures.immediateFuture(new TbProtoQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder()
.setReleaseResponse(JsInvokeProtos.JsReleaseResponse.newBuilder()
.setSuccess(true)
.build())
.build())))
.when(jsRequestTemplate).send(any());
remoteJsInvokeService.release(scriptId1).get();
verifyNoInteractions(jsRequestTemplate);
assertThat(remoteJsInvokeService.scriptHashToBodysMap).containsKey(scriptHash);
remoteJsInvokeService.release(scriptId2).get();
verify(jsRequestTemplate).send(any());
assertThat(remoteJsInvokeService.scriptHashToBodysMap).isEmpty();
}
private String getScriptHash(UUID scriptUuid) {
return remoteJsInvokeService.scriptIdToNameAndHashMap.get(scriptUuid).getSecond();
}
private void mockJsEvalResponse() {
doAnswer(methodCall -> Futures.immediateFuture(new TbProtoJsQueueMsg<>(UUID.randomUUID(), RemoteJsResponse.newBuilder()
.setCompileResponse(JsInvokeProtos.JsCompileResponse.newBuilder()
.setSuccess(true)
.setScriptHash(methodCall.<TbProtoQueueMsg<RemoteJsRequest>>getArgument(0).getValue().getCompileRequest().getScriptHash())
.build())
.build())))
.when(jsRequestTemplate).send(argThat(jsQueueMsg -> jsQueueMsg.getValue().hasCompileRequest()));
}
}

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

@ -37,12 +37,14 @@ import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.device.data.DefaultDeviceTransportConfiguration;
import org.thingsboard.server.common.data.device.data.DeviceData;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileTransportConfiguration;
import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -64,6 +66,7 @@ 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.EntityImportSettings;
import org.thingsboard.server.controller.AbstractControllerTest;
import org.thingsboard.server.dao.asset.AssetProfileService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
@ -97,6 +100,8 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
@Autowired
protected DeviceProfileService deviceProfileService;
@Autowired
protected AssetProfileService assetProfileService;
@Autowired
protected AssetService assetService;
@Autowired
protected CustomerService customerService;
@ -206,11 +211,26 @@ public abstract class BaseExportImportServiceTest extends AbstractControllerTest
assertThat(initialProfile.getDescription()).isEqualTo(importedProfile.getDescription());
}
protected Asset createAsset(TenantId tenantId, CustomerId customerId, String type, String name) {
protected AssetProfile createAssetProfile(TenantId tenantId, RuleChainId defaultRuleChainId, DashboardId defaultDashboardId, String name) {
AssetProfile assetProfile = new AssetProfile();
assetProfile.setTenantId(tenantId);
assetProfile.setName(name);
assetProfile.setDescription("dscrptn");
assetProfile.setDefaultRuleChainId(defaultRuleChainId);
assetProfile.setDefaultDashboardId(defaultDashboardId);
return assetProfileService.saveAssetProfile(assetProfile);
}
protected void checkImportedAssetProfileData(AssetProfile initialProfile, AssetProfile importedProfile) {
assertThat(initialProfile.getName()).isEqualTo(importedProfile.getName());
assertThat(initialProfile.getDescription()).isEqualTo(importedProfile.getDescription());
}
protected Asset createAsset(TenantId tenantId, CustomerId customerId, AssetProfileId assetProfileId, String name) {
Asset asset = new Asset();
asset.setTenantId(tenantId);
asset.setCustomerId(customerId);
asset.setType(type);
asset.setAssetProfileId(assetProfileId);
asset.setName(name);
asset.setLabel("lbl");
asset.setAdditionalInfo(JacksonUtil.newObjectNode().set("a", new TextNode("b")));

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

@ -32,12 +32,13 @@ import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.ExportableEntity;
import org.thingsboard.server.common.data.OtaPackage;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.DeviceProfileId;
@ -60,7 +61,6 @@ import org.thingsboard.server.common.data.sync.ie.EntityImportResult;
import org.thingsboard.server.common.data.sync.ie.EntityImportSettings;
import org.thingsboard.server.common.data.sync.ie.RuleChainExportData;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileDao;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
@ -77,7 +77,6 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Mockito.verify;
@DaoSqlTest
@ -91,18 +90,30 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
private OtaPackageStateService otaPackageStateService;
@Test
public void testExportImportAsset_betweenTenants() throws Exception {
Asset asset = createAsset(tenantId1, null, "AB", "Asset of tenant 1");
EntityExportData<Asset> exportData = exportEntity(tenantAdmin1, asset.getId());
public void testExportImportAssetWithProfile_betweenTenants() throws Exception {
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile of tenant 1");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset of tenant 1");
EntityImportResult<Asset> importResult = importEntity(tenantAdmin2, exportData);
checkImportedEntity(tenantId1, asset, tenantId2, importResult.getSavedEntity());
checkImportedAssetData(asset, importResult.getSavedEntity());
EntityExportData<AssetProfile> profileExportData = exportEntity(tenantAdmin1, assetProfile.getId());
EntityExportData<Asset> assetExportData = exportEntity(tenantAdmin1, asset.getId());
EntityImportResult<AssetProfile> profileImportResult = importEntity(tenantAdmin2, profileExportData);
checkImportedEntity(tenantId1, assetProfile, tenantId2, profileImportResult.getSavedEntity());
checkImportedAssetProfileData(assetProfile, profileImportResult.getSavedEntity());
EntityImportResult<Asset> assetImportResult = importEntity(tenantAdmin2, assetExportData);
Asset importedAsset = assetImportResult.getSavedEntity();
checkImportedEntity(tenantId1, asset, tenantId2, importedAsset);
checkImportedAssetData(asset, importedAsset);
assertThat(importedAsset.getAssetProfileId()).isEqualTo(profileImportResult.getSavedEntity().getId());
}
@Test
public void testExportImportAsset_sameTenant() throws Exception {
Asset asset = createAsset(tenantId1, null, "AB", "Asset v1.0");
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile v1.0");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset v1.0");
EntityExportData<Asset> exportData = exportEntity(tenantAdmin1, asset.getId());
EntityImportResult<Asset> importResult = importEntity(tenantAdmin1, exportData);
@ -112,8 +123,9 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void testExportImportAsset_sameTenant_withCustomer() throws Exception {
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile v1.0");
Customer customer = createCustomer(tenantId1, "My customer");
Asset asset = createAsset(tenantId1, customer.getId(), "AB", "My asset");
Asset asset = createAsset(tenantId1, customer.getId(), assetProfile.getId(), "My asset");
Asset importedAsset = importEntity(tenantAdmin1, this.<Asset, AssetId>exportEntity(tenantAdmin1, asset.getId())).getSavedEntity();
assertThat(importedAsset.getCustomerId()).isEqualTo(asset.getCustomerId());
@ -238,8 +250,9 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void testExportImportDashboard_betweenTenants_withEntityAliases() throws Exception {
Asset asset1 = createAsset(tenantId1, null, "A", "Asset 1");
Asset asset2 = createAsset(tenantId1, null, "A", "Asset 2");
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "A");
Asset asset1 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
Asset asset2 = createAsset(tenantId1, null, assetProfile.getId(), "Asset 2");
Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1");
String entityAliases = "{\n" +
@ -263,10 +276,13 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
dashboard.setConfiguration(dashboardConfiguration);
dashboard = dashboardService.saveDashboard(dashboard);
EntityExportData<AssetProfile> profileExportData = exportEntity(tenantAdmin1, assetProfile.getId());
EntityExportData<Asset> asset1ExportData = exportEntity(tenantAdmin1, asset1.getId());
EntityExportData<Asset> asset2ExportData = exportEntity(tenantAdmin1, asset2.getId());
EntityExportData<Dashboard> dashboardExportData = exportEntity(tenantAdmin1, dashboard.getId());
AssetProfile importedProfile = importEntity(tenantAdmin2, profileExportData).getSavedEntity();
Asset importedAsset1 = importEntity(tenantAdmin2, asset1ExportData).getSavedEntity();
Asset importedAsset2 = importEntity(tenantAdmin2, asset2ExportData).getSavedEntity();
Dashboard importedDashboard = importEntity(tenantAdmin2, dashboardExportData).getSavedEntity();
@ -310,7 +326,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void testExportImportWithInboundRelations_betweenTenants() throws Exception {
Asset asset = createAsset(tenantId1, null, "A", "Asset 1");
Asset asset = createAsset(tenantId1, null, null, "Asset 1");
Device device = createDevice(tenantId1, null, null, "Device 1");
EntityRelation relation = createRelation(asset.getId(), device.getId());
@ -324,6 +340,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
assertThat(deviceExportData.getRelations().get(0)).matches(entityRelation -> {
return entityRelation.getFrom().equals(asset.getId()) && entityRelation.getTo().equals(device.getId());
});
((Asset) assetExportData.getEntity()).setAssetProfileId(null);
((Device) deviceExportData.getEntity()).setDeviceProfileId(null);
Asset importedAsset = importEntity(tenantAdmin2, assetExportData).getSavedEntity();
@ -344,7 +361,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void testExportImportWithRelations_betweenTenants() throws Exception {
Asset asset = createAsset(tenantId1, null, "A", "Asset 1");
Asset asset = createAsset(tenantId1, null, null, "Asset 1");
Device device = createDevice(tenantId1, null, null, "Device 1");
EntityRelation relation = createRelation(asset.getId(), device.getId());
@ -353,6 +370,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
.exportRelations(true)
.exportCredentials(false)
.build());
assetExportData.getEntity().setAssetProfileId(null);
deviceExportData.getEntity().setDeviceProfileId(null);
Asset importedAsset = importEntity(tenantAdmin2, assetExportData).getSavedEntity();
@ -371,7 +389,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void testExportImportWithRelations_sameTenant() throws Exception {
Asset asset = createAsset(tenantId1, null, "A", "Asset 1");
Asset asset = createAsset(tenantId1, null, null, "Asset 1");
Device device1 = createDevice(tenantId1, null, null, "Device 1");
EntityRelation relation1 = createRelation(asset.getId(), device1.getId());
@ -394,7 +412,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void textExportImportWithRelations_sameTenant_removeExisting() throws Exception {
Asset asset1 = createAsset(tenantId1, null, "A", "Asset 1");
Asset asset1 = createAsset(tenantId1, null, null, "Asset 1");
Device device = createDevice(tenantId1, null, null, "Device 1");
EntityRelation relation1 = createRelation(asset1.getId(), device.getId());
@ -403,7 +421,7 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
.build());
assertThat(deviceExportData.getRelations()).size().isOne();
Asset asset2 = createAsset(tenantId1, null, "A", "Asset 2");
Asset asset2 = createAsset(tenantId1, null, null, "Asset 2");
EntityRelation relation2 = createRelation(asset2.getId(), device.getId());
importEntity(tenantAdmin1, deviceExportData, EntityImportSettings.builder()
@ -443,14 +461,15 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void testEntityEventsOnImport() throws Exception {
Customer customer = createCustomer(tenantId1, "Customer 1");
Asset asset = createAsset(tenantId1, null, "A", "Asset 1");
RuleChain ruleChain = createRuleChain(tenantId1, "Rule chain 1");
Dashboard dashboard = createDashboard(tenantId1, null, "Dashboard 1");
AssetProfile assetProfile = createAssetProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Asset profile 1");
Asset asset = createAsset(tenantId1, null, assetProfile.getId(), "Asset 1");
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, null, deviceProfile.getId(), "Device 1");
Map<EntityType, EntityExportData> entitiesExportData = Stream.of(customer.getId(), asset.getId(), device.getId(),
ruleChain.getId(), dashboard.getId(), deviceProfile.getId())
ruleChain.getId(), dashboard.getId(), assetProfile.getId(), deviceProfile.getId())
.map(entityId -> {
try {
return exportEntity(tenantAdmin1, entityId, EntityExportSettings.builder()
@ -480,6 +499,21 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
Mockito.reset(entityActionService);
RuleChain importedRuleChain = (RuleChain) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.RULE_CHAIN)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedRuleChain.getId()), eq(importedRuleChain),
any(), eq(ActionType.ADDED), isNull());
verify(tbClusterService).broadcastEntityStateChangeEvent(any(), eq(importedRuleChain.getId()), eq(ComponentLifecycleEvent.CREATED));
Dashboard importedDashboard = (Dashboard) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DASHBOARD)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedDashboard.getId()), eq(importedDashboard),
any(), eq(ActionType.ADDED), isNull());
AssetProfile importedAssetProfile = (AssetProfile) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.ASSET_PROFILE)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedAssetProfile.getId()), eq(importedAssetProfile),
any(), eq(ActionType.ADDED), isNull());
verify(tbClusterService).broadcastEntityStateChangeEvent(any(), eq(importedAssetProfile.getId()), eq(ComponentLifecycleEvent.CREATED));
verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedAssetProfile.getId()), any(), any(), eq(EdgeEventActionType.ADDED));
Asset importedAsset = (Asset) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.ASSET)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedAsset.getId()), eq(importedAsset),
any(), eq(ActionType.ADDED), isNull());
@ -496,15 +530,6 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
any(), eq(ActionType.UPDATED), isNull());
verify(tbClusterService).sendNotificationMsgToEdge(any(), any(), eq(importedAsset.getId()), any(), any(), eq(EdgeEventActionType.UPDATED));
RuleChain importedRuleChain = (RuleChain) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.RULE_CHAIN)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedRuleChain.getId()), eq(importedRuleChain),
any(), eq(ActionType.ADDED), isNull());
verify(tbClusterService).broadcastEntityStateChangeEvent(any(), eq(importedRuleChain.getId()), eq(ComponentLifecycleEvent.CREATED));
Dashboard importedDashboard = (Dashboard) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DASHBOARD)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedDashboard.getId()), eq(importedDashboard),
any(), eq(ActionType.ADDED), isNull());
DeviceProfile importedDeviceProfile = (DeviceProfile) importEntity(tenantAdmin2, getAndClone(entitiesExportData, EntityType.DEVICE_PROFILE)).getSavedEntity();
verify(entityActionService).logEntityAction(any(), eq(importedDeviceProfile.getId()), eq(importedDeviceProfile),
any(), eq(ActionType.ADDED), isNull());
@ -529,15 +554,21 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
@Test
public void testExternalIdsInExportData() throws Exception {
Customer customer = createCustomer(tenantId1, "Customer 1");
Asset asset = createAsset(tenantId1, customer.getId(), "A", "Asset 1");
AssetProfile assetProfile = createAssetProfile(tenantId1, null, null, "Asset profile 1");
Asset asset = createAsset(tenantId1, customer.getId(), assetProfile.getId(), "Asset 1");
RuleChain ruleChain = createRuleChain(tenantId1, "Rule chain 1", asset.getId());
Dashboard dashboard = createDashboard(tenantId1, customer.getId(), "Dashboard 1", asset.getId());
assetProfile.setDefaultRuleChainId(ruleChain.getId());
assetProfile.setDefaultDashboardId(dashboard.getId());
assetProfile = assetProfileService.saveAssetProfile(assetProfile);
DeviceProfile deviceProfile = createDeviceProfile(tenantId1, ruleChain.getId(), dashboard.getId(), "Device profile 1");
Device device = createDevice(tenantId1, customer.getId(), deviceProfile.getId(), "Device 1");
EntityView entityView = createEntityView(tenantId1, customer.getId(), device.getId(), "Entity view 1");
Map<EntityId, EntityId> ids = new HashMap<>();
for (EntityId entityId : List.of(customer.getId(), asset.getId(), ruleChain.getId(), dashboard.getId(),
for (EntityId entityId : List.of(customer.getId(), ruleChain.getId(), dashboard.getId(), assetProfile.getId(), asset.getId(),
deviceProfile.getId(), device.getId(), entityView.getId(), ruleChain.getId(), dashboard.getId())) {
EntityExportData exportData = exportEntity(getSecurityUser(tenantAdmin1), entityId);
EntityImportResult importResult = importEntity(getSecurityUser(tenantAdmin2), exportData, EntityImportSettings.builder()
@ -546,6 +577,10 @@ public class ExportImportServiceSqlTest extends BaseExportImportServiceTest {
ids.put(entityId, (EntityId) importResult.getSavedEntity().getId());
}
AssetProfile exportedAssetProfile = (AssetProfile) exportEntity(tenantAdmin2, (AssetProfileId) ids.get(assetProfile.getId())).getEntity();
assertThat(exportedAssetProfile.getDefaultRuleChainId()).isEqualTo(ruleChain.getId());
assertThat(exportedAssetProfile.getDefaultDashboardId()).isEqualTo(dashboard.getId());
Asset exportedAsset = (Asset) exportEntity(tenantAdmin2, (AssetId) ids.get(asset.getId())).getEntity();
assertThat(exportedAsset.getCustomerId()).isEqualTo(customer.getId());

5
application/src/test/resources/application-test.properties

@ -56,4 +56,7 @@ queue.rule-engine.queues[2].processing-strategy.retries=1
queue.rule-engine.queues[2].processing-strategy.pause-between-retries=0
queue.rule-engine.queues[2].processing-strategy.max-pause-between-retries=0
usage.stats.report.enabled=false
usage.stats.report.enabled=false
sql.audit_logs.partition_size=24
sql.ttl.audit_logs.ttl=2592000

6
common/cluster-api/src/main/java/org/thingsboard/server/cluster/TbClusterService.java

@ -29,6 +29,8 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg;
import org.thingsboard.server.common.msg.edge.FromEdgeSyncResponse;
import org.thingsboard.server.common.msg.edge.ToEdgeSyncRequest;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.gen.transport.TransportProtos.ToVersionControlServiceMsg;
@ -88,5 +90,9 @@ public interface TbClusterService extends TbQueueClusterService {
void onEdgeEventUpdate(TenantId tenantId, EdgeId edgeId);
void pushEdgeSyncRequestToCore(ToEdgeSyncRequest toEdgeSyncRequest);
void pushEdgeSyncResponseToCore(FromEdgeSyncResponse fromEdgeSyncResponse);
void sendNotificationMsgToEdge(TenantId tenantId, EdgeId edgeId, EntityId entityId, String body, EdgeEventType type, EdgeEventActionType action);
}

17
common/cluster-api/src/main/proto/jsinvoke.proto

@ -23,6 +23,7 @@ enum JsInvokeErrorCode {
COMPILATION_ERROR = 0;
RUNTIME_ERROR = 1;
TIMEOUT_ERROR = 2;
NOT_FOUND_ERROR = 3;
}
message RemoteJsRequest {
@ -40,39 +41,34 @@ message RemoteJsResponse {
}
message JsCompileRequest {
int64 scriptIdMSB = 1;
int64 scriptIdLSB = 2;
string functionName = 3;
string scriptBody = 4;
string scriptHash = 5;
}
message JsReleaseRequest {
int64 scriptIdMSB = 1;
int64 scriptIdLSB = 2;
string functionName = 3;
string scriptHash = 4;
}
message JsReleaseResponse {
bool success = 1;
int64 scriptIdMSB = 2;
int64 scriptIdLSB = 3;
string scriptHash = 4;
}
message JsCompileResponse {
bool success = 1;
int64 scriptIdMSB = 2;
int64 scriptIdLSB = 3;
JsInvokeErrorCode errorCode = 4;
string errorDetails = 5;
string scriptHash = 6;
}
message JsInvokeRequest {
int64 scriptIdMSB = 1;
int64 scriptIdLSB = 2;
string functionName = 3;
string scriptBody = 4;
int32 timeout = 5;
repeated string args = 6;
string scriptHash = 7;
}
message JsInvokeResponse {
@ -81,4 +77,3 @@ message JsInvokeResponse {
JsInvokeErrorCode errorCode = 3;
string errorDetails = 4;
}

2
common/cluster-api/src/main/proto/queue.proto

@ -932,6 +932,8 @@ message ToCoreNotificationMsg {
QueueUpdateMsg queueUpdateMsg = 5;
QueueDeleteMsg queueDeleteMsg = 6;
VersionControlResponseMsg vcResponseMsg = 7;
bytes toEdgeSyncRequestMsg = 8;
bytes fromEdgeSyncResponseMsg = 9;
}
/* Messages that are handled by ThingsBoard RuleEngine Service */

17
common/coap-server/src/main/java/org/thingsboard/server/coapserver/TbCoapDtlsSettings.java

@ -16,10 +16,8 @@
package org.thingsboard.server.coapserver;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.californium.elements.config.CertificateAuthenticationMode;
import org.eclipse.californium.elements.config.Configuration;
import org.eclipse.californium.elements.util.SslContextUtil;
import org.eclipse.californium.scandium.config.DtlsConfig;
import org.eclipse.californium.scandium.config.DtlsConnectorConfig;
import org.eclipse.californium.scandium.dtls.CertificateType;
import org.eclipse.californium.scandium.dtls.x509.SingleCertificateProvider;
@ -40,6 +38,13 @@ import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.eclipse.californium.elements.config.CertificateAuthenticationMode.WANTED;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_CLIENT_AUTHENTICATION_MODE;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_RETRANSMISSION_TIMEOUT;
import static org.eclipse.californium.scandium.config.DtlsConfig.DTLS_ROLE;
import static org.eclipse.californium.scandium.config.DtlsConfig.DtlsRole.SERVER_ONLY;
@Slf4j
@ConditionalOnProperty(prefix = "transport.coap.dtls", value = "enabled", havingValue = "true", matchIfMissing = false)
@Component
@ -51,6 +56,9 @@ public class TbCoapDtlsSettings {
@Value("${transport.coap.dtls.bind_port}")
private Integer port;
@Value("${transport.coap.dtls.retransmission_timeout:9000}")
private int dtlsRetransmissionTimeout;
@Bean
@ConfigurationProperties(prefix = "transport.coap.dtls.credentials")
public SslCredentialsConfig coapDtlsCredentials() {
@ -82,8 +90,9 @@ public class TbCoapDtlsSettings {
SslCredentials sslCredentials = this.coapDtlsCredentialsConfig.getCredentials();
SslContextUtil.Credentials serverCredentials =
new SslContextUtil.Credentials(sslCredentials.getPrivateKey(), null, sslCredentials.getCertificateChain());
configBuilder.set(DtlsConfig.DTLS_ROLE, DtlsConfig.DtlsRole.SERVER_ONLY);
configBuilder.set(DtlsConfig.DTLS_CLIENT_AUTHENTICATION_MODE, CertificateAuthenticationMode.WANTED);
configBuilder.set(DTLS_CLIENT_AUTHENTICATION_MODE, WANTED);
configBuilder.set(DTLS_RETRANSMISSION_TIMEOUT, dtlsRetransmissionTimeout, MILLISECONDS);
configBuilder.set(DTLS_ROLE, SERVER_ONLY);
configBuilder.setAdvancedCertificateVerifier(
new TbCoapDtlsCertificateVerifier(
transportService,

53
common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetProfileService.java

@ -0,0 +1,53 @@
/**
* Copyright © 2016-2022 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.dao.asset;
import org.thingsboard.server.common.data.asset.AssetProfile;
import org.thingsboard.server.common.data.asset.AssetProfileInfo;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
public interface AssetProfileService {
AssetProfile findAssetProfileById(TenantId tenantId, AssetProfileId assetProfileId);
AssetProfile findAssetProfileByName(TenantId tenantId, String profileName);
AssetProfileInfo findAssetProfileInfoById(TenantId tenantId, AssetProfileId assetProfileId);
AssetProfile saveAssetProfile(AssetProfile assetProfile);
void deleteAssetProfile(TenantId tenantId, AssetProfileId assetProfileId);
PageData<AssetProfile> findAssetProfiles(TenantId tenantId, PageLink pageLink);
PageData<AssetProfileInfo> findAssetProfileInfos(TenantId tenantId, PageLink pageLink);
AssetProfile findOrCreateAssetProfile(TenantId tenantId, String profileName);
AssetProfile createDefaultAssetProfile(TenantId tenantId);
AssetProfile findDefaultAssetProfile(TenantId tenantId);
AssetProfileInfo findDefaultAssetProfileInfo(TenantId tenantId);
boolean setDefaultAssetProfile(TenantId tenantId, AssetProfileId assetProfileId);
void deleteAssetProfilesByTenantId(TenantId tenantId);
}

7
common/dao-api/src/main/java/org/thingsboard/server/dao/asset/AssetService.java

@ -21,15 +21,14 @@ import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.asset.AssetInfo;
import org.thingsboard.server.common.data.asset.AssetSearchQuery;
import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.AssetProfileId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import java.util.List;
import java.util.Optional;
public interface AssetService {
@ -57,6 +56,8 @@ public interface AssetService {
PageData<AssetInfo> findAssetInfosByTenantIdAndType(TenantId tenantId, String type, PageLink pageLink);
PageData<AssetInfo> findAssetInfosByTenantIdAndAssetProfileId(TenantId tenantId, AssetProfileId assetProfileId, PageLink pageLink);
ListenableFuture<List<Asset>> findAssetsByTenantIdAndIdsAsync(TenantId tenantId, List<AssetId> assetIds);
void deleteAssetsByTenantId(TenantId tenantId);
@ -69,6 +70,8 @@ public interface AssetService {
PageData<AssetInfo> findAssetInfosByTenantIdAndCustomerIdAndType(TenantId tenantId, CustomerId customerId, String type, PageLink pageLink);
PageData<AssetInfo> findAssetInfosByTenantIdAndCustomerIdAndAssetProfileId(TenantId tenantId, CustomerId customerId, AssetProfileId assetProfileId, PageLink pageLink);
ListenableFuture<List<Asset>> findAssetsByTenantIdCustomerIdAndIdsAsync(TenantId tenantId, CustomerId customerId, List<AssetId> assetIds);
void unassignCustomerAssets(TenantId tenantId, CustomerId customerId);

2
common/dao-api/src/main/java/org/thingsboard/server/dao/attributes/AttributesService.java

@ -39,6 +39,8 @@ public interface AttributesService {
ListenableFuture<List<String>> save(TenantId tenantId, EntityId entityId, String scope, List<AttributeKvEntry> attributes);
ListenableFuture<String> save(TenantId tenantId, EntityId entityId, String scope, AttributeKvEntry attribute);
ListenableFuture<List<String>> removeAll(TenantId tenantId, EntityId entityId, String scope, List<String> attributeKeys);
List<String> findAllKeysByDeviceProfileId(TenantId tenantId, DeviceProfileId deviceProfileId);

5
common/dao-api/src/main/java/org/thingsboard/server/dao/device/DeviceCredentialsService.java

@ -18,8 +18,7 @@ package org.thingsboard.server.dao.device;
import org.thingsboard.server.common.data.id.DeviceId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import java.util.List;
import com.fasterxml.jackson.databind.JsonNode;
public interface DeviceCredentialsService {
@ -33,6 +32,8 @@ public interface DeviceCredentialsService {
void formatCredentials(DeviceCredentials deviceCredentials);
JsonNode toCredentialsInfo(DeviceCredentials deviceCredentials);
void deleteDeviceCredentials(TenantId tenantId, DeviceCredentials deviceCredentials);
}

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

@ -21,19 +21,25 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.DeleteTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQueryResult;
import org.thingsboard.server.common.data.kv.TsKvLatestRemovingResult;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
/**
* @author Andrew Shvayka
*/
public interface TimeseriesService {
ListenableFuture<List<ReadTsKvQueryResult>> findAllByQueries(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries);
ListenableFuture<List<TsKvEntry>> findAll(TenantId tenantId, EntityId entityId, List<ReadTsKvQuery> queries);
ListenableFuture<Optional<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, String keys);
ListenableFuture<List<TsKvEntry>> findLatest(TenantId tenantId, EntityId entityId, Collection<String> keys);
ListenableFuture<List<TsKvEntry>> findAllLatest(TenantId tenantId, EntityId entityId);

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

Loading…
Cancel
Save