diff --git a/application/pom.xml b/application/pom.xml
index f81ae293d2..c2ae7eccc2 100644
--- a/application/pom.xml
+++ b/application/pom.xml
@@ -20,7 +20,7 @@
4.0.0
org.thingsboard
- 3.1.1-SNAPSHOT
+ 3.2.0-SNAPSHOT
thingsboard
application
@@ -46,10 +46,6 @@
-
- de.ruedigermoeller
- fst
-
io.netty
netty-transport-native-epoll
diff --git a/application/src/main/data/json/demo/rule_chains/root_rule_chain.json b/application/src/main/data/json/demo/rule_chains/root_rule_chain.json
index 9805c6f996..97ad46c756 100644
--- a/application/src/main/data/json/demo/rule_chains/root_rule_chain.json
+++ b/application/src/main/data/json/demo/rule_chains/root_rule_chain.json
@@ -43,7 +43,8 @@
"name": "Save Client Attributes",
"debugMode": false,
"configuration": {
- "scope": "CLIENT_SCOPE"
+ "scope": "CLIENT_SCOPE",
+ "notifyDevice": "false"
}
},
{
diff --git a/application/src/main/data/json/system/widget_bundles/charts.json b/application/src/main/data/json/system/widget_bundles/charts.json
index 26292c60b3..22c9d20a23 100644
--- a/application/src/main/data/json/system/widget_bundles/charts.json
+++ b/application/src/main/data/json/system/widget_bundles/charts.json
@@ -166,7 +166,7 @@
"controllerScript": "self.onInit = function() {\n self.ctx.flot = new TbFlot(self.ctx, 'state'); \n}\n\nself.onDataUpdated = function() {\n self.ctx.flot.update();\n}\n\nself.onResize = function() {\n self.ctx.flot.resize();\n}\n\nself.typeParameters = function() {\n return {\n stateData: true\n };\n}\n\nself.onEditModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.onMobileModeChanged = function() {\n self.ctx.flot.checkMouseEvents();\n}\n\nself.getSettingsSchema = function() {\n return TbFlot.settingsSchema('graph');\n}\n\nself.getDataKeySettingsSchema = function() {\n return TbFlot.datakeySettingsSchema(true, 'graph');\n}\n\nself.onDestroy = function() {\n self.ctx.flot.destroy();\n}\n",
"settingsSchema": "{}",
"dataKeySettingsSchema": "{}",
- "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
+ "defaultConfig": "{\"datasources\":[{\"type\":\"function\",\"name\":\"function\",\"dataKeys\":[{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 1\",\"color\":\"#2196f3\",\"settings\":{\"showLines\":true,\"fillLines\":true,\"showPoints\":false,\"axisPosition\":\"left\",\"showSeparateAxis\":false},\"_hash\":0.8587686344902596,\"funcBody\":\"return Math.random() > 0.5 ? 1 : 0;\"},{\"name\":\"f(x)\",\"type\":\"function\",\"label\":\"Switch 2\",\"color\":\"#ffc107\",\"settings\":{\"showLines\":true,\"fillLines\":false,\"showPoints\":false,\"axisPosition\":\"left\"},\"_hash\":0.12775350966079668,\"funcBody\":\"return Math.random() <= 0.5 ? 1 : 0;\"}]}],\"timewindow\":{\"realtime\":{\"timewindowMs\":60000}},\"showTitle\":true,\"backgroundColor\":\"#fff\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"8px\",\"settings\":{\"shadowSize\":4,\"fontColor\":\"#545454\",\"fontSize\":10,\"xaxis\":{\"showLabels\":true,\"color\":\"#545454\"},\"yaxis\":{\"showLabels\":true,\"color\":\"#545454\",\"ticksFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\"},\"grid\":{\"color\":\"#545454\",\"tickColor\":\"#DDDDDD\",\"verticalLines\":true,\"horizontalLines\":true,\"outlineWidth\":1},\"stack\":false,\"tooltipIndividual\":false,\"tooltipValueFormatter\":\"if (value > 0 && value <= 1) {\\n return 'On';\\n} else if (value === 0) {\\n return 'Off';\\n} else {\\n return '';\\n}\",\"smoothLines\":false},\"title\":\"State Chart\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400},\"mobileHeight\":null,\"widgetStyle\":{},\"useDashboardTimewindow\":true,\"showLegend\":true,\"actions\":{},\"legendConfig\":{\"direction\":\"column\",\",position\":\"bottom\",\"showMin\":false,\"showMax\":false,\"showAvg\":false,\"showTotal\":false}}"
}
}
]
diff --git a/application/src/main/data/json/tenant/device_profile/rule_chain_template.json b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json
new file mode 100644
index 0000000000..3d076ff812
--- /dev/null
+++ b/application/src/main/data/json/tenant/device_profile/rule_chain_template.json
@@ -0,0 +1,135 @@
+{
+ "ruleChain": {
+ "additionalInfo": {
+ "description": ""
+ },
+ "name": "Device Profile Rule Chain Template",
+ "firstRuleNodeId": null,
+ "root": false,
+ "debugMode": false,
+ "configuration": null
+ },
+ "metadata": {
+ "firstNodeIndex": 6,
+ "nodes": [
+ {
+ "additionalInfo": {
+ "layoutX": 822,
+ "layoutY": 294
+ },
+ "type": "org.thingsboard.rule.engine.telemetry.TbMsgTimeseriesNode",
+ "name": "Save Timeseries",
+ "debugMode": false,
+ "configuration": {
+ "defaultTTL": 0
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 824,
+ "layoutY": 221
+ },
+ "type": "org.thingsboard.rule.engine.telemetry.TbMsgAttributesNode",
+ "name": "Save Client Attributes",
+ "debugMode": false,
+ "configuration": {
+ "scope": "CLIENT_SCOPE"
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 494,
+ "layoutY": 309
+ },
+ "type": "org.thingsboard.rule.engine.filter.TbMsgTypeSwitchNode",
+ "name": "Message Type Switch",
+ "debugMode": false,
+ "configuration": {
+ "version": 0
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 824,
+ "layoutY": 383
+ },
+ "type": "org.thingsboard.rule.engine.action.TbLogNode",
+ "name": "Log RPC from Device",
+ "debugMode": false,
+ "configuration": {
+ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 823,
+ "layoutY": 444
+ },
+ "type": "org.thingsboard.rule.engine.action.TbLogNode",
+ "name": "Log Other",
+ "debugMode": false,
+ "configuration": {
+ "jsScript": "return '\\nIncoming message:\\n' + JSON.stringify(msg) + '\\nIncoming metadata:\\n' + JSON.stringify(metadata);"
+ }
+ },
+ {
+ "additionalInfo": {
+ "layoutX": 822,
+ "layoutY": 507
+ },
+ "type": "org.thingsboard.rule.engine.rpc.TbSendRPCRequestNode",
+ "name": "RPC Call Request",
+ "debugMode": false,
+ "configuration": {
+ "timeoutInSeconds": 60
+ }
+ },
+ {
+ "additionalInfo": {
+ "description": "",
+ "layoutX": 209,
+ "layoutY": 307
+ },
+ "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
+ "name": "Device Profile Node",
+ "debugMode": false,
+ "configuration": {
+ "persistAlarmRulesState": false
+ }
+ }
+ ],
+ "connections": [
+ {
+ "fromIndex": 2,
+ "toIndex": 4,
+ "type": "Other"
+ },
+ {
+ "fromIndex": 2,
+ "toIndex": 1,
+ "type": "Post attributes"
+ },
+ {
+ "fromIndex": 2,
+ "toIndex": 0,
+ "type": "Post telemetry"
+ },
+ {
+ "fromIndex": 2,
+ "toIndex": 3,
+ "type": "RPC Request from Device"
+ },
+ {
+ "fromIndex": 2,
+ "toIndex": 5,
+ "type": "RPC Request to Device"
+ },
+ {
+ "fromIndex": 6,
+ "toIndex": 2,
+ "type": "Success"
+ }
+ ],
+ "ruleChainConnections": null
+ }
+}
\ No newline at end of file
diff --git a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
index 59b7021aa7..a37b1fc865 100644
--- a/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
+++ b/application/src/main/data/json/tenant/rule_chains/root_rule_chain.json
@@ -8,7 +8,7 @@
"configuration": null
},
"metadata": {
- "firstNodeIndex": 2,
+ "firstNodeIndex": 6,
"nodes": [
{
"additionalInfo": {
@@ -31,7 +31,8 @@
"name": "Save Client Attributes",
"debugMode": false,
"configuration": {
- "scope": "CLIENT_SCOPE"
+ "scope": "CLIENT_SCOPE",
+ "notifyDevice": "false"
}
},
{
@@ -81,9 +82,28 @@
"configuration": {
"timeoutInSeconds": 60
}
+ },
+ {
+ "additionalInfo": {
+ "description": "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.",
+ "layoutX": 204,
+ "layoutY": 240
+ },
+ "type": "org.thingsboard.rule.engine.profile.TbDeviceProfileNode",
+ "name": "Device Profile Node",
+ "debugMode": false,
+ "configuration": {
+ "persistAlarmRulesState": false,
+ "fetchAlarmRulesStateOnStart": false
+ }
}
],
"connections": [
+ {
+ "fromIndex": 6,
+ "toIndex": 2,
+ "type": "Success"
+ },
{
"fromIndex": 2,
"toIndex": 4,
diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql b/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql
index 0916c241a1..41e1cfbb7a 100644
--- a/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql
+++ b/application/src/main/data/upgrade/2.4.3/schema_update_psql_drop_partitions.sql
@@ -64,6 +64,7 @@ BEGIN
AND tablename like 'ts_kv_' || '%'
AND tablename != 'ts_kv_latest'
AND tablename != 'ts_kv_dictionary'
+ AND tablename != 'ts_kv_indefinite'
LOOP
IF partition != partition_by_max_ttl_date THEN
IF partition_year IS NOT NULL THEN
diff --git a/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql b/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql
index 5e20e1c664..7de74032ce 100644
--- a/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql
+++ b/application/src/main/data/upgrade/2.4.3/schema_update_ttl.sql
@@ -59,8 +59,8 @@ $$
DECLARE
tenant_cursor CURSOR FOR select tenant.id as tenant_id
from tenant;
- tenant_id_record varchar;
- customer_id_record varchar;
+ tenant_id_record uuid;
+ customer_id_record uuid;
tenant_ttl bigint;
customer_ttl bigint;
deleted_for_entities bigint;
diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_after.sql b/application/src/main/data/upgrade/3.1.1/schema_update_after.sql
new file mode 100644
index 0000000000..c8f9d2970e
--- /dev/null
+++ b/application/src/main/data/upgrade/3.1.1/schema_update_after.sql
@@ -0,0 +1,28 @@
+--
+-- Copyright © 2016-2020 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_tenant_profiles;
+DROP PROCEDURE IF EXISTS update_device_profiles;
+
+ALTER TABLE tenant ALTER COLUMN tenant_profile_id SET NOT NULL;
+ALTER TABLE tenant DROP CONSTRAINT IF EXISTS fk_tenant_profile;
+ALTER TABLE tenant ADD CONSTRAINT fk_tenant_profile FOREIGN KEY (tenant_profile_id) REFERENCES tenant_profile(id);
+ALTER TABLE tenant DROP COLUMN IF EXISTS isolated_tb_core;
+ALTER TABLE tenant DROP COLUMN IF EXISTS isolated_tb_rule_engine;
+
+ALTER TABLE device ALTER COLUMN device_profile_id SET NOT NULL;
+ALTER TABLE device DROP CONSTRAINT IF EXISTS fk_device_profile;
+ALTER TABLE device ADD CONSTRAINT fk_device_profile FOREIGN KEY (device_profile_id) REFERENCES device_profile(id);
diff --git a/application/src/main/data/upgrade/3.1.1/schema_update_before.sql b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql
new file mode 100644
index 0000000000..c1591e7831
--- /dev/null
+++ b/application/src/main/data/upgrade/3.1.1/schema_update_before.sql
@@ -0,0 +1,81 @@
+--
+-- Copyright © 2016-2020 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 device_profile (
+ id uuid NOT NULL CONSTRAINT device_profile_pkey PRIMARY KEY,
+ created_time bigint NOT NULL,
+ name varchar(255),
+ type varchar(255),
+ transport_type varchar(255),
+ profile_data jsonb,
+ description varchar,
+ search_text varchar(255),
+ is_default boolean,
+ tenant_id uuid,
+ default_rule_chain_id uuid,
+ CONSTRAINT device_profile_name_unq_key UNIQUE (tenant_id, name),
+ CONSTRAINT fk_default_rule_chain_device_profile FOREIGN KEY (default_rule_chain_id) REFERENCES rule_chain(id)
+);
+
+CREATE TABLE IF NOT EXISTS tenant_profile (
+ id uuid NOT NULL CONSTRAINT tenant_profile_pkey PRIMARY KEY,
+ created_time bigint NOT NULL,
+ name varchar(255),
+ profile_data jsonb,
+ description varchar,
+ search_text varchar(255),
+ is_default boolean,
+ isolated_tb_core boolean,
+ isolated_tb_rule_engine boolean,
+ CONSTRAINT tenant_profile_name_unq_key UNIQUE (name)
+);
+
+CREATE OR REPLACE PROCEDURE update_tenant_profiles()
+ LANGUAGE plpgsql AS
+$$
+BEGIN
+ UPDATE tenant as t SET tenant_profile_id = p.id
+ FROM
+ (SELECT id from tenant_profile WHERE isolated_tb_core = false AND isolated_tb_rule_engine = false) as p
+ WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = false AND t.isolated_tb_rule_engine = false;
+
+ UPDATE tenant as t SET tenant_profile_id = p.id
+ FROM
+ (SELECT id from tenant_profile WHERE isolated_tb_core = true AND isolated_tb_rule_engine = false) as p
+ WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = true AND t.isolated_tb_rule_engine = false;
+
+ UPDATE tenant as t SET tenant_profile_id = p.id
+ FROM
+ (SELECT id from tenant_profile WHERE isolated_tb_core = false AND isolated_tb_rule_engine = true) as p
+ WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = false AND t.isolated_tb_rule_engine = true;
+
+ UPDATE tenant as t SET tenant_profile_id = p.id
+ FROM
+ (SELECT id from tenant_profile WHERE isolated_tb_core = true AND isolated_tb_rule_engine = true) as p
+ WHERE t.tenant_profile_id IS NULL AND t.isolated_tb_core = true AND t.isolated_tb_rule_engine = true;
+END;
+$$;
+
+CREATE OR REPLACE PROCEDURE update_device_profiles()
+ LANGUAGE plpgsql AS
+$$
+BEGIN
+ UPDATE device as d SET device_profile_id = p.id, device_data = '{"configuration":{"type":"DEFAULT"}, "transportConfiguration":{"type":"DEFAULT"}}'
+ FROM
+ (SELECT id, tenant_id, name from device_profile) as p
+ WHERE d.device_profile_id IS NULL AND p.tenant_id = d.tenant_id AND d.type = p.name;
+END;
+$$;
diff --git a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
index b8425ccfed..8f6a290af4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ActorSystemContext.java
@@ -32,6 +32,7 @@ import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.thingsboard.rule.engine.api.MailService;
+import org.thingsboard.rule.engine.api.RuleEngineDeviceProfileCache;
import org.thingsboard.server.actors.service.ActorService;
import org.thingsboard.server.actors.tenant.DebugTbRateLimits;
import org.thingsboard.server.common.data.DataConstants;
@@ -44,7 +45,6 @@ import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
-import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.audit.AuditLogService;
@@ -58,17 +58,20 @@ import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.dao.nosql.CassandraBufferedRateExecutor;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.rule.RuleNodeStateService;
+import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.discovery.TbServiceInfoProvider;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
-import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
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.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
@@ -89,7 +92,6 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
@Component
@@ -125,6 +127,10 @@ public class ActorSystemContext {
@Getter
private DeviceService deviceService;
+ @Autowired
+ @Getter
+ private TbDeviceProfileCache deviceProfileCache;
+
@Autowired
@Getter
private AssetService assetService;
@@ -137,6 +143,10 @@ public class ActorSystemContext {
@Getter
private TenantService tenantService;
+ @Autowired
+ @Getter
+ private TenantProfileService tenantProfileService;
+
@Autowired
@Getter
private CustomerService customerService;
@@ -149,6 +159,10 @@ public class ActorSystemContext {
@Getter
private RuleChainService ruleChainService;
+ @Autowired
+ @Getter
+ private RuleNodeStateService ruleNodeStateService;
+
@Autowired
private PartitionService partitionService;
@@ -527,4 +541,5 @@ public class ActorSystemContext {
log.debug("Scheduling msg {} with delay {} ms", msg, delayInMs);
getScheduler().schedule(() -> ctx.tell(msg), delayInMs, TimeUnit.MILLISECONDS);
}
+
}
diff --git a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
index 953188f3f7..16beb3a045 100644
--- a/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/app/AppActor.java
@@ -27,6 +27,7 @@ import org.thingsboard.server.actors.service.DefaultActorService;
import org.thingsboard.server.actors.tenant.TenantActor;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageDataIterable;
@@ -116,7 +117,9 @@ public class AppActor extends ContextAwareActor {
boolean isRuleEngine = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
boolean isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE);
for (Tenant tenant : tenantIterator) {
- if (isCore || (isRuleEngine && !tenant.isIsolatedTbRuleEngine())) {
+ // TODO: Tenant Profile from cache
+ TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId());
+ if (isCore || (isRuleEngine && !tenantProfile.isIsolatedTbRuleEngine())) {
log.debug("[{}] Creating tenant actor", tenant.getId());
getOrCreateTenantActor(tenant.getId());
log.debug("[{}] Tenant actor created.", tenant.getId());
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
index d20554357b..a9ddaea2e4 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
@@ -23,6 +23,7 @@ import org.springframework.data.redis.core.RedisTemplate;
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.RuleEngineDeviceProfileCache;
import org.thingsboard.rule.engine.api.RuleEngineRpcService;
import org.thingsboard.rule.engine.api.RuleEngineTelemetryService;
import org.thingsboard.rule.engine.api.ScriptEngine;
@@ -39,13 +40,15 @@ 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.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.rule.RuleNode;
+import org.thingsboard.server.common.data.rule.RuleNodeState;
import org.thingsboard.server.common.msg.TbActorMsg;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
-import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.cassandra.CassandraCluster;
@@ -67,7 +70,6 @@ import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import java.util.Collections;
import java.util.Set;
-import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
@@ -105,6 +107,7 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) {
relationTypes.forEach(relationType -> mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), msg, relationType, th));
}
+ msg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId());
nodeCtx.getChainActor().tell(new RuleNodeToRuleChainTellNextMsg(nodeCtx.getSelf().getId(), relationTypes, msg, th != null ? th.getMessage() : null));
}
@@ -122,7 +125,7 @@ class DefaultTbContext implements TbContext {
@Override
public void enqueue(TbMsg tbMsg, String queueName, Runnable onSuccess, Consumer onFailure) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName);
enqueue(tpi, tbMsg, onFailure, onSuccess);
}
@@ -139,46 +142,54 @@ class DefaultTbContext implements TbContext {
@Override
public void enqueueForTellFailure(TbMsg tbMsg, String failureMessage) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = resolvePartition(tbMsg);
enqueueForTellNext(tpi, tbMsg, Collections.singleton(TbRelationTypes.FAILURE), failureMessage, null, null);
}
@Override
public void enqueueForTellNext(TbMsg tbMsg, String relationType) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = resolvePartition(tbMsg);
enqueueForTellNext(tpi, tbMsg, Collections.singleton(relationType), null, null, null);
}
@Override
public void enqueueForTellNext(TbMsg tbMsg, Set relationTypes) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = resolvePartition(tbMsg);
enqueueForTellNext(tpi, tbMsg, relationTypes, null, null, null);
}
@Override
public void enqueueForTellNext(TbMsg tbMsg, String relationType, Runnable onSuccess, Consumer onFailure) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = resolvePartition(tbMsg);
enqueueForTellNext(tpi, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure);
}
@Override
public void enqueueForTellNext(TbMsg tbMsg, Set relationTypes, Runnable onSuccess, Consumer onFailure) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = resolvePartition(tbMsg);
enqueueForTellNext(tpi, tbMsg, relationTypes, null, onSuccess, onFailure);
}
@Override
public void enqueueForTellNext(TbMsg tbMsg, String queueName, String relationType, Runnable onSuccess, Consumer onFailure) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName);
enqueueForTellNext(tpi, tbMsg, Collections.singleton(relationType), null, onSuccess, onFailure);
}
@Override
public void enqueueForTellNext(TbMsg tbMsg, String queueName, Set relationTypes, Runnable onSuccess, Consumer onFailure) {
- TopicPartitionInfo tpi = mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator());
+ TopicPartitionInfo tpi = resolvePartition(tbMsg, queueName);
enqueueForTellNext(tpi, tbMsg, relationTypes, null, onSuccess, onFailure);
}
+ private TopicPartitionInfo resolvePartition(TbMsg tbMsg, String queueName) {
+ return mainCtx.resolve(ServiceType.TB_RULE_ENGINE, queueName, getTenantId(), tbMsg.getOriginator());
+ }
+
+ private TopicPartitionInfo resolvePartition(TbMsg tbMsg) {
+ return resolvePartition(tbMsg, tbMsg.getQueueName());
+ }
+
private void enqueueForTellNext(TopicPartitionInfo tpi, TbMsg source, Set relationTypes, String failureMessage, Runnable onSuccess, Consumer onFailure) {
RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId();
RuleNodeId ruleNodeId = nodeCtx.getSelf().getId();
@@ -203,6 +214,7 @@ class DefaultTbContext implements TbContext {
if (nodeCtx.getSelf().isDebugMode()) {
mainCtx.persistDebugOutput(nodeCtx.getTenantId(), nodeCtx.getSelf().getId(), tbMsg, "ACK", null);
}
+ tbMsg.getCallback().onProcessingEnd(nodeCtx.getSelf().getId());
tbMsg.getCallback().onSuccess();
}
@@ -388,6 +400,11 @@ class DefaultTbContext implements TbContext {
return mainCtx.getEntityViewService();
}
+ @Override
+ public RuleEngineDeviceProfileCache getDeviceProfileCache() {
+ return mainCtx.getDeviceProfileCache();
+ }
+
@Override
public EventLoopGroup getSharedEventLoop() {
return mainCtx.getSharedEventLoopGroupService().getSharedEventLoopGroup();
@@ -422,6 +439,30 @@ class DefaultTbContext implements TbContext {
return mainCtx.getRedisTemplate();
}
+ @Override
+ public PageData findRuleNodeStates(PageLink pageLink) {
+ if (log.isDebugEnabled()) {
+ log.debug("[{}][{}] Fetch Rule Node States.", getTenantId(), getSelfId());
+ }
+ return mainCtx.getRuleNodeStateService().findByRuleNodeId(getTenantId(), getSelfId(), pageLink);
+ }
+
+ @Override
+ public RuleNodeState findRuleNodeStateForEntity(EntityId entityId) {
+ if (log.isDebugEnabled()) {
+ log.debug("[{}][{}][{}] Fetch Rule Node State for entity.", getTenantId(), getSelfId(), entityId);
+ }
+ return mainCtx.getRuleNodeStateService().findByRuleNodeIdAndEntityId(getTenantId(), getSelfId(), entityId);
+ }
+
+ @Override
+ public RuleNodeState saveRuleNodeState(RuleNodeState state) {
+ if (log.isDebugEnabled()) {
+ log.debug("[{}][{}][{}] Persist Rule Node State for entity: {}", getTenantId(), getSelfId(), state.getEntityId(), state.getStateData());
+ }
+ state.setRuleNodeId(getSelfId());
+ return mainCtx.getRuleNodeStateService().save(getTenantId(), state);
+ }
private TbMsgMetaData getActionMetaData(RuleNodeId ruleNodeId) {
TbMsgMetaData metaData = new TbMsgMetaData();
diff --git a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
index 7583ea553e..db55ff8edf 100644
--- a/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
+++ b/application/src/main/java/org/thingsboard/server/actors/ruleChain/RuleNodeActorMessageProcessor.java
@@ -103,7 +103,7 @@ public class RuleNodeActorMessageProcessor extends ComponentMsgProcessor isolatedTenantId = systemContext.getServiceInfoProvider().getIsolatedTenant();
+ // TODO: Tenant Profile from cache
+
+ TenantProfile tenantProfile = systemContext.getTenantProfileService().findTenantProfileById(tenantId, tenant.getTenantProfileId());
+
isRuleEngineForCurrentTenant = systemContext.getServiceInfoProvider().isService(ServiceType.TB_RULE_ENGINE);
isCore = systemContext.getServiceInfoProvider().isService(ServiceType.TB_CORE);
if (isRuleEngineForCurrentTenant) {
try {
- if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenant.isIsolatedTbRuleEngine())) {
+ if (isolatedTenantId.map(id -> id.equals(tenantId)).orElseGet(() -> !tenantProfile.isIsolatedTbRuleEngine())) {
log.info("[{}] Going to init rule chains", tenantId);
initRuleChains();
} else {
@@ -111,6 +117,9 @@ public class TenantActor extends RuleChainManagerActor {
if (msg.getMsgType().equals(MsgType.QUEUE_TO_RULE_ENGINE_MSG)) {
QueueToRuleEngineMsg queueMsg = (QueueToRuleEngineMsg) msg;
queueMsg.getTbMsg().getCallback().onSuccess();
+ } else if (msg.getMsgType().equals(MsgType.TRANSPORT_TO_DEVICE_ACTOR_MSG)){
+ TransportToDeviceActorMsgWrapper transportMsg = (TransportToDeviceActorMsgWrapper) msg;
+ transportMsg.getCallback().onSuccess();
}
return true;
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
index f692d8dd60..4d063d07f1 100644
--- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java
@@ -90,7 +90,7 @@ public class AlarmController extends BaseController {
checkEntity(alarm.getId(), alarm, Resource.ALARM);
Alarm savedAlarm = checkNotNull(alarmService.createOrUpdateAlarm(alarm));
- logEntityAction(savedAlarm.getId(), savedAlarm,
+ logEntityAction(savedAlarm.getOriginator(), savedAlarm,
getCurrentUser().getCustomerId(),
alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
return savedAlarm;
@@ -126,7 +126,7 @@ public class AlarmController extends BaseController {
long ackTs = System.currentTimeMillis();
alarmService.ackAlarm(getCurrentUser().getTenantId(), alarmId, ackTs).get();
alarm.setAckTs(ackTs);
- logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null);
+ logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_ACK, null);
} catch (Exception e) {
throw handleException(e);
}
@@ -143,7 +143,7 @@ public class AlarmController extends BaseController {
long clearTs = System.currentTimeMillis();
alarmService.clearAlarm(getCurrentUser().getTenantId(), alarmId, null, clearTs).get();
alarm.setClearTs(clearTs);
- logEntityAction(alarmId, alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null);
+ logEntityAction(alarm.getOriginator(), alarm, getCurrentUser().getCustomerId(), ActionType.ALARM_CLEAR, null);
} catch (Exception e) {
throw handleException(e);
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
index 2c202e71b2..8434f67d1d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java
@@ -33,12 +33,15 @@ import org.thingsboard.server.common.data.DashboardInfo;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.DeviceInfo;
+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.EntityViewInfo;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.HasTenantId;
import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TenantInfo;
+import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.alarm.Alarm;
import org.thingsboard.server.common.data.alarm.AlarmInfo;
@@ -52,12 +55,14 @@ import org.thingsboard.server.common.data.id.AssetId;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DashboardId;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
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.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.id.WidgetTypeId;
import org.thingsboard.server.common.data.id.WidgetsBundleId;
@@ -82,6 +87,7 @@ 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.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
@@ -89,6 +95,7 @@ import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
+import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetTypeService;
@@ -98,6 +105,7 @@ import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.component.ComponentDiscoveryService;
+import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.TbClusterService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.AccessControlService;
@@ -134,6 +142,9 @@ public abstract class BaseController {
@Autowired
protected TenantService tenantService;
+ @Autowired
+ protected TenantProfileService tenantProfileService;
+
@Autowired
protected CustomerService customerService;
@@ -143,6 +154,9 @@ public abstract class BaseController {
@Autowired
protected DeviceService deviceService;
+ @Autowired
+ protected DeviceProfileService deviceProfileService;
+
@Autowired
protected AssetService assetService;
@@ -197,6 +211,9 @@ public abstract class BaseController {
@Autowired
protected TbQueueProducerProvider producerProvider;
+ @Autowired
+ protected TbDeviceProfileCache deviceProfileCache;
+
@Value("${server.log_controller_error_stack_trace}")
@Getter
private boolean logControllerErrorStackTrace;
@@ -312,6 +329,30 @@ public abstract class BaseController {
}
}
+ TenantInfo checkTenantInfoId(TenantId tenantId, Operation operation) throws ThingsboardException {
+ try {
+ validateId(tenantId, INCORRECT_TENANT_ID + tenantId);
+ TenantInfo tenant = tenantService.findTenantInfoById(tenantId);
+ checkNotNull(tenant);
+ accessControlService.checkPermission(getCurrentUser(), Resource.TENANT, operation, tenantId, tenant);
+ return tenant;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
+ TenantProfile checkTenantProfileId(TenantProfileId tenantProfileId, Operation operation) throws ThingsboardException {
+ try {
+ validateId(tenantProfileId, "Incorrect tenantProfileId " + tenantProfileId);
+ TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(getTenantId(), tenantProfileId);
+ checkNotNull(tenantProfile);
+ accessControlService.checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, operation);
+ return tenantProfile;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
protected TenantId getTenantId() throws ThingsboardException {
return getCurrentUser().getTenantId();
}
@@ -360,12 +401,18 @@ public abstract class BaseController {
case DEVICE:
checkDeviceId(new DeviceId(entityId.getId()), operation);
return;
+ case DEVICE_PROFILE:
+ checkDeviceProfileId(new DeviceProfileId(entityId.getId()), operation);
+ return;
case CUSTOMER:
checkCustomerId(new CustomerId(entityId.getId()), operation);
return;
case TENANT:
checkTenantId(new TenantId(entityId.getId()), operation);
return;
+ case TENANT_PROFILE:
+ checkTenantProfileId(new TenantProfileId(entityId.getId()), operation);
+ return;
case RULE_CHAIN:
checkRuleChain(new RuleChainId(entityId.getId()), operation);
return;
@@ -422,6 +469,18 @@ public abstract class BaseController {
}
}
+ DeviceProfile checkDeviceProfileId(DeviceProfileId deviceProfileId, Operation operation) throws ThingsboardException {
+ try {
+ validateId(deviceProfileId, "Incorrect deviceProfileId " + deviceProfileId);
+ DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(getCurrentUser().getTenantId(), deviceProfileId);
+ checkNotNull(deviceProfile);
+ accessControlService.checkPermission(getCurrentUser(), Resource.DEVICE_PROFILE, operation, deviceProfileId, deviceProfile);
+ return deviceProfile;
+ } catch (Exception e) {
+ throw handleException(e, false);
+ }
+ }
+
protected EntityView checkEntityViewId(EntityViewId entityViewId, Operation operation) throws ThingsboardException {
try {
validateId(entityViewId, "Incorrect entityViewId " + entityViewId);
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
index d0a662d2ec..c8fde63c4d 100644
--- a/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceController.java
@@ -47,6 +47,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
@@ -315,6 +316,7 @@ public class DeviceController extends BaseController {
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String type,
+ @RequestParam(required = false) String deviceProfileId,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
@@ -323,6 +325,9 @@ public class DeviceController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndType(tenantId, type, pageLink));
+ } else if (deviceProfileId != null && deviceProfileId.length() > 0) {
+ DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId));
+ return checkNotNull(deviceService.findDeviceInfosByTenantIdAndDeviceProfileId(tenantId, profileId, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantId(tenantId, pageLink));
}
@@ -379,6 +384,7 @@ public class DeviceController extends BaseController {
@RequestParam int pageSize,
@RequestParam int page,
@RequestParam(required = false) String type,
+ @RequestParam(required = false) String deviceProfileId,
@RequestParam(required = false) String textSearch,
@RequestParam(required = false) String sortProperty,
@RequestParam(required = false) String sortOrder) throws ThingsboardException {
@@ -390,6 +396,9 @@ public class DeviceController extends BaseController {
PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
if (type != null && type.trim().length() > 0) {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndType(tenantId, customerId, type, pageLink));
+ } else if (deviceProfileId != null && deviceProfileId.length() > 0) {
+ DeviceProfileId profileId = new DeviceProfileId(toUUID(deviceProfileId));
+ return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerIdAndDeviceProfileId(tenantId, customerId, profileId, pageLink));
} else {
return checkNotNull(deviceService.findDeviceInfosByTenantIdAndCustomerId(tenantId, customerId, pageLink));
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
new file mode 100644
index 0000000000..b474a0c0f1
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
@@ -0,0 +1,203 @@
+/**
+ * Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j;
+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.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceProfileInfo;
+import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.audit.ActionType;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.security.permission.Resource;
+
+@RestController
+@TbCoreComponent
+@RequestMapping("/api")
+@Slf4j
+public class DeviceProfileController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.GET)
+ @ResponseBody
+ public DeviceProfile getDeviceProfileById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
+ checkParameter("deviceProfileId", strDeviceProfileId);
+ try {
+ DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
+ return checkDeviceProfileId(deviceProfileId, Operation.READ);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/deviceProfileInfo/{deviceProfileId}", method = RequestMethod.GET)
+ @ResponseBody
+ public DeviceProfileInfo getDeviceProfileInfoById(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
+ checkParameter("deviceProfileId", strDeviceProfileId);
+ try {
+ DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
+ return checkNotNull(deviceProfileService.findDeviceProfileInfoById(getTenantId(), deviceProfileId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/deviceProfileInfo/default", method = RequestMethod.GET)
+ @ResponseBody
+ public DeviceProfileInfo getDefaultDeviceProfileInfo() throws ThingsboardException {
+ try {
+ return checkNotNull(deviceProfileService.findDefaultDeviceProfileInfo(getTenantId()));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/deviceProfile", method = RequestMethod.POST)
+ @ResponseBody
+ public DeviceProfile saveDeviceProfile(@RequestBody DeviceProfile deviceProfile) throws ThingsboardException {
+ try {
+ boolean created = deviceProfile.getId() == null;
+ deviceProfile.setTenantId(getTenantId());
+
+ checkEntity(deviceProfile.getId(), deviceProfile, Resource.DEVICE_PROFILE);
+
+ DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile));
+
+ deviceProfileCache.put(savedDeviceProfile);
+ tbClusterService.onDeviceProfileChange(savedDeviceProfile, null);
+ tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), savedDeviceProfile.getId(),
+ created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
+
+ logEntityAction(savedDeviceProfile.getId(), savedDeviceProfile,
+ null,
+ savedDeviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
+
+ return savedDeviceProfile;
+ } catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE_PROFILE), deviceProfile,
+ null, deviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/deviceProfile/{deviceProfileId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
+ checkParameter("deviceProfileId", strDeviceProfileId);
+ try {
+ DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
+ DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
+ deviceProfileService.deleteDeviceProfile(getTenantId(), deviceProfileId);
+ deviceProfileCache.evict(deviceProfileId);
+
+ tbClusterService.onDeviceProfileDelete(deviceProfile, null);
+ tbClusterService.onEntityStateChange(deviceProfile.getTenantId(), deviceProfile.getId(), ComponentLifecycleEvent.DELETED);
+
+ logEntityAction(deviceProfileId, deviceProfile,
+ null,
+ ActionType.DELETED, null, strDeviceProfileId);
+
+ } catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE_PROFILE),
+ null,
+ null,
+ ActionType.DELETED, e, strDeviceProfileId);
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/deviceProfile/{deviceProfileId}/default", method = RequestMethod.POST)
+ @ResponseBody
+ public DeviceProfile setDefaultDeviceProfile(@PathVariable("deviceProfileId") String strDeviceProfileId) throws ThingsboardException {
+ checkParameter("deviceProfileId", strDeviceProfileId);
+ try {
+ DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
+ DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE);
+ DeviceProfile previousDefaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(getTenantId());
+ if (deviceProfileService.setDefaultDeviceProfile(getTenantId(), deviceProfileId)) {
+ if (previousDefaultDeviceProfile != null) {
+ previousDefaultDeviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), previousDefaultDeviceProfile.getId());
+
+ logEntityAction(previousDefaultDeviceProfile.getId(), previousDefaultDeviceProfile,
+ null, ActionType.UPDATED, null);
+ }
+ deviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), deviceProfileId);
+
+ logEntityAction(deviceProfile.getId(), deviceProfile,
+ null, ActionType.UPDATED, null);
+ }
+ return deviceProfile;
+ } catch (Exception e) {
+ logEntityAction(emptyId(EntityType.DEVICE_PROFILE),
+ null,
+ null,
+ ActionType.UPDATED, e, strDeviceProfileId);
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/deviceProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
+ @ResponseBody
+ public PageData getDeviceProfiles(@RequestParam int pageSize,
+ @RequestParam int page,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String sortProperty,
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return checkNotNull(deviceProfileService.findDeviceProfiles(getTenantId(), pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")
+ @RequestMapping(value = "/deviceProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
+ @ResponseBody
+ public PageData getDeviceProfileInfos(@RequestParam int pageSize,
+ @RequestParam int page,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String sortProperty,
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return checkNotNull(deviceProfileService.findDeviceProfileInfos(getTenantId(), pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
index 7e8291356d..5c54c0232e 100644
--- a/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/RuleChainController.java
@@ -24,6 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
@@ -47,7 +48,10 @@ 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.plugin.ComponentLifecycleEvent;
+import org.thingsboard.server.common.data.rule.DefaultRuleChainCreateRequest;
import org.thingsboard.server.common.data.rule.RuleChain;
+import org.thingsboard.server.common.data.rule.RuleChainData;
+import org.thingsboard.server.common.data.rule.RuleChainImportResult;
import org.thingsboard.server.common.data.rule.RuleChainMetaData;
import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.common.msg.TbMsg;
@@ -55,6 +59,7 @@ import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
import org.thingsboard.server.dao.event.EventService;
import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.install.InstallScripts;
import org.thingsboard.server.service.script.JsInvokeService;
import org.thingsboard.server.service.script.RuleNodeJsScriptEngine;
import org.thingsboard.server.service.security.permission.Operation;
@@ -77,6 +82,9 @@ public class RuleChainController extends BaseController {
private static final ObjectMapper objectMapper = new ObjectMapper();
+ @Autowired
+ private InstallScripts installScripts;
+
@Autowired
private EventService eventService;
@@ -146,6 +154,27 @@ public class RuleChainController extends BaseController {
}
}
+ @PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChain/device/default", method = RequestMethod.POST)
+ @ResponseBody
+ public RuleChain saveRuleChain(@RequestBody DefaultRuleChainCreateRequest request) throws ThingsboardException {
+ try {
+ checkNotNull(request);
+ checkNotNull(request.getName());
+
+ RuleChain savedRuleChain = installScripts.createDefaultRuleChain(getCurrentUser().getTenantId(), request.getName());
+
+ logEntityAction(savedRuleChain.getId(), savedRuleChain, null, ActionType.ADDED, null);
+
+ return savedRuleChain;
+ } catch (Exception e) {
+ RuleChain ruleChain = new RuleChain();
+ ruleChain.setName(request.getName());
+ logEntityAction(emptyId(EntityType.RULE_CHAIN), ruleChain, null, ActionType.ADDED, e);
+ throw handleException(e);
+ }
+ }
+
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN')")
@RequestMapping(value = "/ruleChain/{ruleChainId}/root", method = RequestMethod.POST)
@ResponseBody
@@ -360,6 +389,36 @@ public class RuleChainController extends BaseController {
}
}
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChains/export", params = {"limit"}, method = RequestMethod.GET)
+ @ResponseBody
+ public RuleChainData exportRuleChains(@RequestParam("limit") int limit) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ PageLink pageLink = new PageLink(limit);
+ return checkNotNull(ruleChainService.exportTenantRuleChains(tenantId, pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('TENANT_ADMIN')")
+ @RequestMapping(value = "/ruleChains/import", method = RequestMethod.POST)
+ @ResponseBody
+ public void importRuleChains(@RequestBody RuleChainData ruleChainData, @RequestParam(required = false, defaultValue = "false") boolean overwrite) throws ThingsboardException {
+ try {
+ TenantId tenantId = getCurrentUser().getTenantId();
+ List importResults = ruleChainService.importTenantRuleChains(tenantId, ruleChainData, overwrite);
+ if (!CollectionUtils.isEmpty(importResults)) {
+ for (RuleChainImportResult importResult : importResults) {
+ tbClusterService.onEntityStateChange(importResult.getTenantId(), importResult.getRuleChainId(), importResult.getLifecycleEvent());
+ }
+ }
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
private String msgToOutput(TbMsg msg) throws Exception {
ObjectNode msgData = objectMapper.createObjectNode();
if (!StringUtils.isEmpty(msg.getData())) {
diff --git a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
index f61565d320..c8c41ea479 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TelemetryController.java
@@ -197,19 +197,21 @@ public class TelemetryController extends BaseController {
@RequestMapping(value = "/{entityType}/{entityId}/values/timeseries", method = RequestMethod.GET, params = {"keys", "startTs", "endTs"})
@ResponseBody
public DeferredResult getTimeseries(
- @PathVariable("entityType") String entityType, @PathVariable("entityId") String entityIdStr,
+ @PathVariable("entityType") String entityType,
+ @PathVariable("entityId") String entityIdStr,
@RequestParam(name = "keys") String keys,
@RequestParam(name = "startTs") Long startTs,
@RequestParam(name = "endTs") Long endTs,
@RequestParam(name = "interval", defaultValue = "0") Long interval,
@RequestParam(name = "limit", defaultValue = "100") Integer limit,
@RequestParam(name = "agg", defaultValue = "NONE") String aggStr,
+ @RequestParam(name= "orderBy", defaultValue = "DESC") String orderBy,
@RequestParam(name = "useStrictDataTypes", required = false, defaultValue = "false") Boolean useStrictDataTypes) throws ThingsboardException {
return accessValidator.validateEntityAndCallback(getCurrentUser(), Operation.READ_TELEMETRY, entityType, entityIdStr,
(result, tenantId, entityId) -> {
// If interval is 0, convert this to a NONE aggregation, which is probably what the user really wanted
Aggregation agg = interval == 0L ? Aggregation.valueOf(Aggregation.NONE.name()) : Aggregation.valueOf(aggStr);
- List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg))
+ List queries = toKeysList(keys).stream().map(key -> new BaseReadTsKvQuery(key, startTs, endTs, interval, limit, agg, orderBy))
.collect(Collectors.toList());
Futures.addCallback(tsService.findAll(tenantId, entityId, queries), getTsKvListCallback(result, useStrictDataTypes), MoreExecutors.directExecutor());
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantController.java b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
index 59eea87f51..ebb46778c9 100644
--- a/application/src/main/java/org/thingsboard/server/controller/TenantController.java
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantController.java
@@ -28,6 +28,7 @@ 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.Tenant;
+import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.page.PageData;
@@ -58,8 +59,20 @@ public class TenantController extends BaseController {
checkParameter("tenantId", strTenantId);
try {
TenantId tenantId = new TenantId(toUUID(strTenantId));
- checkTenantId(tenantId, Operation.READ);
- return checkNotNull(tenantService.findTenantById(tenantId));
+ return checkTenantId(tenantId, Operation.READ);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')")
+ @RequestMapping(value = "/tenant/info/{tenantId}", method = RequestMethod.GET)
+ @ResponseBody
+ public TenantInfo getTenantInfoById(@PathVariable("tenantId") String strTenantId) throws ThingsboardException {
+ checkParameter("tenantId", strTenantId);
+ try {
+ TenantId tenantId = new TenantId(toUUID(strTenantId));
+ return checkTenantInfoId(tenantId, Operation.READ);
} catch (Exception e) {
throw handleException(e);
}
@@ -115,4 +128,20 @@ public class TenantController extends BaseController {
}
}
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
+ @ResponseBody
+ public PageData getTenantInfos(@RequestParam int pageSize,
+ @RequestParam int page,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String sortProperty,
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return checkNotNull(tenantService.findTenantInfos(pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
}
diff --git a/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
new file mode 100644
index 0000000000..dfe0b19a42
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/controller/TenantProfileController.java
@@ -0,0 +1,162 @@
+/**
+ * Copyright © 2016-2020 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 lombok.extern.slf4j.Slf4j;
+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.EntityInfo;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.exception.ThingsboardException;
+import org.thingsboard.server.common.data.id.TenantProfileId;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.queue.util.TbCoreComponent;
+import org.thingsboard.server.service.security.permission.Operation;
+import org.thingsboard.server.service.security.permission.Resource;
+
+@RestController
+@TbCoreComponent
+@RequestMapping("/api")
+@Slf4j
+public class TenantProfileController extends BaseController {
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.GET)
+ @ResponseBody
+ public TenantProfile getTenantProfileById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
+ checkParameter("tenantProfileId", strTenantProfileId);
+ try {
+ TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
+ return checkTenantProfileId(tenantProfileId, Operation.READ);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantProfileInfo/{tenantProfileId}", method = RequestMethod.GET)
+ @ResponseBody
+ public EntityInfo getTenantProfileInfoById(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
+ checkParameter("tenantProfileId", strTenantProfileId);
+ try {
+ TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
+ return checkNotNull(tenantProfileService.findTenantProfileInfoById(getTenantId(), tenantProfileId));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantProfileInfo/default", method = RequestMethod.GET)
+ @ResponseBody
+ public EntityInfo getDefaultTenantProfileInfo() throws ThingsboardException {
+ try {
+ return checkNotNull(tenantProfileService.findDefaultTenantProfileInfo(getTenantId()));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantProfile", method = RequestMethod.POST)
+ @ResponseBody
+ public TenantProfile saveTenantProfile(@RequestBody TenantProfile tenantProfile) throws ThingsboardException {
+ try {
+ boolean newTenantProfile = tenantProfile.getId() == null;
+ if (newTenantProfile) {
+ accessControlService
+ .checkPermission(getCurrentUser(), Resource.TENANT_PROFILE, Operation.CREATE);
+ } else {
+ checkEntityId(tenantProfile.getId(), Operation.WRITE);
+ }
+
+ tenantProfile = checkNotNull(tenantProfileService.saveTenantProfile(getTenantId(), tenantProfile));
+ return tenantProfile;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantProfile/{tenantProfileId}", method = RequestMethod.DELETE)
+ @ResponseStatus(value = HttpStatus.OK)
+ public void deleteTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
+ checkParameter("tenantProfileId", strTenantProfileId);
+ try {
+ TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
+ checkTenantProfileId(tenantProfileId, Operation.DELETE);
+ tenantProfileService.deleteTenantProfile(getTenantId(), tenantProfileId);
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantProfile/{tenantProfileId}/default", method = RequestMethod.POST)
+ @ResponseBody
+ public TenantProfile setDefaultTenantProfile(@PathVariable("tenantProfileId") String strTenantProfileId) throws ThingsboardException {
+ checkParameter("tenantProfileId", strTenantProfileId);
+ try {
+ TenantProfileId tenantProfileId = new TenantProfileId(toUUID(strTenantProfileId));
+ TenantProfile tenantProfile = checkTenantProfileId(tenantProfileId, Operation.WRITE);
+ tenantProfileService.setDefaultTenantProfile(getTenantId(), tenantProfileId);
+ return tenantProfile;
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantProfiles", params = {"pageSize", "page"}, method = RequestMethod.GET)
+ @ResponseBody
+ public PageData getTenantProfiles(@RequestParam int pageSize,
+ @RequestParam int page,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String sortProperty,
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return checkNotNull(tenantProfileService.findTenantProfiles(getTenantId(), pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+
+ @PreAuthorize("hasAuthority('SYS_ADMIN')")
+ @RequestMapping(value = "/tenantProfileInfos", params = {"pageSize", "page"}, method = RequestMethod.GET)
+ @ResponseBody
+ public PageData getTenantProfileInfos(@RequestParam int pageSize,
+ @RequestParam int page,
+ @RequestParam(required = false) String textSearch,
+ @RequestParam(required = false) String sortProperty,
+ @RequestParam(required = false) String sortOrder) throws ThingsboardException {
+ try {
+ PageLink pageLink = createPageLink(pageSize, page, textSearch, sortProperty, sortOrder);
+ return checkNotNull(tenantProfileService.findTenantProfileInfos(getTenantId(), pageLink));
+ } catch (Exception e) {
+ throw handleException(e);
+ }
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
index de7b2bdce3..7bcf38080e 100644
--- a/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
+++ b/application/src/main/java/org/thingsboard/server/install/ThingsboardInstallService.java
@@ -175,6 +175,13 @@ public class ThingsboardInstallService {
case "3.1.0":
log.info("Upgrading ThingsBoard from version 3.1.0 to 3.1.1 ...");
databaseEntitiesUpgradeService.upgradeDatabase("3.1.0");
+ case "3.1.1":
+ log.info("Upgrading ThingsBoard from version 3.1.1 to 3.2.0 ...");
+ if (databaseTsUpgradeService != null) {
+ databaseTsUpgradeService.upgradeDatabase("3.1.1");
+ }
+ databaseEntitiesUpgradeService.upgradeDatabase("3.1.1");
+ dataUpdateService.updateData("3.1.1");
log.info("Updating system data...");
systemDataLoaderService.updateSystemWidgets();
break;
@@ -206,6 +213,7 @@ public class ThingsboardInstallService {
componentDiscoveryService.discoverComponents();
systemDataLoaderService.createSysAdmin();
+ systemDataLoaderService.createDefaultTenantProfiles();
systemDataLoaderService.createAdminSettings();
systemDataLoaderService.loadSystemWidgets();
// systemDataLoaderService.loadSystemPlugins();
diff --git a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java
index 58180583ef..17857e2807 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/CassandraTsDatabaseUpgradeService.java
@@ -49,6 +49,7 @@ public class CassandraTsDatabaseUpgradeService extends AbstractCassandraDatabase
log.info("Schema updated.");
break;
case "2.5.0":
+ case "3.1.1":
break;
default:
throw new RuntimeException("Unable to upgrade Cassandra database, unsupported fromVersion: " + fromVersion);
diff --git a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
index 6aa3e0cd2d..bb875ec5c4 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/DefaultSystemDataLoaderService.java
@@ -27,11 +27,15 @@ import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.Customer;
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.Tenant;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.TenantProfileData;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.asset.Asset;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry;
import org.thingsboard.server.common.data.kv.BooleanDataEntry;
@@ -46,9 +50,12 @@ import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.settings.AdminSettingsService;
+import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.widget.WidgetsBundleService;
@@ -82,6 +89,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private TenantService tenantService;
+ @Autowired
+ private TenantProfileService tenantProfileService;
+
@Autowired
private CustomerService customerService;
@@ -94,6 +104,9 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
@Autowired
private DeviceService deviceService;
+ @Autowired
+ private DeviceProfileService deviceProfileService;
+
@Autowired
private AttributesService attributesService;
@@ -110,6 +123,50 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
createUser(Authority.SYS_ADMIN, null, null, "sysadmin@thingsboard.org", "sysadmin");
}
+ @Override
+ public void createDefaultTenantProfiles() throws Exception {
+ tenantProfileService.findOrCreateDefaultTenantProfile(TenantId.SYS_TENANT_ID);
+
+ TenantProfile isolatedTbCoreProfile = new TenantProfile();
+ isolatedTbCoreProfile.setDefault(false);
+ isolatedTbCoreProfile.setName("Isolated TB Core");
+ isolatedTbCoreProfile.setProfileData(new TenantProfileData());
+ isolatedTbCoreProfile.setDescription("Isolated TB Core tenant profile");
+ isolatedTbCoreProfile.setIsolatedTbCore(true);
+ isolatedTbCoreProfile.setIsolatedTbRuleEngine(false);
+ try {
+ tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreProfile);
+ } catch (DataValidationException e) {
+ log.warn(e.getMessage());
+ }
+
+ TenantProfile isolatedTbRuleEngineProfile = new TenantProfile();
+ isolatedTbRuleEngineProfile.setDefault(false);
+ isolatedTbRuleEngineProfile.setName("Isolated TB Rule Engine");
+ isolatedTbRuleEngineProfile.setProfileData(new TenantProfileData());
+ isolatedTbRuleEngineProfile.setDescription("Isolated TB Rule Engine tenant profile");
+ isolatedTbRuleEngineProfile.setIsolatedTbCore(false);
+ isolatedTbRuleEngineProfile.setIsolatedTbRuleEngine(true);
+ try {
+ tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbRuleEngineProfile);
+ } catch (DataValidationException e) {
+ log.warn(e.getMessage());
+ }
+
+ TenantProfile isolatedTbCoreAndTbRuleEngineProfile = new TenantProfile();
+ isolatedTbCoreAndTbRuleEngineProfile.setDefault(false);
+ isolatedTbCoreAndTbRuleEngineProfile.setName("Isolated TB Core and TB Rule Engine");
+ isolatedTbCoreAndTbRuleEngineProfile.setProfileData(new TenantProfileData());
+ isolatedTbCoreAndTbRuleEngineProfile.setDescription("Isolated TB Core and TB Rule Engine tenant profile");
+ isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbCore(true);
+ isolatedTbCoreAndTbRuleEngineProfile.setIsolatedTbRuleEngine(true);
+ try {
+ tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, isolatedTbCoreAndTbRuleEngineProfile);
+ } catch (DataValidationException e) {
+ log.warn(e.getMessage());
+ }
+ }
+
@Override
public void createAdminSettings() throws Exception {
AdminSettings generalSettings = new AdminSettings();
@@ -162,16 +219,18 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
createUser(Authority.CUSTOMER_USER, demoTenant.getId(), customerB.getId(), "customerB@thingsboard.org", CUSTOMER_CRED);
createUser(Authority.CUSTOMER_USER, demoTenant.getId(), customerC.getId(), "customerC@thingsboard.org", CUSTOMER_CRED);
- createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A1", "A1_TEST_TOKEN", null);
- createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A2", "A2_TEST_TOKEN", null);
- createDevice(demoTenant.getId(), customerA.getId(), DEFAULT_DEVICE_TYPE, "Test Device A3", "A3_TEST_TOKEN", null);
- createDevice(demoTenant.getId(), customerB.getId(), DEFAULT_DEVICE_TYPE, "Test Device B1", "B1_TEST_TOKEN", null);
- createDevice(demoTenant.getId(), customerC.getId(), DEFAULT_DEVICE_TYPE, "Test Device C1", "C1_TEST_TOKEN", null);
+ DeviceProfile defaultDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), DEFAULT_DEVICE_TYPE);
+
+ createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A1", "A1_TEST_TOKEN", null);
+ createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A2", "A2_TEST_TOKEN", null);
+ createDevice(demoTenant.getId(), customerA.getId(), defaultDeviceProfile.getId(), "Test Device A3", "A3_TEST_TOKEN", null);
+ createDevice(demoTenant.getId(), customerB.getId(), defaultDeviceProfile.getId(), "Test Device B1", "B1_TEST_TOKEN", null);
+ createDevice(demoTenant.getId(), customerC.getId(), defaultDeviceProfile.getId(), "Test Device C1", "C1_TEST_TOKEN", null);
- createDevice(demoTenant.getId(), null, DEFAULT_DEVICE_TYPE, "DHT11 Demo Device", "DHT11_DEMO_TOKEN", "Demo device that is used in sample " +
+ createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "DHT11 Demo Device", "DHT11_DEMO_TOKEN", "Demo device that is used in sample " +
"applications that upload data from DHT11 temperature and humidity sensor");
- createDevice(demoTenant.getId(), null, DEFAULT_DEVICE_TYPE, "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " +
+ createDevice(demoTenant.getId(), null, defaultDeviceProfile.getId(), "Raspberry Pi Demo Device", "RASPBERRY_PI_DEMO_TOKEN", "Demo device that is used in " +
"Raspberry Pi GPIO control sample application");
Asset thermostatAlarms = new Asset();
@@ -180,8 +239,10 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
thermostatAlarms.setType("AlarmPropagationAsset");
thermostatAlarms = assetService.saveAsset(thermostatAlarms);
- DeviceId t1Id = createDevice(demoTenant.getId(), null, "thermostat", "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
- DeviceId t2Id = createDevice(demoTenant.getId(), null, "thermostat", "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
+ DeviceProfile thermostatDeviceProfile = this.deviceProfileService.findOrCreateDeviceProfile(demoTenant.getId(), "thermostat");
+
+ DeviceId t1Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T1", "T1_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
+ DeviceId t2Id = createDevice(demoTenant.getId(), null, thermostatDeviceProfile.getId(), "Thermostat T2", "T2_TEST_TOKEN", "Demo device for Thermostats dashboard").getId();
relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t1Id, "ToAlarmPropagationAsset"));
relationService.saveRelation(thermostatAlarms.getTenantId(), new EntityRelation(thermostatAlarms.getId(), t2Id, "ToAlarmPropagationAsset"));
@@ -257,14 +318,14 @@ public class DefaultSystemDataLoaderService implements SystemDataLoaderService {
private Device createDevice(TenantId tenantId,
CustomerId customerId,
- String type,
+ DeviceProfileId deviceProfileId,
String name,
String accessToken,
String description) {
Device device = new Device();
device.setTenantId(tenantId);
device.setCustomerId(customerId);
- device.setType(type);
+ device.setDeviceProfileId(deviceProfileId);
device.setName(name);
if (description != null) {
ObjectNode additionalInfo = objectMapper.createObjectNode();
diff --git a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
index 5181834b80..cd74753f31 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/InstallScripts.java
@@ -57,6 +57,7 @@ public class InstallScripts {
public static final String JSON_DIR = "json";
public static final String SYSTEM_DIR = "system";
public static final String TENANT_DIR = "tenant";
+ public static final String DEVICE_PROFILE_DIR = "device_profile";
public static final String DEMO_DIR = "demo";
public static final String RULE_CHAINS_DIR = "rule_chains";
public static final String WIDGET_BUNDLES_DIR = "widget_bundles";
@@ -83,6 +84,10 @@ public class InstallScripts {
return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, RULE_CHAINS_DIR);
}
+ public Path getDeviceProfileDefaultRuleChainTemplateFilePath() {
+ return Paths.get(getDataDir(), JSON_DIR, TENANT_DIR, DEVICE_PROFILE_DIR, "rule_chain_template.json");
+ }
+
public String getDataDir() {
if (!StringUtils.isEmpty(dataDir)) {
if (!Paths.get(this.dataDir).toFile().isDirectory()) {
@@ -110,15 +115,7 @@ public class InstallScripts {
dirStream.forEach(
path -> {
try {
- JsonNode ruleChainJson = objectMapper.readTree(path.toFile());
- RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
- RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
-
- ruleChain.setTenantId(tenantId);
- ruleChain = ruleChainService.saveRuleChain(ruleChain);
-
- ruleChainMetaData.setRuleChainId(ruleChain.getId());
- ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData);
+ createRuleChainFromFile(tenantId, path, null);
} catch (Exception e) {
log.error("Unable to load rule chain from json: [{}]", path.toString());
throw new RuntimeException("Unable to load rule chain from json", e);
@@ -128,6 +125,28 @@ public class InstallScripts {
}
}
+ public RuleChain createDefaultRuleChain(TenantId tenantId, String ruleChainName) throws IOException {
+ return createRuleChainFromFile(tenantId, getDeviceProfileDefaultRuleChainTemplateFilePath(), ruleChainName);
+ }
+
+ public RuleChain createRuleChainFromFile(TenantId tenantId, Path templateFilePath, String newRuleChainName) throws IOException {
+ JsonNode ruleChainJson = objectMapper.readTree(templateFilePath.toFile());
+ RuleChain ruleChain = objectMapper.treeToValue(ruleChainJson.get("ruleChain"), RuleChain.class);
+ RuleChainMetaData ruleChainMetaData = objectMapper.treeToValue(ruleChainJson.get("metadata"), RuleChainMetaData.class);
+
+ ruleChain.setTenantId(tenantId);
+ if (!StringUtils.isEmpty(newRuleChainName)) {
+ ruleChain.setName(newRuleChainName);
+ }
+ ruleChain = ruleChainService.saveRuleChain(ruleChain);
+
+ ruleChainMetaData.setRuleChainId(ruleChain.getId());
+ ruleChainService.saveRuleChainMetaData(new TenantId(EntityId.NULL_UUID), ruleChainMetaData);
+
+ return ruleChain;
+ }
+
+
public void loadSystemWidgets() throws Exception {
Path widgetBundlesDir = Paths.get(getDataDir(), JSON_DIR, SYSTEM_DIR, WIDGET_BUNDLES_DIR);
try (DirectoryStream dirStream = Files.newDirectoryStream(widgetBundlesDir, path -> path.toString().endsWith(JSON_EXT))) {
diff --git a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
index 7a8174af16..18433c6f53 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/PsqlTsDatabaseUpgradeService.java
@@ -195,6 +195,14 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005001");
}
break;
+ case "3.1.1":
+ try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+ log.info("Load TTL functions ...");
+ loadSql(conn, LOAD_TTL_FUNCTIONS_SQL);
+ log.info("Load Drop Partitions functions ...");
+ loadSql(conn, LOAD_DROP_PARTITIONS_FUNCTIONS_SQL);
+ }
+ break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
@@ -239,4 +247,4 @@ public class PsqlTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgradeSe
log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
}
}
-}
\ No newline at end of file
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
index 8cf471ab38..d6068b9616 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
@@ -20,7 +20,14 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
+import org.thingsboard.server.common.data.EntitySubtype;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.dao.dashboard.DashboardService;
+import org.thingsboard.server.dao.device.DeviceProfileService;
+import org.thingsboard.server.dao.device.DeviceService;
+import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.service.install.sql.SqlDbHelper;
import java.nio.charset.Charset;
@@ -34,6 +41,7 @@ import java.sql.SQLException;
import java.sql.SQLSyntaxErrorException;
import java.sql.SQLWarning;
import java.sql.Statement;
+import java.util.List;
import static org.thingsboard.server.service.install.DatabaseHelper.ADDITIONAL_INFO;
import static org.thingsboard.server.service.install.DatabaseHelper.ASSIGNED_CUSTOMERS;
@@ -76,6 +84,19 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
@Autowired
private InstallScripts installScripts;
+ @Autowired
+ private SystemDataLoaderService systemDataLoaderService;
+
+ @Autowired
+ private TenantService tenantService;
+
+ @Autowired
+ private DeviceService deviceService;
+
+ @Autowired
+ private DeviceProfileService deviceProfileService;
+
+
@Override
public void upgradeDatabase(String fromVersion) throws Exception {
switch (fromVersion) {
@@ -303,6 +324,77 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
log.info("Schema updated.");
}
break;
+ case "3.1.1":
+ try (Connection conn = DriverManager.getConnection(dbUrl, dbUserName, dbPassword)) {
+ log.info("Updating schema ...");
+ if (isOldSchema(conn, 3001000)) {
+
+ try {
+ conn.createStatement().execute("ALTER TABLE device ADD COLUMN device_profile_id uuid, ADD COLUMN device_data jsonb");
+ } catch (Exception e) {
+ }
+
+ try {
+ conn.createStatement().execute("ALTER TABLE tenant ADD COLUMN tenant_profile_id uuid");
+ } catch (Exception e) {
+ }
+
+ try {
+ conn.createStatement().execute("CREATE TABLE IF NOT EXISTS rule_node_state (" +
+ " id uuid NOT NULL CONSTRAINT rule_node_state_pkey PRIMARY KEY," +
+ " created_time bigint NOT NULL," +
+ " rule_node_id uuid NOT NULL," +
+ " entity_type varchar(32) NOT NULL," +
+ " entity_id uuid NOT NULL," +
+ " state_data varchar(16384) NOT NULL," +
+ " CONSTRAINT rule_node_state_unq_key UNIQUE (rule_node_id, entity_id)," +
+ " CONSTRAINT fk_rule_node_state_node_id FOREIGN KEY (rule_node_id) REFERENCES rule_node(id) ON DELETE CASCADE)");
+ } catch (Exception e) {
+ }
+
+ schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.1", "schema_update_before.sql");
+ loadSql(schemaUpdateFile, conn);
+
+ log.info("Creating default tenant profiles...");
+ systemDataLoaderService.createDefaultTenantProfiles();
+
+ log.info("Updating tenant profiles...");
+ conn.createStatement().execute("call update_tenant_profiles()");
+
+ log.info("Creating default device profiles...");
+ PageLink pageLink = new PageLink(100);
+ PageData pageData;
+ do {
+ pageData = tenantService.findTenants(pageLink);
+ for (Tenant tenant : pageData.getData()) {
+ List deviceTypes = deviceService.findDeviceTypesByTenantId(tenant.getId()).get();
+ try {
+ deviceProfileService.createDefaultDeviceProfile(tenant.getId());
+ } catch (Exception e) {
+ }
+ for (EntitySubtype deviceType : deviceTypes) {
+ try {
+ deviceProfileService.findOrCreateDeviceProfile(tenant.getId(), deviceType.getType());
+ } catch (Exception e) {
+ }
+ }
+ }
+ pageLink = pageLink.nextPageLink();
+ } while (pageData.hasNext());
+
+ log.info("Updating device profiles...");
+ conn.createStatement().execute("call update_device_profiles()");
+
+ schemaUpdateFile = Paths.get(installScripts.getDataDir(), "upgrade", "3.1.1", "schema_update_after.sql");
+ loadSql(schemaUpdateFile, conn);
+
+ conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3002000;");
+ }
+ 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);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
index 76e65deaa4..b588c2dff2 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/SystemDataLoaderService.java
@@ -19,6 +19,8 @@ public interface SystemDataLoaderService {
void createSysAdmin() throws Exception;
+ void createDefaultTenantProfiles() throws Exception;
+
void createAdminSettings() throws Exception;
void loadSystemWidgets() throws Exception;
diff --git a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
index d8f7ea61f9..756356581a 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/TimescaleTsDatabaseUpgradeService.java
@@ -177,6 +177,8 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
executeQuery(conn, "UPDATE tb_schema_settings SET schema_version = 2005001");
}
break;
+ case "3.1.1":
+ break;
default:
throw new RuntimeException("Unable to upgrade SQL database, unsupported fromVersion: " + fromVersion);
}
@@ -207,4 +209,4 @@ public class TimescaleTsDatabaseUpgradeService extends AbstractSqlTsDatabaseUpgr
log.info("Failed to load PostgreSQL upgrade functions due to: {}", e.getMessage());
}
}
-}
\ No newline at end of file
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
index f0fbfb3448..bc86857c4d 100644
--- a/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
+++ b/application/src/main/java/org/thingsboard/server/service/install/update/DefaultDataUpdateService.java
@@ -15,6 +15,8 @@
*/
package org.thingsboard.server.service.install.update;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@@ -23,9 +25,13 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import org.thingsboard.rule.engine.profile.TbDeviceProfileNode;
+import org.thingsboard.rule.engine.profile.TbDeviceProfileNodeConfiguration;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.SearchTextBased;
import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
@@ -35,10 +41,13 @@ import org.thingsboard.server.common.data.kv.TsKvEntry;
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.rule.RuleChainMetaData;
+import org.thingsboard.server.common.data.rule.RuleNode;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
+import org.thingsboard.server.dao.util.mapping.JacksonUtil;
import org.thingsboard.server.service.install.InstallScripts;
import javax.annotation.Nullable;
@@ -49,6 +58,7 @@ import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.apache.commons.lang.StringUtils.isBlank;
+import static org.thingsboard.server.service.install.DatabaseHelper.objectMapper;
@Service
@Profile("install")
@@ -81,6 +91,10 @@ public class DefaultDataUpdateService implements DataUpdateService {
log.info("Updating data from version 3.0.1 to 3.1.0 ...");
tenantsEntityViewsUpdater.updateEntities(null);
break;
+ case "3.1.1":
+ log.info("Updating data from version 3.1.1 to 3.2.0 ...");
+ tenantsRootRuleChainUpdater.updateEntities(null);
+ break;
default:
throw new RuntimeException("Unable to update data, unsupported fromVersion: " + fromVersion);
}
@@ -107,6 +121,60 @@ public class DefaultDataUpdateService implements DataUpdateService {
}
};
+ private PaginatedUpdater tenantsRootRuleChainUpdater =
+ new PaginatedUpdater() {
+
+ @Override
+ protected PageData findEntities(String region, PageLink pageLink) {
+ return tenantService.findTenants(pageLink);
+ }
+
+ @Override
+ protected void updateEntity(Tenant tenant) {
+ try {
+ RuleChain ruleChain = ruleChainService.getRootTenantRuleChain(tenant.getId());
+ if (ruleChain == null) {
+ installScripts.createDefaultRuleChains(tenant.getId());
+ } else {
+ RuleChainMetaData md = ruleChainService.loadRuleChainMetaData(tenant.getId(), ruleChain.getId());
+ int oldIdx = md.getFirstNodeIndex();
+ int newIdx = md.getNodes().size();
+
+ if (md.getNodes().size() < oldIdx) {
+ // Skip invalid rule chains
+ return;
+ }
+
+ RuleNode oldFirstNode = md.getNodes().get(oldIdx);
+ if (oldFirstNode.getType().equals(TbDeviceProfileNode.class.getName())) {
+ // No need to update the rule node twice.
+ return;
+ }
+
+ RuleNode ruleNode = new RuleNode();
+ ruleNode.setRuleChainId(ruleChain.getId());
+ ruleNode.setName("Device Profile Node");
+ ruleNode.setType(TbDeviceProfileNode.class.getName());
+ ruleNode.setDebugMode(false);
+ TbDeviceProfileNodeConfiguration ruleNodeConfiguration = new TbDeviceProfileNodeConfiguration().defaultConfiguration();
+ ruleNode.setConfiguration(JacksonUtil.valueToTree(ruleNodeConfiguration));
+ ObjectNode additionalInfo = JacksonUtil.newObjectNode();
+ additionalInfo.put("description", "Process incoming messages from devices with the alarm rules defined in the device profile. Dispatch all incoming messages with \"Success\" relation type.");
+ additionalInfo.put("layoutX", 204);
+ additionalInfo.put("layoutY", 240);
+ ruleNode.setAdditionalInfo(additionalInfo);
+
+ md.getNodes().add(ruleNode);
+ md.setFirstNodeIndex(newIdx);
+ md.addConnectionInfo(newIdx, oldIdx, "Success");
+ ruleChainService.saveRuleChainMetaData(tenant.getId(), md);
+ }
+ } catch (Exception e) {
+ log.error("Unable to update Tenant", e);
+ }
+ }
+ };
+
private PaginatedUpdater tenantsEntityViewsUpdater =
new PaginatedUpdater() {
@@ -121,30 +189,30 @@ public class DefaultDataUpdateService implements DataUpdateService {
}
};
- private void updateTenantEntityViews(TenantId tenantId) {
- PageLink pageLink = new PageLink(100);
- PageData pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink);
- boolean hasNext = true;
- while (hasNext) {
- List>> updateFutures = new ArrayList<>();
- for (EntityView entityView : pageData.getData()) {
- updateFutures.add(updateEntityViewLatestTelemetry(entityView));
- }
-
- try {
- Futures.allAsList(updateFutures).get();
- } catch (InterruptedException | ExecutionException e) {
- log.error("Failed to copy latest telemetry to entity view", e);
- }
-
- if (pageData.hasNext()) {
- pageLink = pageLink.nextPageLink();
- pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink);
- } else {
- hasNext = false;
- }
- }
- }
+ private void updateTenantEntityViews(TenantId tenantId) {
+ PageLink pageLink = new PageLink(100);
+ PageData pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink);
+ boolean hasNext = true;
+ while (hasNext) {
+ List>> updateFutures = new ArrayList<>();
+ for (EntityView entityView : pageData.getData()) {
+ updateFutures.add(updateEntityViewLatestTelemetry(entityView));
+ }
+
+ try {
+ Futures.allAsList(updateFutures).get();
+ } catch (InterruptedException | ExecutionException e) {
+ log.error("Failed to copy latest telemetry to entity view", e);
+ }
+
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ pageData = entityViewService.findEntityViewByTenantId(tenantId, pageLink);
+ } else {
+ hasNext = false;
+ }
+ }
+ }
private ListenableFuture> updateEntityViewLatestTelemetry(EntityView entityView) {
EntityViewId entityId = entityView.getId();
@@ -160,13 +228,13 @@ public class DefaultDataUpdateService implements DataUpdateService {
keysFuture = Futures.immediateFuture(keys);
}
ListenableFuture> latestFuture = Futures.transformAsync(keysFuture, fetchKeys -> {
- List queries = fetchKeys.stream().filter(key -> !isBlank(key)).map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, "DESC")).collect(Collectors.toList());
- if (!queries.isEmpty()) {
- return tsService.findAll(TenantId.SYS_TENANT_ID, entityView.getEntityId(), queries);
- } else {
- return Futures.immediateFuture(null);
- }
- }, MoreExecutors.directExecutor());
+ List queries = fetchKeys.stream().filter(key -> !isBlank(key)).map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, "DESC")).collect(Collectors.toList());
+ if (!queries.isEmpty()) {
+ return tsService.findAll(TenantId.SYS_TENANT_ID, entityView.getEntityId(), queries);
+ } else {
+ return Futures.immediateFuture(null);
+ }
+ }, MoreExecutors.directExecutor());
return Futures.transformAsync(latestFuture, latestValues -> {
if (latestValues != null && !latestValues.isEmpty()) {
ListenableFuture> saveFuture = tsService.saveLatest(TenantId.SYS_TENANT_ID, entityId, latestValues);
diff --git a/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
new file mode 100644
index 0000000000..b0b47fe886
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/profile/DefaultTbDeviceProfileCache.java
@@ -0,0 +1,99 @@
+/**
+ * Copyright © 2016-2020 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.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.device.DeviceProfileService;
+import org.thingsboard.server.dao.device.DeviceService;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Service
+@Slf4j
+public class DefaultTbDeviceProfileCache implements TbDeviceProfileCache {
+
+ private final Lock deviceProfileFetchLock = new ReentrantLock();
+ private final DeviceProfileService deviceProfileService;
+ private final DeviceService deviceService;
+
+ private final ConcurrentMap deviceProfilesMap = new ConcurrentHashMap<>();
+ private final ConcurrentMap devicesMap = new ConcurrentHashMap<>();
+
+ public DefaultTbDeviceProfileCache(DeviceProfileService deviceProfileService, DeviceService deviceService) {
+ this.deviceProfileService = deviceProfileService;
+ this.deviceService = deviceService;
+ }
+
+ @Override
+ public DeviceProfile get(TenantId tenantId, DeviceProfileId deviceProfileId) {
+ DeviceProfile profile = deviceProfilesMap.get(deviceProfileId);
+ if (profile == null) {
+ profile = deviceProfilesMap.get(deviceProfileId);
+ if (profile == null) {
+ deviceProfileFetchLock.lock();
+ try {
+ profile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfileId);
+ if (profile != null) {
+ deviceProfilesMap.put(deviceProfileId, profile);
+ }
+ } finally {
+ deviceProfileFetchLock.unlock();
+ }
+ }
+ }
+ return profile;
+ }
+
+ @Override
+ public DeviceProfile get(TenantId tenantId, DeviceId deviceId) {
+ DeviceProfileId profileId = devicesMap.get(deviceId);
+ if (profileId == null) {
+ Device device = deviceService.findDeviceById(tenantId, deviceId);
+ if (device != null) {
+ profileId = device.getDeviceProfileId();
+ devicesMap.put(deviceId, profileId);
+ }
+ }
+ return get(tenantId, profileId);
+ }
+
+ @Override
+ public void put(DeviceProfile profile) {
+ if (profile.getId() != null) {
+ deviceProfilesMap.put(profile.getId(), profile);
+ }
+ }
+
+ @Override
+ public void evict(DeviceProfileId profileId) {
+ deviceProfilesMap.remove(profileId);
+ }
+
+ @Override
+ public void evict(DeviceId deviceId) {
+ devicesMap.remove(deviceId);
+ }
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java
new file mode 100644
index 0000000000..ec19eb1da6
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/profile/TbDeviceProfileCache.java
@@ -0,0 +1,31 @@
+/**
+ * Copyright © 2016-2020 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.RuleEngineDeviceProfileCache;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+
+public interface TbDeviceProfileCache extends RuleEngineDeviceProfileCache {
+
+ void put(DeviceProfile profile);
+
+ void evict(DeviceProfileId id);
+
+ void evict(DeviceId id);
+
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
index 926b08f38b..28cd998e46 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java
@@ -21,14 +21,20 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
+import org.thingsboard.server.common.data.id.RuleChainId;
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.plugin.ComponentLifecycleMsg;
import org.thingsboard.server.common.msg.queue.ServiceType;
import org.thingsboard.server.common.msg.queue.TopicPartitionInfo;
+import org.thingsboard.server.common.transport.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;
import org.thingsboard.server.gen.transport.TransportProtos.ToCoreNotificationMsg;
@@ -40,7 +46,7 @@ import org.thingsboard.server.queue.TbQueueProducer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionService;
import org.thingsboard.server.queue.provider.TbQueueProducerProvider;
-import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import java.util.HashSet;
@@ -64,11 +70,13 @@ public class DefaultTbClusterService implements TbClusterService {
private final TbQueueProducerProvider producerProvider;
private final PartitionService partitionService;
private final DataDecodingEncodingService encodingService;
+ private final TbDeviceProfileCache deviceProfileCache;
- public DefaultTbClusterService(TbQueueProducerProvider producerProvider, PartitionService partitionService, DataDecodingEncodingService encodingService) {
+ public DefaultTbClusterService(TbQueueProducerProvider producerProvider, PartitionService partitionService, DataDecodingEncodingService encodingService, TbDeviceProfileCache deviceProfileCache) {
this.producerProvider = producerProvider;
this.partitionService = partitionService;
this.encodingService = encodingService;
+ this.deviceProfileCache = deviceProfileCache;
}
@Override
@@ -124,6 +132,12 @@ public class DefaultTbClusterService implements TbClusterService {
log.warn("[{}][{}] Received invalid message: {}", tenantId, entityId, tbMsg);
return;
}
+ } else {
+ if (entityId.getEntityType().equals(EntityType.DEVICE)) {
+ 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())));
+ }
}
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_RULE_ENGINE, tenantId, entityId);
log.trace("PUSHING msg: {} to:{}", tbMsg, tpi);
@@ -135,6 +149,16 @@ public class DefaultTbClusterService implements TbClusterService {
toRuleEngineMsgs.incrementAndGet();
}
+ private TbMsg transformMsg(TbMsg tbMsg, DeviceProfile deviceProfile) {
+ if (deviceProfile != null) {
+ RuleChainId targetRuleChainId = deviceProfile.getDefaultRuleChainId();
+ if (targetRuleChainId != null && !targetRuleChainId.equals(tbMsg.getRuleChainId())) {
+ tbMsg = TbMsg.transformMsg(tbMsg, targetRuleChainId);
+ }
+ }
+ return tbMsg;
+ }
+
@Override
public void pushNotificationToRuleEngine(String serviceId, FromDeviceRpcResponse response, TbQueueCallback callback) {
TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_RULE_ENGINE, serviceId);
@@ -163,6 +187,36 @@ public class DefaultTbClusterService implements TbClusterService {
broadcast(new ComponentLifecycleMsg(tenantId, entityId, state));
}
+ @Override
+ public void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback) {
+ log.trace("[{}][{}] Processing device profile [{}] change event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName());
+ TransportProtos.DeviceProfileUpdateMsg profileUpdateMsg = TransportProtos.DeviceProfileUpdateMsg.newBuilder()
+ .setData(ByteString.copyFrom(encodingService.encode(deviceProfile))).build();
+ ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileUpdateMsg(profileUpdateMsg).build();
+ broadcast(transportMsg);
+ }
+
+ @Override
+ public void onDeviceProfileDelete(DeviceProfile deviceProfile, TbQueueCallback callback) {
+ log.trace("[{}][{}] Processing device profile [{}] delete event", deviceProfile.getTenantId(), deviceProfile.getId(), deviceProfile.getName());
+ TransportProtos.DeviceProfileDeleteMsg profileDeleteMsg = TransportProtos.DeviceProfileDeleteMsg.newBuilder()
+ .setProfileIdMSB(deviceProfile.getId().getId().getMostSignificantBits())
+ .setProfileIdLSB(deviceProfile.getId().getId().getLeastSignificantBits())
+ .build();
+ ToTransportMsg transportMsg = ToTransportMsg.newBuilder().setDeviceProfileDeleteMsg(profileDeleteMsg).build();
+ broadcast(transportMsg);
+ }
+
+ private void broadcast(ToTransportMsg transportMsg) {
+ TbQueueProducer> toTransportNfProducer = producerProvider.getTransportNotificationsMsgProducer();
+ Set tbTransportServices = partitionService.getAllServiceIds(ServiceType.TB_TRANSPORT);
+ for (String transportServiceId : tbTransportServices) {
+ TopicPartitionInfo tpi = partitionService.getNotificationsTopic(ServiceType.TB_TRANSPORT, transportServiceId);
+ toTransportNfProducer.send(tpi, new TbProtoQueueMsg<>(UUID.randomUUID(), transportMsg), null);
+ toTransportNfs.incrementAndGet();
+ }
+ }
+
private void broadcast(ComponentLifecycleMsg msg) {
byte[] msgBytes = encodingService.encode(msg);
TbQueueProducer> toRuleEngineProducer = producerProvider.getRuleEngineNotificationsMsgProducer();
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
index 6e1eca4a7d..5d240790f2 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbCoreConsumerService.java
@@ -21,10 +21,13 @@ import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.RpcError;
import org.thingsboard.server.actors.ActorSystemContext;
+import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.alarm.Alarm;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.MsgType;
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.util.mapping.JacksonUtil;
@@ -47,7 +50,8 @@ import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
import org.thingsboard.server.queue.provider.TbCoreQueueFactory;
import org.thingsboard.server.common.stats.StatsFactory;
import org.thingsboard.server.queue.util.TbCoreComponent;
-import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.AbstractConsumerService;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.rpc.TbCoreDeviceRpcService;
@@ -92,8 +96,8 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService actorMsg = encodingService.decode(toCoreNotification.getComponentLifecycleMsg().toByteArray());
- if (actorMsg.isPresent()) {
- log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
- actorContext.tellWithHighPriority(actorMsg.get());
- }
+ handleComponentLifecycleMsg(id, toCoreNotification.getComponentLifecycleMsg());
callback.onSuccess();
}
if (statsEnabled) {
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
index 0f3efb6f05..fd356cabcc 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbRuleEngineConsumerService.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.service.queue;
+import com.google.protobuf.ByteString;
import com.google.protobuf.ProtocolStringList;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@@ -37,7 +38,8 @@ import org.thingsboard.server.queue.provider.TbRuleEngineQueueFactory;
import org.thingsboard.server.queue.settings.TbQueueRuleEngineSettings;
import org.thingsboard.server.queue.settings.TbRuleEngineQueueConfiguration;
import org.thingsboard.server.queue.util.TbRuleEngineComponent;
-import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.processing.*;
import org.thingsboard.server.service.rpc.FromDeviceRpcResponse;
import org.thingsboard.server.service.rpc.TbRuleEngineDeviceRpcService;
@@ -80,8 +82,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
TbRuleEngineQueueFactory tbRuleEngineQueueFactory, RuleEngineStatisticsService statisticsService,
ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
TbRuleEngineDeviceRpcService tbDeviceRpcService,
- StatsFactory statsFactory) {
- super(actorContext, encodingService, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
+ StatsFactory statsFactory, TbDeviceProfileCache deviceProfileCache) {
+ super(actorContext, encodingService, deviceProfileCache, tbRuleEngineQueueFactory.createToRuleEngineNotificationsMsgConsumer());
this.statisticsService = statisticsService;
this.ruleEngineSettings = ruleEngineSettings;
this.tbRuleEngineQueueFactory = tbRuleEngineQueueFactory;
@@ -144,7 +146,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
submitStrategy.init(msgs);
while (!stopped) {
- TbMsgPackProcessingContext ctx = new TbMsgPackProcessingContext(submitStrategy);
+ TbMsgPackProcessingContext ctx = new TbMsgPackProcessingContext(configuration.getName(), submitStrategy);
submitStrategy.submitAttempt((id, msg) -> submitExecutor.submit(() -> {
log.trace("[{}] Creating callback for message: {}", id, msg.getValue());
ToRuleEngineMsg toRuleEngineMsg = msg.getValue();
@@ -175,6 +177,8 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
if (!ctx.getFailedMap().isEmpty()) {
printFirstOrAll(configuration, ctx, ctx.getFailedMap(), "Failed");
}
+ ctx.printProfilerStats();
+
TbRuleEngineProcessingDecision decision = ackStrategy.analyze(result);
if (statsEnabled) {
stats.log(result, decision.isCommit());
@@ -237,11 +241,7 @@ public class DefaultTbRuleEngineConsumerService extends AbstractConsumerService<
protected void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception {
ToRuleEngineNotificationMsg nfMsg = msg.getValue();
if (nfMsg.getComponentLifecycleMsg() != null && !nfMsg.getComponentLifecycleMsg().isEmpty()) {
- Optional actorMsg = encodingService.decode(nfMsg.getComponentLifecycleMsg().toByteArray());
- if (actorMsg.isPresent()) {
- log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg.get());
- actorContext.tellWithHighPriority(actorMsg.get());
- }
+ handleComponentLifecycleMsg(id, nfMsg.getComponentLifecycleMsg());
callback.onSuccess();
} else if (nfMsg.hasFromDeviceRpcResponse()) {
TransportProtos.FromDeviceRPCResponseProto proto = nfMsg.getFromDeviceRpcResponse();
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
index aae3cef0ac..f89e05a57b 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTenantRoutingInfoService.java
@@ -19,7 +19,9 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.TenantId;
+import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
import org.thingsboard.server.queue.discovery.TenantRoutingInfo;
import org.thingsboard.server.queue.discovery.TenantRoutingInfoService;
@@ -31,15 +33,20 @@ public class DefaultTenantRoutingInfoService implements TenantRoutingInfoService
private final TenantService tenantService;
- public DefaultTenantRoutingInfoService(TenantService tenantService) {
+ private final TenantProfileService tenantProfileService;
+
+ public DefaultTenantRoutingInfoService(TenantService tenantService, TenantProfileService tenantProfileService) {
this.tenantService = tenantService;
+ this.tenantProfileService = tenantProfileService;
}
@Override
public TenantRoutingInfo getRoutingInfo(TenantId tenantId) {
Tenant tenant = tenantService.findTenantById(tenantId);
if (tenant != null) {
- return new TenantRoutingInfo(tenantId, tenant.isIsolatedTbCore(), tenant.isIsolatedTbRuleEngine());
+ // TODO: Tenant Profile from cache
+ TenantProfile tenantProfile = tenantProfileService.findTenantProfileById(tenantId, tenant.getTenantProfileId());
+ return new TenantRoutingInfo(tenantId, tenantProfile.isIsolatedTbCore(), tenantProfile.isIsolatedTbRuleEngine());
} else {
throw new RuntimeException("Tenant not found!");
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java
index cc722720d5..cf212a06f5 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/TbClusterService.java
@@ -16,6 +16,8 @@
package org.thingsboard.server.service.queue;
import org.thingsboard.rule.engine.api.msg.ToDeviceActorNotificationMsg;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
@@ -49,4 +51,7 @@ public interface TbClusterService {
void onEntityStateChange(TenantId tenantId, EntityId entityId, ComponentLifecycleEvent state);
+ void onDeviceProfileChange(DeviceProfile deviceProfile, TbQueueCallback callback);
+
+ void onDeviceProfileDelete(DeviceProfile deviceProfileId, TbQueueCallback callback);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java
index 4d5be5b883..376c64a17d 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackCallback.java
@@ -67,8 +67,14 @@ public class TbMsgPackCallback implements TbMsgCallback {
}
@Override
- public void visit(RuleNodeInfo ruleNodeInfo) {
- log.trace("[{}] ON PROCESS: {}", id, ruleNodeInfo);
- ctx.visit(id, ruleNodeInfo);
+ public void onProcessingStart(RuleNodeInfo ruleNodeInfo) {
+ log.trace("[{}] ON PROCESSING START: {}", id, ruleNodeInfo);
+ ctx.onProcessingStart(id, ruleNodeInfo);
+ }
+
+ @Override
+ public void onProcessingEnd(RuleNodeId ruleNodeId) {
+ log.trace("[{}] ON PROCESSING END: {}", id, ruleNodeId);
+ ctx.onProcessingEnd(id, ruleNodeId);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java
index 1b7ffd9c30..6d88ed204a 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgPackProcessingContext.java
@@ -16,6 +16,7 @@
package org.thingsboard.server.service.queue;
import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.RuleNodeId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.msg.queue.RuleEngineException;
@@ -24,6 +25,8 @@ import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.service.queue.processing.TbRuleEngineSubmitStrategy;
+import java.util.Comparator;
+import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -31,9 +34,13 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
+@Slf4j
public class TbMsgPackProcessingContext {
+ private final String queueName;
private final TbRuleEngineSubmitStrategy submitStrategy;
+ @Getter
+ private final boolean profilerEnabled;
private final AtomicInteger pendingCount;
private final CountDownLatch processingTimeoutLatch = new CountDownLatch(1);
@Getter
@@ -47,14 +54,20 @@ public class TbMsgPackProcessingContext {
private final ConcurrentMap lastRuleNodeMap = new ConcurrentHashMap<>();
- public TbMsgPackProcessingContext(TbRuleEngineSubmitStrategy submitStrategy) {
+ public TbMsgPackProcessingContext(String queueName, TbRuleEngineSubmitStrategy submitStrategy) {
+ this.queueName = queueName;
this.submitStrategy = submitStrategy;
+ this.profilerEnabled = log.isDebugEnabled();
this.pendingMap = submitStrategy.getPendingMap();
this.pendingCount = new AtomicInteger(pendingMap.size());
}
public boolean await(long packProcessingTimeout, TimeUnit milliseconds) throws InterruptedException {
- return processingTimeoutLatch.await(packProcessingTimeout, milliseconds);
+ boolean success = processingTimeoutLatch.await(packProcessingTimeout, milliseconds);
+ if (!success && profilerEnabled) {
+ msgProfilerMap.values().forEach(this::onTimeout);
+ }
+ return success;
}
public void onSuccess(UUID id) {
@@ -85,12 +98,53 @@ public class TbMsgPackProcessingContext {
}
}
- public void visit(UUID id, RuleNodeInfo ruleNodeInfo) {
+ private final ConcurrentHashMap msgProfilerMap = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap ruleNodeProfilerMap = new ConcurrentHashMap<>();
+
+ public void onProcessingStart(UUID id, RuleNodeInfo ruleNodeInfo) {
lastRuleNodeMap.put(id, ruleNodeInfo);
+ if (profilerEnabled) {
+ msgProfilerMap.computeIfAbsent(id, TbMsgProfilerInfo::new).onStart(ruleNodeInfo.getRuleNodeId());
+ ruleNodeProfilerMap.putIfAbsent(ruleNodeInfo.getRuleNodeId().getId(), new TbRuleNodeProfilerInfo(ruleNodeInfo));
+ }
+ }
+
+ public void onProcessingEnd(UUID id, RuleNodeId ruleNodeId) {
+ if (profilerEnabled) {
+ long processingTime = msgProfilerMap.computeIfAbsent(id, TbMsgProfilerInfo::new).onEnd(ruleNodeId);
+ if (processingTime > 0) {
+ ruleNodeProfilerMap.computeIfAbsent(ruleNodeId.getId(), TbRuleNodeProfilerInfo::new).record(processingTime);
+ }
+ }
+ }
+
+ public void onTimeout(TbMsgProfilerInfo profilerInfo) {
+ Map.Entry ruleNodeInfo = profilerInfo.onTimeout();
+ if (ruleNodeInfo != null) {
+ ruleNodeProfilerMap.computeIfAbsent(ruleNodeInfo.getKey(), TbRuleNodeProfilerInfo::new).record(ruleNodeInfo.getValue());
+ }
}
public RuleNodeInfo getLastVisitedRuleNode(UUID id) {
return lastRuleNodeMap.get(id);
}
+ public void printProfilerStats() {
+ if (profilerEnabled) {
+ log.debug("Top Rule Nodes by max execution time:");
+ ruleNodeProfilerMap.values().stream()
+ .sorted(Comparator.comparingLong(TbRuleNodeProfilerInfo::getMaxExecutionTime).reversed()).limit(5)
+ .forEach(info -> log.debug("[{}][{}] max execution time: {}. {}", queueName, info.getRuleNodeId(), info.getMaxExecutionTime(), info.getLabel()));
+
+ log.info("Top Rule Nodes by avg execution time:");
+ ruleNodeProfilerMap.values().stream()
+ .sorted(Comparator.comparingDouble(TbRuleNodeProfilerInfo::getAvgExecutionTime).reversed()).limit(5)
+ .forEach(info -> log.info("[{}][{}] avg execution time: {}. {}", queueName, info.getRuleNodeId(), info.getAvgExecutionTime(), info.getLabel()));
+
+ log.info("Top Rule Nodes by execution count:");
+ ruleNodeProfilerMap.values().stream()
+ .sorted(Comparator.comparingInt(TbRuleNodeProfilerInfo::getExecutionCount).reversed()).limit(5)
+ .forEach(info -> log.info("[{}][{}] execution count: {}. {}", queueName, info.getRuleNodeId(), info.getExecutionCount(), info.getLabel()));
+ }
+ }
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbMsgProfilerInfo.java b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgProfilerInfo.java
new file mode 100644
index 0000000000..f66cd1a50a
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/queue/TbMsgProfilerInfo.java
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2016-2020 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.queue;
+
+import lombok.extern.slf4j.Slf4j;
+import org.thingsboard.server.common.data.id.RuleNodeId;
+import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
+
+import java.util.AbstractMap;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+@Slf4j
+public class TbMsgProfilerInfo {
+ private final UUID msgId;
+ private AtomicLong totalProcessingTime = new AtomicLong();
+ private Lock stateLock = new ReentrantLock();
+ private RuleNodeId currentRuleNodeId;
+ private long stateChangeTime;
+
+ public TbMsgProfilerInfo(UUID msgId) {
+ this.msgId = msgId;
+ }
+
+ public void onStart(RuleNodeId ruleNodeId) {
+ long currentTime = System.currentTimeMillis();
+ stateLock.lock();
+ try {
+ currentRuleNodeId = ruleNodeId;
+ stateChangeTime = currentTime;
+ } finally {
+ stateLock.unlock();
+ }
+ }
+
+ public long onEnd(RuleNodeId ruleNodeId) {
+ long currentTime = System.currentTimeMillis();
+ stateLock.lock();
+ try {
+ if (ruleNodeId.equals(currentRuleNodeId)) {
+ long processingTime = currentTime - stateChangeTime;
+ stateChangeTime = currentTime;
+ totalProcessingTime.addAndGet(processingTime);
+ currentRuleNodeId = null;
+ return processingTime;
+ } else {
+ log.trace("[{}] Invalid sequence of rule node processing detected. Expected [{}] but was [{}]", msgId, currentRuleNodeId, ruleNodeId);
+ return 0;
+ }
+ } finally {
+ stateLock.unlock();
+ }
+ }
+
+ public Map.Entry onTimeout() {
+ long currentTime = System.currentTimeMillis();
+ stateLock.lock();
+ try {
+ if (currentRuleNodeId != null && stateChangeTime > 0) {
+ long timeoutTime = currentTime - stateChangeTime;
+ totalProcessingTime.addAndGet(timeoutTime);
+ return new AbstractMap.SimpleEntry<>(currentRuleNodeId.getId(), timeoutTime);
+ }
+ } finally {
+ stateLock.unlock();
+ }
+ return null;
+ }
+}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/TbRuleNodeProfilerInfo.java b/application/src/main/java/org/thingsboard/server/service/queue/TbRuleNodeProfilerInfo.java
new file mode 100644
index 0000000000..c88532fbc3
--- /dev/null
+++ b/application/src/main/java/org/thingsboard/server/service/queue/TbRuleNodeProfilerInfo.java
@@ -0,0 +1,75 @@
+/**
+ * Copyright © 2016-2020 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.queue;
+
+import lombok.Getter;
+import org.thingsboard.server.common.msg.queue.RuleNodeInfo;
+
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class TbRuleNodeProfilerInfo {
+ @Getter
+ private final UUID ruleNodeId;
+ @Getter
+ private final String label;
+ private AtomicInteger executionCount = new AtomicInteger(0);
+ private AtomicLong executionTime = new AtomicLong(0);
+ private AtomicLong maxExecutionTime = new AtomicLong(0);
+
+ public TbRuleNodeProfilerInfo(RuleNodeInfo ruleNodeInfo) {
+ this.ruleNodeId = ruleNodeInfo.getRuleNodeId().getId();
+ this.label = ruleNodeInfo.toString();
+ }
+
+ public TbRuleNodeProfilerInfo(UUID ruleNodeId) {
+ this.ruleNodeId = ruleNodeId;
+ this.label = "";
+ }
+
+ public void record(long processingTime) {
+ executionCount.incrementAndGet();
+ executionTime.addAndGet(processingTime);
+ while (true) {
+ long value = maxExecutionTime.get();
+ if (value >= processingTime) {
+ break;
+ }
+ if (maxExecutionTime.compareAndSet(value, processingTime)) {
+ break;
+ }
+ }
+ }
+
+ int getExecutionCount() {
+ return executionCount.get();
+ }
+
+ long getMaxExecutionTime() {
+ return maxExecutionTime.get();
+ }
+
+ double getAvgExecutionTime() {
+ double executionCnt = (double) executionCount.get();
+ if (executionCnt > 0) {
+ return executionTime.get() / executionCnt;
+ } else {
+ return 0.0;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
index 4007c9e17d..3f6595f81e 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/AbstractConsumerService.java
@@ -15,23 +15,31 @@
*/
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.ApplicationListener;
import org.springframework.context.event.EventListener;
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.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
+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.queue.TbQueueConsumer;
import org.thingsboard.server.queue.common.TbProtoQueueMsg;
import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
-import org.thingsboard.server.service.encoding.DataDecodingEncodingService;
+import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
+import org.thingsboard.server.service.profile.TbDeviceProfileCache;
import org.thingsboard.server.service.queue.TbPackCallback;
import org.thingsboard.server.service.queue.TbPackProcessingContext;
import javax.annotation.PreDestroy;
import java.util.List;
+import java.util.Optional;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -51,12 +59,15 @@ public abstract class AbstractConsumerService> nfConsumer;
- public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService, TbQueueConsumer> nfConsumer) {
+ public AbstractConsumerService(ActorSystemContext actorContext, DataDecodingEncodingService encodingService,
+ TbDeviceProfileCache deviceProfileCache, TbQueueConsumer> nfConsumer) {
this.actorContext = actorContext;
this.encodingService = encodingService;
+ this.deviceProfileCache = deviceProfileCache;
this.nfConsumer = nfConsumer;
}
@@ -126,18 +137,32 @@ public abstract class AbstractConsumerService actorMsgOpt = encodingService.decode(nfMsg.toByteArray());
+ if (actorMsgOpt.isPresent()) {
+ TbActorMsg actorMsg = actorMsgOpt.get();
+ if (actorMsg instanceof ComponentLifecycleMsg) {
+ ComponentLifecycleMsg componentLifecycleMsg = (ComponentLifecycleMsg) actorMsg;
+ if (EntityType.DEVICE_PROFILE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ deviceProfileCache.evict(new DeviceProfileId(componentLifecycleMsg.getEntityId().getId()));
+ } else if (EntityType.DEVICE.equals(componentLifecycleMsg.getEntityId().getEntityType())) {
+ deviceProfileCache.evict(new DeviceId(componentLifecycleMsg.getEntityId().getId()));
+ }
+ }
+ log.trace("[{}] Forwarding message to App Actor {}", id, actorMsg);
+ actorContext.tellWithHighPriority(actorMsg);
+ }
+ }
+
protected abstract void handleNotification(UUID id, TbProtoQueueMsg msg, TbCallback callback) throws Exception;
@PreDestroy
public void destroy() {
stopped = true;
-
stopMainConsumers();
-
if (nfConsumer != null) {
nfConsumer.unsubscribe();
}
-
if (consumersExecutor != null) {
consumersExecutor.shutdownNow();
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java
index b9741d2433..be299b39b4 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/BatchTbRuleEngineSubmitStrategy.java
@@ -68,18 +68,20 @@ public class BatchTbRuleEngineSubmitStrategy extends AbstractTbRuleEngineSubmitS
int listSize = orderedMsgList.size();
int startIdx = Math.min(packIdx.get() * batchSize, listSize);
int endIdx = Math.min(startIdx + batchSize, listSize);
+ Map> tmpPack;
synchronized (pendingPack) {
pendingPack.clear();
for (int i = startIdx; i < endIdx; i++) {
IdMsgPair pair = orderedMsgList.get(i);
pendingPack.put(pair.uuid, pair.msg);
}
+ tmpPack = new LinkedHashMap<>(pendingPack);
}
int submitSize = pendingPack.size();
if (log.isDebugEnabled() && submitSize > 0) {
log.debug("[{}] submitting [{}] messages to rule engine", queueName, submitSize);
}
- pendingPack.forEach(msgConsumer);
+ tmpPack.forEach(msgConsumer);
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java
index b6220f5f94..b42615ad8f 100644
--- a/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java
+++ b/application/src/main/java/org/thingsboard/server/service/queue/processing/TbRuleEngineProcessingStrategyFactory.java
@@ -56,7 +56,9 @@ public class TbRuleEngineProcessingStrategyFactory {
private final boolean retryTimeout;
private final int maxRetries;
private final double maxAllowedFailurePercentage;
- private final long pauseBetweenRetries;
+ private final long maxPauseBetweenRetries;
+
+ private long pauseBetweenRetries;
private int initialTotalCount;
private int retryCount;
@@ -69,6 +71,7 @@ public class TbRuleEngineProcessingStrategyFactory {
this.maxRetries = configuration.getRetries();
this.maxAllowedFailurePercentage = configuration.getFailurePercentage();
this.pauseBetweenRetries = configuration.getPauseBetweenRetries();
+ this.maxPauseBetweenRetries = configuration.getMaxPauseBetweenRetries();
}
@Override
@@ -108,6 +111,9 @@ public class TbRuleEngineProcessingStrategyFactory {
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
+ if (maxPauseBetweenRetries > pauseBetweenRetries) {
+ pauseBetweenRetries = Math.min(maxPauseBetweenRetries, pauseBetweenRetries * 2);
+ }
}
return new TbRuleEngineProcessingDecision(false, toReprocess);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
index b08ec2625b..b4bc79e06e 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/AccessValidator.java
@@ -27,6 +27,7 @@ import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.common.util.ThingsBoardThreadFactory;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@@ -35,6 +36,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
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.DeviceProfileId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
import org.thingsboard.server.common.data.id.EntityViewId;
@@ -48,6 +50,7 @@ import org.thingsboard.server.controller.HttpValidationCallback;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.customer.CustomerService;
+import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.rule.RuleChainService;
@@ -72,6 +75,7 @@ import java.util.function.BiConsumer;
@Component
public class AccessValidator {
+ public static final String ONLY_SYSTEM_ADMINISTRATOR_IS_ALLOWED_TO_PERFORM_THIS_OPERATION = "Only system administrator is allowed to perform this operation!";
public static final String CUSTOMER_USER_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "Customer user is not allowed to perform this operation!";
public static final String SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION = "System administrator is not allowed to perform this operation!";
public static final String DEVICE_WITH_REQUESTED_ID_NOT_FOUND = "Device with requested id wasn't found!";
@@ -89,6 +93,9 @@ public class AccessValidator {
@Autowired
protected DeviceService deviceService;
+ @Autowired
+ protected DeviceProfileService deviceProfileService;
+
@Autowired
protected AssetService assetService;
@@ -162,6 +169,9 @@ public class AccessValidator {
case DEVICE:
validateDevice(currentUser, operation, entityId, callback);
return;
+ case DEVICE_PROFILE:
+ validateDeviceProfile(currentUser, operation, entityId, callback);
+ return;
case ASSET:
validateAsset(currentUser, operation, entityId, callback);
return;
@@ -174,6 +184,9 @@ public class AccessValidator {
case TENANT:
validateTenant(currentUser, operation, entityId, callback);
return;
+ case TENANT_PROFILE:
+ validateTenantProfile(currentUser, operation, entityId, callback);
+ return;
case USER:
validateUser(currentUser, operation, entityId, callback);
return;
@@ -206,6 +219,24 @@ public class AccessValidator {
}
}
+ private void validateDeviceProfile(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) {
+ if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ } else {
+ DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(currentUser.getTenantId(), new DeviceProfileId(entityId.getId()));
+ if (deviceProfile == null) {
+ callback.onSuccess(ValidationResult.entityNotFound("Device profile with requested id wasn't found!"));
+ } else {
+ try {
+ accessControlService.checkPermission(currentUser, Resource.DEVICE_PROFILE, operation, entityId, deviceProfile);
+ } catch (ThingsboardException e) {
+ callback.onSuccess(ValidationResult.accessDenied(e.getMessage()));
+ }
+ callback.onSuccess(ValidationResult.ok(deviceProfile));
+ }
+ }
+ }
+
private void validateAsset(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) {
if (currentUser.isSystemAdmin()) {
callback.onSuccess(ValidationResult.accessDenied(SYSTEM_ADMINISTRATOR_IS_NOT_ALLOWED_TO_PERFORM_THIS_OPERATION));
@@ -313,6 +344,14 @@ public class AccessValidator {
}
}
+ private void validateTenantProfile(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) {
+ if (currentUser.isSystemAdmin()) {
+ callback.onSuccess(ValidationResult.ok(null));
+ } else {
+ callback.onSuccess(ValidationResult.accessDenied(ONLY_SYSTEM_ADMINISTRATOR_IS_ALLOWED_TO_PERFORM_THIS_OPERATION));
+ }
+ }
+
private void validateUser(final SecurityUser currentUser, Operation operation, EntityId entityId, FutureCallback callback) {
ListenableFuture userFuture = userService.findUserByIdAsync(currentUser.getTenantId(), new UserId(entityId.getId()));
Futures.addCallback(userFuture, getCallback(callback, user -> {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
index a66b822fca..96eff6efd0 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/permission/Resource.java
@@ -31,7 +31,9 @@ public enum Resource {
RULE_CHAIN(EntityType.RULE_CHAIN),
USER(EntityType.USER),
WIDGETS_BUNDLE(EntityType.WIDGETS_BUNDLE),
- WIDGET_TYPE(EntityType.WIDGET_TYPE);
+ WIDGET_TYPE(EntityType.WIDGET_TYPE),
+ TENANT_PROFILE(EntityType.TENANT_PROFILE),
+ DEVICE_PROFILE(EntityType.DEVICE_PROFILE);
private final EntityType entityType;
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
index cd79a29f0b..766290298a 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/permission/SysAdminPermissions.java
@@ -39,6 +39,7 @@ public class SysAdminPermissions extends AbstractPermissions {
put(Resource.USER, userPermissionChecker);
put(Resource.WIDGETS_BUNDLE, systemEntityPermissionChecker);
put(Resource.WIDGET_TYPE, systemEntityPermissionChecker);
+ put(Resource.TENANT_PROFILE, PermissionChecker.allowAllPermissionChecker);
}
private static final PermissionChecker systemEntityPermissionChecker = new PermissionChecker() {
diff --git a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
index 794fb72398..3caa405214 100644
--- a/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
+++ b/application/src/main/java/org/thingsboard/server/service/security/permission/TenantAdminPermissions.java
@@ -42,6 +42,7 @@ public class TenantAdminPermissions extends AbstractPermissions {
put(Resource.USER, userPermissionChecker);
put(Resource.WIDGETS_BUNDLE, widgetsPermissionChecker);
put(Resource.WIDGET_TYPE, widgetsPermissionChecker);
+ put(Resource.DEVICE_PROFILE, tenantEntityPermissionChecker);
}
public static final PermissionChecker tenantEntityPermissionChecker = new PermissionChecker() {
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
index 5d283bbf0e..5120698856 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/DefaultSubscriptionManagerService.java
@@ -226,6 +226,11 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer
@Override
public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, TbCallback callback) {
+ onAttributesUpdate(tenantId, entityId, scope, attributes, true, callback);
+ }
+
+ @Override
+ public void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback) {
onLocalTelemetrySubUpdate(entityId,
s -> {
if (TbSubscriptionType.ATTRIBUTES.equals(s.getType())) {
@@ -254,7 +259,7 @@ public class DefaultSubscriptionManagerService implements SubscriptionManagerSer
deviceStateService.onDeviceInactivityTimeoutUpdate(new DeviceId(entityId.getId()), attribute.getLongValue().orElse(0L));
}
}
- } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope)) {
+ } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) {
clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId,
new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes))
, null);
diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
index 5ca6b4f82e..c0ca01685d 100644
--- a/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
+++ b/application/src/main/java/org/thingsboard/server/service/subscription/SubscriptionManagerService.java
@@ -17,13 +17,12 @@ package org.thingsboard.server.service.subscription;
import org.springframework.context.ApplicationListener;
import org.thingsboard.server.common.data.alarm.Alarm;
-import org.thingsboard.server.common.data.id.AlarmId;
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.TsKvEntry;
-import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
import org.thingsboard.server.common.msg.queue.TbCallback;
+import org.thingsboard.server.queue.discovery.PartitionChangeEvent;
import java.util.List;
@@ -37,9 +36,13 @@ public interface SubscriptionManagerService extends ApplicationListener attributes, TbCallback callback);
+ void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, TbCallback callback);
+
void onAttributesDelete(TenantId tenantId, EntityId entityId, String scope, List keys, TbCallback empty);
void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback);
void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback);
+
+
}
diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
index 16d95a48ce..aecd888dce 100644
--- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
+++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetrySubscriptionService.java
@@ -171,9 +171,14 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
@Override
public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, FutureCallback callback) {
+ saveAndNotify(tenantId, entityId, scope, attributes, true, callback);
+ }
+
+ @Override
+ public void saveAndNotify(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice, FutureCallback callback) {
ListenableFuture> saveFuture = attrService.save(tenantId, entityId, scope, attributes);
addMainCallback(saveFuture, callback);
- addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes));
+ addWsCallback(saveFuture, success -> onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice));
}
@Override
@@ -236,11 +241,11 @@ public class DefaultTelemetrySubscriptionService extends AbstractSubscriptionSer
, System.currentTimeMillis())), callback);
}
- private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes) {
+ private void onAttributesUpdate(TenantId tenantId, EntityId entityId, String scope, List attributes, boolean notifyDevice) {
TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId);
if (currentPartitions.contains(tpi)) {
if (subscriptionManagerService.isPresent()) {
- subscriptionManagerService.get().onAttributesUpdate(tenantId, entityId, scope, attributes, TbCallback.EMPTY);
+ subscriptionManagerService.get().onAttributesUpdate(tenantId, entityId, scope, attributes, notifyDevice, TbCallback.EMPTY);
} else {
log.warn("Possible misconfiguration because subscriptionManagerService is null!");
}
diff --git a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
index 7399380c26..7d41190ac0 100644
--- a/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
+++ b/application/src/main/java/org/thingsboard/server/service/transport/DefaultTransportApiService.java
@@ -21,26 +21,36 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
+import com.google.protobuf.ByteString;
import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
-import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.device.credentials.BasicMqttCredentials;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
+import org.thingsboard.server.common.msg.EncryptionUtil;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
import org.thingsboard.server.common.msg.TbMsgMetaData;
+import org.thingsboard.server.common.transport.util.DataDecodingEncodingService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
+import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.relation.RelationService;
+import org.thingsboard.server.dao.tenant.TenantProfileService;
import org.thingsboard.server.dao.tenant.TenantService;
+import org.thingsboard.server.dao.util.mapping.JacksonUtil;
+import org.thingsboard.server.gen.transport.TransportProtos;
import org.thingsboard.server.gen.transport.TransportProtos.DeviceInfoProto;
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg;
import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayResponseMsg;
@@ -74,44 +84,64 @@ public class DefaultTransportApiService implements TransportApiService {
private static final ObjectMapper mapper = new ObjectMapper();
//TODO: Constructor dependencies;
- @Autowired
- private TenantService tenantService;
+ private final DeviceProfileService deviceProfileService;
+ private final TenantService tenantService;
+ private final TenantProfileService tenantProfileService;
+ private final DeviceService deviceService;
+ private final RelationService relationService;
+ private final DeviceCredentialsService deviceCredentialsService;
+ private final DeviceStateService deviceStateService;
+ private final DbCallbackExecutorService dbCallbackExecutorService;
+ private final TbClusterService tbClusterService;
+ private final DataDecodingEncodingService dataDecodingEncodingService;
- @Autowired
- private DeviceService deviceService;
-
- @Autowired
- private RelationService relationService;
-
- @Autowired
- private DeviceCredentialsService deviceCredentialsService;
-
- @Autowired
- private DeviceStateService deviceStateService;
-
- @Autowired
- private DbCallbackExecutorService dbCallbackExecutorService;
-
- @Autowired
- protected TbClusterService tbClusterService;
private final ConcurrentMap deviceCreationLocks = new ConcurrentHashMap<>();
+ public DefaultTransportApiService(DeviceProfileService deviceProfileService, TenantService tenantService,
+ TenantProfileService tenantProfileService, DeviceService deviceService,
+ RelationService relationService, DeviceCredentialsService deviceCredentialsService,
+ DeviceStateService deviceStateService, DbCallbackExecutorService dbCallbackExecutorService,
+ TbClusterService tbClusterService, DataDecodingEncodingService dataDecodingEncodingService) {
+ this.deviceProfileService = deviceProfileService;
+ this.tenantService = tenantService;
+ this.tenantProfileService = tenantProfileService;
+ this.deviceService = deviceService;
+ this.relationService = relationService;
+ this.deviceCredentialsService = deviceCredentialsService;
+ this.deviceStateService = deviceStateService;
+ this.dbCallbackExecutorService = dbCallbackExecutorService;
+ this.tbClusterService = tbClusterService;
+ this.dataDecodingEncodingService = dataDecodingEncodingService;
+ }
+
@Override
public ListenableFuture> handle(TbProtoQueueMsg tbProtoQueueMsg) {
TransportApiRequestMsg transportApiRequestMsg = tbProtoQueueMsg.getValue();
if (transportApiRequestMsg.hasValidateTokenRequestMsg()) {
ValidateDeviceTokenRequestMsg msg = transportApiRequestMsg.getValidateTokenRequestMsg();
- return Futures.transform(validateCredentials(msg.getToken(), DeviceCredentialsType.ACCESS_TOKEN), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
+ return Futures.transform(validateCredentials(msg.getToken(), DeviceCredentialsType.ACCESS_TOKEN),
+ value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
+ } else if (transportApiRequestMsg.hasValidateBasicMqttCredRequestMsg()) {
+ TransportProtos.ValidateBasicMqttCredRequestMsg msg = transportApiRequestMsg.getValidateBasicMqttCredRequestMsg();
+ return Futures.transform(validateCredentials(msg),
+ value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasValidateX509CertRequestMsg()) {
ValidateDeviceX509CertRequestMsg msg = transportApiRequestMsg.getValidateX509CertRequestMsg();
- return Futures.transform(validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
+ return Futures.transform(validateCredentials(msg.getHash(), DeviceCredentialsType.X509_CERTIFICATE),
+ value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasGetOrCreateDeviceRequestMsg()) {
- return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
+ return Futures.transform(handle(transportApiRequestMsg.getGetOrCreateDeviceRequestMsg()),
+ value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
} else if (transportApiRequestMsg.hasGetTenantRoutingInfoRequestMsg()) {
- return Futures.transform(handle(transportApiRequestMsg.getGetTenantRoutingInfoRequestMsg()), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
+ return Futures.transform(handle(transportApiRequestMsg.getGetTenantRoutingInfoRequestMsg()),
+ value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
+ } else if (transportApiRequestMsg.hasGetDeviceProfileRequestMsg()) {
+ return Futures.transform(handle(transportApiRequestMsg.getGetDeviceProfileRequestMsg()),
+ value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
}
- return Futures.transform(getEmptyTransportApiResponseFuture(), value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
+ return Futures.transform(getEmptyTransportApiResponseFuture(),
+ value -> new TbProtoQueueMsg<>(tbProtoQueueMsg.getKey(), value, tbProtoQueueMsg.getHeaders()), MoreExecutors.directExecutor());
}
private ListenableFuture validateCredentials(String credentialsId, DeviceCredentialsType credentialsType) {
@@ -124,6 +154,62 @@ public class DefaultTransportApiService implements TransportApiService {
}
}
+ private ListenableFuture validateCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg mqtt) {
+ DeviceCredentials credentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(mqtt.getUserName());
+ if (credentials != null) {
+ if (credentials.getCredentialsType() == DeviceCredentialsType.ACCESS_TOKEN) {
+ return getDeviceInfo(credentials.getDeviceId(), credentials);
+ } else if (credentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) {
+ if (!checkMqttCredentials(mqtt, credentials)) {
+ credentials = null;
+ }
+ }
+ }
+ if (credentials == null) {
+ credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash("|", mqtt.getClientId(), mqtt.getUserName()));
+ if (credentials == null) {
+ credentials = checkMqttCredentials(mqtt, EncryptionUtil.getSha3Hash(mqtt.getClientId()));
+ }
+ }
+ if (credentials != null) {
+ return getDeviceInfo(credentials.getDeviceId(), credentials);
+ } else {
+ return getEmptyTransportApiResponseFuture();
+ }
+ }
+
+ private DeviceCredentials checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, String credId) {
+ DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByCredentialsId(credId);
+ if (deviceCredentials != null && deviceCredentials.getCredentialsType() == DeviceCredentialsType.MQTT_BASIC) {
+ if (!checkMqttCredentials(clientCred, deviceCredentials)) {
+ return null;
+ } else {
+ return deviceCredentials;
+ }
+ }
+ return null;
+ }
+
+ private boolean checkMqttCredentials(TransportProtos.ValidateBasicMqttCredRequestMsg clientCred, DeviceCredentials deviceCredentials) {
+ BasicMqttCredentials dbCred = JacksonUtil.fromString(deviceCredentials.getCredentialsValue(), BasicMqttCredentials.class);
+ if (!StringUtils.isEmpty(dbCred.getClientId()) && !dbCred.getClientId().equals(clientCred.getClientId())) {
+ return false;
+ }
+ if (!StringUtils.isEmpty(dbCred.getUserName()) && !dbCred.getUserName().equals(clientCred.getUserName())) {
+ return false;
+ }
+ if (!StringUtils.isEmpty(dbCred.getPassword())) {
+ if (StringUtils.isEmpty(clientCred.getPassword())) {
+ return false;
+ } else {
+ if (!dbCred.getPassword().equals(clientCred.getPassword())) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
private ListenableFuture handle(GetOrCreateDeviceFromGatewayRequestMsg requestMsg) {
DeviceId gatewayId = new DeviceId(new UUID(requestMsg.getGatewayIdMSB(), requestMsg.getGatewayIdLSB()));
ListenableFuture gatewayFuture = deviceService.findDeviceByIdAsync(TenantId.SYS_TENANT_ID, gatewayId);
@@ -139,6 +225,8 @@ public class DefaultTransportApiService implements TransportApiService {
device.setName(requestMsg.getDeviceName());
device.setType(requestMsg.getDeviceType());
device.setCustomerId(gateway.getCustomerId());
+ DeviceProfile deviceProfile = deviceProfileService.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType());
+ device.setDeviceProfileId(deviceProfile.getId());
device = deviceService.saveDevice(device);
relationService.saveRelationAsync(TenantId.SYS_TENANT_ID, new EntityRelation(gateway.getId(), device.getId(), "Created"));
deviceStateService.onDeviceAdded(device);
@@ -155,10 +243,19 @@ public class DefaultTransportApiService implements TransportApiService {
TbMsg tbMsg = TbMsg.newMsg(DataConstants.ENTITY_CREATED, deviceId, metaData, TbMsgDataType.JSON, mapper.writeValueAsString(entityNode));
tbClusterService.pushMsgToRuleEngine(tenantId, deviceId, tbMsg, null);
}
+ GetOrCreateDeviceFromGatewayResponseMsg.Builder builder = GetOrCreateDeviceFromGatewayResponseMsg.newBuilder()
+ .setDeviceInfo(getDeviceInfoProto(device));
+ DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId());
+ if (deviceProfile != null) {
+ builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)));
+ } else {
+ log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId());
+ }
return TransportApiResponseMsg.newBuilder()
- .setGetOrCreateDeviceResponseMsg(GetOrCreateDeviceFromGatewayResponseMsg.newBuilder().setDeviceInfo(getDeviceInfoProto(device)).build()).build();
+ .setGetOrCreateDeviceResponseMsg(builder.build())
+ .build();
} catch (JsonProcessingException e) {
- log.warn("[{}][{}] Failed to lookup device by gateway id and name", gatewayId, requestMsg.getDeviceName(), e);
+ log.warn("[{}] Failed to lookup device by gateway id and name: [{}]", gatewayId, requestMsg.getDeviceName(), e);
throw new RuntimeException(e);
} finally {
deviceCreationLock.unlock();
@@ -168,10 +265,23 @@ public class DefaultTransportApiService implements TransportApiService {
private ListenableFuture handle(GetTenantRoutingInfoRequestMsg requestMsg) {
TenantId tenantId = new TenantId(new UUID(requestMsg.getTenantIdMSB(), requestMsg.getTenantIdLSB()));
- ListenableFuture tenantFuture = tenantService.findTenantByIdAsync(TenantId.SYS_TENANT_ID, tenantId);
- return Futures.transform(tenantFuture, tenant -> TransportApiResponseMsg.newBuilder()
- .setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenant.isIsolatedTbCore())
- .setIsolatedTbRuleEngine(tenant.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService);
+ // TODO: Tenant Profile from cache
+ ListenableFuture tenantProfileFuture =
+ Futures.transform(tenantService.findTenantByIdAsync(TenantId.SYS_TENANT_ID, tenantId), tenant ->
+ tenantProfileService.findTenantProfileById(TenantId.SYS_TENANT_ID, tenant.getTenantProfileId()), dbCallbackExecutorService);
+ return Futures.transform(tenantProfileFuture, tenantProfile -> TransportApiResponseMsg.newBuilder()
+ .setGetTenantRoutingInfoResponseMsg(GetTenantRoutingInfoResponseMsg.newBuilder().setIsolatedTbCore(tenantProfile.isIsolatedTbCore())
+ .setIsolatedTbRuleEngine(tenantProfile.isIsolatedTbRuleEngine()).build()).build(), dbCallbackExecutorService);
+ }
+
+ private ListenableFuture handle(TransportProtos.GetDeviceProfileRequestMsg requestMsg) {
+ DeviceProfileId profileId = new DeviceProfileId(new UUID(requestMsg.getProfileIdMSB(), requestMsg.getProfileIdLSB()));
+ DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(TenantId.SYS_TENANT_ID, profileId);
+ return Futures.immediateFuture(TransportApiResponseMsg.newBuilder()
+ .setGetDeviceProfileResponseMsg(
+ TransportProtos.GetDeviceProfileResponseMsg.newBuilder()
+ .setData(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)))
+ .build()).build());
}
private ListenableFuture getDeviceInfo(DeviceId deviceId, DeviceCredentials credentials) {
@@ -183,11 +293,17 @@ public class DefaultTransportApiService implements TransportApiService {
try {
ValidateDeviceCredentialsResponseMsg.Builder builder = ValidateDeviceCredentialsResponseMsg.newBuilder();
builder.setDeviceInfo(getDeviceInfoProto(device));
+ DeviceProfile deviceProfile = deviceProfileService.findDeviceProfileById(device.getTenantId(), device.getDeviceProfileId());
+ if (deviceProfile != null) {
+ builder.setProfileBody(ByteString.copyFrom(dataDecodingEncodingService.encode(deviceProfile)));
+ } else {
+ log.warn("[{}] Failed to find device profile [{}] for device. ", device.getId(), device.getDeviceProfileId());
+ }
if (!StringUtils.isEmpty(credentials.getCredentialsValue())) {
builder.setCredentialsBody(credentials.getCredentialsValue());
}
return TransportApiResponseMsg.newBuilder()
- .setValidateTokenResponseMsg(builder.build()).build();
+ .setValidateCredResponseMsg(builder.build()).build();
} catch (JsonProcessingException e) {
log.warn("[{}] Failed to lookup device by id", deviceId, e);
return getEmptyTransportApiResponse();
@@ -203,6 +319,8 @@ public class DefaultTransportApiService implements TransportApiService {
.setDeviceIdLSB(device.getId().getId().getLeastSignificantBits())
.setDeviceName(device.getName())
.setDeviceType(device.getType())
+ .setDeviceProfileIdMSB(device.getDeviceProfileId().getId().getMostSignificantBits())
+ .setDeviceProfileIdLSB(device.getDeviceProfileId().getId().getLeastSignificantBits())
.setAdditionalInfo(mapper.writeValueAsString(device.getAdditionalInfo()))
.build();
}
@@ -213,6 +331,6 @@ public class DefaultTransportApiService implements TransportApiService {
private TransportApiResponseMsg getEmptyTransportApiResponse() {
return TransportApiResponseMsg.newBuilder()
- .setValidateTokenResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build();
+ .setValidateCredResponseMsg(ValidateDeviceCredentialsResponseMsg.getDefaultInstance()).build();
}
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java
index 4fc4df0048..41f90c642c 100644
--- a/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ttl/AbstractCleanUpService.java
@@ -38,19 +38,14 @@ public abstract class AbstractCleanUpService {
@Value("${spring.datasource.password}")
protected String dbPassword;
- protected long executeQuery(Connection conn, String query) {
- long removed = 0L;
- try {
- Statement statement = conn.createStatement();
- ResultSet resultSet = statement.executeQuery(query);
- getWarnings(statement);
+ protected long executeQuery(Connection conn, String query) throws SQLException {
+ try (Statement statement = conn.createStatement(); ResultSet resultSet = statement.executeQuery(query)) {
+ if (log.isDebugEnabled()) {
+ getWarnings(statement);
+ }
resultSet.next();
- removed = resultSet.getLong(1);
- log.debug("Successfully executed query: {}", query);
- } catch (SQLException e) {
- log.debug("Failed to execute query: {} due to: {}", query, e.getMessage());
+ return resultSet.getLong(1);
}
- return removed;
}
protected void getWarnings(Statement statement) throws SQLException {
@@ -65,6 +60,6 @@ public abstract class AbstractCleanUpService {
}
}
- protected abstract void doCleanUp(Connection connection);
+ protected abstract void doCleanUp(Connection connection) throws SQLException;
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java
index a608ca257b..0f3cd71f00 100644
--- a/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ttl/events/EventsCleanUpService.java
@@ -52,7 +52,7 @@ public class EventsCleanUpService extends AbstractCleanUpService {
}
@Override
- protected void doCleanUp(Connection connection) {
+ protected void doCleanUp(Connection connection) throws SQLException {
long totalEventsRemoved = executeQuery(connection, "call cleanup_events_by_ttl(" + ttl + ", " + debugTtl + ", 0);");
log.info("Total events removed by TTL: [{}]", totalEventsRemoved);
}
diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java
index fb09a7eab4..73a5c73732 100644
--- a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/PsqlTimeseriesCleanUpService.java
@@ -23,6 +23,7 @@ import org.thingsboard.server.dao.util.PsqlDao;
import org.thingsboard.server.dao.util.SqlTsDao;
import java.sql.Connection;
+import java.sql.SQLException;
@SqlTsDao
@PsqlDao
@@ -34,10 +35,10 @@ public class PsqlTimeseriesCleanUpService extends AbstractTimeseriesCleanUpServi
private String partitionType;
@Override
- protected void doCleanUp(Connection connection) {
- long totalPartitionsRemoved = executeQuery(connection, "call drop_partitions_by_max_ttl('" + partitionType + "'," + systemTtl + ", 0);");
- log.info("Total partitions removed by TTL: [{}]", totalPartitionsRemoved);
- long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);");
- log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
+ protected void doCleanUp(Connection connection) throws SQLException {
+ long totalPartitionsRemoved = executeQuery(connection, "call drop_partitions_by_max_ttl('" + partitionType + "'," + systemTtl + ", 0);");
+ log.info("Total partitions removed by TTL: [{}]", totalPartitionsRemoved);
+ long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);");
+ log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
}
}
\ No newline at end of file
diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java
index 7070ef5dc3..40febd1988 100644
--- a/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java
+++ b/application/src/main/java/org/thingsboard/server/service/ttl/timeseries/TimescaleTimeseriesCleanUpService.java
@@ -21,6 +21,7 @@ import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.TimescaleDBTsDao;
import java.sql.Connection;
+import java.sql.SQLException;
@TimescaleDBTsDao
@Service
@@ -28,8 +29,8 @@ import java.sql.Connection;
public class TimescaleTimeseriesCleanUpService extends AbstractTimeseriesCleanUpService {
@Override
- protected void doCleanUp(Connection connection) {
+ protected void doCleanUp(Connection connection) throws SQLException {
long totalEntitiesTelemetryRemoved = executeQuery(connection, "call cleanup_timeseries_by_ttl('" + ModelConstants.NULL_UUID + "'," + systemTtl + ", 0);");
log.info("Total telemetry removed stats by TTL for entities: [{}]", totalEntitiesTelemetryRemoved);
}
-}
\ No newline at end of file
+}
diff --git a/application/src/main/resources/logback.xml b/application/src/main/resources/logback.xml
index 02101a8eaa..e154de2d9b 100644
--- a/application/src/main/resources/logback.xml
+++ b/application/src/main/resources/logback.xml
@@ -29,6 +29,8 @@
+
+
diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml
index a1b7633f83..51363d3c83 100644
--- a/application/src/main/resources/thingsboard.yml
+++ b/application/src/main/resources/thingsboard.yml
@@ -194,8 +194,21 @@ cassandra:
url: "${CASSANDRA_URL:127.0.0.1:9042}"
# Specify local datacenter name
local_datacenter: "${CASSANDRA_LOCAL_DATACENTER:datacenter1}"
- # Enable/disable secure connection
- ssl: "${CASSANDRA_USE_SSL:false}"
+ ssl:
+ # Enable/disable secure connection
+ enabled: "${CASSANDRA_USE_SSL:false}"
+ # Enable/disable validation of Cassandra server hostname
+ # If enabled, hostname of Cassandra server must match CN of server certificate
+ hostname_validation: "${CASSANDRA_SSL_HOSTNAME_VALIDATION:true}"
+ # Set trust store for client authentication of server (optional, uses trust store from default SSLContext if not set)
+ trust_store: "${CASSANDRA_SSL_TRUST_STORE:}"
+ trust_store_password: "${CASSANDRA_SSL_TRUST_STORE_PASSWORD:}"
+ # Set key store for server authentication of client (optional, uses key store from default SSLContext if not set)
+ # A key store is only needed if the Cassandra server requires client authentication
+ key_store: "${CASSANDRA_SSL_KEY_STORE:}"
+ key_store_password: "${CASSANDRA_SSL_KEY_STORE_PASSWORD:}"
+ # Comma separated list of cipher suites (optional, uses Java default cipher suites if not set)
+ cipher_suites: "${CASSANDRA_SSL_CIPHER_SUITES:}"
# Enable/disable JMX
jmx: "${CASSANDRA_USE_JMX:false}"
# Enable/disable metrics collection.
@@ -271,6 +284,8 @@ sql:
batch_max_delay: "${SQL_TS_LATEST_BATCH_MAX_DELAY_MS:100}"
stats_print_interval_ms: "${SQL_TS_LATEST_BATCH_STATS_PRINT_MS:10000}"
batch_threads: "${SQL_TS_LATEST_BATCH_THREADS:4}"
+ # 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
remove_null_chars: "${SQL_REMOVE_NULL_CHARS:true}"
# Specify whether to log database queries and their parameters generated by entity query repository
@@ -372,6 +387,12 @@ caffeine:
securitySettings:
timeToLiveInMinutes: 1440
maxSize: 0
+ tenantProfiles:
+ timeToLiveInMinutes: 1440
+ maxSize: 0
+ deviceProfiles:
+ timeToLiveInMinutes: 1440
+ maxSize: 0
redis:
# standalone or cluster
@@ -418,6 +439,9 @@ updates:
# Enable/disable updates checking.
enabled: "${UPDATES_ENABLED:true}"
+# spring freemarker configuration
+spring.freemarker.checkTemplateLocation: "false"
+
# spring CORS configuration
spring.mvc.cors:
mappings:
@@ -483,6 +507,7 @@ audit-log:
"rule_chain": "${AUDIT_LOG_MASK_RULE_CHAIN:W}"
"alarm": "${AUDIT_LOG_MASK_ALARM:W}"
"entity_view": "${AUDIT_LOG_MASK_ENTITY_VIEW:W}"
+ "device_profile": "${AUDIT_LOG_MASK_DEVICE_PROFILE:W}"
sink:
# Type of external sink. possible options: none, elasticsearch
type: "${AUDIT_LOG_SINK_TYPE:none}"
@@ -583,6 +608,8 @@ transport:
key_password: "${MQTT_SSL_KEY_PASSWORD:server_key_password}"
# Type of the key store
key_store_type: "${MQTT_SSL_KEY_STORE_TYPE:JKS}"
+ # Skip certificate validity check for client certificates.
+ skip_validity_check_for_client_cert: "${MQTT_SSL_SKIP_VALIDITY_CHECK_FOR_CLIENT_CERT:false}"
# Local CoAP transport parameters
coap:
# Enable/disable coap transport protocol.
@@ -608,6 +635,10 @@ swagger:
queue:
type: "${TB_QUEUE_TYPE:in-memory}" # in-memory or kafka (Apache Kafka) or aws-sqs (AWS SQS) or pubsub (PubSub) or service-bus (Azure Service Bus) or rabbitmq (RabbitMQ)
+ in_memory:
+ stats:
+ # For debug lvl
+ print-interval-ms: "${TB_QUEUE_IN_MEMORY_STATS_PRINT_INTERVAL_MS:60000}"
kafka:
bootstrap.servers: "${TB_KAFKA_SERVERS:localhost:9092}"
acks: "${TB_KAFKA_ACKS:all}"
@@ -627,11 +658,11 @@ queue:
security.protocol: "${TB_QUEUE_KAFKA_CONFLUENT_SECURITY_PROTOCOL:SASL_SSL}"
other:
topic-properties:
- rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"
- core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"
- transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"
- notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000}"
- js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600}"
+ rule-engine: "${TB_QUEUE_KAFKA_RE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
+ core: "${TB_QUEUE_KAFKA_CORE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
+ transport-api: "${TB_QUEUE_KAFKA_TA_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
+ notifications: "${TB_QUEUE_KAFKA_NOTIFICATIONS_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:1048576000;partitions:1}"
+ js-executor: "${TB_QUEUE_KAFKA_JE_TOPIC_PROPERTIES:retention.ms:604800000;segment.bytes:26214400;retention.bytes:104857600;partitions:100}"
aws_sqs:
use_default_credential_provider_chain: "${TB_QUEUE_AWS_SQS_USE_DEFAULT_CREDENTIAL_PROVIDER_CHAIN:false}"
access_key_id: "${TB_QUEUE_AWS_SQS_ACCESS_KEY_ID:YOUR_KEY}"
@@ -739,6 +770,7 @@ queue:
retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited
failure-percentage: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages;
pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_RETRY_PAUSE:3}"# Time in seconds to wait in consumer thread before retries;
+ max-pause-between-retries: "${TB_QUEUE_RE_MAIN_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:3}"# Max allowed time in seconds for pause between retries.
- name: "${TB_QUEUE_RE_HP_QUEUE_NAME:HighPriority}"
topic: "${TB_QUEUE_RE_HP_TOPIC:tb_rule_engine.hp}"
poll-interval: "${TB_QUEUE_RE_HP_POLL_INTERVAL_MS:25}"
@@ -754,6 +786,7 @@ queue:
retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRIES:0}" # Number of retries, 0 is unlimited
failure-percentage: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages;
pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries;
+ max-pause-between-retries: "${TB_QUEUE_RE_HP_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}"# Max allowed time in seconds for pause between retries.
- name: "${TB_QUEUE_RE_SQ_QUEUE_NAME:SequentialByOriginator}"
topic: "${TB_QUEUE_RE_SQ_TOPIC:tb_rule_engine.sq}"
poll-interval: "${TB_QUEUE_RE_SQ_POLL_INTERVAL_MS:25}"
@@ -769,6 +802,7 @@ queue:
retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRIES:3}" # Number of retries, 0 is unlimited
failure-percentage: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_FAILURE_PERCENTAGE:0}" # Skip retry if failures or timeouts are less then X percentage of messages;
pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_RETRY_PAUSE:5}"# Time in seconds to wait in consumer thread before retries;
+ max-pause-between-retries: "${TB_QUEUE_RE_SQ_PROCESSING_STRATEGY_MAX_RETRY_PAUSE:5}"# Max allowed time in seconds for pause between retries.
transport:
# For high priority notifications that require minimum latency and processing time
notifications_topic: "${TB_QUEUE_TRANSPORT_NOTIFICATIONS_TOPIC:tb_transport.notifications}"
@@ -793,4 +827,4 @@ management:
web:
exposure:
# Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics).
- include: '${METRICS_ENDPOINTS_EXPOSE:info}'
\ No newline at end of file
+ include: '${METRICS_ENDPOINTS_EXPOSE:info}'
diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
index 992b39c472..8d3391bb65 100644
--- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
@@ -15,6 +15,7 @@
*/
package org.thingsboard.server.controller;
+import com.datastax.oss.driver.api.core.uuid.Uuids;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -59,8 +60,16 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.context.WebApplicationContext;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceProfileType;
+import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
+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.HasId;
+import org.thingsboard.server.common.data.id.RuleChainId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.page.PageLink;
@@ -217,6 +226,10 @@ public abstract class AbstractWebTest {
login(CUSTOMER_USER_EMAIL, CUSTOMER_USER_PASSWORD);
}
+ protected void loginUser(String userName, String password) throws Exception {
+ login(userName, password);
+ }
+
private Tenant savedDifferentTenant;
protected void loginDifferentTenant() throws Exception {
@@ -242,15 +255,27 @@ public abstract class AbstractWebTest {
protected User createUserAndLogin(User user, String password) throws Exception {
User savedUser = doPost("/api/user", user, User.class);
logout();
+ JsonNode activateRequest = getActivateRequest(password);
+ JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
+ validateAndSetJwtToken(tokenInfo, user.getEmail());
+ return savedUser;
+ }
+
+ protected User createUser(User user, String password) throws Exception {
+ User savedUser = doPost("/api/user", user, User.class);
+ JsonNode activateRequest = getActivateRequest(password);
+ ResultActions resultActions = doPost("/api/noauth/activate", activateRequest);
+ resultActions.andExpect(status().isOk());
+ return savedUser;
+ }
+
+ private JsonNode getActivateRequest(String password) throws Exception {
doGet("/api/noauth/activate?activateToken={activateToken}", TestMailService.currentActivateToken)
.andExpect(status().isSeeOther())
.andExpect(header().string(HttpHeaders.LOCATION, "/login/createPassword?activateToken=" + TestMailService.currentActivateToken));
- JsonNode activateRequest = new ObjectMapper().createObjectNode()
+ return new ObjectMapper().createObjectNode()
.put("activateToken", TestMailService.currentActivateToken)
.put("password", password);
- JsonNode tokenInfo = readResponse(doPost("/api/noauth/activate", activateRequest).andExpect(status().isOk()), JsonNode.class);
- validateAndSetJwtToken(tokenInfo, user.getEmail());
- return savedUser;
}
protected void login(String username, String password) throws Exception {
@@ -304,6 +329,23 @@ public abstract class AbstractWebTest {
}
}
+ protected DeviceProfile createDeviceProfile(String name) {
+ DeviceProfile deviceProfile = new DeviceProfile();
+ deviceProfile.setName(name);
+ deviceProfile.setType(DeviceProfileType.DEFAULT);
+ deviceProfile.setTransportType(DeviceTransportType.DEFAULT);
+ deviceProfile.setDescription(name + " Test");
+ DeviceProfileData deviceProfileData = new DeviceProfileData();
+ DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
+ DefaultDeviceProfileTransportConfiguration transportConfiguration = new DefaultDeviceProfileTransportConfiguration();
+ deviceProfileData.setConfiguration(configuration);
+ deviceProfileData.setTransportConfiguration(transportConfiguration);
+ deviceProfile.setProfileData(deviceProfileData);
+ deviceProfile.setDefault(false);
+ deviceProfile.setDefaultRuleChainId(null);
+ return deviceProfile;
+ }
+
protected ResultActions doGet(String urlTemplate, Object... urlVariables) throws Exception {
MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables);
setJwtToken(getRequest);
@@ -416,6 +458,10 @@ public abstract class AbstractWebTest {
return readResponse(doPostAsync(urlTemplate, content, timeout, params).andExpect(resultMatcher), responseClass);
}
+ protected T doPostClaimAsync(String urlTemplate, Object content, Class responseClass, ResultMatcher resultMatcher, String... params) throws Exception {
+ return readResponse(doPostAsync(urlTemplate, content, DEFAULT_TIMEOUT, params).andExpect(resultMatcher), responseClass);
+ }
+
protected T doDelete(String urlTemplate, Class responseClass, String... params) throws Exception {
return readResponse(doDelete(urlTemplate, params).andExpect(status().isOk()), responseClass);
}
@@ -486,7 +532,7 @@ public abstract class AbstractWebTest {
return mapper.readerFor(type).readValue(content);
}
- public class IdComparator> implements Comparator {
+ public class IdComparator implements Comparator {
@Override
public int compare(D o1, D o2) {
return o1.getId().getId().compareTo(o2.getId().getId());
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
index f87dc5de6d..72a885b062 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceControllerTest.java
@@ -24,6 +24,7 @@ import org.junit.Before;
import org.junit.Test;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.User;
@@ -185,9 +186,8 @@ public abstract class BaseDeviceControllerTest extends AbstractControllerTest {
public void testSaveDeviceWithEmptyType() throws Exception {
Device device = new Device();
device.setName("My device");
- doPost("/api/device", device)
- .andExpect(status().isBadRequest())
- .andExpect(statusReason(containsString("Device type should be specified")));
+ Device savedDevice = doPost("/api/device", device, Device.class);
+ Assert.assertEquals("default", savedDevice.getType());
}
@Test
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java
new file mode 100644
index 0000000000..b2334d7c46
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseDeviceProfileControllerTest.java
@@ -0,0 +1,309 @@
+/**
+ * Copyright © 2016-2020 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.Ignore;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceProfileInfo;
+import org.thingsboard.server.common.data.DeviceProfileType;
+import org.thingsboard.server.common.data.DeviceTransportType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.page.PageData;
+import org.thingsboard.server.common.data.page.PageLink;
+import org.thingsboard.server.common.data.security.Authority;
+
+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;
+
+public abstract class BaseDeviceProfileControllerTest extends AbstractControllerTest {
+
+ private IdComparator idComparator = new IdComparator<>();
+ private IdComparator deviceProfileInfoIdComparator = new IdComparator<>();
+
+ private Tenant savedTenant;
+ private User tenantAdmin;
+
+ @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 testSaveDeviceProfile() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
+ Assert.assertNotNull(savedDeviceProfile);
+ Assert.assertNotNull(savedDeviceProfile.getId());
+ Assert.assertTrue(savedDeviceProfile.getCreatedTime() > 0);
+ Assert.assertEquals(deviceProfile.getName(), savedDeviceProfile.getName());
+ Assert.assertEquals(deviceProfile.getDescription(), savedDeviceProfile.getDescription());
+ Assert.assertEquals(deviceProfile.getProfileData(), savedDeviceProfile.getProfileData());
+ Assert.assertEquals(deviceProfile.isDefault(), savedDeviceProfile.isDefault());
+ Assert.assertEquals(deviceProfile.getDefaultRuleChainId(), savedDeviceProfile.getDefaultRuleChainId());
+ savedDeviceProfile.setName("New device profile");
+ doPost("/api/deviceProfile", savedDeviceProfile, DeviceProfile.class);
+ DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class);
+ Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfile.getName());
+ }
+
+ @Test
+ public void testFindDeviceProfileById() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
+ DeviceProfile foundDeviceProfile = doGet("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString(), DeviceProfile.class);
+ Assert.assertNotNull(foundDeviceProfile);
+ Assert.assertEquals(savedDeviceProfile, foundDeviceProfile);
+ }
+
+ @Test
+ public void testFindDeviceProfileInfoById() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
+ DeviceProfileInfo foundDeviceProfileInfo = doGet("/api/deviceProfileInfo/"+savedDeviceProfile.getId().getId().toString(), DeviceProfileInfo.class);
+ Assert.assertNotNull(foundDeviceProfileInfo);
+ Assert.assertEquals(savedDeviceProfile.getId(), foundDeviceProfileInfo.getId());
+ Assert.assertEquals(savedDeviceProfile.getName(), foundDeviceProfileInfo.getName());
+ Assert.assertEquals(savedDeviceProfile.getType(), foundDeviceProfileInfo.getType());
+ }
+
+ @Test
+ public void testFindDefaultDeviceProfileInfo() throws Exception {
+ DeviceProfileInfo foundDefaultDeviceProfileInfo = doGet("/api/deviceProfileInfo/default", DeviceProfileInfo.class);
+ Assert.assertNotNull(foundDefaultDeviceProfileInfo);
+ Assert.assertNotNull(foundDefaultDeviceProfileInfo.getId());
+ Assert.assertNotNull(foundDefaultDeviceProfileInfo.getName());
+ Assert.assertNotNull(foundDefaultDeviceProfileInfo.getType());
+ Assert.assertEquals(DeviceProfileType.DEFAULT, foundDefaultDeviceProfileInfo.getType());
+ Assert.assertEquals("default", foundDefaultDeviceProfileInfo.getName());
+ }
+
+ @Test
+ public void testSetDefaultDeviceProfile() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile 1");
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
+ DeviceProfile defaultDeviceProfile = doPost("/api/deviceProfile/"+savedDeviceProfile.getId().getId().toString()+"/default", null, DeviceProfile.class);
+ Assert.assertNotNull(defaultDeviceProfile);
+ DeviceProfileInfo foundDefaultDeviceProfile = doGet("/api/deviceProfileInfo/default", DeviceProfileInfo.class);
+ Assert.assertNotNull(foundDefaultDeviceProfile);
+ Assert.assertEquals(savedDeviceProfile.getName(), foundDefaultDeviceProfile.getName());
+ Assert.assertEquals(savedDeviceProfile.getId(), foundDefaultDeviceProfile.getId());
+ Assert.assertEquals(savedDeviceProfile.getType(), foundDefaultDeviceProfile.getType());
+ }
+
+ @Test
+ public void testSaveDeviceProfileWithEmptyName() throws Exception {
+ DeviceProfile deviceProfile = new DeviceProfile();
+ doPost("/api/deviceProfile", deviceProfile).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Device profile name should be specified")));
+ }
+
+ @Test
+ public void testSaveDeviceProfileWithSameName() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
+ doPost("/api/deviceProfile", deviceProfile).andExpect(status().isOk());
+ DeviceProfile deviceProfile2 = this.createDeviceProfile("Device Profile");
+ doPost("/api/deviceProfile", deviceProfile2).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Device profile with such name already exists")));
+ }
+
+ @Ignore
+ @Test
+ public void testChangeDeviceProfileTypeWithExistingDevices() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
+ Device device = new Device();
+ device.setName("Test device");
+ device.setType("default");
+ device.setDeviceProfileId(savedDeviceProfile.getId());
+ doPost("/api/device", device, Device.class);
+ //TODO uncomment once we have other device types;
+ //savedDeviceProfile.setType(DeviceProfileType.LWM2M);
+ doPost("/api/deviceProfile", savedDeviceProfile).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Can't change device profile type because devices referenced it")));
+ }
+
+ @Test
+ public void testChangeDeviceProfileTransportTypeWithExistingDevices() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
+ Device device = new Device();
+ device.setName("Test device");
+ device.setType("default");
+ device.setDeviceProfileId(savedDeviceProfile.getId());
+ doPost("/api/device", device, Device.class);
+ savedDeviceProfile.setTransportType(DeviceTransportType.MQTT);
+ doPost("/api/deviceProfile", savedDeviceProfile).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Can't change device profile transport type because devices referenced it")));
+ }
+
+ @Test
+ public void testDeleteDeviceProfileWithExistingDevice() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
+
+ Device device = new Device();
+ device.setName("Test device");
+ device.setType("default");
+ device.setDeviceProfileId(savedDeviceProfile.getId());
+
+ Device savedDevice = doPost("/api/device", device, Device.class);
+
+ doDelete("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString())
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("The device profile referenced by the devices cannot be deleted")));
+ }
+
+ @Test
+ public void testDeleteDeviceProfile() throws Exception {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile");
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class);
+
+ doDelete("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doGet("/api/deviceProfile/" + savedDeviceProfile.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testFindDeviceProfiles() throws Exception {
+ List deviceProfiles = new ArrayList<>();
+ PageLink pageLink = new PageLink(17);
+ PageData pageData = doGetTypedWithPageLink("/api/deviceProfiles?",
+ new TypeReference>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getTotalElements());
+ deviceProfiles.addAll(pageData.getData());
+
+ for (int i=0;i<28;i++) {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i);
+ deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class));
+ }
+
+ List loadedDeviceProfiles = new ArrayList<>();
+ pageLink = new PageLink(17);
+ do {
+ pageData = doGetTypedWithPageLink("/api/deviceProfiles?",
+ new TypeReference>(){}, pageLink);
+ loadedDeviceProfiles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(deviceProfiles, idComparator);
+ Collections.sort(loadedDeviceProfiles, idComparator);
+
+ Assert.assertEquals(deviceProfiles, loadedDeviceProfiles);
+
+ for (DeviceProfile deviceProfile : loadedDeviceProfiles) {
+ if (!deviceProfile.isDefault()) {
+ doDelete("/api/deviceProfile/" + deviceProfile.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+ }
+
+ pageLink = new PageLink(17);
+ pageData = doGetTypedWithPageLink("/api/deviceProfiles?",
+ new TypeReference>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getTotalElements());
+ }
+
+ @Test
+ public void testFindDeviceProfileInfos() throws Exception {
+ List deviceProfiles = new ArrayList<>();
+ PageLink pageLink = new PageLink(17);
+ PageData deviceProfilePageData = doGetTypedWithPageLink("/api/deviceProfiles?",
+ new TypeReference>(){}, pageLink);
+ Assert.assertFalse(deviceProfilePageData.hasNext());
+ Assert.assertEquals(1, deviceProfilePageData.getTotalElements());
+ deviceProfiles.addAll(deviceProfilePageData.getData());
+
+ for (int i=0;i<28;i++) {
+ DeviceProfile deviceProfile = this.createDeviceProfile("Device Profile"+i);
+ deviceProfiles.add(doPost("/api/deviceProfile", deviceProfile, DeviceProfile.class));
+ }
+
+ List loadedDeviceProfileInfos = new ArrayList<>();
+ pageLink = new PageLink(17);
+ PageData pageData;
+ do {
+ pageData = doGetTypedWithPageLink("/api/deviceProfileInfos?",
+ new TypeReference>(){}, pageLink);
+ loadedDeviceProfileInfos.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(deviceProfiles, idComparator);
+ Collections.sort(loadedDeviceProfileInfos, deviceProfileInfoIdComparator);
+
+ List deviceProfileInfos = deviceProfiles.stream().map(deviceProfile -> new DeviceProfileInfo(deviceProfile.getId(),
+ deviceProfile.getName(), deviceProfile.getType(), deviceProfile.getTransportType())).collect(Collectors.toList());
+
+ Assert.assertEquals(deviceProfileInfos, loadedDeviceProfileInfos);
+
+ for (DeviceProfile deviceProfile : deviceProfiles) {
+ if (!deviceProfile.isDefault()) {
+ doDelete("/api/deviceProfile/" + deviceProfile.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+ }
+
+ pageLink = new PageLink(17);
+ pageData = doGetTypedWithPageLink("/api/deviceProfileInfos?",
+ new TypeReference>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getTotalElements());
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
index 1d474b8b31..5aeb12699b 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseEntityViewControllerTest.java
@@ -22,6 +22,7 @@ import org.apache.commons.lang3.RandomStringUtils;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
@@ -424,7 +425,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
- MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId);
+ MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
@@ -466,7 +467,7 @@ public abstract class BaseEntityViewControllerTest extends AbstractControllerTes
assertNotNull(accessToken);
String clientId = MqttAsyncClient.generateClientId();
- MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId);
+ MqttAsyncClient client = new MqttAsyncClient("tcp://localhost:1883", clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(accessToken);
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java
index e9641646ff..2294987668 100644
--- a/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantControllerTest.java
@@ -15,21 +15,21 @@
*/
package org.thingsboard.server.controller;
-import static org.hamcrest.Matchers.containsString;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
+import com.fasterxml.jackson.core.type.TypeReference;
import org.apache.commons.lang3.RandomStringUtils;
+import org.junit.Assert;
+import org.junit.Test;
import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TenantInfo;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
-import org.junit.Assert;
-import org.junit.Test;
-import com.fasterxml.jackson.core.type.TypeReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static org.hamcrest.Matchers.containsString;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class BaseTenantControllerTest extends AbstractControllerTest {
@@ -65,6 +65,19 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest {
doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
.andExpect(status().isOk());
}
+
+ @Test
+ public void testFindTenantInfoById() throws Exception {
+ loginSysAdmin();
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant");
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+ TenantInfo foundTenant = doGet("/api/tenant/info/"+savedTenant.getId().getId().toString(), TenantInfo.class);
+ Assert.assertNotNull(foundTenant);
+ Assert.assertEquals(new TenantInfo(savedTenant, "Default"), foundTenant);
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
@Test
public void testSaveTenantWithEmptyTitle() throws Exception {
@@ -217,4 +230,48 @@ public abstract class BaseTenantControllerTest extends AbstractControllerTest {
Assert.assertFalse(pageData.hasNext());
Assert.assertEquals(0, pageData.getData().size());
}
+
+ @Test
+ public void testFindTenantInfos() throws Exception {
+ loginSysAdmin();
+ List tenants = new ArrayList<>();
+ PageLink pageLink = new PageLink(17);
+ PageData pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getData().size());
+ tenants.addAll(pageData.getData());
+
+ for (int i=0;i<56;i++) {
+ Tenant tenant = new Tenant();
+ tenant.setTitle("Tenant"+i);
+ tenants.add(new TenantInfo(doPost("/api/tenant", tenant, Tenant.class), "Default"));
+ }
+
+ List loadedTenants = new ArrayList<>();
+ pageLink = new PageLink(17);
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink);
+ loadedTenants.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenants, idComparator);
+ Collections.sort(loadedTenants, idComparator);
+
+ Assert.assertEquals(tenants, loadedTenants);
+
+ for (TenantInfo tenant : loadedTenants) {
+ if (!tenant.getTitle().equals(TEST_TENANT_NAME)) {
+ doDelete("/api/tenant/"+tenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+ }
+
+ pageLink = new PageLink(17);
+ pageData = doGetTypedWithPageLink("/api/tenantInfos?", new TypeReference>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getData().size());
+ }
}
diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java
new file mode 100644
index 0000000000..a4d615ae2b
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/BaseTenantProfileControllerTest.java
@@ -0,0 +1,294 @@
+/**
+ * Copyright © 2016-2020 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.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.thingsboard.server.common.data.EntityInfo;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TenantProfile;
+import org.thingsboard.server.common.data.TenantProfileData;
+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.tenant.TenantProfileService;
+
+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;
+
+public abstract class BaseTenantProfileControllerTest extends AbstractControllerTest {
+
+ private IdComparator idComparator = new IdComparator<>();
+ private IdComparator tenantProfileInfoIdComparator = new IdComparator<>();
+
+ @Autowired
+ private TenantProfileService tenantProfileService;
+
+ @After
+ @Override
+ public void teardown() throws Exception {
+ super.teardown();
+ tenantProfileService.deleteTenantProfiles(TenantId.SYS_TENANT_ID);
+ }
+
+ @Test
+ public void testSaveTenantProfile() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
+ TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+ Assert.assertNotNull(savedTenantProfile);
+ Assert.assertNotNull(savedTenantProfile.getId());
+ Assert.assertTrue(savedTenantProfile.getCreatedTime() > 0);
+ Assert.assertEquals(tenantProfile.getName(), savedTenantProfile.getName());
+ Assert.assertEquals(tenantProfile.getDescription(), savedTenantProfile.getDescription());
+ Assert.assertEquals(tenantProfile.getProfileData(), savedTenantProfile.getProfileData());
+ Assert.assertEquals(tenantProfile.isDefault(), savedTenantProfile.isDefault());
+ Assert.assertEquals(tenantProfile.isIsolatedTbCore(), savedTenantProfile.isIsolatedTbCore());
+ Assert.assertEquals(tenantProfile.isIsolatedTbRuleEngine(), savedTenantProfile.isIsolatedTbRuleEngine());
+
+ savedTenantProfile.setName("New tenant profile");
+ doPost("/api/tenantProfile", savedTenantProfile, TenantProfile.class);
+ TenantProfile foundTenantProfile = doGet("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString(), TenantProfile.class);
+ Assert.assertEquals(foundTenantProfile.getName(), savedTenantProfile.getName());
+ }
+
+ @Test
+ public void testFindTenantProfileById() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
+ TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+ TenantProfile foundTenantProfile = doGet("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString(), TenantProfile.class);
+ Assert.assertNotNull(foundTenantProfile);
+ Assert.assertEquals(savedTenantProfile, foundTenantProfile);
+ }
+
+ @Test
+ public void testFindTenantProfileInfoById() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
+ TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+ EntityInfo foundTenantProfileInfo = doGet("/api/tenantProfileInfo/"+savedTenantProfile.getId().getId().toString(), EntityInfo.class);
+ Assert.assertNotNull(foundTenantProfileInfo);
+ Assert.assertEquals(savedTenantProfile.getId(), foundTenantProfileInfo.getId());
+ Assert.assertEquals(savedTenantProfile.getName(), foundTenantProfileInfo.getName());
+ }
+
+ @Test
+ public void testFindDefaultTenantProfileInfo() throws Exception {
+ loginSysAdmin();
+ EntityInfo foundDefaultTenantProfile = doGet("/api/tenantProfileInfo/default", EntityInfo.class);
+ Assert.assertNotNull(foundDefaultTenantProfile);
+ Assert.assertEquals("Default", foundDefaultTenantProfile.getName());
+ }
+
+ @Test
+ public void testSetDefaultTenantProfile() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile 1");
+ TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+ TenantProfile defaultTenantProfile = doPost("/api/tenantProfile/"+savedTenantProfile.getId().getId().toString()+"/default", null, TenantProfile.class);
+ Assert.assertNotNull(defaultTenantProfile);
+ EntityInfo foundDefaultTenantProfile = doGet("/api/tenantProfileInfo/default", EntityInfo.class);
+ Assert.assertNotNull(foundDefaultTenantProfile);
+ Assert.assertEquals(savedTenantProfile.getName(), foundDefaultTenantProfile.getName());
+ Assert.assertEquals(savedTenantProfile.getId(), foundDefaultTenantProfile.getId());
+ }
+
+ @Test
+ public void testSaveTenantProfileWithEmptyName() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = new TenantProfile();
+ doPost("/api/tenantProfile", tenantProfile).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Tenant profile name should be specified")));
+ }
+
+ @Test
+ public void testSaveTenantProfileWithSameName() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
+ doPost("/api/tenantProfile", tenantProfile).andExpect(status().isOk());
+ TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile");
+ doPost("/api/tenantProfile", tenantProfile2).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Tenant profile with such name already exists")));
+ }
+
+ @Test
+ public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
+ TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+ savedTenantProfile.setIsolatedTbRuleEngine(true);
+ doPost("/api/tenantProfile", savedTenantProfile).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Can't update isolatedTbRuleEngine property")));
+ }
+
+ @Test
+ public void testSaveSameTenantProfileWithDifferentIsolatedTbCore() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
+ TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+ savedTenantProfile.setIsolatedTbCore(true);
+ doPost("/api/tenantProfile", savedTenantProfile).andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("Can't update isolatedTbCore property")));
+ }
+
+ @Test
+ public void testDeleteTenantProfileWithExistingTenant() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
+ TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+
+ Tenant tenant = new Tenant();
+ tenant.setTitle("My tenant with tenant profile");
+ tenant.setTenantProfileId(savedTenantProfile.getId());
+ Tenant savedTenant = doPost("/api/tenant", tenant, Tenant.class);
+
+ doDelete("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString())
+ .andExpect(status().isBadRequest())
+ .andExpect(statusReason(containsString("The tenant profile referenced by the tenants cannot be deleted")));
+
+ doDelete("/api/tenant/"+savedTenant.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+
+ @Test
+ public void testDeleteTenantProfile() throws Exception {
+ loginSysAdmin();
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile");
+ TenantProfile savedTenantProfile = doPost("/api/tenantProfile", tenantProfile, TenantProfile.class);
+
+ doDelete("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString())
+ .andExpect(status().isOk());
+
+ doGet("/api/tenantProfile/" + savedTenantProfile.getId().getId().toString())
+ .andExpect(status().isNotFound());
+ }
+
+ @Test
+ public void testFindTenantProfiles() throws Exception {
+ loginSysAdmin();
+ List tenantProfiles = new ArrayList<>();
+ PageLink pageLink = new PageLink(17);
+ PageData pageData = doGetTypedWithPageLink("/api/tenantProfiles?",
+ new TypeReference>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getTotalElements());
+ tenantProfiles.addAll(pageData.getData());
+
+ for (int i=0;i<28;i++) {
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"+i);
+ tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class));
+ }
+
+ List loadedTenantProfiles = new ArrayList<>();
+ pageLink = new PageLink(17);
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenantProfiles?",
+ new TypeReference>(){}, pageLink);
+ loadedTenantProfiles.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantProfiles, idComparator);
+ Collections.sort(loadedTenantProfiles, idComparator);
+
+ Assert.assertEquals(tenantProfiles, loadedTenantProfiles);
+
+ for (TenantProfile tenantProfile : loadedTenantProfiles) {
+ if (!tenantProfile.isDefault()) {
+ doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+ }
+
+ pageLink = new PageLink(17);
+ pageData = doGetTypedWithPageLink("/api/tenantProfiles?",
+ new TypeReference>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getTotalElements());
+ }
+
+ @Test
+ public void testFindTenantProfileInfos() throws Exception {
+ loginSysAdmin();
+ List tenantProfiles = new ArrayList<>();
+ PageLink pageLink = new PageLink(17);
+ PageData tenantProfilePageData = doGetTypedWithPageLink("/api/tenantProfiles?",
+ new TypeReference>(){}, pageLink);
+ Assert.assertFalse(tenantProfilePageData.hasNext());
+ Assert.assertEquals(1, tenantProfilePageData.getTotalElements());
+ tenantProfiles.addAll(tenantProfilePageData.getData());
+
+ for (int i=0;i<28;i++) {
+ TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"+i);
+ tenantProfiles.add(doPost("/api/tenantProfile", tenantProfile, TenantProfile.class));
+ }
+
+ List loadedTenantProfileInfos = new ArrayList<>();
+ pageLink = new PageLink(17);
+ PageData pageData;
+ do {
+ pageData = doGetTypedWithPageLink("/api/tenantProfileInfos?",
+ new TypeReference>(){}, pageLink);
+ loadedTenantProfileInfos.addAll(pageData.getData());
+ if (pageData.hasNext()) {
+ pageLink = pageLink.nextPageLink();
+ }
+ } while (pageData.hasNext());
+
+ Collections.sort(tenantProfiles, idComparator);
+ Collections.sort(loadedTenantProfileInfos, tenantProfileInfoIdComparator);
+
+ List tenantProfileInfos = tenantProfiles.stream().map(tenantProfile -> new EntityInfo(tenantProfile.getId(),
+ tenantProfile.getName())).collect(Collectors.toList());
+
+ Assert.assertEquals(tenantProfileInfos, loadedTenantProfileInfos);
+
+ for (TenantProfile tenantProfile : tenantProfiles) {
+ if (!tenantProfile.isDefault()) {
+ doDelete("/api/tenantProfile/" + tenantProfile.getId().getId().toString())
+ .andExpect(status().isOk());
+ }
+ }
+
+ pageLink = new PageLink(17);
+ pageData = doGetTypedWithPageLink("/api/tenantProfileInfos?",
+ new TypeReference>(){}, pageLink);
+ Assert.assertFalse(pageData.hasNext());
+ Assert.assertEquals(1, pageData.getTotalElements());
+ }
+
+ private TenantProfile createTenantProfile(String name) {
+ TenantProfile tenantProfile = new TenantProfile();
+ tenantProfile.setName(name);
+ tenantProfile.setDescription(name + " Test");
+ tenantProfile.setProfileData(new TenantProfileData());
+ tenantProfile.setDefault(false);
+ tenantProfile.setIsolatedTbCore(false);
+ tenantProfile.setIsolatedTbRuleEngine(false);
+ return tenantProfile;
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java
index 15da972cf5..64f9b8fddf 100644
--- a/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java
+++ b/application/src/test/java/org/thingsboard/server/controller/ControllerSqlTestSuite.java
@@ -34,7 +34,7 @@ public class ControllerSqlTestSuite {
@ClassRule
public static CustomSqlUnit sqlUnit = new CustomSqlUnit(
- Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql"),
+ Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/schema-entities-idx.sql", "sql/system-data.sql"),
"sql/hsql/drop-all-tables.sql",
"sql-test.properties");
diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java
new file mode 100644
index 0000000000..493c15b578
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/sql/DeviceProfileControllerSqlTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2020 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.BaseDeviceProfileControllerTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+@DaoSqlTest
+public class DeviceProfileControllerSqlTest extends BaseDeviceProfileControllerTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java b/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java
new file mode 100644
index 0000000000..869fd41470
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/controller/sql/TenantProfileControllerSqlTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2020 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.BaseTenantProfileControllerTest;
+import org.thingsboard.server.dao.service.DaoSqlTest;
+
+@DaoSqlTest
+public class TenantProfileControllerSqlTest extends BaseTenantProfileControllerTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java
new file mode 100644
index 0000000000..337904718c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/AbstractMqttIntegrationTest.java
@@ -0,0 +1,246 @@
+/**
+ * Copyright © 2016-2020 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.mqtt;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;
+import org.junit.Assert;
+import org.springframework.util.StringUtils;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceProfileType;
+import org.thingsboard.server.common.data.DeviceTransportType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
+import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
+import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.controller.AbstractControllerTest;
+import org.thingsboard.server.gen.transport.TransportProtos;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@Slf4j
+public abstract class AbstractMqttIntegrationTest extends AbstractControllerTest {
+
+ protected static final String MQTT_URL = "tcp://localhost:1883";
+
+ private static final AtomicInteger atomicInteger = new AtomicInteger(2);
+
+ protected Tenant savedTenant;
+ protected User tenantAdmin;
+
+ protected Device savedDevice;
+ protected String accessToken;
+
+ protected Device savedGateway;
+ protected String gatewayAccessToken;
+
+ protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) 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("tenant" + atomicInteger.getAndIncrement() + "@thingsboard.org");
+ tenantAdmin.setFirstName("Joe");
+ tenantAdmin.setLastName("Downs");
+
+ tenantAdmin = createUserAndLogin(tenantAdmin, "testPassword1");
+
+ Device device = new Device();
+ device.setName(deviceName);
+ device.setType("default");
+
+ Device gateway = new Device();
+ gateway.setName(gatewayName);
+ gateway.setType("default");
+ ObjectNode additionalInfo = mapper.createObjectNode();
+ additionalInfo.put("gateway", true);
+ gateway.setAdditionalInfo(additionalInfo);
+
+ if (payloadType != null) {
+ DeviceProfile mqttDeviceProfile = createMqttDeviceProfile(payloadType, telemetryTopic, attributesTopic);
+ DeviceProfile savedDeviceProfile = doPost("/api/deviceProfile", mqttDeviceProfile, DeviceProfile.class);
+ device.setType(savedDeviceProfile.getName());
+ device.setDeviceProfileId(savedDeviceProfile.getId());
+ gateway.setType(savedDeviceProfile.getName());
+ gateway.setDeviceProfileId(savedDeviceProfile.getId());
+ }
+
+ savedDevice = doPost("/api/device", device, Device.class);
+
+ DeviceCredentials deviceCredentials =
+ doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+
+ savedGateway = doPost("/api/device", gateway, Device.class);
+
+ DeviceCredentials gatewayCredentials =
+ doGet("/api/device/" + savedGateway.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+
+ assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
+ accessToken = deviceCredentials.getCredentialsId();
+ assertNotNull(accessToken);
+
+ assertEquals(savedGateway.getId(), gatewayCredentials.getDeviceId());
+ gatewayAccessToken = gatewayCredentials.getCredentialsId();
+ assertNotNull(gatewayAccessToken);
+
+ }
+
+ protected void processAfterTest() throws Exception {
+ loginSysAdmin();
+ if (savedTenant != null) {
+ doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
+ }
+ }
+
+ protected MqttAsyncClient getMqttAsyncClient(String accessToken) throws MqttException {
+ String clientId = MqttAsyncClient.generateClientId();
+ MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId, new MemoryPersistence());
+
+ MqttConnectOptions options = new MqttConnectOptions();
+ options.setUserName(accessToken);
+ client.connect(options).waitForCompletion();
+ return client;
+ }
+
+ protected void publishMqttMsg(MqttAsyncClient client, byte[] payload, String topic) throws MqttException {
+ MqttMessage message = new MqttMessage();
+ message.setPayload(payload);
+ client.publish(topic, message);
+ }
+
+ protected List getKvProtos(List expectedKeys) {
+ List keyValueProtos = new ArrayList<>();
+ TransportProtos.KeyValueProto strKeyValueProto = getKeyValueProto(expectedKeys.get(0), "value1", TransportProtos.KeyValueType.STRING_V);
+ TransportProtos.KeyValueProto boolKeyValueProto = getKeyValueProto(expectedKeys.get(1), "true", TransportProtos.KeyValueType.BOOLEAN_V);
+ TransportProtos.KeyValueProto dblKeyValueProto = getKeyValueProto(expectedKeys.get(2), "3.0", TransportProtos.KeyValueType.DOUBLE_V);
+ TransportProtos.KeyValueProto longKeyValueProto = getKeyValueProto(expectedKeys.get(3), "4", TransportProtos.KeyValueType.LONG_V);
+ TransportProtos.KeyValueProto jsonKeyValueProto = getKeyValueProto(expectedKeys.get(4), "{\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}", TransportProtos.KeyValueType.JSON_V);
+ keyValueProtos.add(strKeyValueProto);
+ keyValueProtos.add(boolKeyValueProto);
+ keyValueProtos.add(dblKeyValueProto);
+ keyValueProtos.add(longKeyValueProto);
+ keyValueProtos.add(jsonKeyValueProto);
+ return keyValueProtos;
+ }
+
+ protected TransportProtos.KeyValueProto getKeyValueProto(String key, String strValue, TransportProtos.KeyValueType type) {
+ TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder();
+ keyValueProtoBuilder.setKey(key);
+ keyValueProtoBuilder.setType(type);
+ switch (type) {
+ case BOOLEAN_V:
+ keyValueProtoBuilder.setBoolV(Boolean.parseBoolean(strValue));
+ break;
+ case LONG_V:
+ keyValueProtoBuilder.setLongV(Long.parseLong(strValue));
+ break;
+ case DOUBLE_V:
+ keyValueProtoBuilder.setDoubleV(Double.parseDouble(strValue));
+ break;
+ case STRING_V:
+ keyValueProtoBuilder.setStringV(strValue);
+ break;
+ case JSON_V:
+ keyValueProtoBuilder.setJsonV(strValue);
+ break;
+ }
+ return keyValueProtoBuilder.build();
+ }
+
+ protected DeviceProfile createMqttDeviceProfile(TransportPayloadType transportPayloadType, String telemetryTopic, String attributesTopic) {
+ DeviceProfile deviceProfile = new DeviceProfile();
+ deviceProfile.setName(transportPayloadType.name());
+ deviceProfile.setType(DeviceProfileType.DEFAULT);
+ deviceProfile.setTransportType(DeviceTransportType.MQTT);
+ deviceProfile.setDescription(transportPayloadType.name() + " Test");
+ DeviceProfileData deviceProfileData = new DeviceProfileData();
+ DefaultDeviceProfileConfiguration configuration = new DefaultDeviceProfileConfiguration();
+ MqttDeviceProfileTransportConfiguration transportConfiguration = new MqttDeviceProfileTransportConfiguration();
+ transportConfiguration.setTransportPayloadType(transportPayloadType);
+ if (!StringUtils.isEmpty(telemetryTopic)) {
+ transportConfiguration.setDeviceTelemetryTopic(telemetryTopic);
+ }
+ if (!StringUtils.isEmpty(attributesTopic)) {
+ transportConfiguration.setDeviceAttributesTopic(attributesTopic);
+ }
+ deviceProfileData.setTransportConfiguration(transportConfiguration);
+ deviceProfileData.setConfiguration(configuration);
+ deviceProfile.setProfileData(deviceProfileData);
+ deviceProfile.setDefault(false);
+ deviceProfile.setDefaultRuleChainId(null);
+ return deviceProfile;
+ }
+
+ protected TransportProtos.PostAttributeMsg getPostAttributeMsg(List expectedKeys) {
+ List kvProtos = getKvProtos(expectedKeys);
+ TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder();
+ builder.addAllKv(kvProtos);
+ return builder.build();
+ }
+
+ protected T doExecuteWithRetriesAndInterval(SupplierWithThrowable supplier, int retries, int intervalMs) throws Exception {
+ int count = 0;
+ T result = null;
+ Throwable lastException = null;
+ while (count < retries) {
+ try {
+ result = supplier.get();
+ if (result != null) {
+ return result;
+ }
+ } catch (Throwable e) {
+ lastException = e;
+ }
+ count++;
+ if (count < retries) {
+ Thread.sleep(intervalMs);
+ }
+ }
+ if (lastException != null) {
+ throw new RuntimeException(lastException);
+ } else {
+ return result;
+ }
+ }
+
+ @FunctionalInterface
+ public interface SupplierWithThrowable {
+ T get() throws Throwable;
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java
index a062fb7ebe..bebf1ae017 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttNoSqlTestSuite.java
@@ -33,7 +33,7 @@ public class MqttNoSqlTestSuite {
@ClassRule
public static CustomSqlUnit sqlUnit = new CustomSqlUnit(
- Arrays.asList("sql/schema-entities-hsql.sql", "sql/system-data.sql"),
+ Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"),
"sql/hsql/drop-all-tables.sql",
"nosql-test.properties");
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java
index 0e7234c716..095a5c3e7a 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/MqttSqlTestSuite.java
@@ -27,13 +27,17 @@ import java.util.Arrays;
@RunWith(ClasspathSuite.class)
@ClasspathSuite.ClassnameFilters({
"org.thingsboard.server.mqtt.rpc.sql.*Test",
- "org.thingsboard.server.mqtt.telemetry.sql.*Test"
+ "org.thingsboard.server.mqtt.telemetry.timeseries.sql.*Test",
+ "org.thingsboard.server.mqtt.telemetry.attributes.sql.*Test",
+ "org.thingsboard.server.mqtt.attributes.updates.sql.*Test",
+ "org.thingsboard.server.mqtt.attributes.request.sql.*Test",
+ "org.thingsboard.server.mqtt.claim.sql.*Test"
})
public class MqttSqlTestSuite {
@ClassRule
public static CustomSqlUnit sqlUnit = new CustomSqlUnit(
- Arrays.asList("sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"),
+ Arrays.asList("sql/schema-types-hsql.sql", "sql/schema-ts-hsql.sql", "sql/schema-entities-hsql.sql", "sql/system-data.sql"),
"sql/hsql/drop-all-tables.sql",
"sql-test.properties");
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java
new file mode 100644
index 0000000000..32488c8eb0
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/AbstractMqttAttributesIntegrationTest.java
@@ -0,0 +1,111 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.gen.transport.TransportProtos;
+import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+
+@Slf4j
+public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest {
+
+ protected static final String POST_ATTRIBUTES_PAYLOAD = "{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73," +
+ "\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}";
+
+ protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception {
+ super.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic);
+ }
+
+ protected void processAfterTest() throws Exception {
+ super.processAfterTest();
+ }
+
+ protected List getTsKvProtoList() {
+ TransportProtos.TsKvProto tsKvProtoAttribute1 = getTsKvProto("attribute1", "value1", TransportProtos.KeyValueType.STRING_V);
+ TransportProtos.TsKvProto tsKvProtoAttribute2 = getTsKvProto("attribute2", "true", TransportProtos.KeyValueType.BOOLEAN_V);
+ TransportProtos.TsKvProto tsKvProtoAttribute3 = getTsKvProto("attribute3", "42.0", TransportProtos.KeyValueType.DOUBLE_V);
+ TransportProtos.TsKvProto tsKvProtoAttribute4 = getTsKvProto("attribute4", "73", TransportProtos.KeyValueType.LONG_V);
+ TransportProtos.TsKvProto tsKvProtoAttribute5 = getTsKvProto("attribute5", "{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}", TransportProtos.KeyValueType.JSON_V);
+ List tsKvProtoList = new ArrayList<>();
+ tsKvProtoList.add(tsKvProtoAttribute1);
+ tsKvProtoList.add(tsKvProtoAttribute2);
+ tsKvProtoList.add(tsKvProtoAttribute3);
+ tsKvProtoList.add(tsKvProtoAttribute4);
+ tsKvProtoList.add(tsKvProtoAttribute5);
+ return tsKvProtoList;
+ }
+
+
+ protected TransportProtos.TsKvProto getTsKvProto(String key, String value, TransportProtos.KeyValueType keyValueType) {
+ TransportProtos.TsKvProto.Builder tsKvProtoBuilder = TransportProtos.TsKvProto.newBuilder();
+ TransportProtos.KeyValueProto keyValueProto = getKeyValueProto(key, value, keyValueType);
+ tsKvProtoBuilder.setKv(keyValueProto);
+ return tsKvProtoBuilder.build();
+ }
+
+ protected TestMqttCallback getTestMqttCallback() {
+ CountDownLatch latch = new CountDownLatch(1);
+ return new TestMqttCallback(latch);
+ }
+
+ protected static class TestMqttCallback implements MqttCallback {
+
+ private final CountDownLatch latch;
+ private Integer qoS;
+ private byte[] payloadBytes;
+
+ TestMqttCallback(CountDownLatch latch) {
+ this.latch = latch;
+ }
+
+ public int getQoS() {
+ return qoS;
+ }
+
+ public byte[] getPayloadBytes() {
+ return payloadBytes;
+ }
+
+ public CountDownLatch getLatch() {
+ return latch;
+ }
+
+ @Override
+ public void connectionLost(Throwable throwable) {
+ }
+
+ @Override
+ public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
+ qoS = mqttMessage.getQos();
+ payloadBytes = mqttMessage.getPayload();
+ latch.countDown();
+ }
+
+ @Override
+ public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
+
+ }
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java
new file mode 100644
index 0000000000..9e4529caf2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestIntegrationTest.java
@@ -0,0 +1,154 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.request;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
+import org.thingsboard.server.dao.util.mapping.JacksonUtil;
+import org.thingsboard.server.mqtt.attributes.AbstractMqttAttributesIntegrationTest;
+
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@Slf4j
+public abstract class AbstractMqttAttributesRequestIntegrationTest extends AbstractMqttAttributesIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("Test Request attribute values from the server", "Gateway Test Request attribute values from the server", null, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ processAfterTest();
+ }
+
+ @Test
+ public void testRequestAttributesValuesFromTheServer() throws Exception {
+ processTestRequestAttributesValuesFromTheServer();
+ }
+
+ @Test
+ public void testRequestAttributesValuesFromTheServerGateway() throws Exception {
+ processTestGatewayRequestAttributesValuesFromTheServer();
+ }
+
+ protected void processTestRequestAttributesValuesFromTheServer() throws Exception {
+
+ MqttAsyncClient client = getMqttAsyncClient(accessToken);
+
+ postAttributesAndSubscribeToTopic(savedDevice, client);
+
+ Thread.sleep(1000);
+
+ TestMqttCallback callback = getTestMqttCallback();
+ client.setCallback(callback);
+
+ validateResponse(client, callback.getLatch(), callback);
+ }
+
+ protected void processTestGatewayRequestAttributesValuesFromTheServer() throws Exception {
+
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+
+ postGatewayDeviceClientAttributes(client);
+
+ Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Request Attributes", Device.class),
+ 20,
+ 100);
+
+ assertNotNull(savedDevice);
+
+ Thread.sleep(1000);
+
+ doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
+
+ Thread.sleep(1000);
+
+ client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC, MqttQoS.AT_LEAST_ONCE.value());
+
+ TestMqttCallback clientAttributesCallback = getTestMqttCallback();
+ client.setCallback(clientAttributesCallback);
+ validateClientResponseGateway(client, clientAttributesCallback);
+
+ TestMqttCallback sharedAttributesCallback = getTestMqttCallback();
+ client.setCallback(sharedAttributesCallback);
+ validateSharedResponseGateway(client, sharedAttributesCallback);
+ }
+
+ protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client) throws Exception {
+ doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
+ client.publish(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, new MqttMessage(POST_ATTRIBUTES_PAYLOAD.getBytes()));
+ client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttQoS.AT_MOST_ONCE.value());
+ }
+
+ protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception {
+ String postClientAttributes = "{\"" + "Gateway Device Request Attributes" + "\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}";
+ client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(postClientAttributes.getBytes()));
+ }
+
+ protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException {
+ String keys = "attribute1,attribute2,attribute3,attribute4,attribute5";
+ String payloadStr = "{\"clientKeys\":\"" + keys + "\", \"sharedKeys\":\"" + keys + "\"}";
+ MqttMessage mqttMessage = new MqttMessage();
+ mqttMessage.setPayload(payloadStr.getBytes());
+ client.publish(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX + "1", mqttMessage);
+ latch.await(3, TimeUnit.SECONDS);
+ assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
+ String expectedRequestPayload = "{\"client\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}},\"shared\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}";
+ assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8)));
+ }
+
+ protected void validateClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException {
+ String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": true, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}";
+ MqttMessage mqttMessage = new MqttMessage();
+ mqttMessage.setPayload(payloadStr.getBytes());
+ client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage);
+ callback.getLatch().await(3, TimeUnit.SECONDS);
+ assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
+ String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}";
+ assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8)));
+ }
+
+ protected void validateSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException {
+ String payloadStr = "{\"id\": 1, \"device\": \"" + "Gateway Device Request Attributes" + "\", \"client\": false, \"keys\": [\"attribute1\", \"attribute2\", \"attribute3\", \"attribute4\", \"attribute5\"]}";
+ MqttMessage mqttMessage = new MqttMessage();
+ mqttMessage.setPayload(payloadStr.getBytes());
+ client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, mqttMessage);
+ callback.getLatch().await(3, TimeUnit.SECONDS);
+ assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
+ String expectedRequestPayload = "{\"id\":1,\"device\":\"" + "Gateway Device Request Attributes" + "\",\"values\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}";
+ assertEquals(JacksonUtil.toJsonNode(expectedRequestPayload), JacksonUtil.toJsonNode(new String(callback.getPayloadBytes(), StandardCharsets.UTF_8)));
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java
new file mode 100644
index 0000000000..4b824ea0b5
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestJsonIntegrationTest.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.request;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.thingsboard.server.common.data.TransportPayloadType;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@Slf4j
+public abstract class AbstractMqttAttributesRequestJsonIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("Test Request attribute values from the server json", "Gateway Test Request attribute values from the server json", TransportPayloadType.JSON, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ processAfterTest();
+ }
+
+ @Test
+ public void testRequestAttributesValuesFromTheServer() throws Exception {
+ processTestRequestAttributesValuesFromTheServer();
+ }
+
+ @Test
+ public void testRequestAttributesValuesFromTheServerGateway() throws Exception {
+ processTestGatewayRequestAttributesValuesFromTheServer();
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java
new file mode 100644
index 0000000000..96cc88fa2f
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/AbstractMqttAttributesRequestProtoIntegrationTest.java
@@ -0,0 +1,201 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.request;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
+import org.thingsboard.server.gen.transport.TransportApiProtos;
+import org.thingsboard.server.gen.transport.TransportProtos;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@Slf4j
+public abstract class AbstractMqttAttributesRequestProtoIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("Test Request attribute values from the server proto", "Gateway Test Request attribute values from the server proto", TransportPayloadType.PROTOBUF, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ processAfterTest();
+ }
+
+ @Test
+ public void testRequestAttributesValuesFromTheServer() throws Exception {
+ processTestRequestAttributesValuesFromTheServer();
+ }
+
+
+ @Test
+ public void testRequestAttributesValuesFromTheServerGateway() throws Exception {
+ processTestGatewayRequestAttributesValuesFromTheServer();
+ }
+
+ protected void postAttributesAndSubscribeToTopic(Device savedDevice, MqttAsyncClient client) throws Exception {
+ doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
+ String keys = "attribute1,attribute2,attribute3,attribute4,attribute5";
+ List expectedKeys = Arrays.asList(keys.split(","));
+ TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys);
+ byte[] payload = postAttributeMsg.toByteArray();
+ client.publish(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, new MqttMessage(payload));
+ client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC, MqttQoS.AT_MOST_ONCE.value());
+ }
+
+ protected void postGatewayDeviceClientAttributes(MqttAsyncClient client) throws Exception {
+ String keys = "attribute1,attribute2,attribute3,attribute4,attribute5";
+ List expectedKeys = Arrays.asList(keys.split(","));
+ TransportProtos.PostAttributeMsg postAttributeMsg = getPostAttributeMsg(expectedKeys);
+ TransportApiProtos.AttributesMsg.Builder attributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder();
+ attributesMsgBuilder.setDeviceName("Gateway Device Request Attributes");
+ attributesMsgBuilder.setMsg(postAttributeMsg);
+ TransportApiProtos.AttributesMsg attributesMsg = attributesMsgBuilder.build();
+ TransportApiProtos.GatewayAttributesMsg.Builder gatewayAttributeMsgBuilder = TransportApiProtos.GatewayAttributesMsg.newBuilder();
+ gatewayAttributeMsgBuilder.addMsg(attributesMsg);
+ byte[] bytes = gatewayAttributeMsgBuilder.build().toByteArray();
+ client.publish(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, new MqttMessage(bytes));
+ }
+
+ protected void validateResponse(MqttAsyncClient client, CountDownLatch latch, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException {
+ String keys = "attribute1,attribute2,attribute3,attribute4,attribute5";
+ TransportApiProtos.AttributesRequest.Builder attributesRequestBuilder = TransportApiProtos.AttributesRequest.newBuilder();
+ attributesRequestBuilder.setClientKeys(keys);
+ attributesRequestBuilder.setSharedKeys(keys);
+ TransportApiProtos.AttributesRequest attributesRequest = attributesRequestBuilder.build();
+ MqttMessage mqttMessage = new MqttMessage();
+ mqttMessage.setPayload(attributesRequest.toByteArray());
+ client.publish(MqttTopics.DEVICE_ATTRIBUTES_REQUEST_TOPIC_PREFIX + "1", mqttMessage);
+ latch.await(3, TimeUnit.SECONDS);
+ assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
+ TransportProtos.GetAttributeResponseMsg expectedAttributesResponse = getExpectedAttributeResponseMsg();
+ TransportProtos.GetAttributeResponseMsg actualAttributesResponse = TransportProtos.GetAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
+ assertEquals(expectedAttributesResponse.getRequestId(), actualAttributesResponse.getRequestId());
+ List expectedClientKeyValueProtos = expectedAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ List expectedSharedKeyValueProtos = expectedAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ List actualClientKeyValueProtos = actualAttributesResponse.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ List actualSharedKeyValueProtos = actualAttributesResponse.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos));
+ assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos));
+ }
+
+ protected void validateClientResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException {
+ String keys = "attribute1,attribute2,attribute3,attribute4,attribute5";
+ TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, true);
+ client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray()));
+ callback.getLatch().await(3, TimeUnit.SECONDS);
+ assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
+ TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(true);
+ TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
+ assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName());
+
+ TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg();
+ TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg();
+ assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId());
+
+ List expectedClientKeyValueProtos = expectedResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ List actualClientKeyValueProtos = actualResponseMsg.getClientAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ assertTrue(actualClientKeyValueProtos.containsAll(expectedClientKeyValueProtos));
+ }
+
+ protected void validateSharedResponseGateway(MqttAsyncClient client, TestMqttCallback callback) throws MqttException, InterruptedException, InvalidProtocolBufferException {
+ String keys = "attribute1,attribute2,attribute3,attribute4,attribute5";
+ TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = getGatewayAttributesRequestMsg(keys, false);
+ client.publish(MqttTopics.GATEWAY_ATTRIBUTES_REQUEST_TOPIC, new MqttMessage(gatewayAttributesRequestMsg.toByteArray()));
+ callback.getLatch().await(3, TimeUnit.SECONDS);
+ assertEquals(MqttQoS.AT_LEAST_ONCE.value(), callback.getQoS());
+ TransportApiProtos.GatewayAttributeResponseMsg expectedGatewayAttributeResponseMsg = getExpectedGatewayAttributeResponseMsg(false);
+ TransportApiProtos.GatewayAttributeResponseMsg actualGatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.parseFrom(callback.getPayloadBytes());
+ assertEquals(expectedGatewayAttributeResponseMsg.getDeviceName(), actualGatewayAttributeResponseMsg.getDeviceName());
+
+ TransportProtos.GetAttributeResponseMsg expectedResponseMsg = expectedGatewayAttributeResponseMsg.getResponseMsg();
+ TransportProtos.GetAttributeResponseMsg actualResponseMsg = actualGatewayAttributeResponseMsg.getResponseMsg();
+ assertEquals(expectedResponseMsg.getRequestId(), actualResponseMsg.getRequestId());
+
+ List expectedSharedKeyValueProtos = expectedResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ List actualSharedKeyValueProtos = actualResponseMsg.getSharedAttributeListList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+
+ assertTrue(actualSharedKeyValueProtos.containsAll(expectedSharedKeyValueProtos));
+ }
+
+ private TransportApiProtos.GatewayAttributesRequestMsg getGatewayAttributesRequestMsg(String keys, boolean client) {
+ return TransportApiProtos.GatewayAttributesRequestMsg.newBuilder()
+ .setClient(client)
+ .addAllKeys(Arrays.asList(keys.split(",")))
+ .setDeviceName("Gateway Device Request Attributes")
+ .setId(1).build();
+ }
+
+ private TransportProtos.GetAttributeResponseMsg getExpectedAttributeResponseMsg() {
+ TransportProtos.GetAttributeResponseMsg.Builder result = TransportProtos.GetAttributeResponseMsg.newBuilder();
+ List tsKvProtoList = getTsKvProtoList();
+ result.addAllClientAttributeList(tsKvProtoList);
+ result.addAllSharedAttributeList(tsKvProtoList);
+ result.setRequestId(1);
+ return result.build();
+ }
+
+ private TransportApiProtos.GatewayAttributeResponseMsg getExpectedGatewayAttributeResponseMsg(boolean client) {
+ TransportApiProtos.GatewayAttributeResponseMsg.Builder gatewayAttributeResponseMsg = TransportApiProtos.GatewayAttributeResponseMsg.newBuilder();
+ TransportProtos.GetAttributeResponseMsg.Builder getAttributeResponseMsgBuilder = TransportProtos.GetAttributeResponseMsg.newBuilder();
+ List tsKvProtoList = getTsKvProtoList();
+ if (client) {
+ getAttributeResponseMsgBuilder.addAllClientAttributeList(tsKvProtoList);
+ } else {
+ getAttributeResponseMsgBuilder.addAllSharedAttributeList(tsKvProtoList);
+ }
+ getAttributeResponseMsgBuilder.setRequestId(1);
+ TransportProtos.GetAttributeResponseMsg getAttributeResponseMsg = getAttributeResponseMsgBuilder.build();
+ gatewayAttributeResponseMsg.setDeviceName("Gateway Device Request Attributes");
+ gatewayAttributeResponseMsg.setResponseMsg(getAttributeResponseMsg);
+ return gatewayAttributeResponseMsg.build();
+ }
+
+ protected List getKvProtos(List expectedKeys) {
+ List keyValueProtos = new ArrayList<>();
+ TransportProtos.KeyValueProto strKeyValueProto = getKeyValueProto(expectedKeys.get(0), "value1", TransportProtos.KeyValueType.STRING_V);
+ TransportProtos.KeyValueProto boolKeyValueProto = getKeyValueProto(expectedKeys.get(1), "true", TransportProtos.KeyValueType.BOOLEAN_V);
+ TransportProtos.KeyValueProto dblKeyValueProto = getKeyValueProto(expectedKeys.get(2), "42.0", TransportProtos.KeyValueType.DOUBLE_V);
+ TransportProtos.KeyValueProto longKeyValueProto = getKeyValueProto(expectedKeys.get(3), "73", TransportProtos.KeyValueType.LONG_V);
+ TransportProtos.KeyValueProto jsonKeyValueProto = getKeyValueProto(expectedKeys.get(4), "{\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}", TransportProtos.KeyValueType.JSON_V);
+ keyValueProtos.add(strKeyValueProto);
+ keyValueProtos.add(boolKeyValueProto);
+ keyValueProtos.add(dblKeyValueProto);
+ keyValueProtos.add(longKeyValueProto);
+ keyValueProtos.add(jsonKeyValueProto);
+ return keyValueProtos;
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java
new file mode 100644
index 0000000000..e94ad9519c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/nosql/MqttAttributesRequestNoSqlIntegrationTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.request.nosql;
+
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
+import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest;
+
+
+@DaoNoSqlTest
+public class MqttAttributesRequestNoSqlIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java
new file mode 100644
index 0000000000..d3750d49a3
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestJsonSqlIntegrationTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.request.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest;
+import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestJsonIntegrationTest;
+
+@DaoSqlTest
+public class MqttAttributesRequestJsonSqlIntegrationTest extends AbstractMqttAttributesRequestJsonIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java
new file mode 100644
index 0000000000..f52e79b2ab
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestProtoSqlIntegrationTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.request.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestJsonIntegrationTest;
+import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestProtoIntegrationTest;
+
+@DaoSqlTest
+public class MqttAttributesRequestProtoSqlIntegrationTest extends AbstractMqttAttributesRequestProtoIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java
new file mode 100644
index 0000000000..af18294b47
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/request/sql/MqttAttributesRequestSqlIntegrationTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.request.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.attributes.request.AbstractMqttAttributesRequestIntegrationTest;
+
+@DaoSqlTest
+public class MqttAttributesRequestSqlIntegrationTest extends AbstractMqttAttributesRequestIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java
new file mode 100644
index 0000000000..d2febdf357
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesIntegrationTest.java
@@ -0,0 +1,171 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.updates;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
+import org.thingsboard.server.dao.util.mapping.JacksonUtil;
+import org.thingsboard.server.mqtt.attributes.AbstractMqttAttributesIntegrationTest;
+
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@Slf4j
+public abstract class AbstractMqttAttributesUpdatesIntegrationTest extends AbstractMqttAttributesIntegrationTest {
+
+ private static final String RESPONSE_ATTRIBUTES_PAYLOAD_DELETED = "{\"deleted\":[\"attribute5\"]}";
+
+ private static String getResponseGatewayAttributesUpdatedPayload() {
+ return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\"," +
+ "\"data\":{\"attribute1\":\"value1\",\"attribute2\":true,\"attribute3\":42.0,\"attribute4\":73,\"attribute5\":{\"someNumber\":42,\"someArray\":[1,2,3],\"someNestedObject\":{\"key\":\"value\"}}}}";
+ }
+
+ private static String getResponseGatewayAttributesDeletedPayload() {
+ return "{\"device\":\"" + "Gateway Device Subscribe to attribute updates" + "\",\"data\":{\"deleted\":[\"attribute5\"]}}";
+ }
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.JSON, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ processAfterTest();
+ }
+
+ @Test
+ public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
+ processTestSubscribeToAttributesUpdates();
+ }
+
+ @Test
+ public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception {
+ processGatewayTestSubscribeToAttributesUpdates();
+ }
+
+ protected void processTestSubscribeToAttributesUpdates() throws Exception {
+
+ MqttAsyncClient client = getMqttAsyncClient(accessToken);
+
+ TestMqttCallback onUpdateCallback = getTestMqttCallback();
+ client.setCallback(onUpdateCallback);
+
+ client.subscribe(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value());
+
+ Thread.sleep(1000);
+
+ doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
+ onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS);
+
+ validateUpdateAttributesResponse(onUpdateCallback);
+
+ TestMqttCallback onDeleteCallback = getTestMqttCallback();
+ client.setCallback(onDeleteCallback);
+
+ doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class);
+ onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS);
+
+ validateDeleteAttributesResponse(onDeleteCallback);
+ }
+
+ protected void validateUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException {
+ assertNotNull(callback.getPayloadBytes());
+ String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8);
+ assertEquals(JacksonUtil.toJsonNode(POST_ATTRIBUTES_PAYLOAD), JacksonUtil.toJsonNode(response));
+ }
+
+ protected void validateDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException {
+ assertNotNull(callback.getPayloadBytes());
+ String response = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8);
+ assertEquals(JacksonUtil.toJsonNode(RESPONSE_ATTRIBUTES_PAYLOAD_DELETED), JacksonUtil.toJsonNode(response));
+ }
+
+ protected void processGatewayTestSubscribeToAttributesUpdates() throws Exception {
+
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+
+ TestMqttCallback onUpdateCallback = getTestMqttCallback();
+ client.setCallback(onUpdateCallback);
+
+ Device device = new Device();
+ device.setName("Gateway Device Subscribe to attribute updates");
+ device.setType("default");
+
+ byte[] connectPayloadBytes = getConnectPayloadBytes();
+
+ publishMqttMsg(client, connectPayloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC);
+
+ Device savedDevice = doExecuteWithRetriesAndInterval(() -> doGet("/api/tenant/devices?deviceName=" + "Gateway Device Subscribe to attribute updates", Device.class),
+ 20,
+ 100);
+
+ assertNotNull(savedDevice);
+
+ client.subscribe(MqttTopics.GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE.value());
+
+ Thread.sleep(1000);
+
+ doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", POST_ATTRIBUTES_PAYLOAD, String.class, status().isOk());
+ onUpdateCallback.getLatch().await(3, TimeUnit.SECONDS);
+
+ validateGatewayUpdateAttributesResponse(onUpdateCallback);
+
+ TestMqttCallback onDeleteCallback = getTestMqttCallback();
+ client.setCallback(onDeleteCallback);
+
+ doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=attribute5", String.class);
+ onDeleteCallback.getLatch().await(3, TimeUnit.SECONDS);
+
+ validateGatewayDeleteAttributesResponse(onDeleteCallback);
+
+ }
+
+ protected void validateGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException {
+ assertNotNull(callback.getPayloadBytes());
+ String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8);
+ assertEquals(getResponseGatewayAttributesUpdatedPayload(), s);
+ }
+
+ protected void validateGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException {
+ assertNotNull(callback.getPayloadBytes());
+ String s = new String(callback.getPayloadBytes(), StandardCharsets.UTF_8);
+ assertEquals(s, getResponseGatewayAttributesDeletedPayload());
+ }
+
+ protected byte[] getConnectPayloadBytes() {
+ String connectPayload = "{\"device\": \"Gateway Device Subscribe to attribute updates\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}";
+ return connectPayload.getBytes();
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java
new file mode 100644
index 0000000000..58379e4016
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesJsonIntegrationTest.java
@@ -0,0 +1,51 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.updates;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.TransportPayloadType;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@Slf4j
+public abstract class AbstractMqttAttributesUpdatesJsonIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.JSON, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ processAfterTest();
+ }
+
+ @Test
+ public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
+ processTestSubscribeToAttributesUpdates();
+ }
+
+ @Test
+ public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception {
+ processGatewayTestSubscribeToAttributesUpdates();
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java
new file mode 100644
index 0000000000..faf8e1ce4d
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/AbstractMqttAttributesUpdatesProtoIntegrationTest.java
@@ -0,0 +1,149 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.updates;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
+import org.thingsboard.server.gen.transport.TransportApiProtos;
+import org.thingsboard.server.gen.transport.TransportProtos;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@Slf4j
+public abstract class AbstractMqttAttributesUpdatesProtoIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("Test Subscribe to attribute updates", "Gateway Test Subscribe to attribute updates", TransportPayloadType.PROTOBUF, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ processAfterTest();
+ }
+
+ @Test
+ public void testSubscribeToAttributesUpdatesFromTheServer() throws Exception {
+ processTestSubscribeToAttributesUpdates();
+ }
+
+ @Test
+ public void testSubscribeToAttributesUpdatesFromTheServerGateway() throws Exception {
+ processGatewayTestSubscribeToAttributesUpdates();
+ }
+
+ protected void validateUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException {
+ assertNotNull(callback.getPayloadBytes());
+ TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
+ List tsKvProtoList = getTsKvProtoList();
+ attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList);
+
+ TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build();
+ TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes());
+
+ List actualSharedUpdatedList = actualAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ List expectedSharedUpdatedList = expectedAttributeUpdateNotificationMsg.getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+
+ assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size());
+ assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList));
+
+ }
+
+ protected void validateDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException {
+ assertNotNull(callback.getPayloadBytes());
+ TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
+ attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5");
+
+ TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build();
+ TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = TransportProtos.AttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes());
+
+ assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size());
+ assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0));
+
+ }
+
+ protected void validateGatewayUpdateAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException {
+ assertNotNull(callback.getPayloadBytes());
+
+ TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
+ List tsKvProtoList = getTsKvProtoList();
+ attributeUpdateNotificationMsgBuilder.addAllSharedUpdated(tsKvProtoList);
+ TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build();
+
+ TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder();
+ gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates");
+ gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(expectedAttributeUpdateNotificationMsg);
+
+ TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build();
+ TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes());
+
+ assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName());
+
+ List actualSharedUpdatedList = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+ List expectedSharedUpdatedList = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg().getSharedUpdatedList().stream().map(TransportProtos.TsKvProto::getKv).collect(Collectors.toList());
+
+ assertEquals(expectedSharedUpdatedList.size(), actualSharedUpdatedList.size());
+ assertTrue(actualSharedUpdatedList.containsAll(expectedSharedUpdatedList));
+
+ }
+
+ protected void validateGatewayDeleteAttributesResponse(TestMqttCallback callback) throws InvalidProtocolBufferException {
+ assertNotNull(callback.getPayloadBytes());
+ TransportProtos.AttributeUpdateNotificationMsg.Builder attributeUpdateNotificationMsgBuilder = TransportProtos.AttributeUpdateNotificationMsg.newBuilder();
+ attributeUpdateNotificationMsgBuilder.addSharedDeleted("attribute5");
+ TransportProtos.AttributeUpdateNotificationMsg attributeUpdateNotificationMsg = attributeUpdateNotificationMsgBuilder.build();
+
+ TransportApiProtos.GatewayAttributeUpdateNotificationMsg.Builder gatewayAttributeUpdateNotificationMsgBuilder = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.newBuilder();
+ gatewayAttributeUpdateNotificationMsgBuilder.setDeviceName("Gateway Device Subscribe to attribute updates");
+ gatewayAttributeUpdateNotificationMsgBuilder.setNotificationMsg(attributeUpdateNotificationMsg);
+
+ TransportApiProtos.GatewayAttributeUpdateNotificationMsg expectedGatewayAttributeUpdateNotificationMsg = gatewayAttributeUpdateNotificationMsgBuilder.build();
+ TransportApiProtos.GatewayAttributeUpdateNotificationMsg actualGatewayAttributeUpdateNotificationMsg = TransportApiProtos.GatewayAttributeUpdateNotificationMsg.parseFrom(callback.getPayloadBytes());
+
+ assertEquals(expectedGatewayAttributeUpdateNotificationMsg.getDeviceName(), actualGatewayAttributeUpdateNotificationMsg.getDeviceName());
+
+ TransportProtos.AttributeUpdateNotificationMsg expectedAttributeUpdateNotificationMsg = expectedGatewayAttributeUpdateNotificationMsg.getNotificationMsg();
+ TransportProtos.AttributeUpdateNotificationMsg actualAttributeUpdateNotificationMsg = actualGatewayAttributeUpdateNotificationMsg.getNotificationMsg();
+
+ assertEquals(expectedAttributeUpdateNotificationMsg.getSharedDeletedList().size(), actualAttributeUpdateNotificationMsg.getSharedDeletedList().size());
+ assertEquals("attribute5", actualAttributeUpdateNotificationMsg.getSharedDeletedList().get(0));
+
+ }
+
+ protected byte[] getConnectPayloadBytes() {
+ TransportApiProtos.ConnectMsg connectProto = getConnectProto();
+ return connectProto.toByteArray();
+ }
+
+ private TransportApiProtos.ConnectMsg getConnectProto() {
+ TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder();
+ builder.setDeviceName("Gateway Device Subscribe to attribute updates");
+ builder.setDeviceType(TransportPayloadType.PROTOBUF.name());
+ return builder.build();
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java
new file mode 100644
index 0000000000..993e0869e4
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/nosql/MqttAttributesUpdatesNoSqlIntegrationTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.updates.nosql;
+
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
+import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest;
+
+
+@DaoNoSqlTest
+public class MqttAttributesUpdatesNoSqlIntegrationTest extends AbstractMqttAttributesUpdatesJsonIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java
new file mode 100644
index 0000000000..cdafc3a9ac
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlIntegrationTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.updates.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesIntegrationTest;
+
+@DaoSqlTest
+public class MqttAttributesUpdatesSqlIntegrationTest extends AbstractMqttAttributesUpdatesIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java
new file mode 100644
index 0000000000..a8fd4687f7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlJsonIntegrationTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.updates.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesIntegrationTest;
+import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest;
+
+@DaoSqlTest
+public class MqttAttributesUpdatesSqlJsonIntegrationTest extends AbstractMqttAttributesUpdatesJsonIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java
new file mode 100644
index 0000000000..723e5e3dcd
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/attributes/updates/sql/MqttAttributesUpdatesSqlProtoIntegrationTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.attributes.updates.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesJsonIntegrationTest;
+import org.thingsboard.server.mqtt.attributes.updates.AbstractMqttAttributesUpdatesProtoIntegrationTest;
+
+@DaoSqlTest
+public class MqttAttributesUpdatesSqlProtoIntegrationTest extends AbstractMqttAttributesUpdatesProtoIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java
new file mode 100644
index 0000000000..dd16c17be2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimDeviceTest.java
@@ -0,0 +1,206 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.claim;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.thingsboard.server.common.data.ClaimRequest;
+import org.thingsboard.server.common.data.Customer;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.dao.device.claim.ClaimResponse;
+import org.thingsboard.server.dao.device.claim.ClaimResult;
+import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@Slf4j
+public abstract class AbstractMqttClaimDeviceTest extends AbstractMqttIntegrationTest {
+
+ protected static final String CUSTOMER_USER_PASSWORD = "customerUser123!";
+
+ protected User customerAdmin;
+ protected Customer savedCustomer;
+
+ @Before
+ public void beforeTest() throws Exception {
+ super.processBeforeTest("Test Claim device", "Test Claim gateway", null, null, null);
+ createCustomerAndUser();
+ }
+
+ protected void createCustomerAndUser() throws Exception {
+ Customer customer = new Customer();
+ customer.setTenantId(savedTenant.getId());
+ customer.setTitle("Test Claiming Customer");
+ savedCustomer = doPost("/api/customer", customer, Customer.class);
+ assertNotNull(savedCustomer);
+ assertEquals(savedTenant.getId(), savedCustomer.getTenantId());
+
+ User user = new User();
+ user.setAuthority(Authority.CUSTOMER_USER);
+ user.setTenantId(savedTenant.getId());
+ user.setCustomerId(savedCustomer.getId());
+ user.setEmail("customer@thingsboard.org");
+
+ customerAdmin = createUser(user, CUSTOMER_USER_PASSWORD);
+ assertNotNull(customerAdmin);
+ assertEquals(customerAdmin.getCustomerId(), savedCustomer.getId());
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ super.processAfterTest();
+ }
+
+ @Test
+ public void testClaimingDevice() throws Exception {
+ processTestClaimingDevice(false);
+ }
+
+ @Test
+ public void testClaimingDeviceWithoutSecretAndDuration() throws Exception {
+ processTestClaimingDevice(true);
+ }
+
+ @Test
+ public void testGatewayClaimingDevice() throws Exception {
+ processTestGatewayClaimingDevice("Test claiming gateway device", false);
+ }
+
+ @Test
+ public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception {
+ processTestGatewayClaimingDevice("Test claiming gateway device empty payload", true);
+ }
+
+
+ protected void processTestClaimingDevice(boolean emptyPayload) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(accessToken);
+ byte[] payloadBytes;
+ byte[] failurePayloadBytes;
+ if (emptyPayload) {
+ payloadBytes = "{}".getBytes();
+ failurePayloadBytes = "{\"durationMs\":1}".getBytes();
+ } else {
+ payloadBytes = "{\"secretKey\":\"value\", \"durationMs\":60000}".getBytes();
+ failurePayloadBytes = "{\"secretKey\":\"value\", \"durationMs\":1}".getBytes();
+ }
+ validateClaimResponse(emptyPayload, client, payloadBytes, failurePayloadBytes);
+ }
+
+ protected void validateClaimResponse(boolean emptyPayload, MqttAsyncClient client, byte[] payloadBytes, byte[] failurePayloadBytes) throws Exception {
+ client.publish(MqttTopics.DEVICE_CLAIM_TOPIC, new MqttMessage(failurePayloadBytes));
+
+ loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD);
+ ClaimRequest claimRequest;
+ if (!emptyPayload) {
+ claimRequest = new ClaimRequest("value");
+ } else {
+ claimRequest = new ClaimRequest(null);
+ }
+
+ ClaimResponse claimResponse = doExecuteWithRetriesAndInterval(
+ () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest()),
+ 20,
+ 100
+ );
+
+ assertEquals(claimResponse, ClaimResponse.FAILURE);
+
+ client.publish(MqttTopics.DEVICE_CLAIM_TOPIC, new MqttMessage(payloadBytes));
+
+ ClaimResult claimResult = doExecuteWithRetriesAndInterval(
+ () -> doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResult.class, status().isOk()),
+ 20,
+ 100
+ );
+ assertEquals(claimResult.getResponse(), ClaimResponse.SUCCESS);
+ Device claimedDevice = claimResult.getDevice();
+ assertNotNull(claimedDevice);
+ assertNotNull(claimedDevice.getCustomerId());
+ assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId());
+
+ claimResponse = doPostClaimAsync("/api/customer/device/" + savedDevice.getName() + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest());
+ assertEquals(claimResponse, ClaimResponse.CLAIMED);
+ }
+
+ protected void validateGatewayClaimResponse(String deviceName, boolean emptyPayload, MqttAsyncClient client, byte[] failurePayloadBytes, byte[] payloadBytes) throws Exception {
+ client.publish(MqttTopics.GATEWAY_CLAIM_TOPIC, new MqttMessage(failurePayloadBytes));
+
+ Device savedDevice = doExecuteWithRetriesAndInterval(
+ () -> doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class),
+ 20,
+ 100
+ );
+
+ assertNotNull(savedDevice);
+
+ loginUser(customerAdmin.getName(), CUSTOMER_USER_PASSWORD);
+ ClaimRequest claimRequest;
+ if (!emptyPayload) {
+ claimRequest = new ClaimRequest("value");
+ } else {
+ claimRequest = new ClaimRequest(null);
+ }
+
+ ClaimResponse claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest());
+ assertEquals(claimResponse, ClaimResponse.FAILURE);
+
+ client.publish(MqttTopics.GATEWAY_CLAIM_TOPIC, new MqttMessage(payloadBytes));
+
+ ClaimResult claimResult = doExecuteWithRetriesAndInterval(
+ () -> doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResult.class, status().isOk()),
+ 20,
+ 100
+ );
+
+ assertEquals(claimResult.getResponse(), ClaimResponse.SUCCESS);
+ Device claimedDevice = claimResult.getDevice();
+ assertNotNull(claimedDevice);
+ assertNotNull(claimedDevice.getCustomerId());
+ assertEquals(customerAdmin.getCustomerId(), claimedDevice.getCustomerId());
+
+ claimResponse = doPostClaimAsync("/api/customer/device/" + deviceName + "/claim", claimRequest, ClaimResponse.class, status().isBadRequest());
+ assertEquals(claimResponse, ClaimResponse.CLAIMED);
+ }
+
+ protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+ byte[] failurePayloadBytes;
+ byte[] payloadBytes;
+ String failurePayload;
+ String payload;
+ if (emptyPayload) {
+ failurePayload = "{\"" + deviceName + "\": " + "{\"durationMs\":1}" + "}";
+ payload = "{\"" + deviceName + "\": " + "{}" + "}";
+ } else {
+ failurePayload = "{\"" + deviceName + "\": " + "{\"secretKey\":\"value\", \"durationMs\":1}" + "}";
+ payload = "{\"" + deviceName + "\": " + "{\"secretKey\":\"value\", \"durationMs\":60000}" + "}";
+ }
+ payloadBytes = payload.getBytes();
+ failurePayloadBytes = failurePayload.getBytes();
+ validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java
new file mode 100644
index 0000000000..f55cfa57c8
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimJsonDeviceTest.java
@@ -0,0 +1,58 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.claim;
+
+import lombok.extern.slf4j.Slf4j;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.thingsboard.server.common.data.TransportPayloadType;
+
+@Slf4j
+public abstract class AbstractMqttClaimJsonDeviceTest extends AbstractMqttClaimDeviceTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ super.processBeforeTest("Test Claim device", "Test Claim gateway", TransportPayloadType.JSON, null, null);
+ createCustomerAndUser();
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ super.afterTest();
+ }
+
+ @Test
+ public void testClaimingDevice() throws Exception {
+ processTestClaimingDevice(false);
+ }
+
+ @Test
+ public void testClaimingDeviceWithoutSecretAndDuration() throws Exception {
+ processTestClaimingDevice(true);
+ }
+
+ @Test
+ public void testGatewayClaimingDevice() throws Exception {
+ processTestGatewayClaimingDevice("Test claiming gateway device Json", false);
+ }
+
+ @Test
+ public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception {
+ processTestGatewayClaimingDevice("Test claiming gateway device empty payload Json", true);
+ }
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java
new file mode 100644
index 0000000000..d371c09f37
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/AbstractMqttClaimProtoDeviceTest.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.claim;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.gen.transport.TransportApiProtos;
+
+@Slf4j
+public abstract class AbstractMqttClaimProtoDeviceTest extends AbstractMqttClaimDeviceTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("Test Claim device", "Test Claim gateway", TransportPayloadType.PROTOBUF, null, null);
+ createCustomerAndUser();
+ }
+
+ @After
+ public void afterTest() throws Exception { super.afterTest(); }
+
+ @Test
+ public void testClaimingDevice() throws Exception {
+ processTestClaimingDevice(false);
+ }
+
+ @Test
+ public void testClaimingDeviceWithoutSecretAndDuration() throws Exception {
+ processTestClaimingDevice(true);
+ }
+
+ @Test
+ public void testGatewayClaimingDevice() throws Exception {
+ processTestGatewayClaimingDevice("Test claiming gateway device Proto", false);
+ }
+
+ @Test
+ public void testGatewayClaimingDeviceWithoutSecretAndDuration() throws Exception {
+ processTestGatewayClaimingDevice("Test claiming gateway device empty payload Proto", true);
+ }
+
+ protected void processTestClaimingDevice(boolean emptyPayload) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(accessToken);
+ byte[] payloadBytes;
+ if (emptyPayload) {
+ payloadBytes = getClaimDevice(0, emptyPayload).toByteArray();
+ } else {
+ payloadBytes = getClaimDevice(60000, emptyPayload).toByteArray();
+ }
+ byte[] failurePayloadBytes = getClaimDevice(1, emptyPayload).toByteArray();
+ validateClaimResponse(emptyPayload, client, payloadBytes, failurePayloadBytes);
+ }
+
+ protected void processTestGatewayClaimingDevice(String deviceName, boolean emptyPayload) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+ byte[] failurePayloadBytes;
+ byte[] payloadBytes;
+ if (emptyPayload) {
+ payloadBytes = getGatewayClaimMsg(deviceName, 0, emptyPayload).toByteArray();
+ } else {
+ payloadBytes = getGatewayClaimMsg(deviceName, 60000, emptyPayload).toByteArray();
+ }
+ failurePayloadBytes = getGatewayClaimMsg(deviceName, 1, emptyPayload).toByteArray();
+
+ validateGatewayClaimResponse(deviceName, emptyPayload, client, failurePayloadBytes, payloadBytes);
+ }
+
+ private TransportApiProtos.GatewayClaimMsg getGatewayClaimMsg(String deviceName, long duration, boolean emptyPayload) {
+ TransportApiProtos.GatewayClaimMsg.Builder gatewayClaimMsgBuilder = TransportApiProtos.GatewayClaimMsg.newBuilder();
+ TransportApiProtos.ClaimDeviceMsg.Builder claimDeviceMsgBuilder = TransportApiProtos.ClaimDeviceMsg.newBuilder();
+ TransportApiProtos.ClaimDevice.Builder claimDeviceBuilder = TransportApiProtos.ClaimDevice.newBuilder();
+ if (!emptyPayload) {
+ claimDeviceBuilder.setSecretKey("value");
+ }
+ if (duration > 0) {
+ claimDeviceBuilder.setDurationMs(duration);
+ }
+ TransportApiProtos.ClaimDevice claimDevice = claimDeviceBuilder.build();
+ claimDeviceMsgBuilder.setClaimRequest(claimDevice);
+ claimDeviceMsgBuilder.setDeviceName(deviceName);
+ TransportApiProtos.ClaimDeviceMsg claimDeviceMsg = claimDeviceMsgBuilder.build();
+ gatewayClaimMsgBuilder.addMsg(claimDeviceMsg);
+ return gatewayClaimMsgBuilder.build();
+ }
+
+ private TransportApiProtos.ClaimDevice getClaimDevice(long duration, boolean emptyPayload) {
+ TransportApiProtos.ClaimDevice.Builder claimDeviceBuilder = TransportApiProtos.ClaimDevice.newBuilder();
+ if (!emptyPayload) {
+ claimDeviceBuilder.setSecretKey("value");
+ }
+ if (duration > 0) {
+ claimDeviceBuilder.setSecretKey("value");
+ claimDeviceBuilder.setDurationMs(duration);
+ }
+ return claimDeviceBuilder.build();
+ }
+
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java
new file mode 100644
index 0000000000..72b9f95328
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/nosql/MqttClaimDeviceNoSqlTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.claim.nosql;
+
+import org.thingsboard.server.dao.service.DaoNoSqlTest;
+import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest;
+
+
+@DaoNoSqlTest
+public class MqttClaimDeviceNoSqlTest extends AbstractMqttClaimDeviceTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java
new file mode 100644
index 0000000000..da794288f4
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceJsonSqlTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.claim.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest;
+import org.thingsboard.server.mqtt.claim.AbstractMqttClaimJsonDeviceTest;
+
+@DaoSqlTest
+public class MqttClaimDeviceJsonSqlTest extends AbstractMqttClaimJsonDeviceTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java
new file mode 100644
index 0000000000..a63978e4de
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceProtoSqlTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.claim.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.claim.AbstractMqttClaimJsonDeviceTest;
+import org.thingsboard.server.mqtt.claim.AbstractMqttClaimProtoDeviceTest;
+
+@DaoSqlTest
+public class MqttClaimDeviceProtoSqlTest extends AbstractMqttClaimProtoDeviceTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java
new file mode 100644
index 0000000000..ff0c2becb7
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/claim/sql/MqttClaimDeviceSqlTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.claim.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.claim.AbstractMqttClaimDeviceTest;
+
+@DaoSqlTest
+public class MqttClaimDeviceSqlTest extends AbstractMqttClaimDeviceTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java
new file mode 100644
index 0000000000..23b93f427e
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcDefaultIntegrationTest.java
@@ -0,0 +1,137 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.rpc;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.nimbusds.jose.util.StandardCharset;
+import com.datastax.oss.driver.api.core.uuid.Uuids;
+import io.netty.handler.codec.mqtt.MqttQoS;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttCallback;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceProfileType;
+import org.thingsboard.server.common.data.DeviceTransportType;
+import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
+import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
+import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
+import org.thingsboard.server.common.data.security.Authority;
+import org.thingsboard.server.common.data.security.DeviceCredentials;
+import org.thingsboard.server.controller.AbstractControllerTest;
+import org.thingsboard.server.dao.util.mapping.JacksonUtil;
+import org.thingsboard.server.service.security.AccessValidator;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * @author Valerii Sosliuk
+ */
+@Slf4j
+public abstract class AbstractMqttServerSideRpcDefaultIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("RPC test device", "RPC test gateway", null, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ super.processAfterTest();
+ }
+
+ @Test
+ public void testServerMqttOneWayRpcDeviceOffline() throws Exception {
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"24\",\"value\": 1},\"timeout\": 6000}";
+ String deviceId = savedDevice.getId().getId().toString();
+
+ doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(409),
+ asyncContextTimeoutToUseRpcPlugin);
+ }
+
+ @Test
+ public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception {
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"25\",\"value\": 1}}";
+ String nonExistentDeviceId = Uuids.timeBased().toString();
+
+ String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
+ status().isNotFound());
+ Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpcDeviceOffline() throws Exception {
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"27\",\"value\": 1},\"timeout\": 6000}";
+ String deviceId = savedDevice.getId().getId().toString();
+
+ doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(409),
+ asyncContextTimeoutToUseRpcPlugin);
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception {
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"28\",\"value\": 1}}";
+ String nonExistentDeviceId = Uuids.timeBased().toString();
+
+ String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
+ status().isNotFound());
+ Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
+ }
+
+ @Test
+ public void testServerMqttOneWayRpc() throws Exception {
+ processOneWayRpcTest();
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpc() throws Exception {
+ processTwoWayRpcTest();
+ }
+
+ @Test
+ public void testGatewayServerMqttOneWayRpc() throws Exception {
+ processOneWayRpcTestGateway("Gateway Device OneWay RPC");
+ }
+
+ @Test
+ public void testGatewayServerMqttTwoWayRpc() throws Exception {
+ processTwoWayRpcTestGateway("Gateway Device TwoWay RPC");
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
index abd13f99a6..e08f1665a4 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcIntegrationTest.java
@@ -16,6 +16,10 @@
package org.thingsboard.server.mqtt.rpc;
import com.datastax.oss.driver.api.core.uuid.Uuids;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.nimbusds.jose.util.StandardCharset;
import io.netty.handler.codec.mqtt.MqttQoS;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@@ -23,15 +27,28 @@ import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttMessage;
-import org.junit.*;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.DeviceProfile;
+import org.thingsboard.server.common.data.DeviceProfileType;
+import org.thingsboard.server.common.data.DeviceTransportType;
import org.thingsboard.server.common.data.Tenant;
+import org.thingsboard.server.common.data.TransportPayloadType;
import org.thingsboard.server.common.data.User;
+import org.thingsboard.server.common.data.device.profile.DefaultDeviceProfileConfiguration;
+import org.thingsboard.server.common.data.device.profile.DeviceProfileData;
+import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.controller.AbstractControllerTest;
-import org.thingsboard.server.mqtt.telemetry.AbstractMqttTelemetryIntegrationTest;
+import org.thingsboard.server.dao.util.mapping.JacksonUtil;
+import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest;
import org.thingsboard.server.service.security.AccessValidator;
import java.util.Arrays;
@@ -47,72 +64,27 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* @author Valerii Sosliuk
*/
@Slf4j
-public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractControllerTest {
+public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractMqttIntegrationTest {
- private static final String MQTT_URL = "tcp://localhost:1883";
- private static final Long TIME_TO_HANDLE_REQUEST = 500L;
+ protected static final String DEVICE_RESPONSE = "{\"value1\":\"A\",\"value2\":\"B\"}";
- private Tenant savedTenant;
- private User tenantAdmin;
- private Long asyncContextTimeoutToUseRpcPlugin;
-
- private static final AtomicInteger atomicInteger = new AtomicInteger(2);
-
-
- @Before
- public void beforeTest() throws Exception {
- loginSysAdmin();
+ protected Long asyncContextTimeoutToUseRpcPlugin;
+ protected void processBeforeTest(String deviceName, String gatewayName, TransportPayloadType payloadType, String telemetryTopic, String attributesTopic) throws Exception {
+ super.processBeforeTest(deviceName, gatewayName, payloadType, telemetryTopic, attributesTopic);
asyncContextTimeoutToUseRpcPlugin = 10000L;
-
- 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("tenant" + atomicInteger.getAndIncrement() + "@thingsboard.org");
- tenantAdmin.setFirstName("Joe");
- tenantAdmin.setLastName("Downs");
-
- createUserAndLogin(tenantAdmin, "testPassword1");
}
- @After
- public void afterTest() throws Exception {
- loginSysAdmin();
- if (savedTenant != null) {
- doDelete("/api/tenant/" + savedTenant.getId().getId().toString()).andExpect(status().isOk());
- }
- }
-
- @Test
- public void testServerMqttOneWayRpc() throws Exception {
- Device device = new Device();
- device.setName("Test One-Way Server-Side RPC");
- device.setType("default");
- Device savedDevice = getSavedDevice(device);
- DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
- assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
- String accessToken = deviceCredentials.getCredentialsId();
- assertNotNull(accessToken);
-
- String clientId = MqttAsyncClient.generateClientId();
- MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
-
- MqttConnectOptions options = new MqttConnectOptions();
- options.setUserName(accessToken);
- client.connect(options).waitForCompletion();
+ protected void processOneWayRpcTest() throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(accessToken);
CountDownLatch latch = new CountDownLatch(1);
TestMqttCallback callback = new TestMqttCallback(client, latch);
client.setCallback(callback);
- client.subscribe("v1/devices/me/rpc/request/+", MqttQoS.AT_MOST_ONCE.value());
+ client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, MqttQoS.AT_MOST_ONCE.value());
- Thread.sleep(2000);
+ Thread.sleep(1000);
String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
@@ -122,100 +94,112 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
}
- @Test
- public void testServerMqttOneWayRpcDeviceOffline() throws Exception {
- Device device = new Device();
- device.setName("Test One-Way Server-Side RPC Device Offline");
- device.setType("default");
- Device savedDevice = getSavedDevice(device);
- DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
- assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
- String accessToken = deviceCredentials.getCredentialsId();
- assertNotNull(accessToken);
-
- String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"24\",\"value\": 1},\"timeout\": 6000}";
+ protected void processOneWayRpcTestGateway(String deviceName) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+ String payload = "{\"device\":\"" + deviceName + "\"}";
+ byte[] payloadBytes = payload.getBytes();
+ validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes);
+ }
+
+ protected void processTwoWayRpcTest() throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(accessToken);
+ client.subscribe(MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC, 1);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ TestMqttCallback callback = new TestMqttCallback(client, latch);
+ client.setCallback(callback);
+
+ Thread.sleep(1000);
+
+ String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}";
String deviceId = savedDevice.getId().getId().toString();
- doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().is(409),
- asyncContextTimeoutToUseRpcPlugin);
+ String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
+ String expected = "{\"value1\":\"A\",\"value2\":\"B\"}";
+ latch.await(3, TimeUnit.SECONDS);
+ Assert.assertEquals(expected, result);
}
- @Test
- public void testServerMqttOneWayRpcDeviceDoesNotExist() throws Exception {
- String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"25\",\"value\": 1}}";
- String nonExistentDeviceId = Uuids.timeBased().toString();
+ protected void processTwoWayRpcTestGateway(String deviceName) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+
+ String payload = "{\"device\":\"" + deviceName + "\"}";
+ byte[] payloadBytes = payload.getBytes();
- String result = doPostAsync("/api/plugins/rpc/oneway/" + nonExistentDeviceId, setGpioRequest, String.class,
- status().isNotFound());
- Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
+ validateTwoWayRpcGateway(deviceName, client, payloadBytes);
}
- @Test
- public void testServerMqttTwoWayRpc() throws Exception {
- Device device = new Device();
- device.setName("Test Two-Way Server-Side RPC");
- device.setType("default");
- Device savedDevice = getSavedDevice(device);
- DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
- assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
- String accessToken = deviceCredentials.getCredentialsId();
- assertNotNull(accessToken);
+ protected void validateOneWayRpcGatewayResponse(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception {
+ publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC);
- String clientId = MqttAsyncClient.generateClientId();
- MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
+ Device savedDevice = doExecuteWithRetriesAndInterval(
+ () -> getDeviceByName(deviceName),
+ 20,
+ 100
+ );
+ assertNotNull(savedDevice);
- MqttConnectOptions options = new MqttConnectOptions();
- options.setUserName(accessToken);
- client.connect(options).waitForCompletion();
- client.subscribe("v1/devices/me/rpc/request/+", 1);
- client.setCallback(new TestMqttCallback(client, new CountDownLatch(1)));
+ CountDownLatch latch = new CountDownLatch(1);
+ TestMqttCallback callback = new TestMqttCallback(client, latch);
+ client.setCallback(callback);
- Thread.sleep(2000);
+ client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value());
- String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}";
- String deviceId = savedDevice.getId().getId().toString();
+ Thread.sleep(1000);
- String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
- Assert.assertEquals("{\"value1\":\"A\",\"value2\":\"B\"}", result);
+ String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}";
+ String deviceId = savedDevice.getId().getId().toString();
+ String result = doPostAsync("/api/plugins/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk());
+ Assert.assertTrue(StringUtils.isEmpty(result));
+ latch.await(3, TimeUnit.SECONDS);
+ assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
}
- @Test
- public void testServerMqttTwoWayRpcDeviceOffline() throws Exception {
- Device device = new Device();
- device.setName("Test Two-Way Server-Side RPC Device Offline");
- device.setType("default");
- Device savedDevice = getSavedDevice(device);
- DeviceCredentials deviceCredentials = getDeviceCredentials(savedDevice);
- assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
- String accessToken = deviceCredentials.getCredentialsId();
- assertNotNull(accessToken);
-
- String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"27\",\"value\": 1},\"timeout\": 6000}";
- String deviceId = savedDevice.getId().getId().toString();
+ protected void validateTwoWayRpcGateway(String deviceName, MqttAsyncClient client, byte[] payloadBytes) throws Exception {
+ publishMqttMsg(client, payloadBytes, MqttTopics.GATEWAY_CONNECT_TOPIC);
- doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().is(409),
- asyncContextTimeoutToUseRpcPlugin);
- }
+ Device savedDevice = doExecuteWithRetriesAndInterval(
+ () -> getDeviceByName(deviceName),
+ 20,
+ 100
+ );
+ assertNotNull(savedDevice);
+
+ CountDownLatch latch = new CountDownLatch(1);
+ TestMqttCallback callback = new TestMqttCallback(client, latch);
+ client.setCallback(callback);
+
+ client.subscribe(MqttTopics.GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE.value());
- @Test
- public void testServerMqttTwoWayRpcDeviceDoesNotExist() throws Exception {
- String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"28\",\"value\": 1}}";
- String nonExistentDeviceId = Uuids.timeBased().toString();
+ Thread.sleep(1000);
- String result = doPostAsync("/api/plugins/rpc/twoway/" + nonExistentDeviceId, setGpioRequest, String.class,
- status().isNotFound());
- Assert.assertEquals(AccessValidator.DEVICE_WITH_REQUESTED_ID_NOT_FOUND, result);
+ String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}";
+ String deviceId = savedDevice.getId().getId().toString();
+ String result = doPostAsync("/api/plugins/rpc/twoway/" + deviceId, setGpioRequest, String.class, status().isOk());
+ latch.await(3, TimeUnit.SECONDS);
+ String expected = "{\"success\":true}";
+ assertEquals(expected, result);
+ assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
}
- private Device getSavedDevice(Device device) throws Exception {
- return doPost("/api/device", device, Device.class);
+ private Device getDeviceByName(String deviceName) throws Exception {
+ return doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class);
}
- private DeviceCredentials getDeviceCredentials(Device savedDevice) throws Exception {
- return doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
+ protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException {
+ MqttMessage message = new MqttMessage();
+ if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) {
+ message.setPayload(DEVICE_RESPONSE.getBytes(StandardCharset.UTF_8));
+ } else {
+ JsonNode requestMsgNode = JacksonUtil.toJsonNode(new String(mqttMessage.getPayload(), StandardCharset.UTF_8));
+ String deviceName = requestMsgNode.get("device").asText();
+ int requestId = requestMsgNode.get("data").get("id").asInt();
+ message.setPayload(("{\"device\": \"" + deviceName + "\", \"id\": " + requestId + ", \"data\": {\"success\": true}}").getBytes(StandardCharset.UTF_8));
+ }
+ return message;
}
- private static class TestMqttCallback implements MqttCallback {
+ private class TestMqttCallback implements MqttCallback {
private final MqttAsyncClient client;
private final CountDownLatch latch;
@@ -237,11 +221,9 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractC
@Override
public void messageArrived(String requestTopic, MqttMessage mqttMessage) throws Exception {
log.info("Message Arrived: " + Arrays.toString(mqttMessage.getPayload()));
- MqttMessage message = new MqttMessage();
String responseTopic = requestTopic.replace("request", "response");
- message.setPayload("{\"value1\":\"A\", \"value2\":\"B\"}".getBytes("UTF-8"));
qoS = mqttMessage.getQos();
- client.publish(responseTopic, message);
+ client.publish(responseTopic, processMessageArrived(requestTopic, mqttMessage));
latch.countDown();
}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java
new file mode 100644
index 0000000000..d9ff14e1d2
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcJsonIntegrationTest.java
@@ -0,0 +1,66 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.rpc;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.thingsboard.server.common.data.TransportPayloadType;
+
+@Slf4j
+public abstract class AbstractMqttServerSideRpcJsonIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.JSON, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ super.processAfterTest();
+ }
+
+ @Test
+ public void testServerMqttOneWayRpc() throws Exception {
+ processOneWayRpcTest();
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpc() throws Exception {
+ processTwoWayRpcTest();
+ }
+
+ @Test
+ public void testGatewayServerMqttOneWayRpc() throws Exception {
+ processOneWayRpcTestGateway("Gateway Device OneWay RPC Json");
+ }
+
+ @Test
+ public void testGatewayServerMqttTwoWayRpc() throws Exception {
+ processTwoWayRpcTestGateway("Gateway Device TwoWay RPC Json");
+ }
+
+ protected void processOneWayRpcTestGateway(String deviceName) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+ String payload = "{\"device\": \"" + deviceName + "\", \"type\": \"" + TransportPayloadType.JSON.name() + "\"}";
+ byte[] payloadBytes = payload.getBytes();
+ validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes);
+ }
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java
new file mode 100644
index 0000000000..759a5da912
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/AbstractMqttServerSideRpcProtoIntegrationTest.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.rpc;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.eclipse.paho.client.mqttv3.MqttException;
+import org.eclipse.paho.client.mqttv3.MqttMessage;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.thingsboard.server.common.data.TransportPayloadType;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
+import org.thingsboard.server.gen.transport.TransportApiProtos;
+import org.thingsboard.server.gen.transport.TransportProtos;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@Slf4j
+public abstract class AbstractMqttServerSideRpcProtoIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest {
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("RPC test device", "RPC test gateway", TransportPayloadType.PROTOBUF, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ super.processAfterTest();
+ }
+
+ @Test
+ public void testServerMqttOneWayRpc() throws Exception {
+ processOneWayRpcTest();
+ }
+
+ @Test
+ public void testServerMqttTwoWayRpc() throws Exception {
+ processTwoWayRpcTest();
+ }
+
+ @Test
+ public void testGatewayServerMqttOneWayRpc() throws Exception {
+ processOneWayRpcTestGateway("Gateway Device OneWay RPC Proto");
+ }
+
+ @Test
+ public void testGatewayServerMqttTwoWayRpc() throws Exception {
+ processTwoWayRpcTestGateway("Gateway Device TwoWay RPC Proto");
+ }
+
+ protected void processTwoWayRpcTestGateway(String deviceName) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+ TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName);
+ byte[] payloadBytes = connectMsgProto.toByteArray();
+ validateTwoWayRpcGateway(deviceName, client, payloadBytes);
+ }
+
+ protected void processOneWayRpcTestGateway(String deviceName) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(gatewayAccessToken);
+ TransportApiProtos.ConnectMsg connectMsgProto = getConnectProto(deviceName);
+ byte[] payloadBytes = connectMsgProto.toByteArray();
+ validateOneWayRpcGatewayResponse(deviceName, client, payloadBytes);
+ }
+
+
+ private TransportApiProtos.ConnectMsg getConnectProto(String deviceName) {
+ TransportApiProtos.ConnectMsg.Builder builder = TransportApiProtos.ConnectMsg.newBuilder();
+ builder.setDeviceName(deviceName);
+ builder.setDeviceType(TransportPayloadType.PROTOBUF.name());
+ return builder.build();
+ }
+
+ protected MqttMessage processMessageArrived(String requestTopic, MqttMessage mqttMessage) throws MqttException, InvalidProtocolBufferException {
+ MqttMessage message = new MqttMessage();
+ if (requestTopic.startsWith(MqttTopics.BASE_DEVICE_API_TOPIC)) {
+ TransportProtos.ToDeviceRpcResponseMsg toDeviceRpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder()
+ .setPayload(DEVICE_RESPONSE)
+ .setRequestId(0)
+ .build();
+ message.setPayload(toDeviceRpcResponseMsg.toByteArray());
+ } else {
+ TransportApiProtos.GatewayDeviceRpcRequestMsg msg = TransportApiProtos.GatewayDeviceRpcRequestMsg.parseFrom(mqttMessage.getPayload());
+ String deviceName = msg.getDeviceName();
+ int requestId = msg.getRpcRequestMsg().getRequestId();
+ TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.newBuilder()
+ .setDeviceName(deviceName)
+ .setId(requestId)
+ .setData("{\"success\": true}")
+ .build();
+ message.setPayload(gatewayRpcResponseMsg.toByteArray());
+ }
+ return message;
+ }
+
+
+
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java
index e90c48a974..6a5cb69c52 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/nosql/MqttServerSideRpcNoSqlIntegrationTest.java
@@ -16,11 +16,11 @@
package org.thingsboard.server.mqtt.rpc.nosql;
import org.thingsboard.server.dao.service.DaoNoSqlTest;
-import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest;
+import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcDefaultIntegrationTest;
/**
* Created by Valerii Sosliuk on 8/22/2017.
*/
@DaoNoSqlTest
-public class MqttServerSideRpcNoSqlIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest {
+public class MqttServerSideRpcNoSqlIntegrationTest extends AbstractMqttServerSideRpcDefaultIntegrationTest {
}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java
new file mode 100644
index 0000000000..4d4e900767
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcJsonSqlIntegrationTest.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.rpc.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcJsonIntegrationTest;
+
+@DaoSqlTest
+public class MqttServerSideRpcJsonSqlIntegrationTest extends AbstractMqttServerSideRpcJsonIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java
new file mode 100644
index 0000000000..7fb91a636c
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcProtoSqlIntegrationTest.java
@@ -0,0 +1,24 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.rpc.sql;
+
+import org.thingsboard.server.dao.service.DaoSqlTest;
+import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcProtoIntegrationTest;
+
+
+@DaoSqlTest
+public class MqttServerSideRpcProtoSqlIntegrationTest extends AbstractMqttServerSideRpcProtoIntegrationTest {
+}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
index dc2511f4c3..7bddfbbe52 100644
--- a/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
+++ b/application/src/test/java/org/thingsboard/server/mqtt/rpc/sql/MqttServerSideRpcSqlIntegrationTest.java
@@ -16,11 +16,11 @@
package org.thingsboard.server.mqtt.rpc.sql;
import org.thingsboard.server.dao.service.DaoSqlTest;
-import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcIntegrationTest;
+import org.thingsboard.server.mqtt.rpc.AbstractMqttServerSideRpcDefaultIntegrationTest;
/**
* Created by Valerii Sosliuk on 8/22/2017.
*/
@DaoSqlTest
-public class MqttServerSideRpcSqlIntegrationTest extends AbstractMqttServerSideRpcIntegrationTest {
+public class MqttServerSideRpcSqlIntegrationTest extends AbstractMqttServerSideRpcDefaultIntegrationTest {
}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java
deleted file mode 100644
index 47e9537ef9..0000000000
--- a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/AbstractMqttTelemetryIntegrationTest.java
+++ /dev/null
@@ -1,163 +0,0 @@
-/**
- * Copyright © 2016-2020 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.mqtt.telemetry;
-
-import io.netty.handler.codec.mqtt.MqttQoS;
-import lombok.extern.slf4j.Slf4j;
-import org.eclipse.paho.client.mqttv3.*;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.springframework.web.util.UriComponentsBuilder;
-import org.thingsboard.server.common.data.Device;
-import org.thingsboard.server.common.data.security.DeviceCredentials;
-import org.thingsboard.server.controller.AbstractControllerTest;
-import org.thingsboard.server.dao.service.DaoNoSqlTest;
-
-import java.net.URI;
-import java.util.*;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
-
-/**
- * @author Valerii Sosliuk
- */
-@Slf4j
-public abstract class AbstractMqttTelemetryIntegrationTest extends AbstractControllerTest {
-
- private static final String MQTT_URL = "tcp://localhost:1883";
-
- private Device savedDevice;
- private String accessToken;
-
- @Before
- public void beforeTest() throws Exception {
- loginTenantAdmin();
-
- Device device = new Device();
- device.setName("Test device");
- device.setType("default");
- savedDevice = doPost("/api/device", device, Device.class);
-
- DeviceCredentials deviceCredentials =
- doGet("/api/device/" + savedDevice.getId().getId().toString() + "/credentials", DeviceCredentials.class);
-
- assertEquals(savedDevice.getId(), deviceCredentials.getDeviceId());
- accessToken = deviceCredentials.getCredentialsId();
- assertNotNull(accessToken);
- }
-
- @Test
- public void testPushMqttRpcData() throws Exception {
- String clientId = MqttAsyncClient.generateClientId();
- MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
-
- MqttConnectOptions options = new MqttConnectOptions();
- options.setUserName(accessToken);
- client.connect(options);
- Thread.sleep(3000);
- MqttMessage message = new MqttMessage();
- message.setPayload("{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4}".getBytes());
- client.publish("v1/devices/me/telemetry", message);
-
- String deviceId = savedDevice.getId().getId().toString();
-
- Thread.sleep(2000);
- List actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/timeseries", List.class);
- Set actualKeySet = new HashSet<>(actualKeys);
-
- List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4");
- Set expectedKeySet = new HashSet<>(expectedKeys);
-
- assertEquals(expectedKeySet, actualKeySet);
-
- String getTelemetryValuesUrl = "/api/plugins/telemetry/DEVICE/" + deviceId + "/values/timeseries?keys=" + String.join(",", actualKeySet);
- Map>> values = doGetAsync(getTelemetryValuesUrl, Map.class);
-
- assertEquals("value1", values.get("key1").get(0).get("value"));
- assertEquals("true", values.get("key2").get(0).get("value"));
- assertEquals("3.0", values.get("key3").get(0).get("value"));
- assertEquals("4", values.get("key4").get(0).get("value"));
- }
-
-
-// @Test - Unstable
- public void testMqttQoSLevel() throws Exception {
- String clientId = MqttAsyncClient.generateClientId();
- MqttAsyncClient client = new MqttAsyncClient(MQTT_URL, clientId);
-
- MqttConnectOptions options = new MqttConnectOptions();
- options.setUserName(accessToken);
- CountDownLatch latch = new CountDownLatch(1);
- TestMqttCallback callback = new TestMqttCallback(client, latch);
- client.setCallback(callback);
- client.connect(options).waitForCompletion(5000);
- client.subscribe("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE.value());
- String payload = "{\"key\":\"uniqueValue\"}";
-// TODO 3.1: we need to acknowledge subscription only after it is processed by device actor and not when the message is pushed to queue.
-// MqttClient -> SUB REQUEST -> Transport -> Kafka -> Device Actor (subscribed)
-// MqttClient <- SUB_ACK <- Transport
- Thread.sleep(5000);
- doPostAsync("/api/plugins/telemetry/" + savedDevice.getId() + "/SHARED_SCOPE", payload, String.class, status().isOk());
- latch.await(10, TimeUnit.SECONDS);
- assertEquals(payload, callback.getPayload());
- assertEquals(MqttQoS.AT_MOST_ONCE.value(), callback.getQoS());
- }
-
- private static class TestMqttCallback implements MqttCallback {
-
- private final MqttAsyncClient client;
- private final CountDownLatch latch;
- private volatile Integer qoS;
- private volatile String payload;
-
- String getPayload() {
- return payload;
- }
-
- TestMqttCallback(MqttAsyncClient client, CountDownLatch latch) {
- this.client = client;
- this.latch = latch;
- }
-
- int getQoS() {
- return qoS;
- }
-
- @Override
- public void connectionLost(Throwable throwable) {
- log.error("Client connection lost", throwable);
- }
-
- @Override
- public void messageArrived(String requestTopic, MqttMessage mqttMessage) {
- payload = new String(mqttMessage.getPayload());
- qoS = mqttMessage.getQos();
- latch.countDown();
- }
-
- @Override
- public void deliveryComplete(IMqttDeliveryToken iMqttDeliveryToken) {
-
- }
- }
-
-
-}
diff --git a/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java
new file mode 100644
index 0000000000..5ac0746a43
--- /dev/null
+++ b/application/src/test/java/org/thingsboard/server/mqtt/telemetry/attributes/AbstractMqttAttributesIntegrationTest.java
@@ -0,0 +1,182 @@
+/**
+ * Copyright © 2016-2020 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.mqtt.telemetry.attributes;
+
+import lombok.extern.slf4j.Slf4j;
+import org.eclipse.paho.client.mqttv3.MqttAsyncClient;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.thingsboard.server.common.data.Device;
+import org.thingsboard.server.common.data.device.profile.MqttTopics;
+import org.thingsboard.server.common.data.id.DeviceId;
+import org.thingsboard.server.mqtt.AbstractMqttIntegrationTest;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+@Slf4j
+public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqttIntegrationTest {
+
+ protected static final String PAYLOAD_VALUES_STR = "{\"key1\":\"value1\", \"key2\":true, \"key3\": 3.0, \"key4\": 4," +
+ " \"key5\": {\"someNumber\": 42, \"someArray\": [1,2,3], \"someNestedObject\": {\"key\": \"value\"}}}";
+
+ @Before
+ public void beforeTest() throws Exception {
+ processBeforeTest("Test Post Attributes device", "Test Post Attributes gateway", null, null, null);
+ }
+
+ @After
+ public void afterTest() throws Exception {
+ processAfterTest();
+ }
+
+ @Test
+ public void testPushMqttAttributes() throws Exception {
+ List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5");
+ processAttributesTest(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, expectedKeys, PAYLOAD_VALUES_STR.getBytes());
+ }
+
+ @Test
+ public void testPushMqttAttributesGateway() throws Exception {
+ List expectedKeys = Arrays.asList("key1", "key2", "key3", "key4", "key5");
+ String deviceName1 = "Device A";
+ String deviceName2 = "Device B";
+ String payload = getGatewayAttributesJsonPayload(deviceName1, deviceName2);
+ processGatewayAttributesTest(expectedKeys, payload.getBytes(), deviceName1, deviceName2);
+ }
+
+ protected void processAttributesTest(String topic, List expectedKeys, byte[] payload) throws Exception {
+ MqttAsyncClient client = getMqttAsyncClient(accessToken);
+
+ publishMqttMsg(client, payload, topic);
+
+ DeviceId deviceId = savedDevice.getId();
+
+ long start = System.currentTimeMillis();
+ long end = System.currentTimeMillis() + 5000;
+
+ List actualKeys = null;
+ while (start <= end) {
+ actualKeys = doGetAsync("/api/plugins/telemetry/DEVICE/" + deviceId + "/keys/attributes/CLIENT_SCOPE", List.class);
+ if (actualKeys.size() == expectedKeys.size()) {
+ break;
+ }
+ Thread.sleep(100);
+ start += 100;
+ }
+ assertNotNull(actualKeys);
+
+ Set actualKeySet = new HashSet<>(actualKeys);
+
+ Set expectedKeySet = new HashSet<>(expectedKeys);
+
+ assertEquals(expectedKeySet, actualKeySet);
+
+ String getAttributesValuesUrl = getAttributesValuesUrl(deviceId, actualKeySet);
+ List