diff --git a/application/pom.xml b/application/pom.xml index 0d9982f281..b653465050 100644 --- a/application/pom.xml +++ b/application/pom.xml @@ -144,21 +144,6 @@ org.eclipse.paho org.eclipse.paho.mqttv5.client - - org.cassandraunit - cassandra-unit - - - org.slf4j - slf4j-log4j12 - - - org.hibernate - hibernate-validator - - - test - org.thingsboard ui-ngx @@ -329,6 +314,11 @@ spring-test-dbunit test + + org.testcontainers + cassandra + test + org.testcontainers postgresql diff --git a/application/src/main/data/json/demo/dashboards/gateways.json b/application/src/main/data/json/demo/dashboards/gateways.json index 0a18b10a52..6051fbd514 100644 --- a/application/src/main/data/json/demo/dashboards/gateways.json +++ b/application/src/main/data/json/demo/dashboards/gateways.json @@ -131,7 +131,7 @@ "name": "Add device", "icon": "add", "type": "customPretty", - "customHtml": "
\n \n

Add device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", + "customHtml": "
\n \n

Add device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", "customCss": "", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenAddDeviceDialog();\n\nfunction openAddDeviceDialog() {\n customDialog.customDialog(htmlTemplate, AddDeviceDialogController).subscribe();\n}\n\nfunction AddDeviceDialogController(instance) {\n let vm = instance;\n \n vm.addDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.addDeviceFormGroup.markAsPristine();\n let device = {\n additionalInfo: {gateway: true},\n name: vm.addDeviceFormGroup.get('deviceName').value,\n type: 'gateway',\n label: vm.addDeviceFormGroup.get('deviceLabel').value\n };\n deviceService.saveDevice(device).subscribe(\n function (device) {\n saveAttributes(device.id).subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n function saveAttributes(entityId) {\n let attributes = vm.addDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, \"SERVER_SCOPE\", attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", "customResources": [], @@ -160,7 +160,7 @@ "name": "Edit device", "icon": "edit", "type": "customPretty", - "customHtml": "
\n \n

Edit device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", + "customHtml": "
\n \n

Edit device

\n \n \n
\n \n \n
\n
\n
\n \n Device name\n \n \n Device name is required.\n \n \n
\n \n Latitude\n \n \n \n Longitude\n \n \n
\n \n Label\n \n \n
\n
\n
\n \n \n \n
\n
\n", "customCss": "/*=======================================================================*/\n/*========== There are two examples: for edit and add entity ==========*/\n/*=======================================================================*/\n/*======================== Edit entity example ========================*/\n/*=======================================================================*/\n/*\n.edit-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.edit-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.edit-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n\n.relations-list.old-relations tb-entity-select tb-entity-autocomplete button {\n display: none;\n} \n*/\n/*========================================================================*/\n/*========================= Add entity example =========================*/\n/*========================================================================*/\n/*\n.add-entity-form md-input-container {\n padding-right: 10px;\n}\n\n.add-entity-form .boolean-value-input {\n padding-left: 5px;\n}\n\n.add-entity-form .boolean-value-input .checkbox-label {\n margin-bottom: 8px;\n color: rgba(0,0,0,0.54);\n font-size: 12px;\n}\n\n.relations-list .header {\n padding-right: 5px;\n padding-bottom: 5px;\n padding-left: 5px;\n}\n\n.relations-list .header .cell {\n padding-right: 5px;\n padding-left: 5px;\n font-size: 12px;\n font-weight: 700;\n color: rgba(0, 0, 0, .54);\n white-space: nowrap;\n}\n\n.relations-list .body {\n padding-right: 5px;\n padding-bottom: 15px;\n padding-left: 5px;\n}\n\n.relations-list .body .row {\n padding-top: 5px;\n}\n\n.relations-list .body .cell {\n padding-right: 5px;\n padding-left: 5px;\n}\n\n.relations-list .body md-autocomplete-wrap md-input-container {\n height: 30px;\n}\n\n.relations-list .body .md-button {\n margin: 0;\n}\n*/\n", "customFunction": "let $injector = widgetContext.$scope.$injector;\nlet customDialog = $injector.get(widgetContext.servicesMap.get('customDialog'));\nlet deviceService = $injector.get(widgetContext.servicesMap.get('deviceService'));\nlet attributeService = $injector.get(widgetContext.servicesMap.get('attributeService'));\n\nopenEditDeviceDialog();\n\nfunction openEditDeviceDialog() {\n customDialog.customDialog(htmlTemplate, EditDeviceDialogController).subscribe();\n}\n\nfunction EditDeviceDialogController(instance) {\n let vm = instance;\n \n vm.device = null;\n vm.attributes = {};\n \n vm.editDeviceFormGroup = vm.fb.group({\n deviceName: ['', [vm.validators.required]],\n deviceLabel: [''],\n attributes: vm.fb.group({\n latitude: [null],\n longitude: [null]\n }) \n });\n \n vm.cancel = function() {\n vm.dialogRef.close(null);\n };\n \n vm.save = function() {\n vm.editDeviceFormGroup.markAsPristine();\n vm.device.name = vm.editDeviceFormGroup.get('deviceName').value;\n vm.device.label = vm.editDeviceFormGroup.get('deviceLabel').value;\n deviceService.saveDevice(vm.device).subscribe(\n function () {\n saveAttributes().subscribe(\n function () {\n widgetContext.updateAliases();\n vm.dialogRef.close(null);\n }\n );\n }\n );\n };\n \n getEntityInfo();\n \n function getEntityInfo() {\n deviceService.getDevice(entityId.id).subscribe(\n function (device) {\n attributeService.getEntityAttributes(entityId, 'SERVER_SCOPE',\n ['latitude', 'longitude']).subscribe(\n function (attributes) {\n for (let i = 0; i < attributes.length; i++) {\n vm.attributes[attributes[i].key] = attributes[i].value; \n }\n vm.device = device;\n vm.editDeviceFormGroup.patchValue(\n {\n deviceName: vm.device.name,\n deviceLabel: vm.device.label,\n attributes: {\n latitude: vm.attributes.latitude,\n longitude: vm.attributes.longitude\n }\n }, {emitEvent: false}\n );\n } \n );\n }\n ); \n }\n \n function saveAttributes() {\n let attributes = vm.editDeviceFormGroup.get('attributes').value;\n let attributesArray = [];\n for (let key in attributes) {\n attributesArray.push({key: key, value: attributes[key]});\n }\n if (attributesArray.length > 0) {\n return attributeService.saveEntityAttributes(entityId, 'SERVER_SCOPE', attributesArray);\n } else {\n return widgetContext.rxjs.of([]);\n }\n }\n}\n", "customResources": [], diff --git a/application/src/main/data/json/demo/dashboards/thermostats.json b/application/src/main/data/json/demo/dashboards/thermostats.json index 4f486effdf..c014d8875a 100644 --- a/application/src/main/data/json/demo/dashboards/thermostats.json +++ b/application/src/main/data/json/demo/dashboards/thermostats.json @@ -215,6 +215,8 @@ "displayDetails": true, "allowAcknowledgment": true, "allowClear": true, + "allowAssign": true, + "displayComments": true, "displayPagination": true, "defaultPageSize": 10, "defaultSortOrder": "-createdTime", @@ -277,6 +279,14 @@ "color": "#607d8b", "settings": {}, "_hash": 0.7977920750136249 + }, + { + "name": "assignee", + "type": "alarm", + "label": "Assignee", + "color": "#9c27b0", + "settings": {}, + "_hash": 0.8678751039018493 } ] }, diff --git a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json index 448fcaf52c..e7a97b36f8 100644 --- a/application/src/main/data/json/system/widget_bundles/alarm_widgets.json +++ b/application/src/main/data/json/system/widget_bundles/alarm_widgets.json @@ -23,7 +23,7 @@ "dataKeySettingsSchema": "", "settingsDirective": "tb-alarms-table-widget-settings", "dataKeySettingsDirective": "tb-alarms-table-key-settings", - "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" + "defaultConfig": "{\"timewindow\":{\"realtime\":{\"interval\":1000,\"timewindowMs\":86400000},\"aggregation\":{\"type\":\"NONE\",\"limit\":200}},\"showTitle\":true,\"backgroundColor\":\"rgb(255, 255, 255)\",\"color\":\"rgba(0, 0, 0, 0.87)\",\"padding\":\"4px\",\"settings\":{\"enableSelection\":true,\"enableSearch\":true,\"displayDetails\":true,\"allowAcknowledgment\":true,\"allowClear\":true,\"allowAssign\":true,\"displayComments\":true,\"displayPagination\":true,\"defaultPageSize\":10,\"defaultSortOrder\":\"-createdTime\",\"enableSelectColumnDisplay\":true,\"enableStickyAction\":false,\"enableFilter\":true},\"title\":\"Alarms table\",\"dropShadow\":true,\"enableFullscreen\":true,\"titleStyle\":{\"fontSize\":\"16px\",\"fontWeight\":400,\"padding\":\"5px 10px 5px 10px\"},\"useDashboardTimewindow\":false,\"showLegend\":false,\"alarmSource\":{\"type\":\"function\",\"dataKeys\":[{\"name\":\"createdTime\",\"type\":\"alarm\",\"label\":\"Created time\",\"color\":\"#2196f3\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.021092237451093787},{\"name\":\"originator\",\"type\":\"alarm\",\"label\":\"Originator\",\"color\":\"#4caf50\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.2780007688856758},{\"name\":\"type\",\"type\":\"alarm\",\"label\":\"Type\",\"color\":\"#f44336\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.7323586880398418},{\"name\":\"severity\",\"type\":\"alarm\",\"label\":\"Severity\",\"color\":\"#ffc107\",\"settings\":{\"useCellStyleFunction\":false,\"useCellContentFunction\":false},\"_hash\":0.09927019860088193},{\"name\":\"status\",\"type\":\"alarm\",\"label\":\"Status\",\"color\":\"#607d8b\",\"settings\":{\"useCellStyleFunction\":false,\"cellStyleFunction\":\"\",\"useCellContentFunction\":false,\"cellContentFunction\":\"\"},\"_hash\":0.6588418951443418},{\"name\":\"assignee\",\"type\":\"alarm\",\"label\":\"Assignee\",\"color\":\"#9c27b0\",\"settings\":{},\"_hash\":0.5008441077416634}],\"entityAliasId\":null,\"name\":\"alarms\"},\"alarmSearchStatus\":\"ANY\",\"alarmsPollingInterval\":5,\"showTitleIcon\":false,\"titleIcon\":\"more_horiz\",\"iconColor\":\"rgba(0, 0, 0, 0.87)\",\"iconSize\":\"24px\",\"titleTooltip\":\"\",\"widgetStyle\":{},\"displayTimewindow\":true,\"actions\":{},\"alarmStatusList\":[],\"alarmSeverityList\":[],\"alarmTypeList\":[],\"searchPropagatedAlarms\":false}" } } ] diff --git a/application/src/main/data/upgrade/3.4.4/schema_update.sql b/application/src/main/data/upgrade/3.4.4/schema_update.sql index f8c133f352..d71fc158ef 100644 --- a/application/src/main/data/upgrade/3.4.4/schema_update.sql +++ b/application/src/main/data/upgrade/3.4.4/schema_update.sql @@ -14,6 +14,70 @@ -- limitations under the License. -- +-- USER CREDENTIALS START + +ALTER TABLE user_credentials + ADD COLUMN IF NOT EXISTS additional_info varchar NOT NULL DEFAULT '{}'; + +UPDATE user_credentials + SET additional_info = json_build_object('userPasswordHistory', (u.additional_info::json -> 'userPasswordHistory')) + FROM tb_user u WHERE user_credentials.user_id = u.id AND u.additional_info::jsonb ? 'userPasswordHistory'; + +UPDATE tb_user SET additional_info = tb_user.additional_info::jsonb - 'userPasswordHistory' WHERE additional_info::jsonb ? 'userPasswordHistory'; + +-- USER CREDENTIALS END + +-- ALARM ASSIGN TO USER START + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS assign_ts BIGINT DEFAULT 0; +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS assignee_id UUID; + +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_assignee_created_time ON alarm(tenant_id, assignee_id, created_time DESC); + +-- ALARM ASSIGN TO USER END + +-- ALARM STATUS REFACTORING START + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS acknowledged boolean; +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS cleared boolean; + +ALTER TABLE alarm ADD COLUMN IF NOT EXISTS status varchar; -- to avoid failure of the subsequent upgrade. +UPDATE alarm SET acknowledged = true, cleared = true WHERE status = 'CLEARED_ACK'; +UPDATE alarm SET acknowledged = true, cleared = false WHERE status = 'ACTIVE_ACK'; +UPDATE alarm SET acknowledged = false, cleared = true WHERE status = 'CLEARED_UNACK'; +UPDATE alarm SET acknowledged = false, cleared = false WHERE status = 'ACTIVE_UNACK'; + +-- Drop index by 'status' column and replace with new one that has only active alarms; +DROP INDEX IF EXISTS idx_alarm_originator_alarm_type_active; +CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active + ON alarm USING btree (originator_id, type) WHERE cleared = false; + +-- Cover index by alarm type to optimize propagated alarm queries; +DROP INDEX IF EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id; +CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm +USING btree (tenant_id, entity_id, alarm_type, created_time DESC) INCLUDE(alarm_id); + +DROP INDEX IF EXISTS idx_alarm_tenant_status_created_time; +ALTER TABLE alarm DROP COLUMN IF EXISTS status; + +-- Update old alarms and set their state to clear, if there are newer alarms. +UPDATE alarm a +SET cleared = TRUE +WHERE cleared = FALSE + AND id != (SELECT l.id + FROM alarm l + WHERE l.tenant_id = a.tenant_id + AND l.originator_id = a.originator_id + AND l.type = a.type + ORDER BY l.created_time DESC, l.id + LIMIT 1); + +VACUUM FULL ANALYZE alarm; + +-- ALARM STATUS REFACTORING END + +-- ALARM COMMENTS START + CREATE TABLE IF NOT EXISTS alarm_comment ( id uuid NOT NULL, created_time bigint NOT NULL, @@ -30,3 +94,256 @@ CREATE TABLE IF NOT EXISTS user_settings ( settings varchar(100000), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE ); + +-- ALARM COMMENTS END + +-- ALARM INFO VIEW + +DROP VIEW IF EXISTS alarm_info CASCADE; +CREATE VIEW alarm_info AS +SELECT a.*, +(CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK' + WHEN NOT a.acknowledged AND a.cleared THEN 'CLEARED_UNACK' + WHEN a.acknowledged AND NOT a.cleared THEN 'ACTIVE_ACK' + WHEN NOT a.acknowledged AND NOT a.cleared THEN 'ACTIVE_UNACK' END) as status, +COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id) + WHEN a.originator_type = 1 THEN (select title from customer where id = a.originator_id) + WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id) + WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id) + WHEN a.originator_type = 4 THEN (select name from asset where id = a.originator_id) + WHEN a.originator_type = 5 THEN (select name from device where id = a.originator_id) + WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id) + WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id) + WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id) + WHEN a.originator_type = 18 THEN (select name from edge where id = a.originator_id) END + , 'Deleted') originator_name, +COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id) + WHEN a.originator_type = 1 THEN (select COALESCE(title, email) from customer where id = a.originator_id) + WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id) + WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id) + WHEN a.originator_type = 4 THEN (select COALESCE(label, name) from asset where id = a.originator_id) + WHEN a.originator_type = 5 THEN (select COALESCE(label, name) from device where id = a.originator_id) + WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id) + WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id) + WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id) + WHEN a.originator_type = 18 THEN (select COALESCE(label, name) from edge where id = a.originator_id) END + , 'Deleted') as originator_label, +u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email as assignee_email +FROM alarm a +LEFT JOIN tb_user u ON u.id = a.assignee_id; + +-- ALARM INFO VIEW END + +-- ALARM FUNCTIONS START + +DROP FUNCTION IF EXISTS create_or_update_active_alarm; +CREATE OR REPLACE FUNCTION create_or_update_active_alarm( + t_id uuid, c_id uuid, a_id uuid, a_created_ts bigint, + a_o_id uuid, a_o_type integer, a_type varchar, + a_severity varchar, a_start_ts bigint, a_end_ts bigint, + a_details varchar, + a_propagate boolean, a_propagate_to_owner boolean, + a_propagate_to_tenant boolean, a_propagation_types varchar, + a_creation_enabled boolean) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + null_id constant uuid = '13814000-1dd2-11b2-8080-808080808080'::uuid; + existing alarm; + result alarm_info; + row_count integer; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.originator_id = a_o_id AND a.type = a_type AND a.cleared = false ORDER BY a.start_ts DESC FOR UPDATE; + IF existing.id IS NULL THEN + IF a_creation_enabled = FALSE THEN + RETURN json_build_object('success', false)::text; + END IF; + IF c_id = null_id THEN + c_id = NULL; + end if; + INSERT INTO alarm + (tenant_id, customer_id, id, created_time, + originator_id, originator_type, type, + severity, start_ts, end_ts, + additional_info, + propagate, propagate_to_owner, propagate_to_tenant, propagate_relation_types, + acknowledged, ack_ts, + cleared, clear_ts, + assignee_id, assign_ts) + VALUES + (t_id, c_id, a_id, a_created_ts, + a_o_id, a_o_type, a_type, + a_severity, a_start_ts, a_end_ts, + a_details, + a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types, + false, 0, false, 0, NULL, 0); + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text; + ELSE + UPDATE alarm a + SET severity = a_severity, + start_ts = a_start_ts, + end_ts = a_end_ts, + additional_info = a_details, + propagate = a_propagate, + propagate_to_owner = a_propagate_to_owner, + propagate_to_tenant = a_propagate_to_tenant, + propagate_relation_types = a_propagation_types + WHERE a.id = existing.id + AND a.tenant_id = t_id + AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details + OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR + propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types); + GET DIAGNOSTICS row_count = ROW_COUNT; + SELECT * INTO result FROM alarm_info a WHERE a.id = existing.id AND a.tenant_id = t_id; + IF row_count > 0 THEN + RETURN json_build_object('success', true, 'modified', true, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text; + ELSE + RETURN json_build_object('success', true, 'modified', false, 'alarm', row_to_json(result))::text; + END IF; + END IF; +END +$$; + +DROP FUNCTION IF EXISTS update_alarm; +CREATE OR REPLACE FUNCTION update_alarm(t_id uuid, a_id uuid, a_severity varchar, a_start_ts bigint, a_end_ts bigint, + a_details varchar, + a_propagate boolean, a_propagate_to_owner boolean, + a_propagate_to_tenant boolean, a_propagation_types varchar) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + row_count integer; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + UPDATE alarm a + SET severity = a_severity, + start_ts = a_start_ts, + end_ts = a_end_ts, + additional_info = a_details, + propagate = a_propagate, + propagate_to_owner = a_propagate_to_owner, + propagate_to_tenant = a_propagate_to_tenant, + propagate_relation_types = a_propagation_types + WHERE a.id = a_id + AND a.tenant_id = t_id + AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details + OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR + propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types); + GET DIAGNOSTICS row_count = ROW_COUNT; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + IF row_count > 0 THEN + RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text; + ELSE + RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result))::text; + END IF; +END +$$; + +DROP FUNCTION IF EXISTS acknowledge_alarm; +CREATE OR REPLACE FUNCTION acknowledge_alarm(t_id uuid, a_id uuid, a_ts bigint) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + + IF NOT (existing.acknowledged) THEN + modified = TRUE; + UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END +$$; + +DROP FUNCTION IF EXISTS clear_alarm; +CREATE OR REPLACE FUNCTION clear_alarm(t_id uuid, a_id uuid, a_ts bigint, a_details varchar) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + cleared boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF NOT(existing.cleared) THEN + cleared = TRUE; + UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text; +END +$$; + +DROP FUNCTION IF EXISTS assign_alarm; +CREATE OR REPLACE FUNCTION assign_alarm(t_id uuid, a_id uuid, u_id uuid, a_ts bigint) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF existing.assignee_id IS NULL OR existing.assignee_id != u_id THEN + modified = TRUE; + UPDATE alarm a SET assignee_id = u_id, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END +$$; + +DROP FUNCTION IF EXISTS unassign_alarm; +CREATE OR REPLACE FUNCTION unassign_alarm(t_id uuid, a_id uuid, a_ts bigint) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF existing.assignee_id IS NOT NULL THEN + modified = TRUE; + UPDATE alarm a SET assignee_id = NULL, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END +$$; + +-- ALARM FUNCTIONS END diff --git a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java index 02ede2edaa..0c3589a077 100644 --- a/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java +++ b/application/src/main/java/org/thingsboard/server/actors/device/DeviceActorMessageProcessor.java @@ -116,7 +116,7 @@ import java.util.stream.Collectors; * @author Andrew Shvayka */ @Slf4j -class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { +public class DeviceActorMessageProcessor extends AbstractContextAwareMsgProcessor { static final String SESSION_TIMEOUT_MESSAGE = "session timeout!"; final TenantId tenantId; diff --git a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java index 47355d6c44..9a6ffb0e32 100644 --- a/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/WebSocketConfiguration.java @@ -15,6 +15,8 @@ */ package org.thingsboard.server.config; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; @@ -40,11 +42,15 @@ import java.util.Map; @Configuration @TbCoreComponent @EnableWebSocket +@RequiredArgsConstructor +@Slf4j public class WebSocketConfiguration implements WebSocketConfigurer { public static final String WS_PLUGIN_PREFIX = "/api/ws/plugins/"; private static final String WS_PLUGIN_MAPPING = WS_PLUGIN_PREFIX + "**"; + private final WebSocketHandler wsHandler; + @Bean public ServletServerContainerFactoryBean createWebSocketContainer() { ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean(); @@ -55,7 +61,11 @@ public class WebSocketConfiguration implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { - registry.addHandler(wsHandler(), WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*") + if (!(wsHandler instanceof TbWebSocketHandler)) { + log.error("TbWebSocketHandler expected but [{}] provided", wsHandler); + throw new RuntimeException("TbWebSocketHandler expected but " + wsHandler + " provided"); + } + registry.addHandler(wsHandler, WS_PLUGIN_MAPPING).setAllowedOriginPatterns("*") .addInterceptors(new HttpSessionHandshakeInterceptor(), new HandshakeInterceptor() { @Override @@ -82,11 +92,6 @@ public class WebSocketConfiguration implements WebSocketConfigurer { }); } - @Bean - public WebSocketHandler wsHandler() { - return new TbWebSocketHandler(); - } - protected SecurityUser getCurrentUser() throws ThingsboardException { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.getPrincipal() instanceof SecurityUser) { 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 d810fdede7..78ee6620fe 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AlarmController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AlarmController.java @@ -30,6 +30,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.StringUtils; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; @@ -41,16 +42,23 @@ import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.entitiy.alarm.TbAlarmService; +import org.thingsboard.server.service.security.model.SecurityUser; +import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.permission.Operation; import org.thingsboard.server.service.security.permission.Resource; +import java.util.UUID; + import static org.thingsboard.server.controller.ControllerConstants.ALARM_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_INFO_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ALARM_SORT_PROPERTY_ALLOWABLE_VALUES; +import static org.thingsboard.server.controller.ControllerConstants.ASSIGNEE_ID; +import static org.thingsboard.server.controller.ControllerConstants.ASSIGN_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_ID_PARAM_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_TYPE; @@ -79,6 +87,7 @@ public class AlarmController extends BaseController { private static final String ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES = "ANY, ACTIVE, CLEARED, ACK, UNACK"; private static final String ALARM_QUERY_STATUS_DESCRIPTION = "A string value representing one of the AlarmStatus enumeration value"; private static final String ALARM_QUERY_STATUS_ALLOWABLE_VALUES = "ACTIVE_UNACK, ACTIVE_ACK, CLEARED_UNACK, CLEARED_ACK"; + private static final String ALARM_QUERY_ASSIGNEE_DESCRIPTION = "A string value representing the assignee user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; private static final String ALARM_QUERY_TEXT_SEARCH_DESCRIPTION = "The case insensitive 'substring' filter based on of next alarm fields: type, severity or status"; private static final String ALARM_QUERY_START_TIME_DESCRIPTION = "The start timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; private static final String ALARM_QUERY_END_TIME_DESCRIPTION = "The end timestamp in milliseconds of the search time range over the Alarm class field: 'createdTime'."; @@ -93,12 +102,8 @@ public class AlarmController extends BaseController { public Alarm getAlarmById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); - try { - AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - return checkAlarmId(alarmId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + return checkAlarmId(alarmId, Operation.READ); } @ApiOperation(value = "Get Alarm Info (getAlarmInfoById)", @@ -110,15 +115,11 @@ public class AlarmController extends BaseController { public AlarmInfo getAlarmInfoById(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws ThingsboardException { checkParameter(ALARM_ID, strAlarmId); - try { - AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); - return checkAlarmInfoId(alarmId, Operation.READ); - } catch (Exception e) { - throw handleException(e); - } + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + return checkAlarmInfoId(alarmId, Operation.READ); } - @ApiOperation(value = "Create or update Alarm (saveAlarm)", + @ApiOperation(value = "Create or Update Alarm (saveAlarm)", notes = "Creates or Updates the Alarm. " + "When creating alarm, platform generates Alarm Id as " + UUID_WIKI_LINK + "The newly created Alarm id will be present in the response. Specify existing Alarm id to update the alarm. " + @@ -135,7 +136,9 @@ public class AlarmController extends BaseController { @ResponseBody public Alarm saveAlarm(@ApiParam(value = "A JSON value representing the alarm.") @RequestBody Alarm alarm) throws ThingsboardException { alarm.setTenantId(getTenantId()); + checkNotNull(alarm.getOriginator()); checkEntity(alarm.getId(), alarm, Resource.ALARM); + checkEntityId(alarm.getOriginator(), Operation.READ); return tbAlarmService.save(alarm, getCurrentUser()); } @@ -158,11 +161,12 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/ack", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + public AlarmInfo ackAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { checkParameter(ALARM_ID, strAlarmId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - tbAlarmService.ack(alarm, getCurrentUser()).get(); + //TODO: return correct error code if the alarm is not found or already cleared + return tbAlarmService.ack(alarm, getCurrentUser()); } @ApiOperation(value = "Clear Alarm (clearAlarm)", @@ -172,11 +176,50 @@ public class AlarmController extends BaseController { @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") @RequestMapping(value = "/alarm/{alarmId}/clear", method = RequestMethod.POST) @ResponseStatus(value = HttpStatus.OK) - public void clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + public AlarmInfo clearAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) @PathVariable(ALARM_ID) String strAlarmId) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + //TODO: return correct error code if the alarm is not found or already cleared + return tbAlarmService.clear(alarm, getCurrentUser()); + } + + @ApiOperation(value = "Assign/Reassign Alarm (assignAlarm)", + notes = "Assign the Alarm. " + + "Once assigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_ASSIGNED' " + + "(or ALARM_REASSIGNED in case of assigning already assigned alarm) will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/assign/{assigneeId}", method = RequestMethod.POST) + @ResponseStatus(value = HttpStatus.OK) + public Alarm assignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId, + @ApiParam(value = ASSIGN_ID_PARAM_DESCRIPTION) + @PathVariable(ASSIGNEE_ID) String strAssigneeId + ) throws Exception { checkParameter(ALARM_ID, strAlarmId); + checkParameter(ASSIGNEE_ID, strAssigneeId); AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); - tbAlarmService.clear(alarm, getCurrentUser()).get(); + UserId assigneeId = new UserId(UUID.fromString(strAssigneeId)); + checkUserId(assigneeId, Operation.READ); + return tbAlarmService.assign(alarm, assigneeId, System.currentTimeMillis(), getCurrentUser()); + } + + @ApiOperation(value = "Unassign Alarm (unassignAlarm)", + notes = "Unassign the Alarm. " + + "Once unassigned, the 'assign_ts' field will be set to current timestamp and special rule chain event 'ALARM_UNASSIGNED' will be generated. " + + "Referencing non-existing Alarm Id will cause an error." + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH, produces = MediaType.APPLICATION_JSON_VALUE) + @PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')") + @RequestMapping(value = "/alarm/{alarmId}/assign", method = RequestMethod.DELETE) + @ResponseStatus(value = HttpStatus.OK) + public Alarm unassignAlarm(@ApiParam(value = ALARM_ID_PARAM_DESCRIPTION) + @PathVariable(ALARM_ID) String strAlarmId + ) throws Exception { + checkParameter(ALARM_ID, strAlarmId); + AlarmId alarmId = new AlarmId(toUUID(strAlarmId)); + Alarm alarm = checkAlarmId(alarmId, Operation.WRITE); + return tbAlarmService.unassign(alarm, System.currentTimeMillis(), getCurrentUser()); } @ApiOperation(value = "Get Alarms (getAlarms)", @@ -194,6 +237,8 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -221,10 +266,14 @@ public class AlarmController extends BaseController { "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } checkEntityId(entityId, Operation.READ); + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); try { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(entityId, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } catch (Exception e) { throw handleException(e); } @@ -244,6 +293,8 @@ public class AlarmController extends BaseController { @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId, @ApiParam(value = PAGE_SIZE_DESCRIPTION, required = true) @RequestParam int pageSize, @ApiParam(value = PAGE_NUMBER_DESCRIPTION, required = true) @@ -267,13 +318,17 @@ public class AlarmController extends BaseController { throw new ThingsboardException("Invalid alarms search query: Both parameters 'searchStatus' " + "and 'status' can't be specified at the same time!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); } + UserId assigneeUserId = null; + if (assigneeId != null) { + assigneeUserId = new UserId(UUID.fromString(assigneeId)); + } TimePageLink pageLink = createTimePageLink(pageSize, page, textSearch, sortProperty, sortOrder, startTime, endTime); try { if (getCurrentUser().isCustomerUser()) { - return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findCustomerAlarms(getCurrentUser().getTenantId(), getCurrentUser().getCustomerId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } else { - return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, fetchOriginator)).get()); + return checkNotNull(alarmService.findAlarms(getCurrentUser().getTenantId(), new AlarmQuery(null, pageLink, alarmSearchStatus, alarmStatus, assigneeUserId, fetchOriginator)).get()); } } catch (Exception e) { throw handleException(e); @@ -295,7 +350,9 @@ public class AlarmController extends BaseController { @ApiParam(value = ALARM_QUERY_SEARCH_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_SEARCH_STATUS_ALLOWABLE_VALUES) @RequestParam(required = false) String searchStatus, @ApiParam(value = ALARM_QUERY_STATUS_DESCRIPTION, allowableValues = ALARM_QUERY_STATUS_ALLOWABLE_VALUES) - @RequestParam(required = false) String status + @RequestParam(required = false) String status, + @ApiParam(value = ALARM_QUERY_ASSIGNEE_DESCRIPTION) + @RequestParam(required = false) String assigneeId ) throws ThingsboardException { checkParameter("EntityId", strEntityId); checkParameter("EntityType", strEntityType); @@ -308,7 +365,7 @@ public class AlarmController extends BaseController { } checkEntityId(entityId, Operation.READ); try { - return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus); + return alarmService.findHighestAlarmSeverity(getCurrentUser().getTenantId(), entityId, alarmSearchStatus, alarmStatus, assigneeId); } catch (Exception e) { throw handleException(e); } diff --git a/application/src/main/java/org/thingsboard/server/controller/AuthController.java b/application/src/main/java/org/thingsboard/server/controller/AuthController.java index c73ac1ae66..113c292380 100644 --- a/application/src/main/java/org/thingsboard/server/controller/AuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/AuthController.java @@ -20,6 +20,7 @@ import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiParam; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -41,11 +42,13 @@ import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.SecuritySettings; import org.thingsboard.server.common.data.security.model.UserPasswordPolicy; +import org.thingsboard.server.common.msg.tools.TbRateLimits; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails; @@ -62,6 +65,8 @@ import org.thingsboard.server.service.security.system.SystemSecurityService; import javax.servlet.http.HttpServletRequest; import java.net.URI; import java.net.URISyntaxException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; @RestController @TbCoreComponent @@ -69,6 +74,10 @@ import java.net.URISyntaxException; @Slf4j @RequiredArgsConstructor public class AuthController extends BaseController { + + @Value("${server.rest.rate_limits.reset_password_per_user:5:3600}") + private String defaultLimitsConfiguration; + private final ConcurrentMap resetPasswordRateLimits = new ConcurrentHashMap<>(); private final BCryptPasswordEncoder passwordEncoder; private final JwtTokenFactory tokenFactory; private final MailService mailService; @@ -211,7 +220,12 @@ public class AuthController extends BaseController { HttpStatus responseStatus; String resetURI = "/login/resetPassword"; UserCredentials userCredentials = userService.findUserCredentialsByResetToken(TenantId.SYS_TENANT_ID, resetToken); + if (userCredentials != null) { + TbRateLimits tbRateLimits = getTbRateLimits(userCredentials.getUserId()); + if (!tbRateLimits.tryConsume()) { + return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build(); + } try { URI location = new URI(resetURI + "?resetToken=" + resetToken); headers.setLocation(location); @@ -323,4 +337,9 @@ public class AuthController extends BaseController { throw handleException(e); } } + + private TbRateLimits getTbRateLimits(UserId userId) { + return resetPasswordRateLimits.computeIfAbsent(userId, + key -> new TbRateLimits(defaultLimitsConfiguration, true)); + } } 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 92536b985a..8505b710d5 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -613,7 +613,6 @@ public abstract class BaseController { throw handleException(e, false); } } - Device checkDeviceId(DeviceId deviceId, Operation operation) throws ThingsboardException { try { validateId(deviceId, "Incorrect deviceId " + deviceId); @@ -725,7 +724,7 @@ public abstract class BaseController { AlarmInfo checkAlarmInfoId(AlarmId alarmId, Operation operation) throws ThingsboardException { try { validateId(alarmId, "Incorrect alarmId " + alarmId); - AlarmInfo alarmInfo = alarmService.findAlarmInfoByIdAsync(getCurrentUser().getTenantId(), alarmId).get(); + AlarmInfo alarmInfo = alarmService.findAlarmInfoById(getCurrentUser().getTenantId(), alarmId); checkNotNull(alarmInfo, "Alarm with id [" + alarmId + "] is not found"); accessControlService.checkPermission(getCurrentUser(), Resource.ALARM, operation, alarmId, alarmInfo); return alarmInfo; diff --git a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java index 768d98e403..53734b8633 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java +++ b/application/src/main/java/org/thingsboard/server/controller/ControllerConstants.java @@ -27,6 +27,7 @@ public class ControllerConstants { protected static final String EDGE_ID = "edgeId"; protected static final String RPC_ID = "rpcId"; protected static final String ENTITY_ID = "entityId"; + protected static final String ASSIGNEE_ID = "assigneeId"; protected static final String PAGE_DATA_PARAMETERS = "You can specify parameters to filter the results. " + "The result is wrapped with PageData object that allows you to iterate over result set using pagination. " + "See the 'Model' tab of the Response Class for more details. "; @@ -44,6 +45,7 @@ public class ControllerConstants { protected static final String USER_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ASSET_ID_PARAM_DESCRIPTION = "A string value representing the asset id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_ID_PARAM_DESCRIPTION = "A string value representing the alarm id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; + protected static final String ASSIGN_ID_PARAM_DESCRIPTION = "A string value representing the user id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ALARM_COMMENT_ID_PARAM_DESCRIPTION = "A string value representing the alarm comment id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; protected static final String ENTITY_ID_PARAM_DESCRIPTION = "A string value representing the entity id. For example, '784f394c-42b6-435a-983c-b7beff2784f9'"; diff --git a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java index 716b7ef60f..1d429e9157 100644 --- a/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java +++ b/application/src/main/java/org/thingsboard/server/controller/EntityQueryController.java @@ -29,6 +29,7 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -38,6 +39,7 @@ import org.thingsboard.server.common.data.query.EntityDataPageLink; import org.thingsboard.server.common.data.query.EntityDataQuery; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.query.EntityQueryService; +import org.thingsboard.server.service.security.permission.Operation; import static org.thingsboard.server.controller.ControllerConstants.ALARM_DATA_QUERY_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.ENTITY_COUNT_QUERY_DESCRIPTION; @@ -61,11 +63,7 @@ public class EntityQueryController extends BaseController { @ApiParam(value = "A JSON value representing the entity count query. See API call notes above for more details.") @RequestBody EntityCountQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); - } + return this.entityQueryService.countEntitiesByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Entity Data by Query", notes = ENTITY_DATA_QUERY_DESCRIPTION) @@ -76,11 +74,7 @@ public class EntityQueryController extends BaseController { @ApiParam(value = "A JSON value representing the entity data query. See API call notes above for more details.") @RequestBody EntityDataQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); - } + return this.entityQueryService.findEntityDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Alarms by Query", notes = ALARM_DATA_QUERY_DESCRIPTION) @@ -91,11 +85,12 @@ public class EntityQueryController extends BaseController { @ApiParam(value = "A JSON value representing the alarm data query. See API call notes above for more details.") @RequestBody AlarmDataQuery query) throws ThingsboardException { checkNotNull(query); - try { - return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); - } catch (Exception e) { - throw handleException(e); + checkNotNull(query.getPageLink()); + UserId assigneeId = query.getPageLink().getAssigneeId(); + if (assigneeId != null) { + checkUserId(assigneeId, Operation.READ); } + return this.entityQueryService.findAlarmDataByQuery(getCurrentUser(), query); } @ApiOperation(value = "Find Entity Keys by Query", @@ -112,15 +107,11 @@ public class EntityQueryController extends BaseController { @RequestParam("attributes") boolean isAttributes) throws ThingsboardException { TenantId tenantId = getTenantId(); checkNotNull(query); - try { - EntityDataPageLink pageLink = query.getPageLink(); - if (pageLink.getPageSize() > MAX_PAGE_SIZE) { - pageLink.setPageSize(MAX_PAGE_SIZE); - } - return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes); - } catch (Exception e) { - throw handleException(e); + EntityDataPageLink pageLink = query.getPageLink(); + if (pageLink.getPageSize() > MAX_PAGE_SIZE) { + pageLink.setPageSize(MAX_PAGE_SIZE); } + return entityQueryService.getKeysByQuery(getCurrentUser(), tenantId, query, isTimeseries, isAttributes); } } diff --git a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java index c689ae3593..f43607b24c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java +++ b/application/src/main/java/org/thingsboard/server/controller/plugin/TbWebSocketHandler.java @@ -67,8 +67,8 @@ import static org.thingsboard.server.service.telemetry.DefaultTelemetryWebSocket @Slf4j public class TbWebSocketHandler extends TextWebSocketHandler implements TelemetryWebSocketMsgEndpoint { - private static final ConcurrentMap internalSessionMap = new ConcurrentHashMap<>(); - private static final ConcurrentMap externalSessionMap = new ConcurrentHashMap<>(); + private final ConcurrentMap internalSessionMap = new ConcurrentHashMap<>(); + private final ConcurrentMap externalSessionMap = new ConcurrentHashMap<>(); @Autowired @@ -82,13 +82,13 @@ public class TbWebSocketHandler extends TextWebSocketHandler implements Telemetr @Value("${server.ws.ping_timeout:30000}") private long pingTimeout; - private ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); - private ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>(); + private final ConcurrentMap blacklistedSessions = new ConcurrentHashMap<>(); + private final ConcurrentMap perSessionUpdateLimits = new ConcurrentHashMap<>(); - private ConcurrentMap> tenantSessionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> customerSessionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> regularUserSessionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> publicUserSessionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> tenantSessionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> customerSessionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> regularUserSessionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> publicUserSessionsMap = new ConcurrentHashMap<>(); @Override public void handleTextMessage(WebSocketSession session, TextMessage message) { diff --git a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java index 67b79136ed..5674517ee1 100644 --- a/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java +++ b/application/src/main/java/org/thingsboard/server/service/action/EntityActionService.java @@ -86,6 +86,12 @@ public class EntityActionService { case ALARM_CLEAR: msgType = DataConstants.ALARM_CLEAR; break; + case ALARM_ASSIGN: + msgType = DataConstants.ALARM_ASSIGN; + break; + case ALARM_UNASSIGN: + msgType = DataConstants.ALARM_UNASSIGN; + break; case ALARM_DELETE: msgType = DataConstants.ALARM_DELETE; break; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java index 551412a756..d85750f7d2 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/alarm/BaseAlarmProcessor.java @@ -48,7 +48,7 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { return Futures.immediateFuture(null); } try { - Alarm existentAlarm = alarmService.findLatestByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType()).get(); + Alarm existentAlarm = alarmService.findLatestActiveByOriginatorAndType(tenantId, originatorId, alarmUpdateMsg.getType()); switch (alarmUpdateMsg.getMsgType()) { case ENTITY_CREATED_RPC_MESSAGE: case ENTITY_UPDATED_RPC_MESSAGE: @@ -62,7 +62,9 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { existentAlarm.setClearTs(alarmUpdateMsg.getClearTs()); existentAlarm.setPropagate(alarmUpdateMsg.getPropagate()); } - existentAlarm.setStatus(AlarmStatus.valueOf(alarmUpdateMsg.getStatus())); + var alarmStatus = AlarmStatus.valueOf(alarmUpdateMsg.getStatus()); + existentAlarm.setCleared(alarmStatus.isCleared()); + existentAlarm.setAcknowledged(alarmStatus.isAck()); existentAlarm.setAckTs(alarmUpdateMsg.getAckTs()); existentAlarm.setEndTs(alarmUpdateMsg.getEndTs()); existentAlarm.setDetails(JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); @@ -70,18 +72,18 @@ public abstract class BaseAlarmProcessor extends BaseEdgeProcessor { break; case ALARM_ACK_RPC_MESSAGE: if (existentAlarm != null) { - alarmService.ackAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); + alarmService.acknowledgeAlarm(tenantId, existentAlarm.getId(), alarmUpdateMsg.getAckTs()); } break; case ALARM_CLEAR_RPC_MESSAGE: if (existentAlarm != null) { alarmService.clearAlarm(tenantId, existentAlarm.getId(), - JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails()), alarmUpdateMsg.getAckTs()); + alarmUpdateMsg.getAckTs(), JacksonUtil.OBJECT_MAPPER.readTree(alarmUpdateMsg.getDetails())); } break; case ENTITY_DELETED_RPC_MESSAGE: if (existentAlarm != null) { - alarmService.deleteAlarm(tenantId, existentAlarm.getId()); + alarmService.delAlarm(tenantId, existentAlarm.getId()); } break; } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java index f12010d5d5..3156ff404b 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java @@ -76,7 +76,7 @@ public abstract class AbstractTbEntityService { protected ListenableFuture removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) { ListenableFuture> alarmsFuture = - alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, false)); + alarmService.findAlarms(tenantId, new AlarmQuery(entityId, new TimePageLink(Integer.MAX_VALUE), null, null, null, false)); ListenableFuture> alarmIdsFuture = Futures.transform(alarmsFuture, page -> page.getData().stream().map(AlarmInfo::getId).collect(Collectors.toList()), dbExecutor); diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java index a6620152ac..03a0671162 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java @@ -308,6 +308,10 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS return EdgeEventActionType.ALARM_ACK; case ALARM_CLEAR: return EdgeEventActionType.ALARM_CLEAR; + case ALARM_ASSIGN: + return EdgeEventActionType.ALARM_ASSIGN; + case ALARM_UNASSIGN: + return EdgeEventActionType.ALARM_UNASSIGN; case DELETED: return EdgeEventActionType.DELETED; case RELATION_ADD_OR_UPDATE: diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java index 46b56dec30..e452a766a4 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmService.java @@ -15,22 +15,25 @@ */ package org.thingsboard.server.service.entitiy.alarm; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.EdgeId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.service.entitiy.AbstractTbEntityService; import java.util.List; @@ -44,9 +47,34 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb ActionType actionType = alarm.getId() == null ? ActionType.ADDED : ActionType.UPDATED; TenantId tenantId = alarm.getTenantId(); try { - Alarm savedAlarm = checkNotNull(alarmSubscriptionService.createOrUpdateAlarm(alarm)); - notificationEntityService.notifyCreateOrUpdateAlarm(savedAlarm, actionType, user); - return savedAlarm; + AlarmApiCallResult result; + if (alarm.getId() == null) { + result = alarmSubscriptionService.createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(alarm, user.getId())); + } else { + result = alarmSubscriptionService.updateAlarm(AlarmUpdateRequest.fromAlarm(alarm, user.getId())); + } + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + actionType = result.isCreated() ? ActionType.ADDED : ActionType.UPDATED; + if (result.isModified()) { + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), actionType, user); + } + AlarmInfo resultAlarm = result.getAlarm(); + if (alarm.isAcknowledged() && !resultAlarm.isAcknowledged()) { + resultAlarm = ack(resultAlarm, alarm.getAckTs(), user); + } + if (alarm.isCleared() && !resultAlarm.isCleared()) { + resultAlarm = clear(resultAlarm, alarm.getClearTs(), user); + } + UserId newAssignee = alarm.getAssigneeId(); + UserId curAssignee = resultAlarm.getAssigneeId(); + if (newAssignee != null && !newAssignee.equals(curAssignee)) { + resultAlarm = assign(alarm, newAssignee, alarm.getAssignTs(), user); + } else if (newAssignee == null && curAssignee != null) { + resultAlarm = unassign(alarm, alarm.getAssignTs(), user); + } + return new Alarm(resultAlarm); } catch (Exception e) { notificationEntityService.logEntityAction(tenantId, emptyId(EntityType.ALARM), alarm, actionType, user, e); throw e; @@ -54,43 +82,110 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb } @Override - public ListenableFuture ack(Alarm alarm, User user) { - long ackTs = System.currentTimeMillis(); - ListenableFuture future = alarmSubscriptionService.ackAlarm(alarm.getTenantId(), alarm.getId(), ackTs); - return Futures.transform(future, result -> { + public AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException { + return ack(alarm, System.currentTimeMillis(), user); + } + + @Override + public AlarmInfo ack(Alarm alarm, long ackTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(ackTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + if (result.isModified()) { AlarmComment alarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was acknowledged by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString())) + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "ACK")) .build(); alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); - alarm.setAckTs(ackTs); - alarm.setStatus(alarm.getStatus().isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK); - notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_ACK, user); - return null; - }, MoreExecutors.directExecutor()); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ACK, user); + } else { + throw new ThingsboardException("Alarm was already acknowledged!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return result.getAlarm(); + } + + @Override + public AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException { + return clear(alarm, System.currentTimeMillis(), user); } @Override - public ListenableFuture clear(Alarm alarm, User user) { - long clearTs = System.currentTimeMillis(); - ListenableFuture future = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), null, clearTs); - return Futures.transform(future, result -> { + public AlarmInfo clear(Alarm alarm, long clearTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.clearAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(clearTs), null); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + if (result.isCleared()) { AlarmComment alarmComment = AlarmComment.builder() .alarmId(alarm.getId()) .type(AlarmCommentType.SYSTEM) .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was cleared by user %s", - (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) - .put("userId", user.getId().toString())) + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "CLEAR")) + .build(); + alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_CLEAR, user); + } else { + throw new ThingsboardException("Alarm was already cleared!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return result.getAlarm(); + } + + @Override + public AlarmInfo assign(Alarm alarm, UserId assigneeId, long assignTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.assignAlarm(alarm.getTenantId(), alarm.getId(), assigneeId, getOrDefault(assignTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + AlarmInfo alarmInfo = result.getAlarm(); + if (result.isModified()) { + AlarmAssignee assignee = alarmInfo.getAssignee(); + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was assigned by user %s to user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName(), + (assignee.getFirstName() == null || assignee.getLastName() == null) ? assignee.getEmail() : assignee.getFirstName() + " " + assignee.getLastName())) + .put("userId", user.getId().toString()) + .put("assigneeId", assignee.getId().toString()) + .put("subtype", "ASSIGN")) + .build(); + alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_ASSIGN, user); + } else { + throw new ThingsboardException("Alarm was already assigned to this user!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmInfo; + } + + @Override + public AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException { + AlarmApiCallResult result = alarmSubscriptionService.unassignAlarm(alarm.getTenantId(), alarm.getId(), getOrDefault(unassignTs)); + if (!result.isSuccessful()) { + throw new ThingsboardException(ThingsboardErrorCode.ITEM_NOT_FOUND); + } + AlarmInfo alarmInfo = result.getAlarm(); + if (result.isModified()) { + AlarmComment alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", String.format("Alarm was unassigned by user %s", + (user.getFirstName() == null || user.getLastName() == null) ? user.getName() : user.getFirstName() + " " + user.getLastName())) + .put("userId", user.getId().toString()) + .put("subtype", "ASSIGN")) .build(); alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment); - alarm.setClearTs(clearTs); - alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - notificationEntityService.notifyCreateOrUpdateAlarm(alarm, ActionType.ALARM_CLEAR, user); - return null; - }, MoreExecutors.directExecutor()); + notificationEntityService.notifyCreateOrUpdateAlarm(result.getAlarm(), ActionType.ALARM_UNASSIGN, user); + } else { + throw new ThingsboardException("Alarm was already unassigned!", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + return alarmInfo; } @Override @@ -101,4 +196,8 @@ public class DefaultTbAlarmService extends AbstractTbEntityService implements Tb relatedEdgeIds, user, JacksonUtil.toString(alarm)); return alarmSubscriptionService.deleteAlarm(tenantId, alarm.getId()); } + + private static long getOrDefault(long ts) { + return ts > 0 ? ts : System.currentTimeMillis(); + } } \ No newline at end of file diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java index f6746e20cc..a2ae9c8cc7 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/alarm/TbAlarmService.java @@ -15,18 +15,27 @@ */ package org.thingsboard.server.service.entitiy.alarm; -import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.id.UserId; public interface TbAlarmService { Alarm save(Alarm entity, User user) throws ThingsboardException; - ListenableFuture ack(Alarm alarm, User user); + AlarmInfo ack(Alarm alarm, User user) throws ThingsboardException; - ListenableFuture clear(Alarm alarm, User user); + AlarmInfo ack(Alarm alarm, long ackTs, User user) throws ThingsboardException; + + AlarmInfo clear(Alarm alarm, User user) throws ThingsboardException; + + AlarmInfo clear(Alarm alarm, long clearTs, User user) throws ThingsboardException; + + AlarmInfo assign(Alarm alarm, UserId assigneeId, long assignTs, User user) throws ThingsboardException; + + AlarmInfo unassign(Alarm alarm, long unassignTs, User user) throws ThingsboardException; Boolean delete(Alarm alarm, User user); } 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 13f8672d67..43d6a4df0a 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 @@ -556,7 +556,7 @@ public class DefaultDataUpdateService implements DataUpdateService { }; private void updateTenantAlarmsCustomer(TenantId tenantId, String name, AtomicLong processed) { - AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, false); + AlarmQuery alarmQuery = new AlarmQuery(null, new TimePageLink(1000), null, null, null, false); PageData alarms = alarmDao.findAlarms(tenantId, alarmQuery); boolean hasNext = true; while (hasNext) { 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 b8d0da0678..b55b965330 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 @@ -26,6 +26,7 @@ import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.actors.ActorSystemContext; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.rpc.RpcError; @@ -35,6 +36,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.rpc.FromDeviceRpcResponse; import org.thingsboard.server.common.stats.StatsFactory; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.service.security.auth.jwt.settings.JwtSettingsService; import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.dao.tenant.TbTenantProfileCache; @@ -502,13 +504,14 @@ public class DefaultTbCoreConsumerService extends AbstractConsumerService userPasswordHistoryMap = JacksonUtil.convertValue(userPasswordHistoryJson, new TypeReference<>() {}); 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 96b6c446d5..74089bac3e 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 @@ -26,6 +26,7 @@ import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -40,6 +41,7 @@ import org.thingsboard.server.common.data.kv.TsKvEntry; import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.gen.transport.TransportProtos.LocalSubscriptionServiceMsgProto; @@ -268,7 +270,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene updateDeviceInactivityTimeout(tenantId, entityId, attributes); } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onUpdate(tenantId, - new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) + new DeviceId(entityId.getId()), DataConstants.SHARED_SCOPE, new ArrayList<>(attributes)) , null); } } @@ -292,7 +294,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } @Override - public void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { + public void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { onLocalAlarmSubUpdate(entityId, s -> { if (TbSubscriptionType.ALARMS.equals(s.getType())) { @@ -301,15 +303,14 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene return null; } }, - s -> alarm.getCreatedTime() >= s.getTs(), - s -> alarm, - false + s -> alarm.getCreatedTime() >= s.getTs() || alarm.getAssignTs() >= s.getTs(), + alarm, false ); callback.onSuccess(); } @Override - public void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback) { + public void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback) { onLocalAlarmSubUpdate(entityId, s -> { if (TbSubscriptionType.ALARMS.equals(s.getType())) { @@ -319,8 +320,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene } }, s -> alarm.getCreatedTime() >= s.getTs(), - s -> alarm, - true + alarm, true ); callback.onSuccess(); } @@ -354,7 +354,7 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene deleteDeviceInactivityTimeout(tenantId, entityId, keys); } else if (TbAttributeSubscriptionScope.SHARED_SCOPE.name().equalsIgnoreCase(scope) && notifyDevice) { clusterService.pushMsgToCore(DeviceAttributesEventNotificationMsg.onDelete(tenantId, - new DeviceId(entityId.getId()), scope, keys), null); + new DeviceId(entityId.getId()), scope, keys), null); } } callback.onSuccess(); @@ -414,19 +414,20 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene private void onLocalAlarmSubUpdate(EntityId entityId, Function castFunction, Predicate filterFunction, - Function processFunction, boolean deleted) { + AlarmInfo alarm, boolean deleted) { Set entitySubscriptions = subscriptionsByEntityId.get(entityId); + if (alarm == null) { + log.warn("[{}] empty alarm update!", entityId); + return; + } if (entitySubscriptions != null) { entitySubscriptions.stream().map(castFunction).filter(Objects::nonNull).filter(filterFunction).forEach(s -> { - Alarm alarm = processFunction.apply(s); - if (alarm != null) { - if (serviceId.equals(s.getServiceId())) { - AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); - localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); - } else { - TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); - toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null); - } + if (serviceId.equals(s.getServiceId())) { + AlarmSubscriptionUpdate update = new AlarmSubscriptionUpdate(s.getSubscriptionId(), alarm, deleted); + localSubscriptionService.onSubscriptionUpdate(s.getSessionId(), update, TbCallback.EMPTY); + } else { + TopicPartitionInfo tpi = notificationsTopicService.getNotificationsTopic(ServiceType.TB_CORE, s.getServiceId()); + toCoreNotificationsProducer.send(tpi, toProto(s, alarm, deleted), null); } }); } else { @@ -557,12 +558,12 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene }); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( - LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) + LocalSubscriptionServiceMsgProto.newBuilder().setSubUpdate(builder.build()).build()) .build(); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } - private TbProtoQueueMsg toProto(TbSubscription subscription, Alarm alarm, boolean deleted) { + private TbProtoQueueMsg toProto(TbSubscription subscription, AlarmInfo alarm, boolean deleted) { TbAlarmSubscriptionUpdateProto.Builder builder = TbAlarmSubscriptionUpdateProto.newBuilder(); builder.setSessionId(subscription.getSessionId()); @@ -571,8 +572,8 @@ public class DefaultSubscriptionManagerService extends TbApplicationEventListene builder.setDeleted(deleted); ToCoreNotificationMsg toCoreMsg = ToCoreNotificationMsg.newBuilder().setToLocalSubscriptionServiceMsg( - LocalSubscriptionServiceMsgProto.newBuilder() - .setAlarmSubUpdate(builder.build()).build()) + LocalSubscriptionServiceMsgProto.newBuilder() + .setAlarmSubUpdate(builder.build()).build()) .build(); return new TbProtoQueueMsg<>(subscription.getEntityId().getId(), toCoreMsg); } 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 a3a2062fa6..410b2a4a1c 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,11 +17,13 @@ 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.alarm.AlarmInfo; 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.common.msg.queue.TbCallback; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.queue.discovery.event.PartitionChangeEvent; import java.util.List; @@ -42,9 +44,9 @@ public interface SubscriptionManagerService extends ApplicationListener keys, TbCallback callback); - void onAlarmUpdate(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + void onAlarmUpdate(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback); - void onAlarmDeleted(TenantId tenantId, EntityId entityId, Alarm alarm, TbCallback callback); + void onAlarmDeleted(TenantId tenantId, EntityId entityId, AlarmInfo alarm, TbCallback callback); } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java index c8cbf19b4c..c90302998a 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbAlarmDataSubCtx.java @@ -20,7 +20,9 @@ import lombok.Setter; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.kv.Aggregation; @@ -63,10 +65,8 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { private final AlarmService alarmService; @Getter - @Setter private final LinkedHashMap entitiesMap; @Getter - @Setter private final HashMap alarmsMap; private final int maxEntitiesPerAlarmSubscription; @@ -225,8 +225,7 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { boolean matchesFilter = filter(alarm); if (onCurrentPage) { if (matchesFilter) { - AlarmData updated = new AlarmData(alarm, current.getOriginatorName(), current.getEntityId()); - updated.getLatest().putAll(current.getLatest()); + AlarmData updated = new AlarmData(subscriptionUpdate.getAlarm(), current); alarmsMap.put(alarmId, updated); sendWsMsg(new AlarmDataUpdate(cmdId, null, Collections.singletonList(updated), maxEntitiesPerAlarmSubscription, data.getTotalElements())); } else { @@ -270,8 +269,24 @@ public class TbAlarmDataSubCtx extends TbAbstractDataSubCtx { if (filter.getStatusList() != null && !filter.getStatusList().isEmpty()) { boolean matches = false; for (AlarmSearchStatus status : filter.getStatusList()) { - if (status.getStatuses().contains(alarm.getStatus())) { - matches = true; + switch (status) { + case ANY: + matches = true; + break; + case ACK: + matches = alarm.isAcknowledged(); + break; + case UNACK: + matches = !alarm.isAcknowledged(); + break; + case CLEARED: + matches = alarm.isCleared(); + break; + case ACTIVE: + matches = !alarm.isCleared(); + break; + } + if (matches) { break; } } diff --git a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java index 4fd934b298..c30ac9f0d5 100644 --- a/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java +++ b/application/src/main/java/org/thingsboard/server/service/subscription/TbSubscriptionUtils.java @@ -17,6 +17,8 @@ package org.thingsboard.server.service.subscription; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; @@ -189,8 +191,8 @@ public class TbSubscriptionUtils { if (proto.getErrorCode() > 0) { return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), SubscriptionErrorCode.forCode(proto.getErrorCode()), proto.getErrorMsg()); } else { - Alarm alarm = JacksonUtil.fromString(proto.getAlarm(), Alarm.class); - return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm); + AlarmInfo alarm = JacksonUtil.fromString(proto.getAlarm(), AlarmInfo.class); + return new AlarmSubscriptionUpdate(proto.getSubscriptionId(), alarm, proto.getDeleted()); } } @@ -316,7 +318,7 @@ public class TbSubscriptionUtils { return entry; } - public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, Alarm alarm) { + public static ToCoreMsg toAlarmUpdateProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) { TbAlarmUpdateProto.Builder builder = TbAlarmUpdateProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); @@ -329,7 +331,7 @@ public class TbSubscriptionUtils { return ToCoreMsg.newBuilder().setToSubscriptionMgrMsg(msgBuilder.build()).build(); } - public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, Alarm alarm) { + public static ToCoreMsg toAlarmDeletedProto(TenantId tenantId, EntityId entityId, AlarmInfo alarm) { TbAlarmDeleteProto.Builder builder = TbAlarmDeleteProto.newBuilder(); builder.setEntityType(entityId.getEntityType().name()); builder.setEntityIdMSB(entityId.getId().getMostSignificantBits()); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java index a7b46deaa9..6eb5f16b27 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultAlarmSubscriptionService.java @@ -29,14 +29,18 @@ import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmComment; import org.thingsboard.server.common.data.alarm.AlarmCommentType; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -44,6 +48,7 @@ import org.thingsboard.server.common.msg.queue.ServiceType; import org.thingsboard.server.common.msg.queue.TbCallback; import org.thingsboard.server.common.msg.queue.TopicPartitionInfo; import org.thingsboard.server.common.stats.TbApiUsageReportClient; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import org.thingsboard.server.dao.alarm.AlarmService; @@ -92,6 +97,41 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return "alarm"; } + @Override + public AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request) { + boolean creationEnabled = apiUsageStateService.getApiUsageState(request.getTenantId()).isAlarmCreationEnabled(); + var result = alarmService.createAlarm(request, creationEnabled); + if (result.isCreated()) { + apiUsageClient.report(request.getTenantId(), null, ApiUsageRecordKey.CREATED_ALARMS_COUNT); + } + return withWsCallback(request, result); + } + + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + return withWsCallback(alarmService.updateAlarm(request)); + } + + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { + return withWsCallback(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs)); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { + return withWsCallback(alarmService.clearAlarm(tenantId, alarmId, clearTs, details)); + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs) { + return withWsCallback(alarmService.assignAlarm(tenantId, alarmId, assigneeId, assignTs)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs) { + return withWsCallback(alarmService.unassignAlarm(tenantId, alarmId, assignTs)); + } + @Override public Alarm createOrUpdateAlarm(Alarm alarm) { AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm, apiUsageStateService.getApiUsageState(alarm.getTenantId()).isAlarmCreationEnabled()); @@ -115,29 +155,29 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService @Override public Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId) { - AlarmOperationResult result = alarmService.deleteAlarm(tenantId, alarmId); + AlarmApiCallResult result = alarmService.delAlarm(tenantId, alarmId); onAlarmDeleted(result); return result.isSuccessful(); } @Override public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { - ListenableFuture result = alarmService.ackAlarm(tenantId, alarmId, ackTs); + ListenableFuture result = Futures.immediateFuture(alarmService.acknowledgeAlarm(tenantId, alarmId, ackTs)); Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); - return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + return Futures.transform(result, AlarmApiCallResult::isSuccessful, wsCallBackExecutor); } @Override public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { - ListenableFuture result = clearAlarmForResult(tenantId, alarmId, details, clearTs); - return Futures.transform(result, AlarmOperationResult::isSuccessful, wsCallBackExecutor); + AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details); + return Futures.transform(Futures.immediateFuture(result), AlarmApiCallResult::isSuccessful, wsCallBackExecutor); } @Override public ListenableFuture clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs) { - ListenableFuture result = alarmService.clearAlarm(tenantId, alarmId, details, clearTs); - Futures.addCallback(result, new AlarmUpdateCallback(), wsCallBackExecutor); - return result; + AlarmApiCallResult result = alarmService.clearAlarm(tenantId, alarmId, clearTs, details); + Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor); + return Futures.immediateFuture(new AlarmOperationResult(result)); } @Override @@ -151,8 +191,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } @Override - public ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { - return alarmService.findAlarmInfoByIdAsync(tenantId, alarmId); + public AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId) { + return alarmService.findAlarmInfoById(tenantId, alarmId); } @Override @@ -166,8 +206,8 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } @Override - public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus) { - return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus); + public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId) { + return alarmService.findHighestAlarmSeverity(tenantId, entityId, alarmSearchStatus, alarmStatus, assigneeId); } @Override @@ -175,15 +215,41 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService return alarmService.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); } + @Override + public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + return alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, type); + } + @Override public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { return alarmService.findLatestByOriginatorAndType(tenantId, originator, type); } + @Deprecated private void onAlarmUpdated(AlarmOperationResult result) { wsCallBackExecutor.submit(() -> { Alarm alarm = result.getAlarm(); - TenantId tenantId = result.getAlarm().getTenantId(); + TenantId tenantId = alarm.getTenantId(); + for (EntityId entityId : result.getPropagatedEntitiesList()) { + TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); + if (currentPartitions.contains(tpi)) { + if (subscriptionManagerService.isPresent()) { + subscriptionManagerService.get().onAlarmUpdate(tenantId, entityId, new AlarmInfo(alarm), TbCallback.EMPTY); + } else { + log.warn("Possible misconfiguration because subscriptionManagerService is null!"); + } + } else { + TransportProtos.ToCoreMsg toCoreMsg = TbSubscriptionUtils.toAlarmUpdateProto(tenantId, entityId, new AlarmInfo(alarm)); + clusterService.pushMsgToCore(tpi, entityId.getId(), toCoreMsg, null); + } + } + }); + } + + private void onAlarmUpdated(AlarmApiCallResult result) { + wsCallBackExecutor.submit(() -> { + AlarmInfo alarm = result.getAlarm(); + TenantId tenantId = alarm.getTenantId(); for (EntityId entityId : result.getPropagatedEntitiesList()) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); if (currentPartitions.contains(tpi)) { @@ -200,10 +266,10 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService }); } - private void onAlarmDeleted(AlarmOperationResult result) { + private void onAlarmDeleted(AlarmApiCallResult result) { wsCallBackExecutor.submit(() -> { - Alarm alarm = result.getAlarm(); - TenantId tenantId = result.getAlarm().getTenantId(); + AlarmInfo alarm = result.getAlarm(); + TenantId tenantId = alarm.getTenantId(); for (EntityId entityId : result.getPropagatedEntitiesList()) { TopicPartitionInfo tpi = partitionService.resolve(ServiceType.TB_CORE, tenantId, entityId); if (currentPartitions.contains(tpi)) { @@ -220,9 +286,9 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService }); } - private class AlarmUpdateCallback implements FutureCallback { + private class AlarmUpdateCallback implements FutureCallback { @Override - public void onSuccess(@Nullable AlarmOperationResult result) { + public void onSuccess(@Nullable AlarmApiCallResult result) { onAlarmUpdated(result); } @@ -232,4 +298,27 @@ public class DefaultAlarmSubscriptionService extends AbstractSubscriptionService } } + private AlarmApiCallResult withWsCallback(AlarmApiCallResult result) { + return withWsCallback(null, result); + } + + private AlarmApiCallResult withWsCallback(AlarmModificationRequest request, AlarmApiCallResult result) { + if (result.isSuccessful() && result.isModified()) { + Futures.addCallback(Futures.immediateFuture(result), new AlarmUpdateCallback(), wsCallBackExecutor); + if (result.isSeverityChanged()) { + AlarmInfo alarm = result.getAlarm(); + AlarmComment.AlarmCommentBuilder alarmComment = AlarmComment.builder() + .alarmId(alarm.getId()) + .type(AlarmCommentType.SYSTEM) + .comment(JacksonUtil.newObjectNode().put("text", + String.format("Alarm severity was updated from %s to %s", result.getOldSeverity(), alarm.getSeverity()))); + if (request != null && request.getUserId() != null) { + alarmComment.userId(request.getUserId()); + } + alarmCommentService.createOrUpdateAlarmComment(alarm.getTenantId(), alarmComment.build()); + } + } + return result; + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java index a2a0d0fd8e..ffde6192ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/DefaultTelemetryWebSocketService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.telemetry; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.base.Function; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -27,6 +26,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.web.socket.CloseStatus; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.DataConstants; @@ -95,6 +95,8 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -112,7 +114,6 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi private static final Aggregation DEFAULT_AGGREGATION = Aggregation.NONE; private static final int UNKNOWN_SUBSCRIPTION_ID = 0; private static final String PROCESSING_MSG = "[{}] Processing: {}"; - private static final ObjectMapper jsonMapper = new ObjectMapper(); private static final String FAILED_TO_FETCH_DATA = "Failed to fetch data!"; private static final String FAILED_TO_FETCH_ATTRIBUTES = "Failed to fetch attributes!"; private static final String SESSION_META_DATA_NOT_FOUND = "Session meta-data not found!"; @@ -147,10 +148,10 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @Value("${server.ws.ping_timeout:30000}") private long pingTimeout; - private ConcurrentMap> tenantSubscriptionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> customerSubscriptionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> regularUserSubscriptionsMap = new ConcurrentHashMap<>(); - private ConcurrentMap> publicUserSubscriptionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> tenantSubscriptionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> customerSubscriptionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> regularUserSubscriptionsMap = new ConcurrentHashMap<>(); + private final ConcurrentMap> publicUserSubscriptionsMap = new ConcurrentHashMap<>(); private ExecutorService executor; private String serviceId; @@ -204,7 +205,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi } try { - TelemetryPluginCmdsWrapper cmdsWrapper = jsonMapper.readValue(msg, TelemetryPluginCmdsWrapper.class); + TelemetryPluginCmdsWrapper cmdsWrapper = JacksonUtil.OBJECT_MAPPER.readValue(msg, TelemetryPluginCmdsWrapper.class); if (cmdsWrapper != null) { if (cmdsWrapper.getAttrSubCmds() != null) { cmdsWrapper.getAttrSubCmds().forEach(cmd -> { @@ -450,7 +451,6 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @Override public void onSuccess(List data) { List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); Map subState = new HashMap<>(keys.size()); keys.forEach(key -> subState.put(key, 0L)); @@ -458,6 +458,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope()); + Lock subLock = new ReentrantLock(); TbAttributeSubscription sub = TbAttributeSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) @@ -467,9 +468,24 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi .allKeys(false) .keyStates(subState) .scope(scope) - .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) + .updateConsumer((sessionId, update) -> { + subLock.lock(); + try { + sendWsMsg(sessionId, update); + } finally { + subLock.unlock(); + } + }) .build(); - oldSubService.addSubscription(sub); + + subLock.lock(); + try{ + oldSubService.addSubscription(sub); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); + } finally { + subLock.unlock(); + } + } @Override @@ -550,13 +566,13 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi @Override public void onSuccess(List data) { List attributesData = data.stream().map(d -> new BasicTsKvEntry(d.getLastUpdateTs(), d)).collect(Collectors.toList()); - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); Map subState = new HashMap<>(attributesData.size()); attributesData.forEach(v -> subState.put(v.getKey(), v.getTs())); TbAttributeSubscriptionScope scope = StringUtils.isEmpty(cmd.getScope()) ? TbAttributeSubscriptionScope.ANY_SCOPE : TbAttributeSubscriptionScope.valueOf(cmd.getScope()); + Lock subLock = new ReentrantLock(); TbAttributeSubscription sub = TbAttributeSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) @@ -565,9 +581,24 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi .entityId(entityId) .allKeys(true) .keyStates(subState) - .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) - .scope(scope).build(); - oldSubService.addSubscription(sub); + .updateConsumer((sessionId, update) -> { + subLock.lock(); + try { + sendWsMsg(sessionId, update); + } finally { + subLock.unlock(); + } + }) + .scope(scope) + .build(); + + subLock.lock(); + try { + oldSubService.addSubscription(sub); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), attributesData)); + } finally { + subLock.unlock(); + } } @Override @@ -636,20 +667,34 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi FutureCallback> callback = new FutureCallback>() { @Override public void onSuccess(List data) { - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); Map subState = new HashMap<>(data.size()); data.forEach(v -> subState.put(v.getKey(), v.getTs())); + Lock subLock = new ReentrantLock(); TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) .subscriptionId(cmd.getCmdId()) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) - .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) + .updateConsumer((sessionId, update) -> { + subLock.lock(); + try { + sendWsMsg(sessionId, update); + } finally { + subLock.unlock(); + } + }) .allKeys(true) .keyStates(subState).build(); - oldSubService.addSubscription(sub); + + subLock.lock(); + try { + oldSubService.addSubscription(sub); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); + } finally { + subLock.unlock(); + } } @Override @@ -673,21 +718,35 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi return new FutureCallback<>() { @Override public void onSuccess(List data) { - sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); Map subState = new HashMap<>(keys.size()); keys.forEach(key -> subState.put(key, startTs)); data.forEach(v -> subState.put(v.getKey(), v.getTs())); + Lock subLock = new ReentrantLock(); TbTimeseriesSubscription sub = TbTimeseriesSubscription.builder() .serviceId(serviceId) .sessionId(sessionId) .subscriptionId(cmd.getCmdId()) .tenantId(sessionRef.getSecurityCtx().getTenantId()) .entityId(entityId) - .updateConsumer(DefaultTelemetryWebSocketService.this::sendWsMsg) + .updateConsumer((sessionId, update) -> { + subLock.lock(); + try { + sendWsMsg(sessionId, update); + } finally { + subLock.unlock(); + } + }) .allKeys(false) .keyStates(subState).build(); - oldSubService.addSubscription(sub); + + subLock.lock(); + try{ + oldSubService.addSubscription(sub); + sendWsMsg(sessionRef, new TelemetrySubscriptionUpdate(cmd.getCmdId(), data)); + } finally { + subLock.unlock(); + } } @Override @@ -793,7 +852,7 @@ public class DefaultTelemetryWebSocketService implements TelemetryWebSocketServi private void sendWsMsg(TelemetryWebSocketSessionRef sessionRef, int cmdId, Object update) { try { - String msg = jsonMapper.writeValueAsString(update); + String msg = JacksonUtil.OBJECT_MAPPER.writeValueAsString(update); executor.submit(() -> { try { msgEndpoint.send(sessionRef, cmdId, msg); diff --git a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java index ca3876464e..4961f901c5 100644 --- a/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java +++ b/application/src/main/java/org/thingsboard/server/service/telemetry/sub/AlarmSubscriptionUpdate.java @@ -17,15 +17,8 @@ package org.thingsboard.server.service.telemetry.sub; import lombok.Getter; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.kv.TsKvEntry; -import org.thingsboard.server.common.data.query.AlarmData; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.stream.Collectors; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; +import org.thingsboard.server.common.data.alarm.AlarmInfo; public class AlarmSubscriptionUpdate { @@ -36,15 +29,15 @@ public class AlarmSubscriptionUpdate { @Getter private String errorMsg; @Getter - private Alarm alarm; + private AlarmInfo alarm; @Getter private boolean alarmDeleted; - public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm) { + public AlarmSubscriptionUpdate(int subscriptionId, AlarmInfo alarm) { this(subscriptionId, alarm, false); } - public AlarmSubscriptionUpdate(int subscriptionId, Alarm alarm, boolean alarmDeleted) { + public AlarmSubscriptionUpdate(int subscriptionId, AlarmInfo alarm, boolean alarmDeleted) { super(); this.subscriptionId = subscriptionId; this.alarm = alarm; @@ -64,7 +57,7 @@ public class AlarmSubscriptionUpdate { @Override public String toString() { - return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + ", alarm=" - + alarm + "]"; + return "AlarmUpdate [subscriptionId=" + subscriptionId + ", errorCode=" + errorCode + ", errorMsg=" + errorMsg + + ", alarm=" + alarm + "]"; } -} +} \ No newline at end of file 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 7b1ce89ccd..0c0ff3e0c9 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 @@ -65,7 +65,6 @@ 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.queue.util.DataDecodingEncodingService; import org.thingsboard.server.dao.device.DeviceCredentialsService; import org.thingsboard.server.dao.device.DeviceProvisionService; import org.thingsboard.server.dao.device.DeviceService; @@ -95,6 +94,7 @@ import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceLwM2MC import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceTokenRequestMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; import org.thingsboard.server.queue.common.TbProtoQueueMsg; +import org.thingsboard.server.queue.util.DataDecodingEncodingService; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.apiusage.TbApiUsageStateService; import org.thingsboard.server.service.executors.DbCallbackExecutorService; @@ -290,6 +290,7 @@ public class DefaultTransportApiService implements TransportApiService { device.setType(requestMsg.getDeviceType()); device.setCustomerId(gateway.getCustomerId()); DeviceProfile deviceProfile = deviceProfileCache.findOrCreateDeviceProfile(gateway.getTenantId(), requestMsg.getDeviceType()); + device.setDeviceProfileId(deviceProfile.getId()); ObjectNode additionalInfo = JacksonUtil.newObjectNode(); additionalInfo.put(DataConstants.LAST_CONNECTED_GATEWAY, gatewayId.toString()); diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java index 6ea3208208..ea059fcb2d 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/AlarmsCleanUpService.java @@ -21,6 +21,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.TenantId; diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 61bef18bcd..10c4c9edac 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -73,6 +73,8 @@ server: min_timeout: "${MIN_SERVER_SIDE_RPC_TIMEOUT:5000}" # Default value of the server side RPC timeout. default_timeout: "${DEFAULT_SERVER_SIDE_RPC_TIMEOUT:10000}" + rate_limits: + reset_password_per_user: "${RESET_PASSWORD_PER_USER_RATE_LIMIT_CONFIGURATION:5:3600}" # Application info app: @@ -1210,3 +1212,4 @@ management: exposure: # Expose metrics endpoint (use value 'prometheus' to enable prometheus metrics). include: '${METRICS_ENDPOINTS_EXPOSE:info}' + diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java index 06cee7b71d..32d1f65765 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractControllerTest.java @@ -52,7 +52,7 @@ public abstract class AbstractControllerTest extends AbstractNotifyEntityTest { @LocalServerPort protected int wsPort; - private TbTestWebSocketClient wsClient; // lazy + private volatile TbTestWebSocketClient wsClient; // lazy public TbTestWebSocketClient getWsClient() { if (wsClient == null) { diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java index 6c42c7604d..b3025a2d64 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractNotifyEntityTest.java @@ -18,7 +18,10 @@ package org.thingsboard.server.controller; import lombok.extern.slf4j.Slf4j; import org.mockito.ArgumentMatcher; import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; +import org.thingsboard.server.actors.service.ActorService; +import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.HasName; @@ -38,6 +41,7 @@ import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.ToDeviceActorNotificationMsg; import org.thingsboard.server.dao.audit.AuditLogService; import org.thingsboard.server.dao.model.ModelConstants; +import org.thingsboard.server.service.session.DeviceSessionCacheService; import java.util.ArrayList; import java.util.List; 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 aa4febc303..c09f259aaa 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -26,6 +26,7 @@ import io.jsonwebtoken.Header; import io.jsonwebtoken.Jwt; import io.jsonwebtoken.Jwts; import lombok.extern.slf4j.Slf4j; +import org.awaitility.Awaitility; import org.hamcrest.Matcher; import org.hibernate.exception.ConstraintViolationException; import org.junit.After; @@ -49,6 +50,7 @@ import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.mock.http.MockHttpOutputMessage; +import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultActions; @@ -57,9 +59,15 @@ import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilde import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.context.WebApplicationContext; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.MailService; -import org.thingsboard.rule.engine.api.util.TbNodeUtils; +import org.thingsboard.server.actors.DefaultTbActorSystem; +import org.thingsboard.server.actors.TbActorId; +import org.thingsboard.server.actors.TbActorMailbox; +import org.thingsboard.server.actors.TbEntityActorId; +import org.thingsboard.server.actors.device.DeviceActor; +import org.thingsboard.server.actors.device.DeviceActorMessageProcessor; +import org.thingsboard.server.actors.device.SessionInfo; +import org.thingsboard.server.actors.service.DefaultActorService; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.DeviceProfileType; @@ -79,6 +87,7 @@ import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeCon import org.thingsboard.server.common.data.edge.Edge; 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.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; @@ -89,9 +98,11 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.config.ThingsboardSecurityConfiguration; import org.thingsboard.server.dao.Dao; import org.thingsboard.server.dao.tenant.TenantProfileService; +import org.thingsboard.server.dao.timeseries.TimeseriesService; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRequest; import org.thingsboard.server.service.security.auth.rest.LoginRequest; @@ -103,6 +114,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -167,6 +182,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected TenantId differentTenantId; protected CustomerId differentCustomerId; protected UserId customerUserId; + protected UserId differentCustomerUserId; @SuppressWarnings("rawtypes") private HttpMessageConverter mappingJackson2HttpMessageConverter; @@ -180,6 +196,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @Autowired private TenantProfileService tenantProfileService; + @Autowired + public TimeseriesService tsService; + + @Autowired + protected DefaultActorService actorService; + @SpyBean protected MailService mailService; @@ -211,17 +233,6 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { this.mappingJackson2HttpMessageConverter); } - @BeforeClass - public static void beforeWebTestClass() throws Exception { - - } - - @AfterClass - public static void afterWebTestClass() throws Exception { - Mockito.clearAllCaches(); - } - - @Before public void setupWebTest() throws Exception { log.debug("Executing web test setup"); @@ -374,7 +385,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { differentCustomerUser.setCustomerId(savedDifferentCustomer.getId()); differentCustomerUser.setEmail(DIFFERENT_CUSTOMER_USER_EMAIL); - createUserAndLogin(differentCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD); + differentCustomerUser = createUserAndLogin(differentCustomerUser, DIFFERENT_CUSTOMER_USER_PASSWORD); + differentCustomerUserId = differentCustomerUser.getId(); } } @@ -841,4 +853,37 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return (T) field.get(target); } + protected int getDeviceActorSubscriptionCount(DeviceId deviceId, FeatureType featureType) { + DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId); + Map subscriptions = (Map) ReflectionTestUtils.getField(processor, getMapName(featureType)); + return subscriptions.size(); + } + + protected void awaitForDeviceActorToReceiveSubscription(DeviceId deviceId, FeatureType featureType, int subscriptionCount) { + DeviceActorMessageProcessor processor = getDeviceActorProcessor(deviceId); + Map subscriptions = (Map) ReflectionTestUtils.getField(processor, getMapName(featureType)); + Awaitility.await("Device actor received subscription command from the transport").atMost(TIMEOUT, TimeUnit.SECONDS).until(() -> subscriptions.size() == subscriptionCount); + } + + protected static String getMapName(FeatureType featureType) { + switch (featureType) { + case ATTRIBUTES: + return "attributeSubscriptions"; + case RPC: + return "rpcSubscriptions"; + default: + throw new RuntimeException("Not supported feature " + featureType + "!"); + } + } + + protected DeviceActorMessageProcessor getDeviceActorProcessor(DeviceId deviceId) { + DefaultTbActorSystem actorSystem = (DefaultTbActorSystem) ReflectionTestUtils.getField(actorService, "system"); + ConcurrentMap actors = (ConcurrentMap) ReflectionTestUtils.getField(actorSystem, "actors"); + Awaitility.await("Device actor was created").atMost(TIMEOUT, TimeUnit.SECONDS) + .until(() -> actors.containsKey(new TbEntityActorId(deviceId))); + TbActorMailbox actorMailbox = actors.get(new TbEntityActorId(deviceId)); + DeviceActor actor = (DeviceActor) ReflectionTestUtils.getField(actorMailbox, "actor"); + return (DeviceActorMessageProcessor) ReflectionTestUtils.getField(actor, "processor"); + } + } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java index 7c8b3c4db8..8566d9d6a4 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmCommentControllerTest.java @@ -79,7 +79,6 @@ public abstract class BaseAlarmCommentControllerTest extends AbstractControllerT .tenantId(tenantId) .customerId(customerId) .originator(customerDevice.getId()) - .status(AlarmStatus.ACTIVE_UNACK) .severity(AlarmSeverity.CRITICAL) .type("test alarm type") .build(); @@ -316,7 +315,6 @@ public abstract class BaseAlarmCommentControllerTest extends AbstractControllerT Alarm alarm = Alarm.builder() .originator(device.getId()) - .status(AlarmStatus.ACTIVE_UNACK) .severity(AlarmSeverity.CRITICAL) .type("Test") .build(); diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java index 175f833c1c..6a94cc5a1e 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseAlarmControllerTest.java @@ -29,6 +29,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Primary; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.ResultActions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.EntityType; @@ -124,7 +125,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertNotNull(updatedAlarm); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); - testNotifyEntityAllOneTime(updatedAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(), + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.UPDATED); } @@ -140,8 +142,57 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Assert.assertNotNull(updatedAlarm); Assert.assertEquals(AlarmSeverity.MAJOR, updatedAlarm.getSeverity()); - testNotifyEntityAllOneTime(updatedAlarm, updatedAlarm.getId(), updatedAlarm.getOriginator(), + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.UPDATED); + + alarm = updatedAlarm; + alarm.setAcknowledged(true); + alarm.setAckTs(System.currentTimeMillis() - 1000); + updatedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(updatedAlarm); + Assert.assertTrue(updatedAlarm.isAcknowledged()); + Assert.assertEquals(alarm.getAckTs(), updatedAlarm.getAckTs()); + + foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ACK); + + alarm = updatedAlarm; + alarm.setCleared(true); + alarm.setClearTs(System.currentTimeMillis() - 1000); + updatedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(updatedAlarm); + Assert.assertTrue(updatedAlarm.isCleared()); + Assert.assertEquals(alarm.getClearTs(), updatedAlarm.getClearTs()); + + foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_CLEAR); + + alarm = updatedAlarm; + alarm.setAssigneeId(tenantAdminUserId); + alarm.setAssignTs(System.currentTimeMillis() - 1000); + updatedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(updatedAlarm); + Assert.assertEquals(tenantAdminUserId, updatedAlarm.getAssigneeId()); + Assert.assertEquals(alarm.getAssignTs(), updatedAlarm.getAssignTs()); + + foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + alarm = updatedAlarm; + alarm.setAssigneeId(null); + alarm.setAssignTs(System.currentTimeMillis() - 1000); + updatedAlarm = doPost("/api/alarm", alarm, Alarm.class); + Assert.assertNotNull(updatedAlarm); + Assert.assertNull(updatedAlarm.getAssigneeId()); + Assert.assertEquals(alarm.getAssignTs(), updatedAlarm.getAssignTs()); + + foundAlarm = doGet("/api/alarm/info/" + updatedAlarm.getId(), AlarmInfo.class); + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); } @Test @@ -187,7 +238,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(alarm, alarm.getId(), alarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.DELETED); } @@ -200,7 +251,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { doDelete("/api/alarm/" + alarm.getId()).andExpect(status().isOk()); - testNotifyEntityOneTimeMsgToEdgeServiceNever(alarm, alarm.getId(), alarm.getOriginator(), + testNotifyEntityOneTimeMsgToEdgeServiceNever(new Alarm(alarm), alarm.getId(), alarm.getOriginator(), tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.DELETED); } @@ -245,7 +296,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk()); - Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus()); @@ -261,7 +312,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Mockito.reset(tbClusterService, auditLogService); doPost("/api/alarm/" + alarm.getId() + "/clear").andExpect(status().isOk()); - Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.CLEARED_UNACK, foundAlarm.getStatus()); @@ -278,7 +329,7 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { doPost("/api/alarm/" + alarm.getId() + "/ack").andExpect(status().isOk()); - Alarm foundAlarm = doGet("/api/alarm/" + alarm.getId(), Alarm.class); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); Assert.assertNotNull(foundAlarm); Assert.assertEquals(AlarmStatus.ACTIVE_ACK, foundAlarm.getStatus()); @@ -348,6 +399,135 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { .andExpect(statusReason(containsString(msgErrorPermission))); } + @Test + public void testAssignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + } + + @Test + public void testAssignAlarmViaDifferentTenant() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + + loginDifferentTenant(); + + Mockito.reset(tbClusterService, auditLogService); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isForbidden()); + } + + @Test + public void testReassignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + logout(); + + loginCustomerUser(); + Mockito.reset(tbClusterService, auditLogService); + beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + customerUserId.getId()).andExpect(status().isOk()); + + foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(customerUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_ASSIGN); + } + + @Test + public void testUnassignAlarm() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); + doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk()); + foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_UNASSIGN); + } + + @Test + public void testUnassignTenantAlarmViaCustomer() throws Exception { + loginTenantAdmin(); + Alarm alarm = createAlarm(TEST_ALARM_TYPE); + Mockito.reset(tbClusterService, auditLogService); + long beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); + + doPost("/api/alarm/" + alarm.getId() + "/assign/" + tenantAdminUserId.getId()).andExpect(status().isOk()); + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(tenantAdminUserId, foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, tenantAdminUserId, TENANT_ADMIN_EMAIL, ActionType.ALARM_ASSIGN); + + logout(); + loginCustomerUser(); + + Mockito.reset(tbClusterService, auditLogService); + beforeAssignmentTs = System.currentTimeMillis(); + Thread.sleep(2); + + doDelete("/api/alarm/" + alarm.getId() + "/assign").andExpect(status().isOk()); + foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertNull(foundAlarm.getAssigneeId()); + Assert.assertTrue(foundAlarm.getAssignTs() > beforeAssignmentTs && foundAlarm.getAssignTs() < System.currentTimeMillis()); + + testNotifyEntityAllOneTime(foundAlarm, foundAlarm.getId(), foundAlarm.getOriginator(), + tenantId, customerId, customerUserId, CUSTOMER_USER_EMAIL, ActionType.ALARM_UNASSIGN); + } + @Test public void testFindAlarmsViaCustomerUser() throws Exception { loginCustomerUser(); @@ -364,7 +544,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { var response = doGetTyped( "/api/alarm/" + EntityType.DEVICE + "/" + customerDevice.getUuidId() + "?page=0&pageSize=" + size, - new TypeReference>() {} + new TypeReference>() { + } ); var foundAlarmInfos = response.getData(); Assert.assertNotNull("Found pageData is null", foundAlarmInfos); @@ -412,7 +593,6 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { Alarm alarm = Alarm.builder() .originator(device.getId()) - .status(AlarmStatus.ACTIVE_UNACK) .severity(AlarmSeverity.CRITICAL) .type("Test") .build(); @@ -431,7 +611,8 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { this.token = tokens.get("token").asText(); PageData pageData = doGetTyped( - "/api/alarm/DEVICE/" + device.getUuidId() + "?page=0&pageSize=1", new TypeReference>() {} + "/api/alarm/DEVICE/" + device.getUuidId() + "?page=0&pageSize=1", new TypeReference>() { + } ); Assert.assertNotNull("Found pageData is null", pageData); @@ -457,12 +638,11 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { testEntityDaoWithRelationsTransactionalException(alarmDao, customerDevice.getId(), alarmId, "/api/alarm/" + alarmId); } - private Alarm createAlarm(String type) throws Exception { + private AlarmInfo createAlarm(String type) throws Exception { Alarm alarm = Alarm.builder() .tenantId(tenantId) .customerId(customerId) .originator(customerDevice.getId()) - .status(AlarmStatus.ACTIVE_UNACK) .severity(AlarmSeverity.CRITICAL) .type(type) .build(); @@ -470,6 +650,10 @@ public abstract class BaseAlarmControllerTest extends AbstractControllerTest { alarm = doPost("/api/alarm", alarm, Alarm.class); Assert.assertNotNull(alarm); - return alarm; + AlarmInfo foundAlarm = doGet("/api/alarm/info/" + alarm.getId(), AlarmInfo.class); + Assert.assertNotNull(foundAlarm); + Assert.assertEquals(alarm, new Alarm(foundAlarm)); + + return foundAlarm; } } diff --git a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java index 09fb5c505d..ce354749f3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/BaseWebsocketApiTest.java @@ -548,7 +548,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { SingleEntityFilter entityFilter = new SingleEntityFilter(); entityFilter.setSingleEntity(tenantId); - assertThatNoException().isThrownBy(() -> { + assertThatNoException().as("subscribeForAttributes").isThrownBy(() -> { JsonNode update = getWsClient().subscribeForAttributes(tenantId, TbAttributeSubscriptionScope.SERVER_SCOPE.name(), List.of("attr")); assertThat(update.get("errorMsg").isNull()).isTrue(); assertThat(update.get("errorCode").asInt()).isEqualTo(SubscriptionErrorCode.NO_ERROR.getCode()); @@ -560,7 +560,7 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { new BaseAttributeKvEntry(System.currentTimeMillis(), new StringDataEntry("attr", expectedAttrValue)) )); JsonNode update = JacksonUtil.toJsonNode(getWsClient().waitForUpdate()); - assertThat(update).isNotNull(); + assertThat(update).as("waitForUpdate").isNotNull(); assertThat(update.get("data").get("attr").get(0).get(1).asText()).isEqualTo(expectedAttrValue); } @@ -569,15 +569,17 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { tsService.saveAndNotify(device.getTenantId(), null, device.getId(), tsData, 0, new FutureCallback() { @Override public void onSuccess(@Nullable Void result) { + log.debug("sendTelemetry callback onSuccess"); latch.countDown(); } @Override public void onFailure(Throwable t) { + log.error("Failed to send telemetry", t); latch.countDown(); } }); - latch.await(3, TimeUnit.SECONDS); + assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS)).as("await sendTelemetry callback"); } private void sendAttributes(Device device, TbAttributeSubscriptionScope scope, List attrData) throws InterruptedException { @@ -589,14 +591,16 @@ public abstract class BaseWebsocketApiTest extends AbstractControllerTest { tsService.saveAndNotify(tenantId, entityId, scope.name(), attrData, new FutureCallback() { @Override public void onSuccess(@Nullable Void result) { + log.debug("sendAttributes callback onSuccess"); latch.countDown(); } @Override public void onFailure(Throwable t) { + log.error("Failed to sendAttributes", t); latch.countDown(); } }); - latch.await(3, TimeUnit.SECONDS); + assertThat(latch.await(TIMEOUT, TimeUnit.SECONDS)).as("await sendAttributes callback").isTrue(); } } diff --git a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java index db26e9f6df..1f9ac4ea69 100644 --- a/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java +++ b/application/src/test/java/org/thingsboard/server/controller/TbTestWebSocketClient.java @@ -47,6 +47,7 @@ import java.util.concurrent.TimeUnit; @Slf4j public class TbTestWebSocketClient extends WebSocketClient { + private static final long TIMEOUT = TimeUnit.SECONDS.toMillis(30); private volatile String lastMsg; private volatile CountDownLatch reply; private volatile CountDownLatch update; @@ -87,12 +88,14 @@ public class TbTestWebSocketClient extends WebSocketClient { } public void registerWaitForUpdate(int count) { + log.debug("registerWaitForUpdate [{}]", count); lastMsg = null; update = new CountDownLatch(count); } @Override public void send(String text) throws NotYetConnectedException { + log.debug("send [{}]", text); reply = new CountDownLatch(1); super.send(text); } @@ -110,21 +113,31 @@ public class TbTestWebSocketClient extends WebSocketClient { } public String waitForUpdate() { - return waitForUpdate(TimeUnit.SECONDS.toMillis(3)); + return waitForUpdate(TIMEOUT); } public String waitForUpdate(long ms) { + log.debug("waitForUpdate [{}]", ms); try { - update.await(ms, TimeUnit.MILLISECONDS); + if (!update.await(ms, TimeUnit.MILLISECONDS)) { + log.warn("Failed to await update (waiting time [{}]ms elapsed)", ms, new RuntimeException("stacktrace")); + } } catch (InterruptedException e) { - log.warn("Failed to await reply", e); + log.warn("Failed to await update", e); } return lastMsg; } public String waitForReply() { + return waitForReply(TIMEOUT); + } + + public String waitForReply(long ms) { + log.debug("waitForReply [{}]", ms); try { - reply.await(3, TimeUnit.SECONDS); + if (!reply.await(ms, TimeUnit.MILLISECONDS)) { + log.warn("Failed to await reply (waiting time [{}]ms elapsed)", ms, new RuntimeException("stacktrace")); + } } catch (InterruptedException e) { log.warn("Failed to await reply", e); } diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java index 5014280b07..1cf2cd44bb 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseAlarmEdgeTest.java @@ -76,7 +76,6 @@ abstract public class BaseAlarmEdgeTest extends AbstractEdgeTest { Device device = findDeviceByName("Edge Device 1"); Alarm alarm = new Alarm(); alarm.setOriginator(device.getId()); - alarm.setStatus(AlarmStatus.ACTIVE_UNACK); alarm.setType("alarm"); alarm.setSeverity(AlarmSeverity.CRITICAL); edgeImitator.expectMessageAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java index b266465bd1..98508e72ce 100644 --- a/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/BaseDeviceEdgeTest.java @@ -49,6 +49,7 @@ import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.DeviceCredentials; import org.thingsboard.server.common.data.security.DeviceCredentialsType; import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import org.thingsboard.server.gen.edge.v1.AttributesRequestMsg; import org.thingsboard.server.gen.edge.v1.DeviceCredentialsRequestMsg; @@ -668,6 +669,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { client.connectAndWait(deviceCredentials.getCredentialsId()); MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); + client.subscribeAndWait("v1/devices/me/attributes", MqttQoS.AT_MOST_ONCE); edgeImitator.expectResponsesAmount(1); diff --git a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java index f9916d27a0..93aeb17de6 100644 --- a/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/entitiy/alarm/DefaultTbAlarmServiceTest.java @@ -27,9 +27,10 @@ import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmCommentService; import org.thingsboard.server.dao.alarm.AlarmService; import org.thingsboard.server.dao.customer.CustomerService; @@ -81,36 +82,41 @@ public class DefaultTbAlarmServiceTest { @Test public void testSave() throws ThingsboardException { - var alarm = new Alarm(); - when(alarmSubscriptionService.createOrUpdateAlarm(alarm)).thenReturn(alarm); + var alarm = new AlarmInfo(); + when(alarmSubscriptionService.createAlarm(any())).thenReturn(AlarmApiCallResult.builder() + .successful(true) + .modified(true) + .alarm(alarm) + .build()); service.save(alarm, new User()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); - verify(alarmSubscriptionService, times(1)).createOrUpdateAlarm(eq(alarm)); + verify(alarmSubscriptionService, times(1)).createAlarm(any()); } @Test - public void testAck() { + public void testAck() throws ThingsboardException { var alarm = new Alarm(); - alarm.setStatus(AlarmStatus.ACTIVE_UNACK); - when(alarmSubscriptionService.ackAlarm(any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); + when(alarmSubscriptionService.acknowledgeAlarm(any(), any(), anyLong())) + .thenReturn(AlarmApiCallResult.builder().successful(true).modified(true).build()); service.ack(alarm, new User(new UserId(UUID.randomUUID()))); verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); - verify(alarmSubscriptionService, times(1)).ackAlarm(any(), any(), anyLong()); + verify(alarmSubscriptionService, times(1)).acknowledgeAlarm(any(), any(), anyLong()); } @Test - public void testClear() { + public void testClear() throws ThingsboardException { var alarm = new Alarm(); - alarm.setStatus(AlarmStatus.ACTIVE_ACK); - when(alarmSubscriptionService.clearAlarm(any(), any(), any(), anyLong())).thenReturn(Futures.immediateFuture(true)); + alarm.setAcknowledged(true); + when(alarmSubscriptionService.clearAlarm(any(), any(), anyLong(), any())) + .thenReturn(AlarmApiCallResult.builder().successful(true).cleared(true).build()); service.clear(alarm, new User(new UserId(UUID.randomUUID()))); verify(alarmCommentService, times(1)).createOrUpdateAlarmComment(any(), any()); verify(notificationEntityService, times(1)).notifyCreateOrUpdateAlarm(any(), any(), any()); - verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), any(), anyLong()); + verify(alarmSubscriptionService, times(1)).clearAlarm(any(), any(), anyLong(), any()); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java index ce02296733..b0282832bc 100644 --- a/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java +++ b/application/src/test/java/org/thingsboard/server/service/resource/sql/BaseTbResourceServiceTest.java @@ -21,6 +21,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.junit.rules.ExpectedException; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityInfo; @@ -237,7 +238,7 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resourceService.delete(savedResource, null); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTbResourceWithExistsFileName() throws Exception { TbResource resource = new TbResource(); resource.setTenantId(tenantId); @@ -256,23 +257,27 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setData("Test Data"); try { - resourceService.save(resource2); + Assertions.assertThrows(DataValidationException.class, () -> { + resourceService.save(resource2); + }); } finally { resourceService.delete(savedResource, null); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveTbResourceWithEmptyTitle() throws Exception { TbResource resource = new TbResource(); resource.setTenantId(tenantId); resource.setResourceType(ResourceType.JKS); resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); - resourceService.save(resource); + Assertions.assertThrows(DataValidationException.class, () -> { + resourceService.save(resource); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTbResourceWithInvalidTenant() throws Exception { TbResource resource = new TbResource(); resource.setTenantId(TenantId.fromUUID(Uuids.timeBased())); @@ -280,7 +285,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { resource.setTitle("My resource"); resource.setFileName(DEFAULT_FILE_NAME); resource.setData("Test Data"); - resourceService.save(resource); + Assertions.assertThrows(DataValidationException.class, () -> { + resourceService.save(resource); + }); } @Test diff --git a/application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java b/application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java index 41cc4c2b76..ab2f6b9bca 100644 --- a/application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java +++ b/application/src/test/java/org/thingsboard/server/transport/TransportNoSqlTestSuite.java @@ -15,30 +15,14 @@ */ package org.thingsboard.server.transport; -import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; -import org.junit.BeforeClass; -import org.junit.ClassRule; import org.junit.extensions.cpsuite.ClasspathSuite; import org.junit.runner.RunWith; -import org.thingsboard.server.dao.CustomCassandraCQLUnit; -import org.thingsboard.server.queue.memory.InMemoryStorage; - -import java.util.Arrays; +import org.thingsboard.server.dao.AbstractNoSqlContainer; @RunWith(ClasspathSuite.class) @ClasspathSuite.ClassnameFilters({ "org.thingsboard.server.transport.*.telemetry.timeseries.nosql.*Test", }) -public class TransportNoSqlTestSuite { - - @ClassRule - public static CustomCassandraCQLUnit cassandraUnit = - new CustomCassandraCQLUnit( - Arrays.asList( - new ClassPathCQLDataSet("cassandra/schema-keyspace.cql", false, false), - new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), - new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false) - ), - "cassandra-test.yaml", 30000l); +public class TransportNoSqlTestSuite extends AbstractNoSqlContainer { } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java index 6cb6e49e79..6b84761208 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/AbstractMqttIntegrationTest.java @@ -16,7 +16,9 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.node.ObjectNode; +import io.netty.handler.codec.mqtt.MqttQoS; import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.client.mqttv3.MqttException; import org.springframework.test.context.TestPropertySource; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -36,9 +38,12 @@ import org.thingsboard.server.common.data.device.profile.JsonTransportPayloadCon import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.security.DeviceCredentials; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; +import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestClient; import java.util.List; @@ -103,6 +108,8 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg if (StringUtils.hasLength(config.getAttributesTopicFilter())) { mqttDeviceProfileTransportConfiguration.setDeviceAttributesTopic(config.getAttributesTopicFilter()); } + mqttDeviceProfileTransportConfiguration.setSparkplug(config.isSparkplug()); + mqttDeviceProfileTransportConfiguration.setSparkplugAttributesMetricNames(config.sparkplugAttributesMetricNames); mqttDeviceProfileTransportConfiguration.setSendAckOnValidationException(config.isSendAckOnValidationException()); TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; if (TransportPayloadType.JSON.equals(transportPayloadType)) { @@ -176,4 +183,25 @@ public abstract class AbstractMqttIntegrationTest extends AbstractTransportInteg builder.addAllKv(kvProtos); return builder.build(); } + + protected void subscribeAndWait(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException { + int subscriptionCount = getDeviceActorSubscriptionCount(deviceId, featureType); + client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below: + // The transport layer acknowledge subscription request once the message about subscription is in the queue. + // Test sends data immediately after acknowledgement. + // But there is a time lag between push to the queue and read from the queue in the tb-core component. + // Ideally, we should reply to device with SUBACK only when the device actor on the tb-core receives the message. + awaitForDeviceActorToReceiveSubscription(deviceId, featureType, subscriptionCount + 1); + } + + protected void subscribeAndCheckSubscription(MqttTestClient client, String attrSubTopic, DeviceId deviceId, FeatureType featureType) throws MqttException { + client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + // TODO: This test awaits for the device actor to receive the subscription. Ideally it should not happen. See details below: + // The transport layer acknowledge subscription request once the message about subscription is in the queue. + // Test sends data immediately after acknowledgement. + // But there is a time lag between push to the queue and read from the queue in the tb-core component. + // Ideally, we should reply to device with SUBACK only when the device actor on the tb-core receives the message. + awaitForDeviceActorToReceiveSubscription(deviceId, featureType, 1); + } } diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java index 8bedc48b73..5815adae76 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/MqttTestConfigProperties.java @@ -20,12 +20,16 @@ import lombok.Data; import org.thingsboard.server.common.data.DeviceProfileProvisionType; import org.thingsboard.server.common.data.TransportPayloadType; +import java.util.Set; + @Data @Builder public class MqttTestConfigProperties { String deviceName; String gatewayName; + boolean isSparkplug; + Set sparkplugAttributesMetricNames; TransportPayloadType transportPayloadType; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java index 511dabb5b7..e4b3058dfd 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/MqttTestClient.java @@ -31,7 +31,7 @@ public class MqttTestClient { private static final String MQTT_URL = "tcp://localhost:1883"; private static final int TIMEOUT = 30; // seconds - private static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT); + public static final long TIMEOUT_MS = TimeUnit.SECONDS.toMillis(TIMEOUT); private final MqttAsyncClient client; diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java index 7feafead95..f8165b0237 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/attributes/AbstractMqttAttributesIntegrationTest.java @@ -32,12 +32,14 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.DeviceTypeFilter; import org.thingsboard.server.common.data.query.EntityData; import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.query.SingleEntityFilter; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.service.telemetry.cmd.v2.EntityDataUpdate; @@ -121,13 +123,16 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt // subscribe to attributes updates from server methods protected void processJsonTestSubscribeToAttributesUpdates(String attrSubTopic) throws Exception { + DeviceId deviceId = savedDevice.getId(); + MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); - client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); - doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + subscribeAndWait(client, attrSubTopic, deviceId, FeatureType.ATTRIBUTES); + + doPostAsync("/api/plugins/telemetry/DEVICE/" + deviceId.getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) .as("await onUpdateCallback").isTrue(); @@ -135,7 +140,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt MqttTestCallback onDeleteCallback = new MqttTestCallback(); client.setCallback(onDeleteCallback); - doDelete("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); + doDelete("/api/plugins/telemetry/DEVICE/" + deviceId.getId() + "/SHARED_SCOPE?keys=sharedJson", String.class); assertThat(onDeleteCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) .as("await onDeleteCallback").isTrue(); validateUpdateAttributesJsonResponse(onDeleteCallback, SHARED_ATTRIBUTES_DELETED_RESPONSE); @@ -147,7 +152,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt client.connectAndWait(accessToken); MqttTestCallback onUpdateCallback = new MqttTestCallback(); client.setCallback(onUpdateCallback); - client.subscribeAndWait(attrSubTopic, MqttQoS.AT_MOST_ONCE); + subscribeAndWait(client, attrSubTopic, savedDevice.getId(), FeatureType.ATTRIBUTES); doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) @@ -213,7 +218,7 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt assertNotNull(savedDevice); - client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE); + subscribeAndCheckSubscription(client, GATEWAY_ATTRIBUTES_TOPIC, savedDevice.getId(), FeatureType.ATTRIBUTES); doPostAsync("/api/plugins/telemetry/DEVICE/" + savedDevice.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); assertThat(onUpdateCallback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS)) @@ -244,7 +249,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt 20, 100); assertNotNull(device); - client.subscribeAndWait(GATEWAY_ATTRIBUTES_TOPIC, MqttQoS.AT_MOST_ONCE); + + subscribeAndCheckSubscription(client, GATEWAY_ATTRIBUTES_TOPIC, device.getId(), FeatureType.ATTRIBUTES); doPostAsync("/api/plugins/telemetry/DEVICE/" + device.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); validateProtoGatewayUpdateAttributesResponse(onUpdateCallback, deviceName); MqttTestCallback onDeleteCallback = new MqttTestCallback(); @@ -409,7 +415,8 @@ public abstract class AbstractMqttAttributesIntegrationTest extends AbstractMqtt Awaitility.await() .atMost(10, TimeUnit.SECONDS) .until(() -> { - List> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() {}); + List> attributes = doGetAsyncTyped(attributeValuesUrl, new TypeReference<>() { + }); return attributes.size() == 5; }); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java index 32f7647048..3880141c36 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/credentials/BasicMqttCredentialsTest.java @@ -20,6 +20,7 @@ import org.eclipse.paho.client.mqttv3.MqttException; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.StringUtils; @@ -115,7 +116,7 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { testTelemetryIsDelivered(accessToken2Device, mqttTestClient5); } - @Test(expected = MqttException.class) + @Test public void testCorrectClientIdAndUserNameButWrongPassword() throws Exception { // Not correct. Correct clientId and username, but wrong password MqttTestClient mqttTestClient = new MqttTestClient(CLIENT_ID); @@ -125,7 +126,9 @@ public class BasicMqttCredentialsTest extends AbstractMqttIntegrationTest { } catch (MqttException e) { Assert.assertEquals(4, e.getReasonCode()); // 4 - Reason code for bad username or password in MQTT v3 } - testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient); + Assertions.assertThrows(MqttException.class, () -> { + testTelemetryIsNotDelivered(clientIdAndUserNameAndPasswordDevice3, mqttTestClient); + }); } private void testTelemetryIsDelivered(Device device, MqttTestClient client) throws Exception { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java index 8610365af8..924b4d50a6 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv3/rpc/AbstractMqttServerSideRpcIntegrationTest.java @@ -37,6 +37,7 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportC import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.device.profile.ProtoTransportPayloadConfiguration; import org.thingsboard.server.common.data.device.profile.TransportPayloadTypeConfiguration; +import org.thingsboard.server.common.msg.session.FeatureType; import org.thingsboard.server.gen.transport.TransportApiProtos; import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; import org.thingsboard.server.transport.mqtt.mqttv3.MqttTestCallback; @@ -82,7 +83,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM client.connectAndWait(accessToken); MqttTestCallback callback = new MqttTestCallback(rpcSubTopic.replace("+", "0")); client.setCallback(callback); - client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_MOST_ONCE); + subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC); String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"23\",\"value\": 1}}"; String result = doPostAsync("/api/rpc/oneway/" + savedDevice.getId(), setGpioRequest, String.class, status().isOk()); @@ -119,7 +120,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM protected void processJsonTwoWayRpcTest(String rpcSubTopic) throws Exception { MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); - client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_LEAST_ONCE); + subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC); MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, rpcSubTopic.replace("+", "0")); client.setCallback(callback); String setGpioRequest = "{\"method\":\"setGpio\",\"params\":{\"pin\": \"26\",\"value\": 1}}"; @@ -133,7 +134,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM protected void processProtoTwoWayRpcTest(String rpcSubTopic) throws Exception { MqttTestClient client = new MqttTestClient(); client.connectAndWait(accessToken); - client.subscribeAndWait(rpcSubTopic, MqttQoS.AT_LEAST_ONCE); + subscribeAndWait(client, rpcSubTopic, savedDevice.getId(), FeatureType.RPC); MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, rpcSubTopic.replace("+", "0")); client.setCallback(callback); @@ -194,7 +195,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM client.enableManualAcks(); MqttTestSequenceCallback callback = new MqttTestSequenceCallback(client, 10, result); client.setCallback(callback); - client.subscribeAndWait(DEVICE_RPC_REQUESTS_SUB_TOPIC, MqttQoS.AT_LEAST_ONCE); + subscribeAndWait(client, DEVICE_RPC_REQUESTS_SUB_TOPIC, savedDevice.getId(), FeatureType.RPC); callback.getSubscribeLatch().await(DEFAULT_WAIT_TIMEOUT_SECONDS, TimeUnit.SECONDS); assertEquals(expected, result); @@ -223,6 +224,8 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM MqttTestCallback callback = new MqttTestCallback(GATEWAY_RPC_TOPIC); client.setCallback(callback); client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); + subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC); + String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); String result = doPostAsync("/api/rpc/oneway/" + deviceId, setGpioRequest, String.class, status().isOk()); @@ -269,7 +272,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM MqttTestRpcJsonCallback callback = new MqttTestRpcJsonCallback(client, GATEWAY_RPC_TOPIC); client.setCallback(callback); - client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); + subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC); String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); @@ -292,7 +295,7 @@ public abstract class AbstractMqttServerSideRpcIntegrationTest extends AbstractM MqttTestRpcProtoCallback callback = new MqttTestRpcProtoCallback(client, GATEWAY_RPC_TOPIC); client.setCallback(callback); - client.subscribeAndWait(GATEWAY_RPC_TOPIC, MqttQoS.AT_MOST_ONCE); + subscribeAndCheckSubscription(client, GATEWAY_RPC_TOPIC, savedDevice.getId(), FeatureType.RPC); String setGpioRequest = "{\"method\": \"toggle_gpio\", \"params\": {\"pin\":1}}"; String deviceId = savedDevice.getId().getId().toString(); diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java index b7ce8d10d3..76629b5a66 100644 --- a/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/mqttv5/MqttV5TestClient.java @@ -89,7 +89,9 @@ public class MqttV5TestClient { // We should copy part of MqttV3TestClient, due if (client == null) { throw new RuntimeException("Failed to connect! MqttAsyncClient is not initialized!"); } - return client.connect(options); + IMqttToken connect = client.connect(options); + connect.waitForCompletion(TIMEOUT_MS); + return connect; } public void disconnectAndWait() throws MqttException { diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java new file mode 100644 index 0000000000..99134be5d4 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/AbstractMqttV5ClientSparkplugTest.java @@ -0,0 +1,462 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug; + +import com.fasterxml.jackson.databind.node.ArrayNode; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.mqttv5.client.IMqttToken; +import org.eclipse.paho.mqttv5.client.MqttCallback; +import org.eclipse.paho.mqttv5.client.MqttConnectionOptions; +import org.eclipse.paho.mqttv5.client.MqttDisconnectResponse; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.eclipse.paho.mqttv5.common.MqttMessage; +import org.eclipse.paho.mqttv5.common.packet.MqttConnAck; +import org.eclipse.paho.mqttv5.common.packet.MqttProperties; +import org.eclipse.paho.mqttv5.common.packet.MqttReturnCode; +import org.eclipse.paho.mqttv5.common.packet.MqttWireMessage; +import org.junit.Assert; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.TransportPayloadType; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.BooleanDataEntry; +import org.thingsboard.server.common.data.kv.DoubleDataEntry; +import org.thingsboard.server.common.data.kv.JsonDataEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.AbstractMqttIntegrationTest; +import org.thingsboard.server.transport.mqtt.MqttTestConfigProperties; +import org.thingsboard.server.transport.mqtt.mqttv5.MqttV5TestClient; +import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.eclipse.paho.mqttv5.common.packet.MqttWireMessage.MESSAGE_TYPE_CONNACK; +import static org.thingsboard.common.util.JacksonUtil.newArrayNode; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Bytes; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int16; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int32; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int64; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.Int8; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt16; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt64; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt8; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +/** + * Created by nickAS21 on 12.01.23 + */ +@Slf4j +public abstract class AbstractMqttV5ClientSparkplugTest extends AbstractMqttIntegrationTest { + + protected MqttV5TestClient client; + protected SparkplugMqttCallback mqttCallback; + protected Calendar calendar = Calendar.getInstance(); + protected ThreadLocalRandom random = ThreadLocalRandom.current(); + + protected static final String groupId = "SparkplugBGroupId"; + protected static final String edgeNode = "SparkpluBNode"; + protected static final String keysBdSeq = "bdSeq"; + protected static final String alias = "Failed Telemetry/Attribute proto sparkplug payload. SparkplugMessageType "; + protected String deviceId = "Test Sparkplug B Device"; + protected int bdSeq = 0; + protected int seq = 0; + protected static final long PUBLISH_TS_DELTA_MS = 86400000;// Publish start TS <-> 24h + + // NBIRTH + protected static final String keyNodeRebirth = "Node Control/Rebirth"; + + //*BIRTH + protected static final MetricDataType metricBirthDataType_Int32 = Int32; + protected static final String metricBirthName_Int32 = "Device Metric int32"; + protected Set sparkplugAttributesMetricNames; + + public void beforeSparkplugTest() throws Exception { + MqttTestConfigProperties configProperties = MqttTestConfigProperties.builder() + .gatewayName("Test Connect Sparkplug client node") + .isSparkplug(true) + .sparkplugAttributesMetricNames(sparkplugAttributesMetricNames) + .transportPayloadType(TransportPayloadType.PROTOBUF) + .build(); + processBeforeTest(configProperties); + } + + public void clientWithCorrectNodeAccessTokenWithNDEATH() throws Exception { + long ts = calendar.getTimeInMillis(); + long value = bdSeq = 0; + clientWithCorrectNodeAccessTokenWithNDEATH(ts, value); + } + + public void clientWithCorrectNodeAccessTokenWithNDEATH(long ts, long value) throws Exception { + IMqttToken connectionResult = clientConnectWithNDEATH(ts, value); + MqttWireMessage response = connectionResult.getResponse(); + Assert.assertEquals(MESSAGE_TYPE_CONNACK, response.getType()); + MqttConnAck connAckMsg = (MqttConnAck) response; + Assert.assertEquals(MqttReturnCode.RETURN_CODE_SUCCESS, connAckMsg.getReturnCode()); + } + + public IMqttToken clientConnectWithNDEATH(long ts, long value, String... nameSpaceBad) throws Exception { + String key = keysBdSeq; + MetricDataType metricDataType = Int64; + SparkplugBProto.Payload.Builder deathPayload = SparkplugBProto.Payload.newBuilder() + .setTimestamp(calendar.getTimeInMillis()); + deathPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + byte[] deathBytes = deathPayload.build().toByteArray(); + this.client = new MqttV5TestClient(); + this.mqttCallback = new SparkplugMqttCallback(); + this.client.setCallback(this.mqttCallback); + MqttConnectionOptions options = new MqttConnectionOptions(); + options.setUserName(gatewayAccessToken); + String nameSpace = nameSpaceBad.length == 0 ? NAMESPACE : nameSpaceBad[0]; + String topic = nameSpace + "/" + groupId + "/" + SparkplugMessageType.NDEATH.name() + "/" + edgeNode; + MqttMessage msg = new MqttMessage(); + msg.setId(0); + msg.setPayload(deathBytes); + options.setWill(topic, msg); + return client.connect(options); + } + + protected List connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices, long ts) throws Exception { + List devices = new ArrayList<>(); + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = Int32; + String key = "Node Metric int32"; + int valueDeviceInt32 = 1024; + SparkplugBProto.Payload.Metric metric = createMetric(valueDeviceInt32, ts, key, metricDataType); + SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts) + .setSeq(getBdSeqNum()); + payloadBirthNode.addMetrics(metric); + payloadBirthNode.setTimestamp(ts); + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, + payloadBirthNode.build().toByteArray(), 0, false); + } + + valueDeviceInt32 = 4024; + metric = createMetric(valueDeviceInt32, ts, metricBirthName_Int32, metricBirthDataType_Int32); + for (int i = 0; i < cntDevices; i++) { + SparkplugBProto.Payload.Builder payloadBirthDevice = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts) + .setSeq(getSeqNum()); + String deviceName = deviceId + "_" + i; + + payloadBirthDevice.addMetrics(metric); + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DBIRTH.name() + "/" + edgeNode + "/" + deviceName, + payloadBirthDevice.build().toByteArray(), 0, false); + AtomicReference device = new AtomicReference<>(); + await(alias + "find device [" + deviceName + "] after created") + .atMost(200, TimeUnit.SECONDS) + .until(() -> { + device.set(doGet("/api/tenant/devices?deviceName=" + deviceName, Device.class)); + return device.get() != null; + }); + devices.add(device.get()); + } + + } + + Assert.assertEquals(cntDevices, devices.size()); + return devices; + } + + protected long getBdSeqNum() throws Exception { + if (bdSeq == 256) { + bdSeq = 0; + } + return bdSeq++; + } + + protected long getSeqNum() throws Exception { + if (seq == 256) { + seq = 0; + } + return seq++; + } + + protected List connectionWithNBirth(MetricDataType metricDataType, String metricKey, Object metricValue) throws Exception { + List listKeys = new ArrayList<>(); + SparkplugBProto.Payload.Builder payloadBirthNode = SparkplugBProto.Payload.newBuilder() + .setTimestamp(calendar.getTimeInMillis()); + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long valueBdSec = getBdSeqNum(); + payloadBirthNode.addMetrics(createMetric(valueBdSec, ts, keysBdSeq, Int64)); + listKeys.add(SparkplugMessageType.NBIRTH.name() + " " + keysBdSeq); + payloadBirthNode.addMetrics(createMetric(false, ts, keyNodeRebirth, MetricDataType.Boolean)); + listKeys.add(keyNodeRebirth); + + payloadBirthNode.addMetrics(createMetric(metricValue, ts, metricKey, metricDataType)); + listKeys.add(metricKey); + + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.NBIRTH.name() + "/" + edgeNode, + payloadBirthNode.build().toByteArray(), 0, false); + } + return listKeys; + } + + protected void createdAddMetricValuePrimitiveTsKv(List listTsKvEntry, List listKeys, + SparkplugBProto.Payload.Builder dataPayload, long ts) throws ThingsboardException { + + String keys = "MyInt8"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt8(), ts, Int8)); + listKeys.add(keys); + + keys = "MyInt16"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt16(), ts, Int16)); + listKeys.add(keys); + + keys = "MyInt32"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt32(), ts, Int32)); + listKeys.add(keys); + + keys = "MyInt64"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextInt64(), ts, Int64)); + listKeys.add(keys); + + keys = "MyUInt8"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt8(), ts, UInt8)); + listKeys.add(keys); + + keys = "MyUInt16"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt16(), ts, UInt16)); + listKeys.add(keys); + + keys = "MyUInt32"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt32(), ts, UInt32)); + listKeys.add(keys); + + keys = "MyUInt64"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextUInt64(), ts, UInt64)); + listKeys.add(keys); + + keys = "MyFloat"; + listTsKvEntry.add(createdAddMetricTsKvFloat(dataPayload, keys, nextFloat(0, 100), ts, MetricDataType.Float)); + listKeys.add(keys); + + keys = "MyDateTime"; + listTsKvEntry.add(createdAddMetricTsKvLong(dataPayload, keys, nextDateTime(), ts, MetricDataType.DateTime)); + listKeys.add(keys); + + keys = "MyDouble"; + listTsKvEntry.add(createdAddMetricTsKvDouble(dataPayload, keys, nextDouble(), ts, MetricDataType.Double)); + listKeys.add(keys); + + keys = "MyBoolean"; + listTsKvEntry.add(createdAddMetricTsKvBoolean(dataPayload, keys, nextBoolean(), ts, MetricDataType.Boolean)); + listKeys.add(keys); + + keys = "MyString"; + listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.String)); + listKeys.add(keys); + + keys = "MyText"; + listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.Text)); + listKeys.add(keys); + + keys = "MyUUID"; + listTsKvEntry.add(createdAddMetricTsKvString(dataPayload, keys, nextString(), ts, MetricDataType.UUID)); + listKeys.add(keys); + + } + + protected void createdAddMetricValueArraysPrimitiveTsKv(List listTsKvEntry, List listKeys, + SparkplugBProto.Payload.Builder dataPayload, long ts) throws ThingsboardException { + String keys = "MyBytesArray"; + byte[] bytes = {nextInt8(), nextInt8(), nextInt8()}; + createdAddMetricTsKvJson(dataPayload, keys, bytes, ts, Bytes, listTsKvEntry, listKeys); + } + + private TsKvEntry createdAddMetricTsKvLong(SparkplugBProto.Payload.Builder dataPayload, String key, Object value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, Long.valueOf(String.valueOf(value)))); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private TsKvEntry createdAddMetricTsKvFloat(SparkplugBProto.Payload.Builder dataPayload, String key, float value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + Double dd = Double.parseDouble(Float.toString(value)); + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new DoubleDataEntry(key, dd)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private TsKvEntry createdAddMetricTsKvDouble(SparkplugBProto.Payload.Builder dataPayload, String key, double value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + Long l = Double.valueOf(value).longValue(); + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(key, l)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private TsKvEntry createdAddMetricTsKvBoolean(SparkplugBProto.Payload.Builder dataPayload, String key, boolean value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new BooleanDataEntry(key, value)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private TsKvEntry createdAddMetricTsKvString(SparkplugBProto.Payload.Builder dataPayload, String key, String value, + long ts, MetricDataType metricDataType) throws ThingsboardException { + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(key, value)); + dataPayload.addMetrics(createMetric(value, ts, key, metricDataType)); + return tsKvEntry; + } + + private void createdAddMetricTsKvJson(SparkplugBProto.Payload.Builder dataPayload, String key, + Object values, long ts, MetricDataType metricDataType, + List listTsKvEntry, + List listKeys) throws ThingsboardException { + ArrayNode nodeArray = newArrayNode(); + switch (metricDataType) { + case Bytes: + for (byte b : (byte[]) values) { + nodeArray.add(b); + } + break; + default: + throw new IllegalStateException("Unexpected value: " + metricDataType); + } + if (nodeArray.size() > 0) { + Optional tsKvEntryOptional = Optional.of(new BasicTsKvEntry(ts, new JsonDataEntry(key, nodeArray.toString()))); + if (tsKvEntryOptional.isPresent()) { + dataPayload.addMetrics(createMetric(values, ts, key, metricDataType)); + listTsKvEntry.add(tsKvEntryOptional.get()); + listKeys.add(key); + } + } + } + + private byte nextInt8() { + return (byte) random.nextInt(Byte.MIN_VALUE, Byte.MAX_VALUE); + } + + private short nextUInt8() { + return (short) random.nextInt(0, Byte.MAX_VALUE * 2 + 1); + } + + private short nextInt16() { + return (short) random.nextInt(Short.MIN_VALUE, Short.MAX_VALUE); + } + + private int nextUInt16() { + return random.nextInt(0, Short.MAX_VALUE * 2 + 1); + } + + protected int nextInt32() { + return random.nextInt(Integer.MIN_VALUE, Integer.MAX_VALUE); + } + + protected long nextUInt32() { + long l = Integer.MAX_VALUE; + return random.nextLong(0, l * 2 + 1); + } + + private long nextInt64() { + return random.nextLong(Long.MIN_VALUE, Long.MAX_VALUE); + } + + private long nextUInt64() { + double d = Long.MAX_VALUE; + return random.nextLong(0, (long) (d * 2 + 1)); + } + + protected double nextDouble() { + return random.nextDouble(Long.MIN_VALUE, Long.MAX_VALUE); + } + + private long nextDateTime() { + long min = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long max = calendar.getTimeInMillis(); + return random.nextLong(min, max); + } + + protected float nextFloat(float min, float max) { + if (min >= max) + throw new IllegalArgumentException("max must be greater than min"); + float result = ThreadLocalRandom.current().nextFloat() * (max - min) + min; + if (result >= max) // correct for rounding + result = Float.intBitsToFloat(Float.floatToIntBits(max) - 1); + return result; + } + + protected boolean nextBoolean() { + return random.nextBoolean(); + } + + protected String nextString() { + return java.util.UUID.randomUUID().toString(); + } + + public class SparkplugMqttCallback implements MqttCallback { + private final List messageArrivedMetrics = new ArrayList<>(); + + @Override + public void disconnected(MqttDisconnectResponse mqttDisconnectResponse) { + + } + + @Override + public void mqttErrorOccurred(MqttException e) { + + } + + @Override + public void messageArrived(String topic, MqttMessage mqttMsg) throws Exception { + SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(mqttMsg.getPayload()); + messageArrivedMetrics.addAll(sparkplugBProtoNode.getMetricsList()); + } + + @Override + public void deliveryComplete(IMqttToken iMqttToken) { + + } + + @Override + public void connectComplete(boolean b, String s) { + + } + + @Override + public void authPacketArrived(int i, MqttProperties mqttProperties) { + + } + + public List getMessageArrivedMetrics() { + return messageArrivedMetrics; + } + + public void deleteMessageArrivedMetrics(int id) { + messageArrivedMetrics.remove(id); + } + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java new file mode 100644 index 0000000000..04349d0672 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/AbstractMqttV5ClientSparkplugAttributesTest.java @@ -0,0 +1,432 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.attributes; + +import com.fasterxml.jackson.core.type.TypeReference; +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; +import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.DataConstants.CLIENT_SCOPE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType.UInt32; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +/** + * Created by nickAS21 on 12.01.23 + */ +@Slf4j +public abstract class AbstractMqttV5ClientSparkplugAttributesTest extends AbstractMqttV5ClientSparkplugTest { + + protected void processClientWithCorrectAccessTokenPublishNCMDReBirth() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + List listKeys = connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + // Shared attribute "Node Control/Rebirth" = true. type = NCMD. + boolean value = true; + Assert.assertTrue(listKeys.contains(keyNodeRebirth)); + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + keyNodeRebirth + "\":" + value + "}"; + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(keyNodeRebirth, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + } + + /** + * If boolean - send long 0 or 1 + * If String - try to parse long + * If double - cast long + * If we can't parse, cast, or JSON there - debug the message with the id of the devise/node, tenant, + * the name and type of the attribute into an error and don't send anything. + */ + protected void processClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = MetricDataType.Boolean; + String metricKey = "MyBoolean"; + Object metricValue = nextBoolean(); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // Boolean <-> String + boolean expectedValue = true; + String valueStr = "123"; + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + expectedValue = false; + valueStr = "0"; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Boolean <-> Integer + expectedValue = true; + Integer valueInt = nextInt32(); + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueInt + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + expectedValue = false; + valueInt = 0; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueInt + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getBooleanValue()); + } + + protected void processClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = UInt32; + String metricKey = "MyLong"; + Object metricValue = nextUInt32(); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // Long <-> String + String valueStr = "123"; + long expectedValue = Long.valueOf(valueStr); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Long <-> Boolean + Boolean valueBoolean = true; + expectedValue = 1L; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = 0L; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getLongValue()); + } + + protected void processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = MetricDataType.Float; + String metricKey = "MyFloat"; + Object metricValue = nextFloat(30, 400); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // Float <-> String + String valueStr = "123.345"; + float expectedValue = Float.valueOf(valueStr); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Float <-> Boolean + Boolean valueBoolean = true; + expectedValue = 1f; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = 0f; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getFloatValue()); + } + + protected void processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = MetricDataType.Double; + String metricKey = "MyDouble"; + Object metricValue = nextDouble(); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // Double <-> String + String valueStr = "123345456"; + double expectedValue = Double.valueOf(valueStr); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Double <-> Boolean + Boolean valueBoolean = true; + expectedValue = 1d; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = 0d; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(expectedValue == mqttCallback.getMessageArrivedMetrics().get(0).getDoubleValue()); + } + + protected void processClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + MetricDataType metricDataType = MetricDataType.String; + String metricKey = "MyString"; + Object metricValue = nextString(); + connectionWithNBirth(metricDataType, metricKey, metricValue); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + + // String <-> Long + long valueLong = 123345456L; + String expectedValue = String.valueOf(valueLong); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueLong + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // String <-> Boolean + Boolean valueBoolean = true; + expectedValue = "true"; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = "false"; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricKey + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricKey, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getStringValue()); + } + + protected void processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + + // Integer <-> Integer + int expectedValueInt = 123456; + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + expectedValueInt + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.DBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValueInt, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + protected void processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + + // Int <-> String + String valueStr = "123"; + long expectedValue = Long.valueOf(valueStr); + + String SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueStr + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.DBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + // Int <-> Boolean + Boolean valueBoolean = true; + expectedValue = 1; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + mqttCallback.deleteMessageArrivedMetrics(0); + + valueBoolean = false; + expectedValue = 0; + SHARED_ATTRIBUTES_PAYLOAD = "{\"" + metricBirthName_Int32 + "\":" + valueBoolean + "}"; + doPostAsync("/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/attributes/SHARED_SCOPE", SHARED_ATTRIBUTES_PAYLOAD, String.class, status().isOk()); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertEquals(expectedValue, mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + protected void processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + String urlTemplate = "/api/plugins/telemetry/DEVICE/" + savedGateway.getId().getId() + "/keys/attributes/" + CLIENT_SCOPE; + AtomicReference> actualKeys = new AtomicReference<>(); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { + })); + return actualKeys.get().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, actualKeys.get().get(0)); + } + + protected void processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + String urlTemplate = "/api/plugins/telemetry/DEVICE/" + devices.get(0).getId().getId() + "/keys/attributes/" + CLIENT_SCOPE; + AtomicReference> actualKeys = new AtomicReference<>(); + await(alias + SparkplugMessageType.DBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + actualKeys.set(doGetAsyncTyped(urlTemplate, new TypeReference<>() { + })); + return actualKeys.get().size() == 1; + }); + Assert.assertEquals(metricBirthName_Int32, actualKeys.get().get(0)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java new file mode 100644 index 0000000000..1d5a2c127f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesInProfileTest.java @@ -0,0 +1,55 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.attributes; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +import java.util.HashSet; + +/** + * Created by nickAS21 on 12.01.23 + */ +@DaoSqlTest +public class MqttV5ClientSparkplugBAttributesInProfileTest extends AbstractMqttV5ClientSparkplugAttributesTest { + + @Before + public void beforeTest() throws Exception { + sparkplugAttributesMetricNames = new HashSet<>(); + sparkplugAttributesMetricNames.add(metricBirthName_Int32); + beforeSparkplugTest(); + } + + @After + public void afterTest () throws MqttException { + if (client.isConnected()) { + client.disconnect(); } + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { + processClientNodeWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes(); + } + + @Test + public void testClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes() throws Exception { + processClientDeviceWithCorrectAccessTokenPublish_AttributesInProfileContainsKeyAttributes(); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java new file mode 100644 index 0000000000..f5d42046b7 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/attributes/MqttV5ClientSparkplugBAttributesTest.java @@ -0,0 +1,81 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.attributes; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +/** + * Created by nickAS21 on 12.01.23 + */ +@DaoSqlTest +public class MqttV5ClientSparkplugBAttributesTest extends AbstractMqttV5ClientSparkplugAttributesTest { + + @Before + public void beforeTest() throws Exception { + beforeSparkplugTest(); + } + + @After + public void afterTest () throws MqttException { + if (client.isConnected()) { + client.disconnect(); } + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMDReBirth() throws Exception { + processClientWithCorrectAccessTokenPublishNCMDReBirth(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_BooleanType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_LongType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_FloatType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_DoubleType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientWithCorrectAccessTokenPublishNCMD_StringType_IfMetricFailedTypeCheck_SendValueOk(); + } + + @Test + public void testClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute() throws Exception { + processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttribute(); + } + + @Test + public void testClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk() throws Exception { + processClientDeviceWithCorrectAccessTokenPublishWithBirth_SharedAttributes_LongType_IfMetricFailedTypeCheck_SendValueOk(); + } + +} \ No newline at end of file diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java new file mode 100644 index 0000000000..5f9b794739 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/AbstractMqttV5ClientSparkplugConnectionTest.java @@ -0,0 +1,178 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.connection; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.Assert; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.common.data.kv.BasicTsKvEntry; +import org.thingsboard.server.common.data.kv.LongDataEntry; +import org.thingsboard.server.common.data.kv.StringDataEntry; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.mqttv5.MqttV5TestClient; +import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +/** + * Created by nickAS21 on 12.01.23 + */ +@Slf4j +public abstract class AbstractMqttV5ClientSparkplugConnectionTest extends AbstractMqttV5ClientSparkplugTest { + + protected void processClientWithCorrectNodeAccessTokenWithNDEATH_Test() throws Exception { + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long value = bdSeq = 0; + clientWithCorrectNodeAccessTokenWithNDEATH(ts, value); + + String keys = SparkplugMessageType.NDEATH.name() + " " + keysBdSeq; + TsKvEntry expectedTsKvEntry = new BasicTsKvEntry(ts, new LongDataEntry(keys, value)); + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + SparkplugMessageType.NDEATH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findLatest(tenantId, savedGateway.getId(), keys)); + return finalFuture.get().get().isPresent(); + }); + TsKvEntry actualTsKvEntry = finalFuture.get().get().get(); + Assert.assertEquals(expectedTsKvEntry, actualTsKvEntry); + } + + protected void processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test() throws Exception { + this.client = new MqttV5TestClient(); + MqttException actualException = Assert.assertThrows(MqttException.class, () -> client.connectAndWait(gatewayAccessToken)); + String expectedMessage = "Server unavailable."; + int expectedReasonCode = 136; + Assert.assertEquals(expectedMessage, actualException.getMessage()); + Assert.assertEquals(expectedReasonCode, actualException.getReasonCode()); + } + + protected void processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test() throws Exception { + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + long value = bdSeq = 0; + MqttException actualException = Assert.assertThrows(MqttException.class, () -> clientConnectWithNDEATH(ts, value, "spBv1.2")); + String expectedMessage = "Server unavailable."; + int expectedReasonCode = 136; + Assert.assertEquals(expectedMessage, actualException.getMessage()); + Assert.assertEquals(expectedReasonCode, actualException.getReasonCode()); + } + + protected void processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(int cntDevices) throws Exception { + long ts = calendar.getTimeInMillis(); + connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); + } + + protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL(int cntDevices) throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); + + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), ONLINE.name())); + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); + return finalFuture.get().get().contains(tsKvEntry); + }); + + for (Device device : devices) { + await(alias + messageName(STATE) + ", device: " + device.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); + return finalFuture.get().get().contains(tsKvEntry); + }); + } + } + + protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE(int cntDevices, int indexDeviceDisconnect) throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); + + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), OFFLINE.name())); + AtomicReference>> finalFuture = new AtomicReference<>(); + + SparkplugBProto.Payload.Builder payloadDeathDevice = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts) + .setSeq(getSeqNum()); + if (client.isConnected()) { + List devicesList = new ArrayList<>(devices); + Device device = devicesList.get(indexDeviceDisconnect); + client.publish(NAMESPACE + "/" + groupId + "/" + SparkplugMessageType.DDEATH.name() + "/" + edgeNode + "/" + device.getName(), + payloadDeathDevice.build().toByteArray(), 0, false); + await(alias + messageName(STATE) + ", device: " + device.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); + return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); + }); + } + } + + protected void processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All(int cntDevices) throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(cntDevices, ts); + + TsKvEntry tsKvEntry = new BasicTsKvEntry(ts, new StringDataEntry(messageName(STATE), OFFLINE.name())); + AtomicReference>> finalFuture = new AtomicReference<>(); + + if (client.isConnected()) { + client.disconnect(); + + await(alias + messageName(STATE) + ", device: " + savedGateway.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); + return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); + }); + + List devicesList = new ArrayList<>(devices); + for (Device device : devicesList) { + await(alias + messageName(STATE) + ", device: " + device.getName()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, device.getId())); + return findEqualsKeyValueInKvEntrys(finalFuture.get().get(), tsKvEntry); + }); + } + } + } + + private boolean findEqualsKeyValueInKvEntrys(List finalFuture, TsKvEntry tsKvEntry) { + for (TsKvEntry kvEntry : finalFuture) { + if (kvEntry.getKey().equals(tsKvEntry.getKey()) && kvEntry.getValue().equals(tsKvEntry.getValue())) { + return true; + } + } + return false; + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java new file mode 100644 index 0000000000..71c9391a29 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/connection/MqttV5ClientSparkplugBConnectionTest.java @@ -0,0 +1,82 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.connection; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +/** + * Created by nickAS21 on 12.01.23 + */ +@DaoSqlTest +public class MqttV5ClientSparkplugBConnectionTest extends AbstractMqttV5ClientSparkplugConnectionTest { + + @Before + public void beforeTest() throws Exception { + beforeSparkplugTest(); + } + + @After + public void afterTest() throws MqttException { + if (client.isConnected()) { + client.disconnect(); + } + } + + @Test + public void testClientWithCorrectAccessTokenWithNDEATH() throws Exception { + processClientWithCorrectNodeAccessTokenWithNDEATH_Test(); + } + + @Test + public void testClientWithCorrectNodeAccessTokenWithoutNDEATH() throws Exception { + processClientWithCorrectNodeAccessTokenWithoutNDEATH_Test(); + } + + @Test + public void testClientWithCorrectNodeAccessTokenNameSpaceInvalid() throws Exception { + processClientWithCorrectNodeAccessTokenNameSpaceInvalid_Test(); + } + + @Test + public void testClientWithCorrectAccessTokenWithNDEATHCreatedOneDevice() throws Exception { + processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1); + } + + @Test + public void testClientWithCorrectAccessTokenWithNDEATHCreatedTwoDevice() throws Exception { + processClientWithCorrectAccessTokenWithNDEATHCreatedDevices(2); + } + + @Test + public void testClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL() throws Exception { + processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_ALL(3); + } + + @Test + public void testConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE() throws Exception { + processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OneDeviceOFFLINE(3, 1); + } + + @Test + public void testConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All() throws Exception { + processConnectClientWithCorrectAccessTokenWithNDEATH_State_ONLINE_All_Then_OFFLINE_All(3); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java new file mode 100644 index 0000000000..b08baac875 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/AbstractMqttV5RpcSparkplugTest.java @@ -0,0 +1,108 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.rpc; + +import io.netty.handler.codec.mqtt.MqttQoS; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; +import org.thingsboard.server.common.data.Device; +import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.List; +import java.util.concurrent.TimeUnit; + +import static org.awaitility.Awaitility.await; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.common.data.exception.ThingsboardErrorCode.INVALID_ARGUMENTS; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DCMD; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NCMD; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +@Slf4j +public abstract class AbstractMqttV5RpcSparkplugTest extends AbstractMqttV5ClientSparkplugTest { + + private static final int metricBirthValue_Int32 = 123456; + private static final String sparkplugRpcRequest = "{\"metricName\":\"" + metricBirthName_Int32 + "\",\"value\":" + metricBirthValue_Int32 + "}"; + + @Test + public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + String expected = "{\"result\":\"Success: " + SparkplugMessageType.NCMD.name() + "\"}"; + String actual = sendRPCSparkplug(NCMD.name(), sparkplugRpcRequest, savedGateway); + await(alias + SparkplugMessageType.NCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(expected, actual); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + @Test + public void processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { + long ts = calendar.getTimeInMillis(); + List devices = connectClientWithCorrectAccessTokenWithNDEATHCreatedDevices(1, ts); + String expected = "{\"result\":\"Success: " + DCMD.name() + "\"}"; + String actual = sendRPCSparkplug(DCMD.name() , sparkplugRpcRequest, devices.get(0)); + await(alias + NCMD.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + return mqttCallback.getMessageArrivedMetrics().size() == 1; + }); + Assert.assertEquals(expected, actual); + Assert.assertEquals(metricBirthName_Int32, mqttCallback.getMessageArrivedMetrics().get(0).getName()); + Assert.assertTrue(metricBirthValue_Int32 == mqttCallback.getMessageArrivedMetrics().get(0).getIntValue()); + } + + @Test + public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + String invalidateTypeMessageName = "RCMD"; + String expected = "{\"result\":\"" + INVALID_ARGUMENTS + "\",\"error\":\"Failed to convert device RPC command to MQTT msg: " + + invalidateTypeMessageName + "{\\\"metricName\\\":\\\"" + metricBirthName_Int32 + "\\\",\\\"value\\\":" + metricBirthValue_Int32 + "}\"}"; + String actual = sendRPCSparkplug(invalidateTypeMessageName, sparkplugRpcRequest, savedGateway); + Assert.assertEquals(expected, actual); + } + + @Test + public void processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InBirthNotHaveMetric_BAD_REQUEST_PARAMS() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + Assert.assertTrue("Connection node is failed", client.isConnected()); + client.subscribeAndWait(NAMESPACE + "/" + groupId + "/" + NCMD.name() + "/" + edgeNode + "/#", MqttQoS.AT_MOST_ONCE); + String metricNameBad = metricBirthName_Int32 + "_Bad"; + String sparkplugRpcRequestBad = "{\"metricName\":\"" + metricNameBad + "\",\"value\":" + metricBirthValue_Int32 + "}"; + String expected = "{\"result\":\"BAD_REQUEST_PARAMS\",\"error\":\"Failed send To Node Rpc Request: " + + DCMD.name() + ". This node does not have a metricName: [" + metricNameBad + "]\"}"; + String actual = sendRPCSparkplug(DCMD.name(), sparkplugRpcRequestBad, savedGateway); + Assert.assertEquals(expected, actual); + } + + private String sendRPCSparkplug(String nameTypeMessage, String keyValue, Device device) throws Exception { + String setRpcRequest = "{\"method\": \"" + nameTypeMessage + "\", \"params\": " + keyValue + "}"; + return doPostAsync("/api/plugins/rpc/twoway/" + device.getId().getId(), setRpcRequest, String.class, status().isOk()); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java new file mode 100644 index 0000000000..9f568b550f --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/rpc/MqttV5RpcSparkplugTest.java @@ -0,0 +1,61 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.rpc; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +@DaoSqlTest +@Slf4j +public class MqttV5RpcSparkplugTest extends AbstractMqttV5RpcSparkplugTest { + + @Before + public void beforeTest() throws Exception { + beforeSparkplugTest(); + } + + @After + public void afterTest() throws MqttException { + if (client.isConnected()) { + client.disconnect(); + } + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { + processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_Success(); + } + + @Test + public void testClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success() throws Exception { + processClientDeviceWithCorrectAccessTokenPublish_TwoWayRpc_Success(); + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS() throws Exception { + processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS(); + } + + @Test + public void testClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InBirthNotHaveMetric_BAD_REQUEST_PARAMS() throws Exception { + processClientNodeWithCorrectAccessTokenPublish_TwoWayRpc_InvalidTypeMessage_INVALID_ARGUMENTS(); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java new file mode 100644 index 0000000000..587494f869 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/AbstractMqttV5ClientSparkplugTelemetryTest.java @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.timeseries; + +import com.google.common.util.concurrent.ListenableFuture; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.thingsboard.server.common.data.kv.TsKvEntry; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.sparkplug.AbstractMqttV5ClientSparkplugTest; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import static org.awaitility.Awaitility.await; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.NAMESPACE; + +/** + * Created by nickAS21 on 12.01.23 + */ +@Slf4j +public abstract class AbstractMqttV5ClientSparkplugTelemetryTest extends AbstractMqttV5ClientSparkplugTest { + + protected void processClientWithCorrectAccessTokenPublishNBIRTH() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + List listKeys = connectionWithNBirth(metricBirthDataType_Int32, metricBirthName_Int32, nextInt32()); + Assert.assertTrue("Connection node is failed", client.isConnected()); + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + SparkplugMessageType.NBIRTH.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findLatest(tenantId, savedGateway.getId(), listKeys)); + return !finalFuture.get().get().isEmpty(); + }); + Assert.assertEquals(listKeys.size(), finalFuture.get().get().size()); + } + + protected void processClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple() throws Exception { + List listKeys = new ArrayList<>(); + clientWithCorrectNodeAccessTokenWithNDEATH(); + + String messageTypeName = SparkplugMessageType.NDATA.name(); + + List listTsKvEntry = new ArrayList<>(); + + SparkplugBProto.Payload.Builder ndataPayload = SparkplugBProto.Payload.newBuilder() + .setTimestamp(calendar.getTimeInMillis()) + .setSeq(getSeqNum()); + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + + createdAddMetricValuePrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); + + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, + ndataPayload.build().toByteArray(), 0, false); + } + + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + SparkplugMessageType.NDATA.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); + return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); + }); + Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); + } + + protected void processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple() throws Exception { + clientWithCorrectNodeAccessTokenWithNDEATH(); + + String messageTypeName = SparkplugMessageType.NDATA.name(); + List listKeys = new ArrayList<>(); + List listTsKvEntry = new ArrayList<>(); + + SparkplugBProto.Payload.Builder ndataPayload = SparkplugBProto.Payload.newBuilder() + .setTimestamp(calendar.getTimeInMillis()) + .setSeq(getSeqNum()); + long ts = calendar.getTimeInMillis() - PUBLISH_TS_DELTA_MS; + + createdAddMetricValueArraysPrimitiveTsKv(listTsKvEntry, listKeys, ndataPayload, ts); + + if (client.isConnected()) { + client.publish(NAMESPACE + "/" + groupId + "/" + messageTypeName + "/" + edgeNode, + ndataPayload.build().toByteArray(), 0, false); + } + + AtomicReference>> finalFuture = new AtomicReference<>(); + await(alias + SparkplugMessageType.NDATA.name()) + .atMost(40, TimeUnit.SECONDS) + .until(() -> { + finalFuture.set(tsService.findAllLatest(tenantId, savedGateway.getId())); + return finalFuture.get().get().size() == (listTsKvEntry.size() + 1); + }); + Assert.assertTrue("Actual tsKvEntrys is not containsAll Expected tsKvEntrys", finalFuture.get().get().containsAll(listTsKvEntry)); + } + +} diff --git a/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java new file mode 100644 index 0000000000..bdd6c7b9b3 --- /dev/null +++ b/application/src/test/java/org/thingsboard/server/transport/mqtt/sparkplug/timeseries/MqttV5ClientSparkplugBTelemetryTest.java @@ -0,0 +1,56 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.sparkplug.timeseries; + +import org.eclipse.paho.mqttv5.common.MqttException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.thingsboard.server.dao.service.DaoSqlTest; + +/** + * Created by nickAS21 on 12.01.23 + */ +@DaoSqlTest +public class MqttV5ClientSparkplugBTelemetryTest extends AbstractMqttV5ClientSparkplugTelemetryTest { + + @Before + public void beforeTest() throws Exception { + beforeSparkplugTest(); + } + + @After + public void afterTest () throws MqttException { + if (client.isConnected()) { + client.disconnect(); } + } + + @Test + public void testClientWithCorrectAccessTokenPublishNBIRTH() throws Exception { + processClientWithCorrectAccessTokenPublishNBIRTH(); + } + + @Test + public void testClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple() throws Exception { + processClientWithCorrectAccessTokenPushNodeMetricBuildPrimitiveSimple(); + } + + @Test + public void testClientWithCorrectAccessTokenPushNodeMetricBuildPArraysPrimitiveSimple() throws Exception { + processClientWithCorrectAccessTokenPushNodeMetricBuildArraysPrimitiveSimple(); + } + +} \ No newline at end of file diff --git a/application/src/test/resources/application-test.properties b/application/src/test/resources/application-test.properties index a4bedd9527..ad86ff736b 100644 --- a/application/src/test/resources/application-test.properties +++ b/application/src/test/resources/application-test.properties @@ -67,4 +67,3 @@ sql.ttl.audit_logs.ttl=2592000 sql.edge_events.partition_size=168 sql.ttl.edge_events.edge_event_ttl=2592000 -spring.test.context.cache.maxSize=1 \ No newline at end of file diff --git a/application/src/test/resources/logback-test.xml b/application/src/test/resources/logback-test.xml index c79a7d3905..23e6d8c2f6 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -13,10 +13,11 @@ - + + diff --git a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java index a9f262d6ca..c5af954392 100644 --- a/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java +++ b/common/cache/src/main/java/org/thingsboard/server/cache/RedisTbTransactionalCache.java @@ -108,6 +108,10 @@ public abstract class RedisTbTransactionalCache keys) { + //Redis expects at least 1 key to delete. Otherwise - ERR wrong number of arguments for 'del' command + if (keys.isEmpty()) { + return; + } try (var connection = connectionFactory.getConnection()) { connection.del(keys.stream().map(this::getRawKey).toArray(byte[][]::new)); } diff --git a/common/cluster-api/src/main/proto/queue.proto b/common/cluster-api/src/main/proto/queue.proto index d8c55c5917..f4992cbba7 100644 --- a/common/cluster-api/src/main/proto/queue.proto +++ b/common/cluster-api/src/main/proto/queue.proto @@ -186,6 +186,7 @@ message GetOrCreateDeviceFromGatewayRequestMsg { int64 gatewayIdLSB = 2; string deviceName = 3; string deviceType = 4; + bool sparkplug = 5; } message GetOrCreateDeviceFromGatewayResponseMsg { diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java new file mode 100644 index 0000000000..a8aebf16e7 --- /dev/null +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmApiCallResult.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.alarm; + +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.id.EntityId; + +import java.util.List; + + +@Data +public class AlarmApiCallResult { + + private final boolean successful; + private final boolean created; + private final boolean modified; + private final boolean cleared; + private final AlarmInfo alarm; + private final Alarm old; + private final List propagatedEntitiesList; + + @Builder + private AlarmApiCallResult(boolean successful, boolean created, boolean modified, boolean cleared, AlarmInfo alarm, Alarm old, List propagatedEntitiesList) { + this.successful = successful; + this.created = created; + this.modified = modified; + this.cleared = cleared; + this.alarm = alarm; + this.old = old; + this.propagatedEntitiesList = propagatedEntitiesList; + } + + public AlarmApiCallResult(AlarmApiCallResult other, List propagatedEntitiesList) { + this.successful = other.successful; + this.created = other.created; + this.modified = other.modified; + this.cleared = other.cleared; + this.alarm = other.alarm; + this.old = other.old; + this.propagatedEntitiesList = propagatedEntitiesList; + } + + public boolean isSeverityChanged() { + if (alarm == null || old == null) { + return false; + } else { + return !alarm.getSeverity().equals(old.getSeverity()); + } + } + + public AlarmSeverity getOldSeverity() { + return isSeverityChanged() ? old.getSeverity() : null; + } + + public boolean isPropagationChanged() { + if (created) { + return true; + } + if (alarm == null || old == null) { + return false; + } + return (alarm.isPropagate() != old.isPropagate()) || + (alarm.isPropagateToOwner() != old.isPropagateToOwner()) || + (alarm.isPropagateToTenant() != old.isPropagateToTenant()) || + (!alarm.getPropagateRelationTypes().equals(old.getPropagateRelationTypes())); + } + +} diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java index 6125d4a255..e3a05a4e2f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmOperationResult.java @@ -16,16 +16,20 @@ package org.thingsboard.server.dao.alarm; import lombok.AllArgsConstructor; +import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.id.EntityId; import java.util.Collections; import java.util.List; +@Builder @Data @AllArgsConstructor +@Deprecated public class AlarmOperationResult { private final Alarm alarm; private final boolean successful; @@ -40,4 +44,21 @@ public class AlarmOperationResult { public AlarmOperationResult(Alarm alarm, boolean successful, List propagatedEntitiesList) { this(alarm, successful, false, null, propagatedEntitiesList); } + + public AlarmOperationResult(Alarm alarm, boolean successful, boolean created, List propagatedEntitiesList) { + this.alarm = alarm; + this.successful = successful; + this.created = created; + this.propagatedEntitiesList = propagatedEntitiesList; + this.oldSeverity = null; + } + + //Temporary while we have not removed the AlarmOperationResult. + public AlarmOperationResult(AlarmApiCallResult result) { + this.alarm = result.getAlarm() != null ? new Alarm(result.getAlarm()) : null; + this.successful = result.isSuccessful() && (result.isCreated() || result.isModified()); + this.created = result.isCreated(); + this.oldSeverity = result.getOldSeverity(); + this.propagatedEntitiesList = result.getPropagatedEntitiesList(); + } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java index 2c60c41062..ad2aa1814f 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/alarm/AlarmService.java @@ -23,10 +23,13 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -34,35 +37,76 @@ import org.thingsboard.server.dao.entity.EntityDaoService; import java.util.Collection; -/** - * Created by ashvayka on 11.05.17. - */ + public interface AlarmService extends EntityDaoService { + /* + * New API, since 3.5. + */ + + /** + * Designed for atomic operations over active alarms. + * Only one active alarm may exist for the pair {originatorId, alarmType} + */ + AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request); + + /** + * Designed for atomic operations over active alarms. + * Only one active alarm may exist for the pair {originatorId, alarmType} + */ + AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled); + + /** + * Designed to update existing alarm. Accepts only part of the alarm fields. + */ + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); + + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long ts); + + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long ts); + + AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId); + + /* + * Legacy API, before 3.5. + */ + @Deprecated(since = "3.5.0", forRemoval = true) AlarmOperationResult createOrUpdateAlarm(Alarm alarm); + @Deprecated(since = "3.5.0", forRemoval = true) AlarmOperationResult createOrUpdateAlarm(Alarm alarm, boolean alarmCreationEnabled); - AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); - + @Deprecated(since = "3.5.0", forRemoval = true) ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + @Deprecated(since = "3.5.0", forRemoval = true) ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + @Deprecated(since = "3.5.0", forRemoval = true) + AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId); + + @Deprecated(since = "3.5.0", forRemoval = true) + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + + // Other API Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); - ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); + AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId); ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, - AlarmStatus alarmStatus); + AlarmStatus alarmStatus, String assigneeId); - ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java index 9a9e000e4c..f2317efa05 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/entity/EntityService.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.dao.entity; +import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; @@ -29,10 +30,13 @@ public interface EntityService { Optional fetchEntityName(TenantId tenantId, EntityId entityId); + Optional fetchEntityLabel(TenantId tenantId, EntityId entityId); + Optional fetchEntityCustomerId(TenantId tenantId, EntityId entityId); + Optional fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId); + long countEntitiesByQuery(TenantId tenantId, CustomerId customerId, EntityCountQuery query); PageData findEntityDataByQuery(TenantId tenantId, CustomerId customerId, EntityDataQuery query); - } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java index efe92977cc..d8904f03e6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/ContactBased.java @@ -21,7 +21,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) -public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasName { +public abstract class ContactBased extends SearchTextBasedWithAdditionalInfo implements HasEmail { private static final long serialVersionUID = 5047448057830660988L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java index c9847b0078..d194fb7755 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Customer.java @@ -29,7 +29,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) -public class Customer extends ContactBased implements HasTenantId, ExportableEntity { +public class Customer extends ContactBased implements HasTenantId, ExportableEntity, HasTitle { private static final long serialVersionUID = -1599722990298929275L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java index e273e1b97e..7c166e53e3 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DashboardInfo.java @@ -31,7 +31,7 @@ import java.util.Objects; import java.util.Set; @ApiModel -public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId { +public class DashboardInfo extends SearchTextBased implements HasName, HasTenantId, HasTitle { private TenantId tenantId; @NoXss diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java index 2cc9836864..fd35c0a67a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DataConstants.java @@ -74,6 +74,8 @@ public class DataConstants { public static final String TIMESERIES_DELETED = "TIMESERIES_DELETED"; public static final String ALARM_ACK = "ALARM_ACK"; public static final String ALARM_CLEAR = "ALARM_CLEAR"; + public static final String ALARM_ASSIGN = "ALARM_ASSIGN"; + public static final String ALARM_UNASSIGN = "ALARM_UNASSIGN"; public static final String ALARM_DELETE = "ALARM_DELETE"; public static final String ENTITY_ASSIGNED_FROM_TENANT = "ENTITY_ASSIGNED_FROM_TENANT"; public static final String ENTITY_ASSIGNED_TO_TENANT = "ENTITY_ASSIGNED_TO_TENANT"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java index c1517e2230..27457bda34 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Device.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Device.java @@ -40,7 +40,7 @@ import java.util.Optional; @ApiModel @EqualsAndHashCode(callSuper = true) @Slf4j -public class Device extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId, HasOtaPackage, ExportableEntity { +public class Device extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId, HasOtaPackage, ExportableEntity { private static final long serialVersionUID = 2807343040519543363L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java index 4b5bb65bcb..1d547fd60c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/EntityType.java @@ -19,5 +19,25 @@ package org.thingsboard.server.common.data; * @author Andrew Shvayka */ public enum EntityType { - TENANT, CUSTOMER, USER, DASHBOARD, ASSET, DEVICE, ALARM, RULE_CHAIN, RULE_NODE, ENTITY_VIEW, WIDGETS_BUNDLE, WIDGET_TYPE, TENANT_PROFILE, DEVICE_PROFILE, ASSET_PROFILE, API_USAGE_STATE, TB_RESOURCE, OTA_PACKAGE, EDGE, RPC, QUEUE; + TENANT, + CUSTOMER, + USER, + DASHBOARD, + ASSET, + DEVICE, + ALARM, + RULE_CHAIN, + RULE_NODE, + ENTITY_VIEW, + WIDGETS_BUNDLE, + WIDGET_TYPE, + TENANT_PROFILE, + DEVICE_PROFILE, + ASSET_PROFILE, + API_USAGE_STATE, + TB_RESOURCE, + OTA_PACKAGE, + EDGE, + RPC, + QUEUE } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasEmail.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasEmail.java new file mode 100644 index 0000000000..fd7fbf19fd --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasEmail.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface HasEmail extends HasName { + + String getEmail(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasLabel.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasLabel.java new file mode 100644 index 0000000000..fe4f46c9de --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasLabel.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface HasLabel extends HasName { + + String getLabel(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/HasTitle.java b/common/data/src/main/java/org/thingsboard/server/common/data/HasTitle.java new file mode 100644 index 0000000000..aab1c6c346 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/HasTitle.java @@ -0,0 +1,22 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public interface HasTitle { + + String getTitle(); + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java index c917d15910..c468cb24f8 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/OtaPackageInfo.java @@ -36,7 +36,7 @@ import org.thingsboard.server.common.data.ota.OtaPackageType; @Slf4j @Data @EqualsAndHashCode(callSuper = true) -public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId { +public class OtaPackageInfo extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasTitle { private static final long serialVersionUID = 3168391583570815419L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java index 6a2b0a58d6..3b38aa57c1 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java @@ -18,9 +18,14 @@ package org.thingsboard.server.common.data; import com.google.common.base.Splitter; import org.apache.commons.lang3.RandomStringUtils; +import java.security.SecureRandom; +import java.util.Base64; + import static org.apache.commons.lang3.StringUtils.repeat; public class StringUtils { + public static final SecureRandom RANDOM = new SecureRandom(); + public static final String EMPTY = ""; public static final int INDEX_NOT_FOUND = -1; @@ -180,4 +185,11 @@ public class StringUtils { return RandomStringUtils.randomAlphabetic(count); } + public static String generateSafeToken(int length) { + byte[] bytes = new byte[length]; + RANDOM.nextBytes(bytes); + Base64.Encoder encoder = Base64.getUrlEncoder().withoutPadding(); + return encoder.encodeToString(bytes); + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java index f9c70a2c19..7e5210e344 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/Tenant.java @@ -28,7 +28,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @EqualsAndHashCode(callSuper = true) -public class Tenant extends ContactBased implements HasTenantId { +public class Tenant extends ContactBased implements HasTenantId, HasTitle { private static final long serialVersionUID = 8057243243859922101L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java index 9db30fd466..34fec2cd82 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/Alarm.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.common.data.alarm; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.JsonNode; import io.swagger.annotations.ApiModel; @@ -22,6 +23,7 @@ import io.swagger.annotations.ApiModelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; +import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.HasCustomerId; import org.thingsboard.server.common.data.HasName; @@ -30,6 +32,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @@ -40,8 +43,10 @@ import java.util.List; */ @ApiModel @Data +@EqualsAndHashCode(callSuper = true) @Builder @AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) public class Alarm extends BaseData implements HasName, HasTenantId, HasCustomerId { @ApiModelProperty(position = 3, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) @@ -58,25 +63,31 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha private EntityId originator; @ApiModelProperty(position = 8, required = true, value = "Alarm severity", example = "CRITICAL") private AlarmSeverity severity; - @ApiModelProperty(position = 9, required = true, value = "Alarm status", example = "CLEARED_UNACK") - private AlarmStatus status; - @ApiModelProperty(position = 10, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") + @ApiModelProperty(position = 9, required = true, value = "Acknowledged", example = "true") + private boolean acknowledged; + @ApiModelProperty(position = 10, required = true, value = "Cleared", example = "false") + private boolean cleared; + @ApiModelProperty(position = 11, value = "Alarm assignee user id") + private UserId assigneeId; + @ApiModelProperty(position = 12, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") private long startTs; - @ApiModelProperty(position = 11, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") + @ApiModelProperty(position = 13, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") private long endTs; - @ApiModelProperty(position = 12, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948") + @ApiModelProperty(position = 14, value = "Timestamp of the alarm acknowledgement, in milliseconds", example = "1634115221948") private long ackTs; - @ApiModelProperty(position = 13, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465") + @ApiModelProperty(position = 15, value = "Timestamp of the alarm clearing, in milliseconds", example = "1634114528465") private long clearTs; - @ApiModelProperty(position = 14, value = "JSON object with alarm details") + @ApiModelProperty(position = 16, value = "Timestamp of the alarm assignment, in milliseconds", example = "1634115928465") + private long assignTs; + @ApiModelProperty(position = 17, value = "JSON object with alarm details") private transient JsonNode details; - @ApiModelProperty(position = 15, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") + @ApiModelProperty(position = 18, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") private boolean propagate; - @ApiModelProperty(position = 16, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") + @ApiModelProperty(position = 19, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") private boolean propagateToOwner; - @ApiModelProperty(position = 17, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") + @ApiModelProperty(position = 20, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") private boolean propagateToTenant; - @ApiModelProperty(position = 18, value = "JSON array of relation types that should be used for propagation. " + + @ApiModelProperty(position = 21, value = "JSON array of relation types that should be used for propagation. " + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") private List propagateRelationTypes; @@ -97,11 +108,14 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha this.type = alarm.getType(); this.originator = alarm.getOriginator(); this.severity = alarm.getSeverity(); - this.status = alarm.getStatus(); + this.assigneeId = alarm.getAssigneeId(); this.startTs = alarm.getStartTs(); this.endTs = alarm.getEndTs(); + this.acknowledged = alarm.isAcknowledged(); this.ackTs = alarm.getAckTs(); this.clearTs = alarm.getClearTs(); + this.cleared = alarm.isCleared(); + this.assignTs = alarm.getAssignTs(); this.details = alarm.getDetails(); this.propagate = alarm.isPropagate(); this.propagateToOwner = alarm.isPropagateToOwner(); @@ -119,7 +133,7 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha @ApiModelProperty(position = 1, value = "JSON object with the alarm Id. " + "Specify this field to update the alarm. " + "Referencing non-existing alarm Id will cause error. " + - "Omit this field to create new alarm." ) + "Omit this field to create new alarm.") @Override public AlarmId getId() { return super.getId(); @@ -132,4 +146,19 @@ public class Alarm extends BaseData implements HasName, HasTenantId, Ha return super.getCreatedTime(); } + @JsonProperty(access = JsonProperty.Access.READ_ONLY) + @ApiModelProperty(position = 22, required = true, value = "status of the Alarm", example = "ACTIVE_UNACK", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + public AlarmStatus getStatus() { + return toStatus(cleared, acknowledged); + } + + public static AlarmStatus toStatus(boolean cleared, boolean acknowledged) { + + if (cleared) { + return acknowledged ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; + } else { + return acknowledged ? AlarmStatus.ACTIVE_ACK : AlarmStatus.ACTIVE_UNACK; + } + } + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java new file mode 100644 index 0000000000..20ed7b549c --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssignee.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.UserId; + +import java.io.Serializable; + +@Builder +@AllArgsConstructor +@Data +public class AlarmAssignee implements Serializable { + + private static final long serialVersionUID = 6628286223963972860L; + + private final UserId id; + private final String firstName; + private final String lastName; + private final String email; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssigneeUpdate.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssigneeUpdate.java new file mode 100644 index 0000000000..d1f32e927b --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmAssigneeUpdate.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class AlarmAssigneeUpdate implements Serializable { + + private static final long serialVersionUID = -2391676304697483808L; + + private final boolean deleted; + private final AlarmAssignee assignee; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java new file mode 100644 index 0000000000..ecf882e2c9 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmCreateOrUpdateActiveRequest.java @@ -0,0 +1,87 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@Data +@Builder +public class AlarmCreateOrUpdateActiveRequest implements AlarmModificationRequest { + + @NotNull + @ApiModelProperty(position = 1, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private TenantId tenantId; + @ApiModelProperty(position = 2, value = "JSON object with Customer Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private CustomerId customerId; + @NotNull + @ApiModelProperty(position = 3, required = true, value = "representing type of the Alarm", example = "High Temperature Alarm") + @Length(fieldName = "type") + private String type; + @NotNull + @ApiModelProperty(position = 4, required = true, value = "JSON object with alarm originator id") + private EntityId originator; + @NotNull + @ApiModelProperty(position = 5, required = true, value = "Alarm severity", example = "CRITICAL") + private AlarmSeverity severity; + @ApiModelProperty(position = 6, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") + private long startTs; + @ApiModelProperty(position = 7, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") + private long endTs; + @NoXss + @ApiModelProperty(position = 8, value = "JSON object with alarm details") + private JsonNode details; + @Valid + @ApiModelProperty(position = 9, value = "JSON object with propagation details") + private AlarmPropagationInfo propagation; + + private UserId userId; + + public static AlarmCreateOrUpdateActiveRequest fromAlarm(Alarm a) { + return fromAlarm(a, null); + } + + public static AlarmCreateOrUpdateActiveRequest fromAlarm(Alarm a, UserId userId) { + return AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(a.getTenantId()) + .customerId(a.getCustomerId()) + .type(a.getType()) + .originator(a.getOriginator()) + .severity((a.getSeverity())) + .startTs(a.getStartTs()) + .endTs(a.getEndTs()) + .details(a.getDetails()) + .propagation(AlarmPropagationInfo.builder() + .propagate(a.isPropagate()) + .propagateToOwner(a.isPropagateToOwner()) + .propagateToTenant(a.isPropagateToTenant()) + .propagateRelationTypes(a.getPropagateRelationTypes()).build()) + .userId(userId) + .build(); + } + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java index 5483c519db..405097f6f5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmInfo.java @@ -17,15 +17,36 @@ package org.thingsboard.server.common.data.alarm; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.thingsboard.server.common.data.User; +import java.util.Objects; + +@EqualsAndHashCode(callSuper = true) +@ToString(callSuper = true) @ApiModel public class AlarmInfo extends Alarm { private static final long serialVersionUID = 2807343093519543363L; + @Getter + @Setter @ApiModelProperty(position = 19, value = "Alarm originator name", example = "Thermostat") private String originatorName; + @Getter + @Setter + @ApiModelProperty(position = 20, value = "Alarm originator label", example = "Thermostat label") + private String originatorLabel; + + @Getter + @Setter + @ApiModelProperty(position = 21, value = "Alarm assignee") + private AlarmAssignee assignee; + public AlarmInfo() { super(); } @@ -34,35 +55,18 @@ public class AlarmInfo extends Alarm { super(alarm); } - public AlarmInfo(Alarm alarm, String originatorName) { - super(alarm); - this.originatorName = originatorName; - } - - public String getOriginatorName() { - return originatorName; + public AlarmInfo(AlarmInfo alarmInfo) { + super(alarmInfo); + this.originatorName = alarmInfo.originatorName; + this.originatorLabel = alarmInfo.originatorLabel; + this.assignee = alarmInfo.getAssignee(); } - public void setOriginatorName(String originatorName) { + public AlarmInfo(Alarm alarm, String originatorName, String originatorLabel, AlarmAssignee assignee) { + super(alarm); this.originatorName = originatorName; + this.originatorLabel = originatorLabel; + this.assignee = assignee; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - if (!super.equals(o)) return false; - - AlarmInfo alarmInfo = (AlarmInfo) o; - - return originatorName != null ? originatorName.equals(alarmInfo.originatorName) : alarmInfo.originatorName == null; - - } - - @Override - public int hashCode() { - int result = super.hashCode(); - result = 31 * result + (originatorName != null ? originatorName.hashCode() : 0); - return result; - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java new file mode 100644 index 0000000000..e40cb15d22 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmModificationRequest.java @@ -0,0 +1,34 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; + +public interface AlarmModificationRequest { + + TenantId getTenantId(); + + long getStartTs(); + + long getEndTs(); + + void setStartTs(long startTs); + + void setEndTs(long endTs); + + UserId getUserId(); +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java new file mode 100644 index 0000000000..10069f8dbe --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmPropagationInfo.java @@ -0,0 +1,46 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.util.Collections; +import java.util.List; + +@Builder +@Data +public class AlarmPropagationInfo { + + public static AlarmPropagationInfo EMPTY = new AlarmPropagationInfo(false, false, false, Collections.emptyList()); + + @ApiModelProperty(position = 1, value = "Propagation flag to specify if alarm should be propagated to parent entities of alarm originator", example = "true") + private boolean propagate; + @ApiModelProperty(position = 2, value = "Propagation flag to specify if alarm should be propagated to the owner (tenant or customer) of alarm originator", example = "true") + private boolean propagateToOwner; + @ApiModelProperty(position = 3, value = "Propagation flag to specify if alarm should be propagated to the tenant entity", example = "true") + private boolean propagateToTenant; + @NoXss + @ApiModelProperty(position = 4, value = "JSON array of relation types that should be used for propagation. " + + "By default, 'propagateRelationTypes' array is empty which means that the alarm will be propagated based on any relation type to parent entities. " + + "This parameter should be used only in case when 'propagate' parameter is set to true, otherwise, 'propagateRelationTypes' array will be ignored.") + private List propagateRelationTypes; + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java index a923b54abc..e4d0fb32df 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmQuery.java @@ -19,6 +19,7 @@ import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.TimePageLink; /** @@ -33,6 +34,8 @@ public class AlarmQuery { private TimePageLink pageLink; private AlarmSearchStatus searchStatus; private AlarmStatus status; + private UserId assigneeId; + @Deprecated private Boolean fetchOriginator; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java index 92af75c8f0..4225f6efbb 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmSearchStatus.java @@ -15,26 +15,12 @@ */ package org.thingsboard.server.common.data.alarm; -import com.fasterxml.jackson.annotation.JsonIgnore; -import lombok.Getter; - -import java.util.Arrays; -import java.util.LinkedHashSet; -import java.util.Set; - public enum AlarmSearchStatus { - ANY(AlarmStatus.values()), - ACTIVE(AlarmStatus.ACTIVE_ACK, AlarmStatus.ACTIVE_UNACK), - CLEARED(AlarmStatus.CLEARED_ACK, AlarmStatus.CLEARED_UNACK), - ACK(AlarmStatus.ACTIVE_ACK, AlarmStatus.CLEARED_ACK), - UNACK(AlarmStatus.ACTIVE_UNACK, AlarmStatus.CLEARED_UNACK); - - @JsonIgnore - @Getter - private Set statuses; + ANY, + ACTIVE, + CLEARED, + ACK, + UNACK; - AlarmSearchStatus(AlarmStatus... statuses) { - this.statuses = new LinkedHashSet<>(Arrays.asList(statuses)); - } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatusFilter.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatusFilter.java new file mode 100644 index 0000000000..c8f40fa048 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmStatusFilter.java @@ -0,0 +1,118 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import java.util.List; +import java.util.Optional; + +public class AlarmStatusFilter { + + private static final AlarmStatusFilter EMPTY = new AlarmStatusFilter(Optional.empty(), Optional.empty()); + + private final Optional clearFilter; + private final Optional ackFilter; + + private AlarmStatusFilter(Optional clearFilter, Optional ackFilter) { + this.clearFilter = clearFilter; + this.ackFilter = ackFilter; + } + + public static AlarmStatusFilter from(AlarmQuery query) { + if (query.getSearchStatus() != null) { + return AlarmStatusFilter.from(query.getSearchStatus()); + } else if (query.getStatus() != null) { + return AlarmStatusFilter.from(query.getStatus()); + } + return AlarmStatusFilter.empty(); + } + + public static AlarmStatusFilter from(AlarmSearchStatus alarmSearchStatus) { + switch (alarmSearchStatus) { + case ACK: + return new AlarmStatusFilter(Optional.empty(), Optional.of(true)); + case UNACK: + return new AlarmStatusFilter(Optional.empty(), Optional.of(false)); + case ACTIVE: + return new AlarmStatusFilter(Optional.of(false), Optional.empty()); + case CLEARED: + return new AlarmStatusFilter(Optional.of(true), Optional.empty()); + default: + return EMPTY; + } + } + + public static AlarmStatusFilter from(AlarmStatus alarmStatus) { + switch (alarmStatus) { + case ACTIVE_UNACK: + return new AlarmStatusFilter(Optional.of(false), Optional.of(false)); + case ACTIVE_ACK: + return new AlarmStatusFilter(Optional.of(false), Optional.of(true)); + case CLEARED_UNACK: + return new AlarmStatusFilter(Optional.of(true), Optional.of(false)); + case CLEARED_ACK: + return new AlarmStatusFilter(Optional.of(true), Optional.of(true)); + default: + return EMPTY; + } + } + + public static AlarmStatusFilter empty() { + return EMPTY; + } + + public boolean hasAnyFilter() { + return clearFilter.isPresent() || ackFilter.isPresent(); + } + + public boolean hasClearFilter() { + return clearFilter.isPresent(); + } + + public boolean hasAckFilter() { + return ackFilter.isPresent(); + } + + public boolean getClearFilter() { + return clearFilter.orElseThrow(() -> new RuntimeException("Clear filter is not set! Use `hasClearFilter` to check.")); + } + + public boolean getAckFilter() { + return ackFilter.orElseThrow(() -> new RuntimeException("Ack filter is not set! Use `hasAckFilter` to check.")); + } + + + public static AlarmStatusFilter fromList(List list) { + if (list == null || list.isEmpty() || list.contains(AlarmSearchStatus.ANY)) { + return EMPTY; + } + boolean clearFilter = list.contains(AlarmSearchStatus.CLEARED); + boolean activeFilter = list.contains(AlarmSearchStatus.ACTIVE); + Optional clear = Optional.empty(); + if (clearFilter && !activeFilter || !clearFilter && activeFilter) { + clear = Optional.of(clearFilter); + } + + boolean ackFilter = list.contains(AlarmSearchStatus.ACK); + boolean unackFilter = list.contains(AlarmSearchStatus.UNACK); + Optional ack = Optional.empty(); + if (ackFilter && !unackFilter || !ackFilter && unackFilter) { + ack = Optional.of(ackFilter); + } + return new AlarmStatusFilter(clear, ack); + } + + +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java new file mode 100644 index 0000000000..23edafd6d2 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/AlarmUpdateRequest.java @@ -0,0 +1,79 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.alarm; + +import com.fasterxml.jackson.databind.JsonNode; +import io.swagger.annotations.ApiModelProperty; +import lombok.Builder; +import lombok.Data; +import org.thingsboard.server.common.data.id.AlarmId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.NoXss; + +import javax.validation.Valid; +import javax.validation.constraints.NotNull; + +@Data +@Builder +public class AlarmUpdateRequest implements AlarmModificationRequest { + + @NotNull + @ApiModelProperty(position = 1, value = "JSON object with Tenant Id", accessMode = ApiModelProperty.AccessMode.READ_ONLY) + private TenantId tenantId; + @NotNull + @ApiModelProperty(position = 2, value = "JSON object with the alarm Id. " + + "Specify this field to update the alarm. " + + "Referencing non-existing alarm Id will cause error. " + + "Omit this field to create new alarm.") + private AlarmId alarmId; + @NotNull + @ApiModelProperty(position = 3, required = true, value = "Alarm severity", example = "CRITICAL") + private AlarmSeverity severity; + @ApiModelProperty(position = 4, value = "Timestamp of the alarm start time, in milliseconds", example = "1634058704565") + private long startTs; + @ApiModelProperty(position = 5, value = "Timestamp of the alarm end time(last time update), in milliseconds", example = "1634111163522") + private long endTs; + @NoXss + @ApiModelProperty(position = 6, value = "JSON object with alarm details") + private JsonNode details; + @Valid + @ApiModelProperty(position = 7, value = "JSON object with propagation details") + private AlarmPropagationInfo propagation; + + private UserId userId; + + public static AlarmUpdateRequest fromAlarm(Alarm a) { + return fromAlarm(a, null); + } + + public static AlarmUpdateRequest fromAlarm(Alarm a, UserId userId) { + return AlarmUpdateRequest.builder() + .tenantId(a.getTenantId()) + .alarmId(a.getId()) + .severity((a.getSeverity())) + .startTs(a.getStartTs()) + .endTs(a.getEndTs()) + .details(a.getDetails()) + .propagation(AlarmPropagationInfo.builder() + .propagate(a.isPropagate()) + .propagateToOwner(a.isPropagateToOwner()) + .propagateToTenant(a.isPropagateToTenant()) + .propagateRelationTypes(a.getPropagateRelationTypes()).build()) + .userId(userId) + .build(); + } +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java index e15557cdfe..8f25c0cbc5 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/alarm/EntityAlarm.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; @Data @NoArgsConstructor @@ -35,6 +36,7 @@ public class EntityAlarm implements HasTenantId { private String alarmType; private CustomerId customerId; + private UserId assigneeId; private AlarmId alarmId; } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java index 0721b1b310..d5f4efa6c2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/asset/Asset.java @@ -23,6 +23,7 @@ import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; @@ -37,7 +38,7 @@ import java.util.Optional; @ApiModel @EqualsAndHashCode(callSuper = true) -public class Asset extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId, ExportableEntity { +public class Asset extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId, ExportableEntity { private static final long serialVersionUID = 2807343040519543363L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java index 876444a1dd..b479f1a236 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/audit/ActionType.java @@ -40,6 +40,8 @@ public enum ActionType { ALARM_ACK(false), ALARM_CLEAR(false), ALARM_DELETE(false), + ALARM_ASSIGN(false), + ALARM_UNASSIGN(false), LOGIN(false), LOGOUT(false), LOCKOUT(false), diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java index 9b944d7902..37ef1b65ac 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/DefaultDeviceProfileTransportConfiguration.java @@ -16,7 +16,6 @@ package org.thingsboard.server.common.data.device.profile; import lombok.Data; -import org.thingsboard.server.common.data.DeviceProfileType; import org.thingsboard.server.common.data.DeviceTransportType; @Data diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java index ae36567ae7..d0b83e56d6 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/device/profile/MqttDeviceProfileTransportConfiguration.java @@ -19,6 +19,8 @@ import lombok.Data; import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.validation.NoXss; +import java.util.Set; + @Data public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTransportConfiguration { @@ -27,6 +29,8 @@ public class MqttDeviceProfileTransportConfiguration implements DeviceProfileTra @NoXss private String deviceAttributesTopic = MqttTopics.DEVICE_ATTRIBUTES_TOPIC; private TransportPayloadTypeConfiguration transportPayloadTypeConfiguration; + private boolean sparkplug; + private Set sparkplugAttributesMetricNames; private boolean sendAckOnValidationException; @Override diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java index 34c0c31428..4304abfb1d 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/Edge.java @@ -21,6 +21,7 @@ import lombok.EqualsAndHashCode; import lombok.Setter; import lombok.ToString; import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo; @@ -35,7 +36,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @EqualsAndHashCode(callSuper = true) @ToString @Setter -public class Edge extends SearchTextBasedWithAdditionalInfo implements HasName, HasTenantId, HasCustomerId { +public class Edge extends SearchTextBasedWithAdditionalInfo implements HasLabel, HasTenantId, HasCustomerId { private static final long serialVersionUID = 4934987555236873728L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java index f57881a63b..9b813ae89c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/edge/EdgeEventActionType.java @@ -31,6 +31,8 @@ public enum EdgeEventActionType { RPC_CALL, ALARM_ACK, ALARM_CLEAR, + ALARM_ASSIGN, + ALARM_UNASSIGN, ASSIGNED_TO_EDGE, UNASSIGNED_FROM_EDGE, CREDENTIALS_REQUEST, diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java index c0960347bc..d3eaddf737 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/EntityIdFactory.java @@ -25,6 +25,14 @@ import java.util.UUID; */ public class EntityIdFactory { + public static EntityId getByTypeAndUuid(int type, String uuid) { + return getByTypeAndUuid(EntityType.values()[type], UUID.fromString(uuid)); + } + + public static EntityId getByTypeAndUuid(String type, String uuid) { + return getByTypeAndUuid(EntityType.valueOf(type), UUID.fromString(uuid)); + } + public static EntityId getByTypeAndId(String type, String uuid) { return getByTypeAndUuid(EntityType.valueOf(type), UUID.fromString(uuid)); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/id/NameLabelAndCustomerDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/id/NameLabelAndCustomerDetails.java new file mode 100644 index 0000000000..8d7413b438 --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/id/NameLabelAndCustomerDetails.java @@ -0,0 +1,27 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data.id; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class NameLabelAndCustomerDetails { + private final String name; + private final String label; + private final CustomerId customerId; +} diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java index d63a5a95bd..1ae434f28c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmData.java @@ -15,24 +15,36 @@ */ package org.thingsboard.server.common.data.query; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; +import org.thingsboard.server.common.data.alarm.AlarmAssigneeUpdate; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.id.EntityId; import java.util.HashMap; import java.util.Map; -import java.util.UUID; +@EqualsAndHashCode(callSuper = true) public class AlarmData extends AlarmInfo { + private static final long serialVersionUID = -7042457913823369638L; + @Getter private final EntityId entityId; @Getter private final Map> latest; - public AlarmData(Alarm alarm, String originatorName, EntityId entityId) { - super(alarm, originatorName); + public AlarmData(AlarmInfo main, AlarmData prototype) { + super(main); + this.entityId = prototype.entityId; + this.latest = new HashMap<>(); + this.latest.putAll(prototype.getLatest()); + } + + public AlarmData(Alarm alarm, EntityId entityId) { + super(alarm); this.entityId = entityId; this.latest = new HashMap<>(); } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java index e46deddfd7..9c0253272c 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/query/AlarmDataPageLink.java @@ -19,11 +19,10 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Data; import lombok.EqualsAndHashCode; -import lombok.Getter; import lombok.ToString; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.id.UserId; import java.util.List; @@ -41,6 +40,7 @@ public class AlarmDataPageLink extends EntityDataPageLink { private List statusList; private List severityList; private boolean searchPropagatedAlarms; + private UserId assigneeId; public AlarmDataPageLink() { super(); @@ -49,7 +49,8 @@ public class AlarmDataPageLink extends EntityDataPageLink { public AlarmDataPageLink(int pageSize, int page, String textSearch, EntityDataSortOrder sortOrder, boolean dynamic, boolean searchPropagatedAlarms, long startTs, long endTs, long timeWindow, - List typeList, List statusList, List severityList) { + List typeList, List statusList, List severityList, + UserId assigneeId) { super(pageSize, page, textSearch, sortOrder, dynamic); this.searchPropagatedAlarms = searchPropagatedAlarms; this.startTs = startTs; @@ -58,6 +59,7 @@ public class AlarmDataPageLink extends EntityDataPageLink { this.typeList = typeList; this.statusList = statusList; this.severityList = severityList; + this.assigneeId = assigneeId; } @JsonIgnore @@ -65,7 +67,8 @@ public class AlarmDataPageLink extends EntityDataPageLink { return new AlarmDataPageLink(this.getPageSize(), this.getPage() + 1, this.getTextSearch(), this.getSortOrder(), this.isDynamic(), this.searchPropagatedAlarms, this.startTs, this.endTs, this.timeWindow, - this.typeList, this.statusList, this.severityList + this.typeList, this.statusList, this.severityList, + this.assigneeId ); } } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java index 900fd966e8..c9bd9fbad2 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/security/UserCredentials.java @@ -15,10 +15,20 @@ */ package org.thingsboard.server.common.data.security; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.JsonNode; import lombok.EqualsAndHashCode; import org.thingsboard.server.common.data.BaseData; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.validation.Length; +import org.thingsboard.server.common.data.validation.NoXss; + +import java.util.Arrays; +import java.util.Objects; + +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.getJson; +import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalInfo.setJson; @EqualsAndHashCode(callSuper = true) public class UserCredentials extends BaseData { @@ -30,6 +40,20 @@ public class UserCredentials extends BaseData { private String password; private String activateToken; private String resetToken; + + @NoXss + private transient JsonNode additionalInfo; + + @JsonIgnore + private byte[] additionalInfoBytes; + + public JsonNode getAdditionalInfo() { + return getJson(() -> additionalInfo, () -> additionalInfoBytes); + } + + public void setAdditionalInfo(JsonNode settings) { + setJson(settings, json -> this.additionalInfo = json, bytes -> this.additionalInfoBytes = bytes); + } public UserCredentials() { super(); @@ -46,6 +70,7 @@ public class UserCredentials extends BaseData { this.enabled = userCredentials.isEnabled(); this.activateToken = userCredentials.getActivateToken(); this.resetToken = userCredentials.getResetToken(); + setAdditionalInfo(userCredentials.getAdditionalInfo()); } public UserId getUserId() { diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java index d47876b268..a7e9fee82a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/BaseWidgetType.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data.widget; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import org.thingsboard.server.common.data.BaseData; +import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetTypeId; @@ -25,7 +26,7 @@ import org.thingsboard.server.common.data.validation.Length; import org.thingsboard.server.common.data.validation.NoXss; @Data -public class BaseWidgetType extends BaseData implements HasTenantId { +public class BaseWidgetType extends BaseData implements HasName, HasTenantId { private static final long serialVersionUID = 8388684344603660756L; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java index 876438f834..679dead962 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/widget/WidgetsBundle.java @@ -25,6 +25,7 @@ import lombok.Setter; import org.thingsboard.server.common.data.ExportableEntity; import org.thingsboard.server.common.data.HasName; import org.thingsboard.server.common.data.HasTenantId; +import org.thingsboard.server.common.data.HasTitle; import org.thingsboard.server.common.data.SearchTextBased; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.WidgetsBundleId; @@ -33,7 +34,7 @@ import org.thingsboard.server.common.data.validation.NoXss; @ApiModel @EqualsAndHashCode(callSuper = true) -public class WidgetsBundle extends SearchTextBased implements HasName, HasTenantId, ExportableEntity { +public class WidgetsBundle extends SearchTextBased implements HasName, HasTenantId, ExportableEntity, HasTitle { private static final long serialVersionUID = -7627368878362410489L; diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java index e2af72892f..cad0d9370c 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/UUIDConverterTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.common.data; import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @@ -58,9 +59,11 @@ public class UUIDConverterTest { Assert.assertEquals(UUID.fromString("58e0a7d7-eebc-11d8-9669-0800200c9a66"), result); } - @Test(expected = IllegalArgumentException.class) + @Test public void nonV1UuidToStringTest() { - UUIDConverter.fromTimeUUID(UUID.fromString("58e0a7d7-eebc-01d8-9669-0800200c9a66")); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + UUIDConverter.fromTimeUUID(UUID.fromString("58e0a7d7-eebc-01d8-9669-0800200c9a66")); + }); } @Test diff --git a/common/transport/mqtt/pom.xml b/common/transport/mqtt/pom.xml index 9604110e13..ae0b35e44d 100644 --- a/common/transport/mqtt/pom.xml +++ b/common/transport/mqtt/pom.xml @@ -97,6 +97,19 @@ awaitility test
+ + com.google.protobuf + protobuf-java + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + + + + diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java index c64fe57b94..0d43fc6c93 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/MqttTransportHandler.java @@ -17,11 +17,11 @@ package org.thingsboard.server.transport.mqtt; import com.fasterxml.jackson.databind.JsonNode; import com.google.gson.JsonParseException; +import com.google.protobuf.InvalidProtocolBufferException; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.handler.codec.mqtt.MqttConnAckMessage; -import io.netty.handler.codec.mqtt.MqttConnAckVariableHeader; import io.netty.handler.codec.mqtt.MqttConnectMessage; import io.netty.handler.codec.mqtt.MqttConnectReturnCode; import io.netty.handler.codec.mqtt.MqttFixedHeader; @@ -43,6 +43,8 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.GenericFutureListener; import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.ResponseCode; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -50,6 +52,8 @@ import org.thingsboard.server.common.data.DeviceTransportType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.TransportPayloadType; import org.thingsboard.server.common.data.device.profile.MqttTopics; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.OtaPackageId; import org.thingsboard.server.common.data.ota.OtaPackageType; @@ -69,13 +73,20 @@ import org.thingsboard.server.common.transport.util.SslUtil; import org.thingsboard.server.gen.transport.TransportProtos; import org.thingsboard.server.gen.transport.TransportProtos.ProvisionDeviceResponseMsg; import org.thingsboard.server.gen.transport.TransportProtos.ValidateDeviceX509CertRequestMsg; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; import org.thingsboard.server.queue.scheduler.SchedulerComponent; import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; import org.thingsboard.server.transport.mqtt.session.DeviceSessionCtx; import org.thingsboard.server.transport.mqtt.session.GatewaySessionHandler; import org.thingsboard.server.transport.mqtt.session.MqttTopicMatcher; +import org.thingsboard.server.transport.mqtt.session.SparkplugNodeSessionHandler; import org.thingsboard.server.transport.mqtt.util.ReturnCode; import org.thingsboard.server.transport.mqtt.util.ReturnCodeResolver; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcRequestHeader; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcResponseBody; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; import javax.net.ssl.SSLPeerUnverifiedException; import java.io.IOException; @@ -84,6 +95,7 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.UUID; @@ -95,15 +107,19 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import static com.amazonaws.util.StringUtils.UTF8; -import static io.netty.handler.codec.mqtt.MqttMessageType.CONNACK; import static io.netty.handler.codec.mqtt.MqttMessageType.CONNECT; import static io.netty.handler.codec.mqtt.MqttMessageType.PINGRESP; import static io.netty.handler.codec.mqtt.MqttMessageType.SUBACK; -import static io.netty.handler.codec.mqtt.MqttMessageType.UNSUBACK; import static io.netty.handler.codec.mqtt.MqttQoS.AT_LEAST_ONCE; import static io.netty.handler.codec.mqtt.MqttQoS.AT_MOST_ONCE; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NDEATH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProto; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.parseTopicPublish; /** * @author Andrew Shvayka @@ -120,7 +136,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement private static final MqttQoS MAX_SUPPORTED_QOS_LVL = AT_LEAST_ONCE; private final UUID sessionId; - private final MqttTransportContext context; + protected final MqttTransportContext context; private final TransportService transportService; private final SchedulerComponent scheduler; private final SslHandler sslHandler; @@ -129,6 +145,7 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement final DeviceSessionCtx deviceSessionCtx; volatile InetSocketAddress address; volatile GatewaySessionHandler gatewaySessionHandler; + volatile SparkplugNodeSessionHandler sparkplugSessionHandler; private final ConcurrentHashMap otaPackSessions; private final ConcurrentHashMap chunkSizes; @@ -320,12 +337,15 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement String topicName = mqttMsg.variableHeader().topicName(); int msgId = mqttMsg.variableHeader().packetId(); log.trace("[{}][{}] Processing publish msg [{}][{}]!", sessionId, deviceSessionCtx.getDeviceId(), topicName, msgId); - if (topicName.startsWith(MqttTopics.BASE_GATEWAY_API_TOPIC)) { if (gatewaySessionHandler != null) { handleGatewayPublishMsg(ctx, topicName, msgId, mqttMsg); transportService.reportActivity(deviceSessionCtx.getSessionInfo()); + } else { + log.error("[gatewaySessionHandler] is null, [{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId); } + } else if (sparkplugSessionHandler != null) { + handleSparkplugPublishMsg(ctx, topicName, mqttMsg); } else { processDevicePublish(ctx, mqttMsg, topicName, msgId); } @@ -368,6 +388,46 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private void handleSparkplugPublishMsg(ChannelHandlerContext ctx, String topicName, MqttPublishMessage mqttMsg) { + int msgId = mqttMsg.variableHeader().packetId(); + try { + SparkplugTopic sparkplugTopic = parseTopicPublish(topicName); + if (sparkplugTopic.isNode()) { + // A node topic + SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(ProtoMqttAdaptor.toBytes(mqttMsg.payload())); + switch (sparkplugTopic.getType()) { + case NBIRTH: + case NCMD: + case NDATA: + sparkplugSessionHandler.onAttributesTelemetryProto(msgId, sparkplugBProtoNode, deviceSessionCtx.getDeviceInfo().getDeviceName(), sparkplugTopic); + break; + default: + } + } else { + // A device topic + SparkplugBProto.Payload sparkplugBProtoDevice = SparkplugBProto.Payload.parseFrom(ProtoMqttAdaptor.toBytes(mqttMsg.payload())); + switch (sparkplugTopic.getType()) { + case DBIRTH: + case DCMD: + case DDATA: + sparkplugSessionHandler.onAttributesTelemetryProto(msgId, sparkplugBProtoDevice, sparkplugTopic.getDeviceId(), sparkplugTopic); + break; + case DDEATH: + sparkplugSessionHandler.onDeviceDisconnect(mqttMsg, sparkplugTopic.getDeviceId()); + break; + default: + } + } + } catch (RuntimeException e) { + log.error("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); + ack(ctx, msgId, ReturnCode.IMPLEMENTATION_SPECIFIC); + ctx.close(); + } catch (AdaptorException | ThingsboardException | InvalidProtocolBufferException e) { + log.error("[{}] Failed to process publish msg [{}][{}]", sessionId, topicName, msgId, e); + sendAckOrCloseSession(ctx, topicName, msgId); + } + } + private void processDevicePublish(ChannelHandlerContext ctx, MqttPublishMessage mqttMsg, String topicName, int msgId) { try { Matcher fwMatcher; @@ -628,69 +688,74 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement String topic = subscription.topicName(); MqttQoS reqQoS = subscription.qualityOfService(); try { - switch (topic) { - case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { - processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); - activityReported = true; - break; - } - case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: { - processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); - activityReported = true; - break; - } - case MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC: { - processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); - activityReported = true; - break; - } - case MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC: { - processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); - activityReported = true; - break; - } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { - processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); - activityReported = true; - break; - } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC: { - processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); - activityReported = true; - break; - } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC: { - processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); - activityReported = true; - break; - } - case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC: { - processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); - activityReported = true; - break; + if (sparkplugSessionHandler != null) { + sparkplugSessionHandler.handleSparkplugSubscribeMsg(grantedQoSList, subscription, reqQoS); + activityReported = true; + } else { + switch (topic) { + case MqttTopics.DEVICE_ATTRIBUTES_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_JSON_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); + activityReported = true; + break; + } + case MqttTopics.DEVICE_ATTRIBUTES_SHORT_PROTO_TOPIC: { + processAttributesSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V1); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_JSON_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_JSON); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_REQUESTS_SUB_SHORT_PROTO_TOPIC: { + processRpcSubscribe(grantedQoSList, topic, reqQoS, TopicType.V2_PROTO); + activityReported = true; + break; + } + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC: + case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC: + case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: + case MqttTopics.GATEWAY_RPC_TOPIC: + case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: + case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: + case MqttTopics.DEVICE_FIRMWARE_RESPONSES_TOPIC: + case MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC: + case MqttTopics.DEVICE_SOFTWARE_RESPONSES_TOPIC: + case MqttTopics.DEVICE_SOFTWARE_ERROR_TOPIC: + registerSubQoS(topic, grantedQoSList, reqQoS); + break; + default: + log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS); + grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), ReturnCode.TOPIC_FILTER_INVALID)); + break; } - case MqttTopics.DEVICE_RPC_RESPONSE_SUB_TOPIC: - case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_TOPIC: - case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_JSON_TOPIC: - case MqttTopics.DEVICE_RPC_RESPONSE_SUB_SHORT_PROTO_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_JSON_TOPIC: - case MqttTopics.DEVICE_ATTRIBUTES_RESPONSES_SHORT_PROTO_TOPIC: - case MqttTopics.GATEWAY_ATTRIBUTES_TOPIC: - case MqttTopics.GATEWAY_RPC_TOPIC: - case MqttTopics.GATEWAY_ATTRIBUTES_RESPONSE_TOPIC: - case MqttTopics.DEVICE_PROVISION_RESPONSE_TOPIC: - case MqttTopics.DEVICE_FIRMWARE_RESPONSES_TOPIC: - case MqttTopics.DEVICE_FIRMWARE_ERROR_TOPIC: - case MqttTopics.DEVICE_SOFTWARE_RESPONSES_TOPIC: - case MqttTopics.DEVICE_SOFTWARE_ERROR_TOPIC: - registerSubQoS(topic, grantedQoSList, reqQoS); - break; - default: - log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS); - grantedQoSList.add(ReturnCodeResolver.getSubscriptionReturnCode(deviceSessionCtx.getMqttVersion(), ReturnCode.TOPIC_FILTER_INVALID)); - break; } } catch (Exception e) { log.warn("[{}] Failed to subscribe to [{}][{}]", sessionId, topic, reqQoS, e); @@ -715,7 +780,16 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement registerSubQoS(topic, grantedQoSList, reqQoS); } - private void registerSubQoS(String topic, List grantedQoSList, MqttQoS reqQoS) { + public void processAttributesRpcSubscribeSparkplugNode(List grantedQoSList, MqttQoS reqQoS) { + transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() + .setSessionInfo(deviceSessionCtx.getSessionInfo()) + .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) + .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) + .build(), null); + registerSubQoS(MqttTopics.DEVICE_ATTRIBUTES_TOPIC, grantedQoSList, reqQoS); + } + + public void registerSubQoS(String topic, List grantedQoSList, MqttQoS reqQoS) { grantedQoSList.add(getMinSupportedQos(reqQoS)); mqttQoSMap.put(new MqttTopicMatcher(topic), getMinSupportedQos(reqQoS)); } @@ -986,6 +1060,39 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement } } + private void checkSparkplugNodeSession(MqttConnectMessage connectMessage, ChannelHandlerContext ctx) { + try { + if (sparkplugSessionHandler == null) { + SparkplugTopic sparkplugTopicNode = validatedSparkplugTopicConnectedNode(connectMessage); + if (sparkplugTopicNode != null) { + SparkplugBProto.Payload sparkplugBProtoNode = SparkplugBProto.Payload.parseFrom(connectMessage.payload().willMessageInBytes()); + sparkplugSessionHandler = new SparkplugNodeSessionHandler(this, deviceSessionCtx, sessionId, sparkplugTopicNode); + sparkplugSessionHandler.onAttributesTelemetryProto(0, sparkplugBProtoNode, + deviceSessionCtx.getDeviceInfo().getDeviceName(), sparkplugTopicNode); + } else { + log.trace("[{}][{}] Failed to fetch sparkplugDevice connect: sparkplugTopicName without SparkplugMessageType.NDEATH.", sessionId, deviceSessionCtx.getDeviceInfo().getDeviceName()); + throw new ThingsboardException("Invalid request body", ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + } catch (Exception e) { + log.trace("[{}][{}] Failed to fetch sparkplugDevice connect, sparkplugTopicName", sessionId, deviceSessionCtx.getDeviceInfo().getDeviceName(), e); + ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SERVER_UNAVAILABLE_5, connectMessage)); + ctx.close(); + } + } + + private SparkplugTopic validatedSparkplugTopicConnectedNode(MqttConnectMessage connectMessage) throws ThingsboardException { + if (StringUtils.isNotBlank(connectMessage.payload().willTopic()) + && connectMessage.payload().willMessageInBytes() != null + && connectMessage.payload().willMessageInBytes().length > 0) { + SparkplugTopic sparkplugTopicNode = parseTopicPublish(connectMessage.payload().willTopic()); + if (NDEATH.equals(sparkplugTopicNode.getType())) { + return sparkplugTopicNode; + } + } + return null; + } + @Override public void operationComplete(Future future) throws Exception { log.trace("[{}] Channel closed!", sessionId); @@ -998,14 +1105,19 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); if (gatewaySessionHandler != null) { - gatewaySessionHandler.onGatewayDisconnect(); + gatewaySessionHandler.onDevicesDisconnect(); + } + if (sparkplugSessionHandler != null) { + // add Msg Telemetry node: key STATE type: String value: OFFLINE ts: sparkplugBProto.getTimestamp() + sparkplugSessionHandler.sendSparkplugStateOnTelemetry(deviceSessionCtx.getSessionInfo(), + deviceSessionCtx.getDeviceInfo().getDeviceName(), OFFLINE, new Date().getTime()); + sparkplugSessionHandler.onDevicesDisconnect(); } deviceSessionCtx.setDisconnected(); } deviceSessionCtx.release(); } - private void onValidateDeviceResponse(ValidateDeviceCredentialsResponse msg, ChannelHandlerContext ctx, MqttConnectMessage connectMessage) { if (!msg.hasDeviceInfo()) { context.onAuthFailure(address); @@ -1032,7 +1144,11 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onSuccess(Void msg) { SessionMetaData sessionMetaData = transportService.registerAsyncSession(deviceSessionCtx.getSessionInfo(), MqttTransportHandler.this); - checkGatewaySession(sessionMetaData); + if (deviceSessionCtx.isSparkplug()) { + checkSparkplugNodeSession(connectMessage, ctx); + } else { + checkGatewaySession(sessionMetaData); + } ctx.writeAndFlush(createMqttConnAckMsg(ReturnCode.SUCCESS, connectMessage)); deviceSessionCtx.setConnected(true); log.debug("[{}] Client connected!", sessionId); @@ -1068,10 +1184,24 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { log.trace("[{}] Received attributes update notification to device", sessionId); - String topic = attrSubTopicType.getAttributesSubTopic(); - MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrSubTopicType); try { - adaptor.convertToPublish(deviceSessionCtx, notification, topic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + if (sparkplugSessionHandler != null) { + log.trace("[{}] Received attributes update notification to sparkplug device", sessionId); + notification.getSharedUpdatedList().forEach(tsKvProto -> { + if (sparkplugSessionHandler.getNodeBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(sparkplugSessionHandler.getSparkplugTopicNode(), + SparkplugMessageType.NCMD); + sparkplugSessionHandler.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(sparkplugSessionHandler::writeAndFlush); + } + }); + } else { + String topic = attrSubTopicType.getAttributesSubTopic(); + MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(attrSubTopicType); + adaptor.convertToPublish(deviceSessionCtx, notification, topic).ifPresent(deviceSessionCtx.getChannel()::writeAndFlush); + } } catch (Exception e) { log.trace("[{}] Failed to convert device attributes update to MQTT msg", sessionId, e); } @@ -1080,47 +1210,84 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement @Override public void onRemoteSessionCloseCommand(UUID sessionId, TransportProtos.SessionCloseNotificationProto sessionCloseNotification) { log.trace("[{}] Received the remote command to close the session: {}", sessionId, sessionCloseNotification.getMessage()); + transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); deviceSessionCtx.getChannel().close(); } @Override public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { log.trace("[{}] Received RPC command to device", sessionId); - String baseTopic = rpcSubTopicType.getRpcRequestTopicBase(); - MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(rpcSubTopicType); try { - adaptor.convertToPublish(deviceSessionCtx, rpcRequest, baseTopic).ifPresent(payload -> { - int msgId = ((MqttPublishMessage) payload).variableHeader().packetId(); - if (isAckExpected(payload)) { - rpcAwaitingAck.put(msgId, rpcRequest); - context.getScheduler().schedule(() -> { - TransportProtos.ToDeviceRpcRequestMsg msg = rpcAwaitingAck.remove(msgId); - if (msg != null) { - transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); - } - }, Math.max(0, Math.min(deviceSessionCtx.getContext().getTimeout(), rpcRequest.getExpirationTime() - System.currentTimeMillis())), TimeUnit.MILLISECONDS); - } - var cf = publish(payload, deviceSessionCtx); - cf.addListener(result -> { - if (result.cause() == null) { - if (!isAckExpected(payload)) { - transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); - } else if (rpcRequest.getPersisted()) { - transportService.process(deviceSessionCtx.getSessionInfo(), rpcRequest, RpcStatus.SENT, TransportServiceCallback.EMPTY); - } - } else { - // TODO: send error - } - }); - }); + if (sparkplugSessionHandler != null) { + handleToSparkplugDeviceRpcRequest(rpcRequest); + } else { + String baseTopic = rpcSubTopicType.getRpcRequestTopicBase(); + MqttTransportAdaptor adaptor = deviceSessionCtx.getAdaptor(rpcSubTopicType); + adaptor.convertToPublish(deviceSessionCtx, rpcRequest, baseTopic) + .ifPresent(payload -> sendToDeviceRpcRequest(payload, rpcRequest, deviceSessionCtx.getSessionInfo())); + } } catch (Exception e) { - transportService.process(deviceSessionCtx.getSessionInfo(), - TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(rpcRequest.getRequestId()).setError("Failed to convert device RPC command to MQTT msg").build(), TransportServiceCallback.EMPTY); log.trace("[{}] Failed to convert device RPC command to MQTT msg", sessionId, e); + this.sendErrorRpcResponse(deviceSessionCtx.getSessionInfo(), rpcRequest.getRequestId(), + ThingsboardErrorCode.INVALID_ARGUMENTS, + "Failed to convert device RPC command to MQTT msg: " + rpcRequest.getMethodName() + rpcRequest.getParams()); } } + private void handleToSparkplugDeviceRpcRequest(TransportProtos.ToDeviceRpcRequestMsg rpcRequest) throws ThingsboardException { + SparkplugMessageType messageType = SparkplugMessageType.parseMessageType(rpcRequest.getMethodName()); + SparkplugRpcRequestHeader header; + if (StringUtils.isNotEmpty(rpcRequest.getParams())) { + header = JacksonUtil.fromString(rpcRequest.getParams(), SparkplugRpcRequestHeader.class); + } else { + header = new SparkplugRpcRequestHeader(); + } + header.setMessageType(messageType.name()); + TransportProtos.TsKvProto tsKvProto = getTsKvProto(header.getMetricName(), header.getValue(), new Date().getTime()); + if (sparkplugSessionHandler.getNodeBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(sparkplugSessionHandler.getSparkplugTopicNode(), + messageType); + sparkplugSessionHandler.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + sparkplugSessionHandler.getNodeBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(payload -> sendToDeviceRpcRequest(payload, rpcRequest, deviceSessionCtx.getSessionInfo())); + } else { + sendErrorRpcResponse(deviceSessionCtx.getSessionInfo(), rpcRequest.getRequestId(), + ThingsboardErrorCode.BAD_REQUEST_PARAMS, "Failed send To Node Rpc Request: " + + rpcRequest.getMethodName() + ". This node does not have a metricName: [" + tsKvProto.getKv().getKey() + "]"); + } + } + + public void sendToDeviceRpcRequest(MqttMessage payload, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) { + int msgId = ((MqttPublishMessage) payload).variableHeader().packetId(); + if (isAckExpected(payload)) { + rpcAwaitingAck.put(msgId, rpcRequest); + context.getScheduler().schedule(() -> { + TransportProtos.ToDeviceRpcRequestMsg msg = rpcAwaitingAck.remove(msgId); + if (msg != null) { + transportService.process(sessionInfo, rpcRequest, RpcStatus.TIMEOUT, TransportServiceCallback.EMPTY); + } + }, Math.max(0, Math.min(deviceSessionCtx.getContext().getTimeout(), rpcRequest.getExpirationTime() - System.currentTimeMillis())), TimeUnit.MILLISECONDS); + } + var cf = publish(payload, deviceSessionCtx); + cf.addListener(result -> { + if (result.cause() == null) { + if (!isAckExpected(payload)) { + transportService.process(sessionInfo, rpcRequest, RpcStatus.DELIVERED, TransportServiceCallback.EMPTY); + } else if (rpcRequest.getPersisted()) { + transportService.process(sessionInfo, rpcRequest, RpcStatus.SENT, TransportServiceCallback.EMPTY); + } + if (sparkplugSessionHandler != null) { + this.sendSuccessRpcResponse(sessionInfo, rpcRequest.getRequestId(), ResponseCode.CONTENT, "Success: " + rpcRequest.getMethodName()); + } + } else { + log.trace("[{}] Failed send To Device Rpc Request [{}]", sessionId, rpcRequest.getMethodName()); + this.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + ThingsboardErrorCode.INVALID_ARGUMENTS, " Failed send To Device Rpc Request: " + rpcRequest.getMethodName()); + } + }); + } + @Override public void onToServerRpcResponse(TransportProtos.ToServerRpcResponseMsg rpcResponse) { log.trace("[{}] Received RPC response from server", sessionId); @@ -1158,4 +1325,16 @@ public class MqttTransportHandler extends ChannelInboundHandlerAdapter implement ctx.close(); } + public void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ThingsboardErrorCode result, String errorMsg) { + String payload = JacksonUtil.toString(SparkplugRpcResponseBody.builder().result(result.name()).error(errorMsg).build()); + TransportProtos.ToDeviceRpcResponseMsg msg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setError(payload).build(); + transportService.process(sessionInfo, msg, null); + } + + public void sendSuccessRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ResponseCode result, String successMsg) { + String payload = JacksonUtil.toString(SparkplugRpcResponseBody.builder().result(result.getName()).result(successMsg).build()); + TransportProtos.ToDeviceRpcResponseMsg msg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder().setRequestId(requestId).setError(payload).build(); + transportService.process(sessionInfo, msg, null); + } + } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java similarity index 93% rename from common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java rename to common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java index a12a590610..e265a4c2f1 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionCtx.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewayDeviceSessionContext.java @@ -35,14 +35,14 @@ import java.util.concurrent.ConcurrentMap; * Created by ashvayka on 19.01.17. */ @Slf4j -public class GatewayDeviceSessionCtx extends MqttDeviceAwareSessionContext implements SessionMsgListener { +public abstract class AbstractGatewayDeviceSessionContext extends MqttDeviceAwareSessionContext implements SessionMsgListener { - private final GatewaySessionHandler parent; + protected final T parent; private final TransportService transportService; - public GatewayDeviceSessionCtx(GatewaySessionHandler parent, TransportDeviceInfo deviceInfo, - DeviceProfile deviceProfile, ConcurrentMap mqttQoSMap, - TransportService transportService) { + public AbstractGatewayDeviceSessionContext(T parent, TransportDeviceInfo deviceInfo, + DeviceProfile deviceProfile, ConcurrentMap mqttQoSMap, + TransportService transportService) { super(UUID.randomUUID(), mqttQoSMap); this.parent = parent; setSessionInfo(SessionInfoProto.newBuilder() diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java new file mode 100644 index 0000000000..2bb59dc9e8 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/AbstractGatewaySessionHandler.java @@ -0,0 +1,767 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.session; + + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonNull; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.ProtocolStringList; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.DeviceId; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.TransportServiceCallback; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; +import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; +import org.thingsboard.server.transport.mqtt.MqttTransportContext; +import org.thingsboard.server.transport.mqtt.MqttTransportHandler; +import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; +import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; +import org.thingsboard.server.transport.mqtt.util.ReturnCode; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; +import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.OFFLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.STATE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.messageName; + +/** + * Created by ashvayka on 19.01.17. + */ +@Slf4j +public abstract class AbstractGatewaySessionHandler { + + protected static final String DEFAULT_DEVICE_TYPE = "default"; + private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; + private static final String DEVICE_PROPERTY = "device"; + + protected final MqttTransportContext context; + protected final TransportService transportService; + protected final TransportDeviceInfo gateway; + protected final UUID sessionId; + private final ConcurrentMap deviceCreationLockMap; + private final ConcurrentMap devices; + private final ConcurrentMap> deviceFutures; + protected final ConcurrentMap mqttQoSMap; + protected final ChannelHandlerContext channel; + protected final DeviceSessionCtx deviceSessionCtx; + + public AbstractGatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { + this.context = deviceSessionCtx.getContext(); + this.transportService = context.getTransportService(); + this.deviceSessionCtx = deviceSessionCtx; + this.gateway = deviceSessionCtx.getDeviceInfo(); + this.sessionId = sessionId; + this.devices = new ConcurrentHashMap<>(); + this.deviceFutures = new ConcurrentHashMap<>(); + this.deviceCreationLockMap = createWeakMap(); + this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); + this.channel = deviceSessionCtx.getChannel(); + } + + ConcurrentReferenceHashMap createWeakMap() { + return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); + } + + public void onDeviceDisconnect(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceDisconnectJson(mqttMsg); + } else { + onGatewayDeviceDisconnectProto(mqttMsg); + } + } + + public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceClaimJson(msgId, payload); + } else { + onDeviceClaimProto(msgId, payload); + } + } + + public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceAttributesJson(msgId, payload); + } else { + onDeviceAttributesProto(msgId, payload); + } + } + + public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { + if (isJsonPayloadType()) { + onDeviceAttributesRequestJson(mqttMsg); + } else { + onDeviceAttributesRequestProto(mqttMsg); + } + } + + public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { + int msgId = getMsgId(mqttMsg); + ByteBuf payload = mqttMsg.payload(); + if (isJsonPayloadType()) { + onDeviceRpcResponseJson(msgId, payload); + } else { + onDeviceRpcResponseProto(msgId, payload); + } + } + + public void onDevicesDisconnect() { + devices.forEach(this::deregisterSession); + } + + public void onDeviceDeleted(String deviceName) { + deregisterSession(deviceName); + } + + public String getNodeId() { + return context.getNodeId(); + } + + public UUID getSessionId() { + return sessionId; + } + + public MqttTransportAdaptor getPayloadAdaptor() { + return deviceSessionCtx.getPayloadAdaptor(); + } + + void deregisterSession(String deviceName) { + MqttDeviceAwareSessionContext deviceSessionCtx = devices.remove(deviceName); + if (deviceSessionCtx != null) { + deregisterSession(deviceName, deviceSessionCtx); + } else { + log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); + } + } + + public ChannelFuture writeAndFlush(MqttMessage mqttMessage) { + return channel.writeAndFlush(mqttMessage); + } + + int nextMsgId() { + return deviceSessionCtx.nextMsgId(); + } + + protected boolean isJsonPayloadType() { + return deviceSessionCtx.isJsonPayloadType(); + } + + protected void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { + log.trace("[{}] onDeviceConnect: {}", sessionId, deviceName); + Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T result) { + ack(msg, ReturnCode.SUCCESS); + log.trace("[{}] onDeviceConnectOk: {}", sessionId, deviceName); + } + + @Override + public void onFailure(Throwable t) { + log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, t); + + } + }, context.getExecutor()); + } + + ListenableFuture onDeviceConnect(String deviceName, String deviceType) { + T result = devices.get(deviceName); + if (result == null) { + Lock deviceCreationLock = deviceCreationLockMap.computeIfAbsent(deviceName, s -> new ReentrantLock()); + deviceCreationLock.lock(); + try { + result = devices.get(deviceName); + if (result == null) { + return getDeviceCreationFuture(deviceName, deviceType); + } else { + return Futures.immediateFuture(result); + } + } finally { + deviceCreationLock.unlock(); + } + } else { + return Futures.immediateFuture(result); + } + } + + private ListenableFuture getDeviceCreationFuture(String deviceName, String deviceType) { + final SettableFuture futureToSet = SettableFuture.create(); + ListenableFuture future = deviceFutures.putIfAbsent(deviceName, futureToSet); + if (future != null) { + return future; + } + try { + transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() + .setDeviceName(deviceName) + .setDeviceType(deviceType) + .setGatewayIdMSB(gateway.getDeviceId().getId().getMostSignificantBits()) + .setGatewayIdLSB(gateway.getDeviceId().getId().getLeastSignificantBits()) + .setSparkplug(this.deviceSessionCtx.isSparkplug()) + .build(), + new TransportServiceCallback<>() { + @Override + public void onSuccess(GetOrCreateDeviceFromGatewayResponse msg) { + T deviceSessionCtx = newDeviceSessionCtx(msg); + if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) { + log.trace("[{}] First got or created device [{}], type [{}] for the gateway session", sessionId, deviceName, deviceType); + SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo(); + transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx); + transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() + .setSessionInfo(deviceSessionInfo) + .setSessionEvent(SESSION_EVENT_MSG_OPEN) + .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) + .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) + .build(), null); + } + futureToSet.set(devices.get(deviceName)); + deviceFutures.remove(deviceName); + } + + @Override + public void onError(Throwable e) { + log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, e); + futureToSet.setException(e); + deviceFutures.remove(deviceName); + } + }); + return futureToSet; + } catch (Throwable e) { + deviceFutures.remove(deviceName); + throw e; + } + } + + protected abstract T newDeviceSessionCtx(GetOrCreateDeviceFromGatewayResponse msg); + + protected int getMsgId(MqttPublishMessage mqttMsg) { + return mqttMsg.variableHeader().packetId(); + } + + protected void onDeviceConnectJson(MqttPublishMessage mqttMsg) throws AdaptorException { + JsonElement json = getJson(mqttMsg); + String deviceName = checkDeviceName(getDeviceName(json)); + String deviceType = getDeviceType(json); + processOnConnect(mqttMsg, deviceName, deviceType); + } + + protected void onDeviceConnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.ConnectMsg connectProto = TransportApiProtos.ConnectMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(connectProto.getDeviceName()); + String deviceType = StringUtils.isEmpty(connectProto.getDeviceType()) ? DEFAULT_DEVICE_TYPE : connectProto.getDeviceType(); + processOnConnect(mqttMsg, deviceName, deviceType); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceDisconnectJson(MqttPublishMessage msg) throws AdaptorException { + String deviceName = checkDeviceName(getDeviceName(getJson(msg))); + processOnDisconnect(msg, deviceName); + } + + protected void onGatewayDeviceDisconnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.DisconnectMsg connectProto = TransportApiProtos.DisconnectMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(connectProto.getDeviceName()); + processOnDisconnect(mqttMsg, deviceName); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + void processOnDisconnect(MqttPublishMessage msg, String deviceName) { + deregisterSession(deviceName); + ack(msg, ReturnCode.SUCCESS); + } + + protected void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + for (Map.Entry deviceEntry : jsonObj.entrySet()) { + String deviceName = deviceEntry.getKey(); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + if (!deviceEntry.getValue().isJsonArray()) { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + try { + TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); + channel.close(); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + protected void onDeviceTelemetryProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); + List deviceMsgList = telemetryMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(deviceMsgList)) { + deviceMsgList.forEach(telemetryMsg -> { + String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); + try { + TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); + processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); + channel.close(); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices telemetry messages is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + public void processPostTelemetryMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); + } + + public TransportProtos.PostTelemetryMsg postTelemetryMsgCreated(TransportProtos.KeyValueProto keyValueProto, long ts) { + List result = new ArrayList<>(); + result.add(keyValueProto); + TransportProtos.PostTelemetryMsg.Builder request = TransportProtos.PostTelemetryMsg.newBuilder(); + TransportProtos.TsKvListProto.Builder builder = TransportProtos.TsKvListProto.newBuilder(); + builder.setTs(ts); + builder.addAllKv(result); + request.addTsKvList(builder.build()); + return request.build(); + } + + private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + for (Map.Entry deviceEntry : jsonObj.entrySet()) { + String deviceName = deviceEntry.getKey(); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + if (!deviceEntry.getValue().isJsonObject()) { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + try { + DeviceId deviceId = deviceCtx.getDeviceId(); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + private void onDeviceClaimProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); + List claimMsgList = claimMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(claimMsgList)) { + claimMsgList.forEach(claimDeviceMsg -> { + String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); + if (claimRequest == null) { + throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); + } + try { + DeviceId deviceId = deviceCtx.getDeviceId(); + TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); + processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, claimRequest, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices claim messages is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processClaimDeviceMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); + } + + private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + for (Map.Entry deviceEntry : jsonObj.entrySet()) { + String deviceName = deviceEntry.getKey(); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + if (!deviceEntry.getValue().isJsonObject()) { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + private void onDeviceAttributesProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); + List attributesMsgList = attributesMsgProto.getMsgList(); + if (!CollectionUtils.isEmpty(attributesMsgList)) { + attributesMsgList.forEach(attributesMsg -> { + String deviceName = checkDeviceName(attributesMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); + if (kvListProto == null) { + throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); + } + try { + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); + throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); + } + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + protected void processPostAttributesMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); + } + + private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + int requestId = jsonObj.get("id").getAsInt(); + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); + boolean clientScope = jsonObj.get("client").getAsBoolean(); + Set keys; + if (jsonObj.has("key")) { + keys = Collections.singleton(jsonObj.get("key").getAsString()); + } else { + JsonArray keysArray = jsonObj.get("keys").getAsJsonArray(); + keys = new HashSet<>(); + for (JsonElement keyObj : keysArray) { + keys.add(keyObj.getAsString()); + } + } + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(msg, deviceName, requestMsg); + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { + try { + TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = TransportApiProtos.GatewayAttributesRequestMsg.parseFrom(getBytes(mqttMsg.payload())); + String deviceName = checkDeviceName(gatewayAttributesRequestMsg.getDeviceName()); + int requestId = gatewayAttributesRequestMsg.getId(); + boolean clientScope = gatewayAttributesRequestMsg.getClient(); + ProtocolStringList keysList = gatewayAttributesRequestMsg.getKeysList(); + Set keys = new HashSet<>(keysList); + TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); + processGetAttributeRequestMessage(mqttMsg, deviceName, requestMsg); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { + JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); + if (json.isJsonObject()) { + JsonObject jsonObj = json.getAsJsonObject(); + String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + Integer requestId = jsonObj.get("id").getAsInt(); + String data = jsonObj.get("data").toString(); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } else { + throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); + } + } + + private void onDeviceRpcResponseProto(int msgId, ByteBuf payload) throws AdaptorException { + try { + TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); + String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + Integer requestId = gatewayRpcResponseMsg.getId(); + String data = gatewayRpcResponseMsg.getData(); + TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() + .setRequestId(requestId).setPayload(data).build(); + processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } catch (RuntimeException | InvalidProtocolBufferException e) { + throw new AdaptorException(e); + } + } + + private void processRpcResponseMsg(MqttDeviceAwareSessionContext deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { + transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); + } + + private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { + int msgId = getMsgId(mqttMsg); + Futures.addCallback(checkDeviceConnected(deviceName), + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable T deviceCtx) { + transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); + } + + @Override + public void onFailure(Throwable t) { + ack(mqttMsg, ReturnCode.IMPLEMENTATION_SPECIFIC); + log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + } + + private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { + TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); + result.setRequestId(requestId); + + if (clientScope) { + result.addAllClientAttributeNames(keys); + } else { + result.addAllSharedAttributeNames(keys); + } + return result.build(); + } + + protected ListenableFuture checkDeviceConnected(String deviceName) { + T ctx = devices.get(deviceName); + if (ctx == null) { + log.debug("[{}] Missing device [{}] for the gateway session", sessionId, deviceName); + return onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); + } else { + return Futures.immediateFuture(ctx); + } + } + + protected String checkDeviceName(String deviceName) { + if (StringUtils.isEmpty(deviceName)) { + throw new RuntimeException("Device name is empty!"); + } else { + return deviceName; + } + } + + private String getDeviceName(JsonElement json) { + return json.getAsJsonObject().get(DEVICE_PROPERTY).getAsString(); + } + + private String getDeviceType(JsonElement json) { + JsonElement type = json.getAsJsonObject().get("type"); + return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString(); + } + + private JsonElement getJson(MqttPublishMessage mqttMsg) throws AdaptorException { + return JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); + } + + protected byte[] getBytes(ByteBuf payload) { + return ProtoMqttAdaptor.toBytes(payload); + } + + protected void ack(MqttPublishMessage msg, ReturnCode returnCode) { + int msgId = getMsgId(msg); + if (msgId > 0) { + writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode)); + } + } + + private void deregisterSession(String deviceName, MqttDeviceAwareSessionContext deviceSessionCtx) { + if (this.deviceSessionCtx.isSparkplug()) { + sendSparkplugStateOnTelemetry(deviceSessionCtx.getSessionInfo(), + deviceSessionCtx.getDeviceInfo().getDeviceName(), OFFLINE, new Date().getTime()); + } + transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); + transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); + log.debug("[{}] Removed device [{}] from the gateway session", sessionId, deviceName); + } + + public void sendSparkplugStateOnTelemetry(TransportProtos.SessionInfoProto sessionInfo, String deviceName, SparkplugConnectionState connectionState, long ts) { + TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); + keyValueProtoBuilder.setKey(messageName(STATE)); + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.STRING_V); + keyValueProtoBuilder.setStringV(connectionState.name()); + TransportProtos.PostTelemetryMsg postTelemetryMsg = postTelemetryMsgCreated(keyValueProtoBuilder.build(), ts); + transportService.process(sessionInfo, postTelemetryMsg, getPubAckCallback(channel, deviceName, -1, postTelemetryMsg)); + } + + private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { + return new TransportServiceCallback() { + @Override + public void onSuccess(Void dummy) { + log.trace("[{}][{}] Published msg: {}", sessionId, deviceName, msg); + if (msgId > 0) { + ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, ReturnCode.SUCCESS)); + } + } + + @Override + public void onError(Throwable e) { + log.trace("[{}] Failed to publish msg: {} for device: {}", sessionId, msg, deviceName, e); + ctx.close(); + } + }; + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java new file mode 100644 index 0000000000..e94a98d287 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewayDeviceSessionContext.java @@ -0,0 +1,37 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.session; + +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; + +import java.util.concurrent.ConcurrentMap; + +/** + * Created by nickAS21 on 26.12.22 + */ +public class GatewayDeviceSessionContext extends AbstractGatewayDeviceSessionContext { + + public GatewayDeviceSessionContext(GatewaySessionHandler parent, + TransportDeviceInfo deviceInfo, + DeviceProfile deviceProfile, + ConcurrentMap mqttQoSMap, + TransportService transportService) { + super(parent, deviceInfo, deviceProfile, mqttQoSMap, transportService); + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java index bf40cb8439..de0f2a502b 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/GatewaySessionHandler.java @@ -15,100 +15,20 @@ */ package org.thingsboard.server.transport.mqtt.session; - -import com.google.common.util.concurrent.FutureCallback; -import com.google.common.util.concurrent.Futures; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.SettableFuture; -import com.google.gson.JsonArray; -import com.google.gson.JsonElement; -import com.google.gson.JsonNull; -import com.google.gson.JsonObject; -import com.google.gson.JsonSyntaxException; -import com.google.protobuf.InvalidProtocolBufferException; -import com.google.protobuf.ProtocolStringList; import io.netty.buffer.ByteBuf; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.handler.codec.mqtt.MqttMessage; import io.netty.handler.codec.mqtt.MqttPublishMessage; -import lombok.extern.slf4j.Slf4j; -import org.springframework.util.CollectionUtils; -import org.springframework.util.ConcurrentReferenceHashMap; -import org.thingsboard.server.common.data.StringUtils; -import org.thingsboard.server.common.data.id.DeviceId; -import org.thingsboard.server.common.transport.TransportService; -import org.thingsboard.server.common.transport.TransportServiceCallback; import org.thingsboard.server.common.transport.adaptor.AdaptorException; -import org.thingsboard.server.common.transport.adaptor.JsonConverter; -import org.thingsboard.server.common.transport.adaptor.ProtoConverter; import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; -import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; -import org.thingsboard.server.gen.transport.TransportApiProtos; -import org.thingsboard.server.gen.transport.TransportProtos; -import org.thingsboard.server.gen.transport.TransportProtos.GetOrCreateDeviceFromGatewayRequestMsg; -import org.thingsboard.server.gen.transport.TransportProtos.SessionInfoProto; -import org.thingsboard.server.transport.mqtt.MqttTransportContext; -import org.thingsboard.server.transport.mqtt.MqttTransportHandler; -import org.thingsboard.server.transport.mqtt.adaptors.JsonMqttAdaptor; -import org.thingsboard.server.transport.mqtt.adaptors.MqttTransportAdaptor; -import org.thingsboard.server.transport.mqtt.adaptors.ProtoMqttAdaptor; -import org.thingsboard.server.transport.mqtt.util.ReturnCode; -import javax.annotation.Nullable; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -import static org.springframework.util.ConcurrentReferenceHashMap.ReferenceType; -import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_CLOSED; -import static org.thingsboard.server.common.transport.service.DefaultTransportService.SESSION_EVENT_MSG_OPEN; -import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG; -import static org.thingsboard.server.common.transport.service.DefaultTransportService.SUBSCRIBE_TO_RPC_ASYNC_MSG; /** - * Created by ashvayka on 19.01.17. + * Created by nickAS21 on 26.12.22 */ -@Slf4j -public class GatewaySessionHandler { - - private static final String DEFAULT_DEVICE_TYPE = "default"; - private static final String CAN_T_PARSE_VALUE = "Can't parse value: "; - private static final String DEVICE_PROPERTY = "device"; - - private final MqttTransportContext context; - private final TransportService transportService; - private final TransportDeviceInfo gateway; - private final UUID sessionId; - private final ConcurrentMap deviceCreationLockMap; - private final ConcurrentMap devices; - private final ConcurrentMap> deviceFutures; - private final ConcurrentMap mqttQoSMap; - private final ChannelHandlerContext channel; - private final DeviceSessionCtx deviceSessionCtx; +public class GatewaySessionHandler extends AbstractGatewaySessionHandler { public GatewaySessionHandler(DeviceSessionCtx deviceSessionCtx, UUID sessionId) { - this.context = deviceSessionCtx.getContext(); - this.transportService = context.getTransportService(); - this.deviceSessionCtx = deviceSessionCtx; - this.gateway = deviceSessionCtx.getDeviceInfo(); - this.sessionId = sessionId; - this.devices = new ConcurrentHashMap<>(); - this.deviceFutures = new ConcurrentHashMap<>(); - this.deviceCreationLockMap = createWeakMap(); - this.mqttQoSMap = deviceSessionCtx.getMqttQoSMap(); - this.channel = deviceSessionCtx.getChannel(); - } - - ConcurrentReferenceHashMap createWeakMap() { - return new ConcurrentReferenceHashMap<>(16, ReferenceType.WEAK); + super(deviceSessionCtx, sessionId); } public void onDeviceConnect(MqttPublishMessage mqttMsg) throws AdaptorException { @@ -119,14 +39,6 @@ public class GatewaySessionHandler { } } - public void onDeviceDisconnect(MqttPublishMessage mqttMsg) throws AdaptorException { - if (isJsonPayloadType()) { - onDeviceDisconnectJson(mqttMsg); - } else { - onDeviceDisconnectProto(mqttMsg); - } - } - public void onDeviceTelemetry(MqttPublishMessage mqttMsg) throws AdaptorException { int msgId = getMsgId(mqttMsg); ByteBuf payload = mqttMsg.payload(); @@ -137,614 +49,9 @@ public class GatewaySessionHandler { } } - public void onDeviceClaim(MqttPublishMessage mqttMsg) throws AdaptorException { - int msgId = getMsgId(mqttMsg); - ByteBuf payload = mqttMsg.payload(); - if (isJsonPayloadType()) { - onDeviceClaimJson(msgId, payload); - } else { - onDeviceClaimProto(msgId, payload); - } - } - - public void onDeviceAttributes(MqttPublishMessage mqttMsg) throws AdaptorException { - int msgId = getMsgId(mqttMsg); - ByteBuf payload = mqttMsg.payload(); - if (isJsonPayloadType()) { - onDeviceAttributesJson(msgId, payload); - } else { - onDeviceAttributesProto(msgId, payload); - } - } - - public void onDeviceAttributesRequest(MqttPublishMessage mqttMsg) throws AdaptorException { - if (isJsonPayloadType()) { - onDeviceAttributesRequestJson(mqttMsg); - } else { - onDeviceAttributesRequestProto(mqttMsg); - } - } - - public void onDeviceRpcResponse(MqttPublishMessage mqttMsg) throws AdaptorException { - int msgId = getMsgId(mqttMsg); - ByteBuf payload = mqttMsg.payload(); - if (isJsonPayloadType()) { - onDeviceRpcResponseJson(msgId, payload); - } else { - onDeviceRpcResponseProto(msgId, payload); - } - } - - public void onGatewayDisconnect() { - devices.forEach(this::deregisterSession); - } - - public void onDeviceDeleted(String deviceName) { - deregisterSession(deviceName); - } - - public String getNodeId() { - return context.getNodeId(); - } - - public UUID getSessionId() { - return sessionId; - } - - public MqttTransportAdaptor getPayloadAdaptor() { - return deviceSessionCtx.getPayloadAdaptor(); - } - - void deregisterSession(String deviceName) { - GatewayDeviceSessionCtx deviceSessionCtx = devices.remove(deviceName); - if (deviceSessionCtx != null) { - deregisterSession(deviceName, deviceSessionCtx); - } else { - log.debug("[{}] Device [{}] was already removed from the gateway session", sessionId, deviceName); - } - } - - ChannelFuture writeAndFlush(MqttMessage mqttMessage) { - return channel.writeAndFlush(mqttMessage); - } - - int nextMsgId() { - return deviceSessionCtx.nextMsgId(); - } - - private boolean isJsonPayloadType() { - return deviceSessionCtx.isJsonPayloadType(); - } - - private void processOnConnect(MqttPublishMessage msg, String deviceName, String deviceType) { - log.trace("[{}] onDeviceConnect: {}", sessionId, deviceName); - Futures.addCallback(onDeviceConnect(deviceName, deviceType), new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx result) { - ack(msg, ReturnCode.SUCCESS); - log.trace("[{}] onDeviceConnectOk: {}", sessionId, deviceName); - } - - @Override - public void onFailure(Throwable t) { - log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, t); - - } - }, context.getExecutor()); - } - - private ListenableFuture onDeviceConnect(String deviceName, String deviceType) { - GatewayDeviceSessionCtx result = devices.get(deviceName); - if (result == null) { - Lock deviceCreationLock = deviceCreationLockMap.computeIfAbsent(deviceName, s -> new ReentrantLock()); - deviceCreationLock.lock(); - try { - result = devices.get(deviceName); - if (result == null) { - return getDeviceCreationFuture(deviceName, deviceType); - } else { - return Futures.immediateFuture(result); - } - } finally { - deviceCreationLock.unlock(); - } - } else { - return Futures.immediateFuture(result); - } - } - - private ListenableFuture getDeviceCreationFuture(String deviceName, String deviceType) { - final SettableFuture futureToSet = SettableFuture.create(); - ListenableFuture future = deviceFutures.putIfAbsent(deviceName, futureToSet); - if (future != null) { - return future; - } - try { - transportService.process(GetOrCreateDeviceFromGatewayRequestMsg.newBuilder() - .setDeviceName(deviceName) - .setDeviceType(deviceType) - .setGatewayIdMSB(gateway.getDeviceId().getId().getMostSignificantBits()) - .setGatewayIdLSB(gateway.getDeviceId().getId().getLeastSignificantBits()).build(), - new TransportServiceCallback() { - @Override - public void onSuccess(GetOrCreateDeviceFromGatewayResponse msg) { - GatewayDeviceSessionCtx deviceSessionCtx = new GatewayDeviceSessionCtx(GatewaySessionHandler.this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap, transportService); - if (devices.putIfAbsent(deviceName, deviceSessionCtx) == null) { - log.trace("[{}] First got or created device [{}], type [{}] for the gateway session", sessionId, deviceName, deviceType); - SessionInfoProto deviceSessionInfo = deviceSessionCtx.getSessionInfo(); - transportService.registerAsyncSession(deviceSessionInfo, deviceSessionCtx); - transportService.process(TransportProtos.TransportToDeviceActorMsg.newBuilder() - .setSessionInfo(deviceSessionInfo) - .setSessionEvent(SESSION_EVENT_MSG_OPEN) - .setSubscribeToAttributes(SUBSCRIBE_TO_ATTRIBUTE_UPDATES_ASYNC_MSG) - .setSubscribeToRPC(SUBSCRIBE_TO_RPC_ASYNC_MSG) - .build(), null); - } - futureToSet.set(devices.get(deviceName)); - deviceFutures.remove(deviceName); - } - - @Override - public void onError(Throwable e) { - log.warn("[{}] Failed to process device connect command: {}", sessionId, deviceName, e); - futureToSet.setException(e); - deviceFutures.remove(deviceName); - } - }); - return futureToSet; - } catch (Throwable e) { - deviceFutures.remove(deviceName); - throw e; - } - } - - private int getMsgId(MqttPublishMessage mqttMsg) { - return mqttMsg.variableHeader().packetId(); - } - - private void onDeviceConnectJson(MqttPublishMessage mqttMsg) throws AdaptorException { - JsonElement json = getJson(mqttMsg); - String deviceName = checkDeviceName(getDeviceName(json)); - String deviceType = getDeviceType(json); - processOnConnect(mqttMsg, deviceName, deviceType); - } - - private void onDeviceConnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { - try { - TransportApiProtos.ConnectMsg connectProto = TransportApiProtos.ConnectMsg.parseFrom(getBytes(mqttMsg.payload())); - String deviceName = checkDeviceName(connectProto.getDeviceName()); - String deviceType = StringUtils.isEmpty(connectProto.getDeviceType()) ? DEFAULT_DEVICE_TYPE : connectProto.getDeviceType(); - processOnConnect(mqttMsg, deviceName, deviceType); - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void onDeviceDisconnectJson(MqttPublishMessage msg) throws AdaptorException { - String deviceName = checkDeviceName(getDeviceName(getJson(msg))); - processOnDisconnect(msg, deviceName); - } - - private void onDeviceDisconnectProto(MqttPublishMessage mqttMsg) throws AdaptorException { - try { - TransportApiProtos.DisconnectMsg connectProto = TransportApiProtos.DisconnectMsg.parseFrom(getBytes(mqttMsg.payload())); - String deviceName = checkDeviceName(connectProto.getDeviceName()); - processOnDisconnect(mqttMsg, deviceName); - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processOnDisconnect(MqttPublishMessage msg, String deviceName) { - deregisterSession(deviceName); - ack(msg, ReturnCode.SUCCESS); - } - - private void onDeviceTelemetryJson(int msgId, ByteBuf payload) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - for (Map.Entry deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback<>() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - if (!deviceEntry.getValue().isJsonArray()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - try { - TransportProtos.PostTelemetryMsg postTelemetryMsg = JsonConverter.convertToTelemetryProto(deviceEntry.getValue().getAsJsonArray()); - processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); - channel.close(); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - } - - private void onDeviceTelemetryProto(int msgId, ByteBuf payload) throws AdaptorException { - try { - TransportApiProtos.GatewayTelemetryMsg telemetryMsgProto = TransportApiProtos.GatewayTelemetryMsg.parseFrom(getBytes(payload)); - List deviceMsgList = telemetryMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(deviceMsgList)) { - deviceMsgList.forEach(telemetryMsg -> { - String deviceName = checkDeviceName(telemetryMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - TransportProtos.PostTelemetryMsg msg = telemetryMsg.getMsg(); - try { - TransportProtos.PostTelemetryMsg postTelemetryMsg = ProtoConverter.validatePostTelemetryMsg(msg.toByteArray()); - processPostTelemetryMsg(deviceCtx, postTelemetryMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, msg, e); - channel.close(); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { - log.debug("[{}] Devices telemetry messages is empty for: [{}]", sessionId, gateway.getDeviceId()); - throw new IllegalArgumentException("[" + sessionId + "] Devices telemetry messages is empty for [" + gateway.getDeviceId() + "]"); - } - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processPostTelemetryMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostTelemetryMsg postTelemetryMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), postTelemetryMsg, getPubAckCallback(channel, deviceName, msgId, postTelemetryMsg)); - } - - private void onDeviceClaimJson(int msgId, ByteBuf payload) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - for (Map.Entry deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - if (!deviceEntry.getValue().isJsonObject()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - try { - DeviceId deviceId = deviceCtx.getDeviceId(); - TransportProtos.ClaimDeviceMsg claimDeviceMsg = JsonConverter.convertToClaimDeviceProto(deviceId, deviceEntry.getValue()); - processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, deviceEntry.getValue(), e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - } - - private void onDeviceClaimProto(int msgId, ByteBuf payload) throws AdaptorException { - try { - TransportApiProtos.GatewayClaimMsg claimMsgProto = TransportApiProtos.GatewayClaimMsg.parseFrom(getBytes(payload)); - List claimMsgList = claimMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(claimMsgList)) { - claimMsgList.forEach(claimDeviceMsg -> { - String deviceName = checkDeviceName(claimDeviceMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - TransportApiProtos.ClaimDevice claimRequest = claimDeviceMsg.getClaimRequest(); - if (claimRequest == null) { - throw new IllegalArgumentException("Claim request for device: " + deviceName + " is null!"); - } - try { - DeviceId deviceId = deviceCtx.getDeviceId(); - TransportProtos.ClaimDeviceMsg claimDeviceMsg = ProtoConverter.convertToClaimDeviceProto(deviceId, claimRequest.toByteArray()); - processClaimDeviceMsg(deviceCtx, claimDeviceMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to convert claim message: {}", gateway.getDeviceId(), deviceName, claimRequest, e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device claiming command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { - log.debug("[{}] Devices claim messages is empty for: [{}]", sessionId, gateway.getDeviceId()); - throw new IllegalArgumentException("[" + sessionId + "] Devices claim messages is empty for [" + gateway.getDeviceId() + "]"); - } - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processClaimDeviceMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ClaimDeviceMsg claimDeviceMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), claimDeviceMsg, getPubAckCallback(channel, deviceName, msgId, claimDeviceMsg)); - } - - private void onDeviceAttributesJson(int msgId, ByteBuf payload) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - for (Map.Entry deviceEntry : jsonObj.entrySet()) { - String deviceName = deviceEntry.getKey(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - if (!deviceEntry.getValue().isJsonObject()) { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - TransportProtos.PostAttributeMsg postAttributeMsg = JsonConverter.convertToAttributesProto(deviceEntry.getValue().getAsJsonObject()); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - } - - private void onDeviceAttributesProto(int msgId, ByteBuf payload) throws AdaptorException { - try { - TransportApiProtos.GatewayAttributesMsg attributesMsgProto = TransportApiProtos.GatewayAttributesMsg.parseFrom(getBytes(payload)); - List attributesMsgList = attributesMsgProto.getMsgList(); - if (!CollectionUtils.isEmpty(attributesMsgList)) { - attributesMsgList.forEach(attributesMsg -> { - String deviceName = checkDeviceName(attributesMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); - if (kvListProto == null) { - throw new IllegalArgumentException("Attributes List for device: " + deviceName + " is empty!"); - } - try { - TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); - processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); - } catch (Throwable e) { - log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); - } - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - }); - } else { - log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); - throw new IllegalArgumentException("[" + sessionId + "] Devices attributes keys list is empty for [" + gateway.getDeviceId() + "]"); - } - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processPostAttributesMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.PostAttributeMsg postAttributeMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), postAttributeMsg, getPubAckCallback(channel, deviceName, msgId, postAttributeMsg)); - } - - private void onDeviceAttributesRequestJson(MqttPublishMessage msg) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, msg.payload()); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - int requestId = jsonObj.get("id").getAsInt(); - String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); - boolean clientScope = jsonObj.get("client").getAsBoolean(); - Set keys; - if (jsonObj.has("key")) { - keys = Collections.singleton(jsonObj.get("key").getAsString()); - } else { - JsonArray keysArray = jsonObj.get("keys").getAsJsonArray(); - keys = new HashSet<>(); - for (JsonElement keyObj : keysArray) { - keys.add(keyObj.getAsString()); - } - } - TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); - processGetAttributeRequestMessage(msg, deviceName, requestMsg); - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } - } - - private void onDeviceAttributesRequestProto(MqttPublishMessage mqttMsg) throws AdaptorException { - try { - TransportApiProtos.GatewayAttributesRequestMsg gatewayAttributesRequestMsg = TransportApiProtos.GatewayAttributesRequestMsg.parseFrom(getBytes(mqttMsg.payload())); - String deviceName = checkDeviceName(gatewayAttributesRequestMsg.getDeviceName()); - int requestId = gatewayAttributesRequestMsg.getId(); - boolean clientScope = gatewayAttributesRequestMsg.getClient(); - ProtocolStringList keysList = gatewayAttributesRequestMsg.getKeysList(); - Set keys = new HashSet<>(keysList); - TransportProtos.GetAttributeRequestMsg requestMsg = toGetAttributeRequestMsg(requestId, clientScope, keys); - processGetAttributeRequestMessage(mqttMsg, deviceName, requestMsg); - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void onDeviceRpcResponseJson(int msgId, ByteBuf payload) throws AdaptorException { - JsonElement json = JsonMqttAdaptor.validateJsonPayload(sessionId, payload); - if (json.isJsonObject()) { - JsonObject jsonObj = json.getAsJsonObject(); - String deviceName = jsonObj.get(DEVICE_PROPERTY).getAsString(); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - Integer requestId = jsonObj.get("id").getAsInt(); - String data = jsonObj.get("data").toString(); - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestId).setPayload(data).build(); - processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } else { - throw new JsonSyntaxException(CAN_T_PARSE_VALUE + json); - } + @Override + protected GatewayDeviceSessionContext newDeviceSessionCtx(GetOrCreateDeviceFromGatewayResponse msg) { + return new GatewayDeviceSessionContext(this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap, transportService); } - private void onDeviceRpcResponseProto(int msgId, ByteBuf payload) throws AdaptorException { - try { - TransportApiProtos.GatewayRpcResponseMsg gatewayRpcResponseMsg = TransportApiProtos.GatewayRpcResponseMsg.parseFrom(getBytes(payload)); - String deviceName = checkDeviceName(gatewayRpcResponseMsg.getDeviceName()); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - Integer requestId = gatewayRpcResponseMsg.getId(); - String data = gatewayRpcResponseMsg.getData(); - TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg = TransportProtos.ToDeviceRpcResponseMsg.newBuilder() - .setRequestId(requestId).setPayload(data).build(); - processRpcResponseMsg(deviceCtx, rpcResponseMsg, deviceName, msgId); - } - - @Override - public void onFailure(Throwable t) { - log.debug("[{}] Failed to process device Rpc response command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } catch (RuntimeException | InvalidProtocolBufferException e) { - throw new AdaptorException(e); - } - } - - private void processRpcResponseMsg(GatewayDeviceSessionCtx deviceCtx, TransportProtos.ToDeviceRpcResponseMsg rpcResponseMsg, String deviceName, int msgId) { - transportService.process(deviceCtx.getSessionInfo(), rpcResponseMsg, getPubAckCallback(channel, deviceName, msgId, rpcResponseMsg)); - } - - private void processGetAttributeRequestMessage(MqttPublishMessage mqttMsg, String deviceName, TransportProtos.GetAttributeRequestMsg requestMsg) { - int msgId = getMsgId(mqttMsg); - Futures.addCallback(checkDeviceConnected(deviceName), - new FutureCallback() { - @Override - public void onSuccess(@Nullable GatewayDeviceSessionCtx deviceCtx) { - transportService.process(deviceCtx.getSessionInfo(), requestMsg, getPubAckCallback(channel, deviceName, msgId, requestMsg)); - } - - @Override - public void onFailure(Throwable t) { - ack(mqttMsg, ReturnCode.IMPLEMENTATION_SPECIFIC); - log.debug("[{}] Failed to process device attributes request command: {}", sessionId, deviceName, t); - } - }, context.getExecutor()); - } - - private TransportProtos.GetAttributeRequestMsg toGetAttributeRequestMsg(int requestId, boolean clientScope, Set keys) { - TransportProtos.GetAttributeRequestMsg.Builder result = TransportProtos.GetAttributeRequestMsg.newBuilder(); - result.setRequestId(requestId); - - if (clientScope) { - result.addAllClientAttributeNames(keys); - } else { - result.addAllSharedAttributeNames(keys); - } - return result.build(); - } - - private ListenableFuture checkDeviceConnected(String deviceName) { - GatewayDeviceSessionCtx ctx = devices.get(deviceName); - if (ctx == null) { - log.debug("[{}] Missing device [{}] for the gateway session", sessionId, deviceName); - return onDeviceConnect(deviceName, DEFAULT_DEVICE_TYPE); - } else { - return Futures.immediateFuture(ctx); - } - } - - private String checkDeviceName(String deviceName) { - if (StringUtils.isEmpty(deviceName)) { - throw new RuntimeException("Device name is empty!"); - } else { - return deviceName; - } - } - - private String getDeviceName(JsonElement json) { - return json.getAsJsonObject().get(DEVICE_PROPERTY).getAsString(); - } - - private String getDeviceType(JsonElement json) { - JsonElement type = json.getAsJsonObject().get("type"); - return type == null || type instanceof JsonNull ? DEFAULT_DEVICE_TYPE : type.getAsString(); - } - - private JsonElement getJson(MqttPublishMessage mqttMsg) throws AdaptorException { - return JsonMqttAdaptor.validateJsonPayload(sessionId, mqttMsg.payload()); - } - - private byte[] getBytes(ByteBuf payload) { - return ProtoMqttAdaptor.toBytes(payload); - } - - private void ack(MqttPublishMessage msg, ReturnCode returnCode) { - int msgId = getMsgId(msg); - if (msgId > 0) { - writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, returnCode)); - } - } - - private void deregisterSession(String deviceName, GatewayDeviceSessionCtx deviceSessionCtx) { - transportService.deregisterSession(deviceSessionCtx.getSessionInfo()); - transportService.process(deviceSessionCtx.getSessionInfo(), SESSION_EVENT_MSG_CLOSED, null); - log.debug("[{}] Removed device [{}] from the gateway session", sessionId, deviceName); - } - - private TransportServiceCallback getPubAckCallback(final ChannelHandlerContext ctx, final String deviceName, final int msgId, final T msg) { - return new TransportServiceCallback() { - @Override - public void onSuccess(Void dummy) { - log.trace("[{}][{}] Published msg: {}", sessionId, deviceName, msg); - if (msgId > 0) { - ctx.writeAndFlush(MqttTransportHandler.createMqttPubAckMsg(deviceSessionCtx, msgId, ReturnCode.SUCCESS)); - } - } - - @Override - public void onError(Throwable e) { - log.trace("[{}] Failed to publish msg: {} for device: {}", sessionId, msg, deviceName, e); - ctx.close(); - } - }; - } } diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java index 4068604b19..0498eadd9f 100644 --- a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/MqttDeviceAwareSessionContext.java @@ -16,18 +16,13 @@ package org.thingsboard.server.transport.mqtt.session; import io.netty.handler.codec.mqtt.MqttQoS; -import org.thingsboard.server.common.data.DeviceProfile; -import org.thingsboard.server.common.data.DeviceTransportType; -import org.thingsboard.server.common.data.TransportPayloadType; -import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; -import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.transport.session.DeviceAwareSessionContext; -import org.thingsboard.server.transport.mqtt.util.MqttTopicFilter; -import org.thingsboard.server.transport.mqtt.util.MqttTopicFilterFactory; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java new file mode 100644 index 0000000000..fb4ab488a8 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugDeviceSessionContext.java @@ -0,0 +1,106 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.session; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.transport.TransportService; +import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugRpcRequestHeader; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; + +import java.util.Date; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; + +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.getTsKvProto; + +@Slf4j +public class SparkplugDeviceSessionContext extends AbstractGatewayDeviceSessionContext { + + private final Map deviceBirthMetrics = new ConcurrentHashMap<>(); + + public SparkplugDeviceSessionContext(SparkplugNodeSessionHandler parent, + TransportDeviceInfo deviceInfo, + DeviceProfile deviceProfile, + ConcurrentMap mqttQoSMap, + TransportService transportService) { + super(parent, deviceInfo, deviceProfile, mqttQoSMap, transportService); + } + + public Map getDeviceBirthMetrics() { + return deviceBirthMetrics; + } + + public void setDeviceBirthMetrics(java.util.List metrics) { + this.deviceBirthMetrics.putAll(metrics.stream() + .collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); + } + + + @Override + public void onAttributeUpdate(UUID sessionId, TransportProtos.AttributeUpdateNotificationMsg notification) { + log.trace("[{}] Received attributes update notification to sparkplug device", sessionId); + notification.getSharedUpdatedList().forEach(tsKvProto -> { + if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), + SparkplugMessageType.DCMD, deviceInfo.getDeviceName()); + parent.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(this.parent::writeAndFlush); + } + }); + } + + @Override + public void onToDeviceRpcRequest(UUID sessionId, TransportProtos.ToDeviceRpcRequestMsg rpcRequest) { + log.trace("[{}] Received RPC Request notification to sparkplug device", sessionId); + try { + SparkplugMessageType messageType = SparkplugMessageType.parseMessageType(rpcRequest.getMethodName()); + SparkplugRpcRequestHeader header = JacksonUtil.fromString(rpcRequest.getParams(), SparkplugRpcRequestHeader.class); + header.setMessageType(messageType.name()); + TransportProtos.TsKvProto tsKvProto = getTsKvProto(header.getMetricName(), header.getValue(), new Date().getTime()); + if (getDeviceBirthMetrics().containsKey(tsKvProto.getKv().getKey())) { + SparkplugTopic sparkplugTopic = new SparkplugTopic(parent.getSparkplugTopicNode(), + messageType, deviceInfo.getDeviceName()); + parent.createSparkplugMqttPublishMsg(tsKvProto, + sparkplugTopic.toString(), + getDeviceBirthMetrics().get(tsKvProto.getKv().getKey())) + .ifPresent(payload -> parent.sendToDeviceRpcRequest(payload, rpcRequest, sessionInfo)); + } else { + parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + ThingsboardErrorCode.BAD_REQUEST_PARAMS, " Failed send To Device Rpc Request: " + + rpcRequest.getMethodName() + ". This device does not have a metricName: [" + tsKvProto.getKv().getKey() + "]"); + } + } catch (ThingsboardException e) { + parent.sendErrorRpcResponse(sessionInfo, rpcRequest.getRequestId(), + ThingsboardErrorCode.BAD_REQUEST_PARAMS, " Failed send To Device Rpc Request: " + + rpcRequest.getMethodName() + ". " + e.getMessage()); + } + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java new file mode 100644 index 0000000000..fc480fa882 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/session/SparkplugNodeSessionHandler.java @@ -0,0 +1,340 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.session; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.gson.JsonParser; +import com.google.gson.JsonSyntaxException; +import com.google.protobuf.Descriptors; +import io.netty.handler.codec.mqtt.MqttMessage; +import io.netty.handler.codec.mqtt.MqttPublishMessage; +import io.netty.handler.codec.mqtt.MqttQoS; +import io.netty.handler.codec.mqtt.MqttTopicSubscription; +import lombok.extern.slf4j.Slf4j; +import org.eclipse.leshan.core.ResponseCode; +import org.springframework.util.CollectionUtils; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.common.transport.adaptor.JsonConverter; +import org.thingsboard.server.common.transport.adaptor.ProtoConverter; +import org.thingsboard.server.common.transport.auth.GetOrCreateDeviceFromGatewayResponse; +import org.thingsboard.server.gen.transport.TransportApiProtos; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; +import org.thingsboard.server.transport.mqtt.MqttTransportHandler; +import org.thingsboard.server.transport.mqtt.util.sparkplug.MetricDataType; +import org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopic; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.DBIRTH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMessageType.NBIRTH; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugConnectionState.ONLINE; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.createMetric; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.fromSparkplugBMetricToKeyValueProto; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugMetricUtil.validatedValueByTypeMetric; +import static org.thingsboard.server.transport.mqtt.util.sparkplug.SparkplugTopicUtil.parseTopicSubscribe; + +/** + * Created by nickAS21 on 12.12.22 + */ +@Slf4j +public class SparkplugNodeSessionHandler extends AbstractGatewaySessionHandler { + + private final SparkplugTopic sparkplugTopicNode; + private final Map nodeBirthMetrics; + private final MqttTransportHandler parent; + + public SparkplugNodeSessionHandler(MqttTransportHandler parent, DeviceSessionCtx deviceSessionCtx, UUID sessionId, + SparkplugTopic sparkplugTopicNode) { + super(deviceSessionCtx, sessionId); + this.parent = parent; + this.sparkplugTopicNode = sparkplugTopicNode; + this.nodeBirthMetrics = new ConcurrentHashMap<>(); + } + + public void setNodeBirthMetrics(java.util.List metrics) { + this.nodeBirthMetrics.putAll(metrics.stream() + .collect(Collectors.toMap(SparkplugBProto.Payload.Metric::getName, metric -> metric))); + } + + public Map getNodeBirthMetrics() { + return this.nodeBirthMetrics; + } + + public TransportProtos.PostTelemetryMsg convertToPostTelemetry(MqttDeviceAwareSessionContext ctx, MqttPublishMessage inbound) throws AdaptorException { + DeviceSessionCtx deviceSessionCtx = (DeviceSessionCtx) ctx; + byte[] bytes = getBytes(inbound.payload()); + Descriptors.Descriptor telemetryDynamicMsgDescriptor = ProtoConverter.validateDescriptor(deviceSessionCtx.getTelemetryDynamicMsgDescriptor()); + try { + return JsonConverter.convertToTelemetryProto(new JsonParser().parse(ProtoConverter.dynamicMsgToJson(bytes, telemetryDynamicMsgDescriptor))); + } catch (Exception e) { + log.debug("Failed to decode post telemetry request", e); + throw new AdaptorException(e); + } + } + + public void onAttributesTelemetryProto(int msgId, SparkplugBProto.Payload sparkplugBProto, String deviceName, SparkplugTopic topic) throws AdaptorException, ThingsboardException { + checkDeviceName(deviceName); + + ListenableFuture contextListenableFuture; + if (topic.isNode()) { + if (topic.isType(NBIRTH)) { + sendSparkplugStateOnTelemetry(this.deviceSessionCtx.getSessionInfo(), deviceName, ONLINE, + sparkplugBProto.getTimestamp()); + setNodeBirthMetrics(sparkplugBProto.getMetricsList()); + } + contextListenableFuture = Futures.immediateFuture(this.deviceSessionCtx); + } else { + ListenableFuture deviceCtx = onDeviceConnectProto(deviceName); + contextListenableFuture = Futures.transform(deviceCtx, ctx -> { + if (topic.isType(DBIRTH)) { + sendSparkplugStateOnTelemetry(ctx.getSessionInfo(), deviceName, ONLINE, + sparkplugBProto.getTimestamp()); + ctx.setDeviceBirthMetrics(sparkplugBProto.getMetricsList()); + } + return ctx; + }, MoreExecutors.directExecutor()); + } + Set attributesMetricNames = ((MqttDeviceProfileTransportConfiguration) deviceSessionCtx + .getDeviceProfile().getProfileData().getTransportConfiguration()).getSparkplugAttributesMetricNames(); + if (attributesMetricNames != null) { + List attributesMsgList = convertToPostAttributes(sparkplugBProto, attributesMetricNames, deviceName); + onDeviceAttributesProto(contextListenableFuture, msgId, attributesMsgList, deviceName); + } + List postTelemetryMsgList = convertToPostTelemetry(sparkplugBProto, attributesMetricNames, topic.getType().name()); + onDeviceTelemetryProto(contextListenableFuture, msgId, postTelemetryMsgList, deviceName); + } + + public void onDeviceTelemetryProto(ListenableFuture contextListenableFuture, + int msgId, List postTelemetryMsgList, String deviceName) throws AdaptorException { + try { + int finalMsgId = msgId; + postTelemetryMsgList.forEach(telemetryMsg -> { + Futures.addCallback(contextListenableFuture, + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { + try { + processPostTelemetryMsg(deviceCtx, telemetryMsg, deviceName, finalMsgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to convert telemetry: {}", gateway.getDeviceId(), deviceName, telemetryMsg, e); + channel.close(); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device telemetry command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } catch (RuntimeException e) { + throw new AdaptorException(e); + } + } + + private void onDeviceAttributesProto(ListenableFuture contextListenableFuture, int msgId, + List attributesMsgList, String deviceName) throws AdaptorException { + try { + if (!CollectionUtils.isEmpty(attributesMsgList)) { + attributesMsgList.forEach(attributesMsg -> { + Futures.addCallback(contextListenableFuture, + new FutureCallback<>() { + @Override + public void onSuccess(@Nullable MqttDeviceAwareSessionContext deviceCtx) { + TransportProtos.PostAttributeMsg kvListProto = attributesMsg.getMsg(); + try { + TransportProtos.PostAttributeMsg postAttributeMsg = ProtoConverter.validatePostAttributeMsg(kvListProto.toByteArray()); + processPostAttributesMsg(deviceCtx, postAttributeMsg, deviceName, msgId); + } catch (Throwable e) { + log.warn("[{}][{}] Failed to process device attributes command: {}", gateway.getDeviceId(), deviceName, kvListProto, e); + } + } + + @Override + public void onFailure(Throwable t) { + log.debug("[{}] Failed to process device attributes command: {}", sessionId, deviceName, t); + } + }, context.getExecutor()); + }); + } else { + log.debug("[{}] Devices attributes keys list is empty for: [{}]", sessionId, gateway.getDeviceId()); + } + } catch (RuntimeException e) { + throw new AdaptorException(e); + } + } + + public void handleSparkplugSubscribeMsg(List grantedQoSList, MqttTopicSubscription subscription, + MqttQoS reqQoS) throws ThingsboardException, AdaptorException, + ExecutionException, InterruptedException { + SparkplugTopic sparkplugTopic = parseTopicSubscribe(subscription.topicName()); + if (sparkplugTopic.getGroupId() == null) { + // TODO SUBSCRIBE NameSpace + } else if (sparkplugTopic.getType() == null) { + // TODO SUBSCRIBE GroupId + } else if (sparkplugTopic.isNode()) { + // SUBSCRIBE Node + parent.processAttributesRpcSubscribeSparkplugNode(grantedQoSList, reqQoS); + } else { + // SUBSCRIBE Device - DO NOTHING, WE HAVE ALREADY SUBSCRIBED. + // TODO: track that node subscribed to # or to particular device. + } + } + + public void onDeviceDisconnect(MqttPublishMessage mqttMsg, String deviceName) throws AdaptorException { + try { + processOnDisconnect(mqttMsg, deviceName); + } catch (RuntimeException e) { + throw new AdaptorException(e); + } + } + + private ListenableFuture onDeviceConnectProto(String deviceName) throws ThingsboardException { + try { + String deviceType = this.gateway.getDeviceType() + "-node"; + return onDeviceConnect(deviceName, deviceType); + } catch (RuntimeException e) { + log.error("Failed Sparkplug Device connect proto!", e); + throw new ThingsboardException(e, ThingsboardErrorCode.BAD_REQUEST_PARAMS); + } + } + + private List convertToPostTelemetry(SparkplugBProto.Payload sparkplugBProto, Set attributesMetricNames, String topicTypeName) throws AdaptorException { + try { + List msgs = new ArrayList<>(); + for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { + if (attributesMetricNames == null || !attributesMetricNames.contains(protoMetric.getName())) { + long ts = protoMetric.getTimestamp(); + String key = "bdSeq".equals(protoMetric.getName()) ? + topicTypeName + " " + protoMetric.getName() : protoMetric.getName(); + Optional keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(key, protoMetric); + if (keyValueProtoOpt.isPresent()) { + msgs.add(postTelemetryMsgCreated(keyValueProtoOpt.get(), ts)); + } + } + } + + if (DBIRTH.name().equals(topicTypeName)) { + TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); + keyValueProtoBuilder.setKey(topicTypeName + " " + "seq"); + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); + keyValueProtoBuilder.setLongV(sparkplugBProto.getSeq()); + msgs.add(postTelemetryMsgCreated(keyValueProtoBuilder.build(), sparkplugBProto.getTimestamp())); + } + return msgs; + } catch (IllegalStateException | JsonSyntaxException | ThingsboardException e) { + log.error("Failed to decode post telemetry request", e); + throw new AdaptorException(e); + } + } + + private List convertToPostAttributes(SparkplugBProto.Payload sparkplugBProto, + Set attributesMetricNames, + String deviceName) throws AdaptorException { + try { + List msgs = new ArrayList<>(); + for (SparkplugBProto.Payload.Metric protoMetric : sparkplugBProto.getMetricsList()) { + if (attributesMetricNames.contains(protoMetric.getName())) { + TransportApiProtos.AttributesMsg.Builder deviceAttributesMsgBuilder = TransportApiProtos.AttributesMsg.newBuilder(); + Optional msgOpt = getPostAttributeMsg(protoMetric); + if (msgOpt.isPresent()) { + deviceAttributesMsgBuilder.setDeviceName(deviceName); + deviceAttributesMsgBuilder.setMsg(msgOpt.get()); + msgs.add(deviceAttributesMsgBuilder.build()); + } + } + } + return msgs; + } catch (IllegalStateException | JsonSyntaxException | ThingsboardException e) { + log.error("Failed to decode post telemetry request", e); + throw new AdaptorException(e); + } + } + + private Optional getPostAttributeMsg(SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { + Optional keyValueProtoOpt = fromSparkplugBMetricToKeyValueProto(protoMetric.getName(), protoMetric); + if (keyValueProtoOpt.isPresent()) { + TransportProtos.PostAttributeMsg.Builder builder = TransportProtos.PostAttributeMsg.newBuilder(); + builder.addKv(keyValueProtoOpt.get()); + return Optional.of(builder.build()); + } + return Optional.empty(); + } + + public SparkplugTopic getSparkplugTopicNode() { + return this.sparkplugTopicNode; + } + + public Optional createSparkplugMqttPublishMsg(TransportProtos.TsKvProto tsKvProto, + String sparkplugTopic, + SparkplugBProto.Payload.Metric metricBirth) { + try { + long ts = tsKvProto.getTs(); + MetricDataType metricDataType = MetricDataType.fromInteger(metricBirth.getDatatype()); + Optional value = validatedValueByTypeMetric(tsKvProto.getKv(), metricDataType); + if (value.isPresent()) { + SparkplugBProto.Payload.Builder cmdPayload = SparkplugBProto.Payload.newBuilder() + .setTimestamp(ts); + cmdPayload.addMetrics(createMetric(value.get(), ts, tsKvProto.getKv().getKey(), metricDataType)); + byte[] payloadInBytes = cmdPayload.build().toByteArray(); + return Optional.of(getPayloadAdaptor().createMqttPublishMsg(deviceSessionCtx, sparkplugTopic, payloadInBytes)); + } else { + log.trace("DeviceId: [{}] tenantId: [{}] sessionId:[{}] Failed to convert device attributes [{}] response to MQTT sparkplug msg", + deviceSessionCtx.getDeviceInfo().getDeviceId(), deviceSessionCtx.getDeviceInfo().getTenantId(), sessionId, tsKvProto.getKv()); + } + } catch (Exception e) { + log.trace("DeviceId: [{}] tenantId: [{}] sessionId:[{}] Failed to convert device attributes response to MQTT sparkplug msg", + deviceSessionCtx.getDeviceInfo().getDeviceId(), deviceSessionCtx.getDeviceInfo().getTenantId(), sessionId, e); + return Optional.empty(); + } + return Optional.empty(); + } + + @Override + protected SparkplugDeviceSessionContext newDeviceSessionCtx(GetOrCreateDeviceFromGatewayResponse msg) { + return new SparkplugDeviceSessionContext(this, msg.getDeviceInfo(), msg.getDeviceProfile(), mqttQoSMap, transportService); + } + + protected void sendToDeviceRpcRequest(MqttMessage payload, TransportProtos.ToDeviceRpcRequestMsg rpcRequest, TransportProtos.SessionInfoProto sessionInfo) { + parent.sendToDeviceRpcRequest(payload, rpcRequest, sessionInfo); + } + + protected void sendErrorRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ThingsboardErrorCode result, String errorMsg) { + parent.sendErrorRpcResponse(sessionInfo, requestId, result, errorMsg); + } + + protected void sendSuccessRpcResponse(TransportProtos.SessionInfoProto sessionInfo, int requestId, ResponseCode result, String successMsg) { + parent.sendSuccessRpcResponse(sessionInfo, requestId, result, successMsg); + } + + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java new file mode 100644 index 0000000000..d5e781a402 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/MetricDataType.java @@ -0,0 +1,156 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util.sparkplug; + +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.server.common.transport.adaptor.AdaptorException; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; + +import java.math.BigInteger; +import java.util.Date; + +/** + * Created by nickAS21 on 10.01.23 + */ + +@Slf4j +public enum MetricDataType { + + // Basic Types + Int8(1, Byte.class), + Int16(2, Short.class), + Int32(3, Integer.class), + Int64(4, Long.class), + UInt8(5, Short.class), + UInt16(6, Integer.class), + UInt32(7, Long.class), + UInt64(8, BigInteger.class), + Float(9, Float.class), + Double(10, Double.class), + Boolean(11, Boolean.class), + String(12, String.class), + DateTime(13, Date.class), + Text(14, String.class), + + // Custom Types for Metrics + UUID(15, String.class), + DataSet(16, SparkplugBProto.Payload.DataSet.class), + Bytes(17, byte[].class), + File(18, SparkplugMetricUtil.File.class), + Template(19, SparkplugBProto.Payload.Template.class), + + // PropertyValue Types (20 and 21) are NOT metric datatypes + + // Unknown + Unknown(0, Object.class); + + private Class clazz = null; + private int intValue = 0; + + /** + * Constructor + * + * @param intValue the integer value of this {@link MetricDataType} + * @param clazz the {@link Class} type associated with this {@link MetricDataType} + */ + private MetricDataType(int intValue, Class clazz) { + this.intValue = intValue; + this.clazz = clazz; + } + + /** + * Checks the type of a specified value against the specified {@link MetricDataType} + * + * @param value the {@link Object} value to check against the {@link MetricDataType} + * @throws AdaptorException if the value is not a valid type for the given {@link MetricDataType} + */ + public void checkType(Object value) throws AdaptorException { + if (value != null && !clazz.isAssignableFrom(value.getClass())) { + String msgError = "Failed type check - " + clazz + " != " + ((value != null) ? value.getClass().toString() : "null"); + log.debug(msgError); + throw new AdaptorException(msgError); + } + } + + /** + * Returns an integer representation of the data type. + * + * @return an integer representation of the data type. + */ + public int toIntValue() { + return this.intValue; + } + + /** + * Converts the integer representation of the data type into a {@link MetricDataType} instance. + * + * @param i the integer representation of the data type. + * @return a {@link MetricDataType} instance. + */ + public static MetricDataType fromInteger(int i) { + switch (i) { + case 1: + return Int8; + case 2: + return Int16; + case 3: + return Int32; + case 4: + return Int64; + case 5: + return UInt8; + case 6: + return UInt16; + case 7: + return UInt32; + case 8: + return UInt64; + case 9: + return Float; + case 10: + return Double; + case 11: + return Boolean; + case 12: + return String; + case 13: + return DateTime; + case 14: + return Text; + case 15: + return UUID; + case 16: + return DataSet; + case 17: + return Bytes; + case 18: + return File; + case 19: + return Template; + default: + return Unknown; + } + } + + /** + * @return the class type for this DataType + */ + public Class getClazz() { + return clazz; + } + + +} \ No newline at end of file diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugConnectionState.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugConnectionState.java new file mode 100644 index 0000000000..646dc1842e --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugConnectionState.java @@ -0,0 +1,30 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util.sparkplug; + +public enum SparkplugConnectionState { + /** + * The EoN node should examine the payload of this + * message to ensure that it is a value of “ONLINE” + */ + OFFLINE, + /** + * If the value is “OFFLINE”, this indicates the Primary Application + * has lost its MQTT Session to this particular MQTT Server. + */ + ONLINE + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java new file mode 100644 index 0000000000..f9ca6299bc --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMessageType.java @@ -0,0 +1,113 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util.sparkplug; + +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; + +/** + * An enumeration of Sparkplug MQTT message types. The type provides an indication as to what the MQTT Payload of + * message will contain. + */ +public enum SparkplugMessageType { + + /** + * Birth certificate for MQTT Edge of Network (EoN) Nodes. + */ + NBIRTH, + + /** + * Death certificate for MQTT Edge of Network (EoN) Nodes. + */ + NDEATH, + + /** + * Birth certificate for MQTT Devices. + */ + DBIRTH, + + /** + * Death certificate for MQTT Devices. + */ + DDEATH, + + /** + * Edge of Network (EoN) Node data message. + */ + NDATA, + + /** + * Device data message. + */ + DDATA, + + /** + * Edge of Network (EoN) Node command message. + */ + NCMD, + + /** + * Device command message. + */ + DCMD, + + /** + * Critical application state message. + */ + STATE, + + /** + * Device record message. + */ + DRECORD, + + /** + * Edge of Network (EoN) Node record message. + */ + NRECORD; + + public static SparkplugMessageType parseMessageType(String type) throws ThingsboardException { + for (SparkplugMessageType messageType : SparkplugMessageType.values()) { + if (messageType.name().equals(type)) { + return messageType; + } + } + throw new ThingsboardException("Invalid message type: " + type, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + public static String messageName(SparkplugMessageType type) { + return STATE.equals(type) ? "sparkplugConnectionState" : type.name(); + } + + public boolean isDeath() { + return this.equals(DDEATH) || this.equals(NDEATH); + } + + public boolean isCommand() { + return this.equals(DCMD) || this.equals(NCMD); + } + + public boolean isData() { + return this.equals(DDATA) || this.equals(NDATA); + } + + public boolean isBirth() { + return this.equals(DBIRTH) || this.equals(NBIRTH); + } + + public boolean isRecord() { + return this.equals(DRECORD) || this.equals(NRECORD); + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java new file mode 100644 index 0000000000..65bf6b2063 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugMetricUtil.java @@ -0,0 +1,452 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.ser.std.FileSerializer; +import com.google.protobuf.ByteString; +import lombok.extern.slf4j.Slf4j; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; +import org.thingsboard.server.gen.transport.TransportProtos; +import org.thingsboard.server.gen.transport.mqtt.SparkplugBProto; + +import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.text.NumberFormat; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.thingsboard.common.util.JacksonUtil.newArrayNode; + +/** + * Provides utility methods for SparkplugB MQTT Payload Metric. + */ +@Slf4j +public class SparkplugMetricUtil { + + public static Optional fromSparkplugBMetricToKeyValueProto(String key, SparkplugBProto.Payload.Metric protoMetric) throws ThingsboardException { + // Check if the null flag has been set indicating that the value is null + if (protoMetric.getIsNull()) { + return Optional.empty(); + } + // Otherwise convert the value based on the type + int metricType = protoMetric.getDatatype(); + TransportProtos.KeyValueProto.Builder builderProto = TransportProtos.KeyValueProto.newBuilder(); + ArrayNode nodeArray = newArrayNode(); + MetricDataType metricDataType = MetricDataType.fromInteger(metricType); + try { + switch (metricDataType) { + case Boolean: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.BOOLEAN_V) + .setBoolV(protoMetric.getBooleanValue()).build()); + case DateTime: + case Int64: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(protoMetric.getLongValue()).build()); + case Float: + var f = new BigDecimal(String.valueOf(protoMetric.getFloatValue())); + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.DOUBLE_V) + .setDoubleV(f.doubleValue()).build()); + case Double: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(Double.valueOf(protoMetric.getDoubleValue()).longValue()).build()); + case Int8: + case UInt8: + case Int16: + case Int32: + case UInt16: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(protoMetric.getIntValue()).build()); + case UInt32: + case UInt64: + if (protoMetric.hasIntValue()) { + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(protoMetric.getIntValue()).build()); + } else if (protoMetric.hasLongValue()) { + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.LONG_V) + .setLongV(protoMetric.getLongValue()).build()); + } else { + log.error("Invalid value for UInt32 datatype"); + throw new ThingsboardException("Invalid value for " + MetricDataType.fromInteger(metricType).name() + " datatype " + metricType, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + case String: + case Text: + case UUID: + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) + .setStringV(protoMetric.getStringValue()).build()); + // byte[] + case Bytes: + ByteBuffer byteBuffer = ByteBuffer.wrap(protoMetric.getBytesValue().toByteArray()); + while (byteBuffer.hasRemaining()) { + nodeArray.add(byteBuffer.get()); + } + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.JSON_V) + .setJsonV(nodeArray.toString()).build()); + case DataSet: + case Template: + case File: + //TODO + // Build the and create the DataSet + /** + SparkplugBProto.Payload.DataSet protoDataSet = protoMetric.getDatasetValue(); + return new SparkplugBProto.Payload.DataSet.Builder(protoDataSet.getNumOfColumns()).addColumnNames(protoDataSet.getColumnsList()) + .addTypes(convertDataSetDataTypes(protoDataSet.getTypesList())) + .addRows(convertDataSetRows(protoDataSet.getRowsList(), protoDataSet.getTypesList())) + .createDataSet(); + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) + .setStringV(protoDataSet.toString()).build()); + **/ + //TODO + // Build the and create the Template + /** + SparkplugBProto.Payload.Template protoTemplate = protoMetric.getTemplateValue(); + return Optional.of(builderProto.setKey(key).setType(TransportProtos.KeyValueType.STRING_V) + .setStringV( protoTemplate.toString()).build()); + **/ + //TODO + // Build the and create the File + /** + String filename = protoMetric.getMetadata().getFileName(); + return Optional.of(builderPrbyteValueoto.setKey(key + "_" + filename).setType(TransportProtos.KeyValueType.STRING_V) + .setStringV(Hex.encodeHexString((protoMetric.getBytesValue().toByteArray()))).build()); + **/ + return Optional.empty(); + case Unknown: + default: + throw new ThingsboardException("Failed to decode: Unknown MetricDataType " + metricType, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } catch (Exception e) { + log.error("", e); + return Optional.empty(); + } + } + + public static SparkplugBProto.Payload.Metric createMetric(Object value, long ts, String key, MetricDataType metricDataType) throws ThingsboardException { + SparkplugBProto.Payload.Metric metric = SparkplugBProto.Payload.Metric.newBuilder() + .setTimestamp(ts) + .setName(key) + .setDatatype(metricDataType.toIntValue()) + .build(); + switch (metricDataType) { + case Int8: // (byte) + return metric.toBuilder().setIntValue(((Byte) value).intValue()).build(); + case Int16: // (short) + case UInt8: + return metric.toBuilder().setIntValue(((Short) value).intValue()).build(); + case UInt16: // (int) + case Int32: + return metric.toBuilder().setIntValue(((Integer) value).intValue()).build(); + case UInt32: // (long) + case Int64: + case UInt64: + case DateTime: + return metric.toBuilder().setLongValue(((Long) value).longValue()).build(); + case Float: // (float) + return metric.toBuilder().setFloatValue(((Float) value).floatValue()).build(); + case Double: // (double) + return metric.toBuilder().setDoubleValue(((Double) value).doubleValue()).build(); + case Boolean: // (boolean) + return metric.toBuilder().setBooleanValue(((Boolean) value).booleanValue()).build(); + case String: // String) + case Text: + case UUID: + return metric.toBuilder().setStringValue((String) value).build(); + case Bytes: + ByteString byteString = ByteString.copyFrom((byte[]) value); + return metric.toBuilder().setBytesValue(byteString).build(); + case DataSet: + return metric.toBuilder().setDatasetValue((SparkplugBProto.Payload.DataSet) value).build(); + case File: + SparkplugMetricUtil.File file = (SparkplugMetricUtil.File) value; + ByteString byteFileString = ByteString.copyFrom(file.getBytes()); + return metric.toBuilder().setBytesValue(byteFileString).build(); + case Template: + return metric.toBuilder().setTemplateValue((SparkplugBProto.Payload.Template) value).build(); + case Unknown: + throw new ThingsboardException("Invalid value for MetricDataType " + metricDataType.name(), ThingsboardErrorCode.INVALID_ARGUMENTS); + } + return metric; + } + + public static TransportProtos.TsKvProto getTsKvProto(String key, Object value, long ts) throws ThingsboardException { + try { + TransportProtos.TsKvProto.Builder tsKvProtoBuilder = TransportProtos.TsKvProto.newBuilder(); + TransportProtos.KeyValueProto.Builder keyValueProtoBuilder = TransportProtos.KeyValueProto.newBuilder(); + keyValueProtoBuilder.setKey(key); + if (value instanceof String) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.STRING_V); + keyValueProtoBuilder.setStringV((String) value); + } else if (value instanceof Integer) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); + keyValueProtoBuilder.setLongV((Integer) value); + } else if (value instanceof Long) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.LONG_V); + keyValueProtoBuilder.setLongV((Long) value); + } else if (value instanceof Boolean) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.BOOLEAN_V); + keyValueProtoBuilder.setBoolV((Boolean) value); + } else if (value instanceof Double) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.DOUBLE_V); + keyValueProtoBuilder.setDoubleV((Double) value); + } else if (value instanceof List) { + keyValueProtoBuilder.setType(TransportProtos.KeyValueType.JSON_V); + ArrayNode arrayNodeBytes = JacksonUtil.convertValue(value, ArrayNode.class); + keyValueProtoBuilder.setJsonV(arrayNodeBytes.toString()); + } else { + throw new ThingsboardException("Failed to convert device/node RPC command to TsKvProto for Sparkplug MQT msg: value [" + value + "]", ThingsboardErrorCode.INVALID_ARGUMENTS); + } + tsKvProtoBuilder.setKv(keyValueProtoBuilder.build()); + tsKvProtoBuilder.setTs(ts); + return tsKvProtoBuilder.build(); + } catch (Exception e) { + throw new ThingsboardException("Failed to convert device/node RPC command to TsKvProto for Sparkplug MQT msg: value [" + value + "]", ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } + + public static Optional validatedValueByTypeMetric(TransportProtos.KeyValueProto kv, MetricDataType metricDataType) throws ThingsboardException { + if (kv.getTypeValue() <= 3) { + return validatedValuePrimitiveByTypeMetric(kv, metricDataType); + } else if (kv.getTypeValue() == 4) { + JsonNode arrayNode = JacksonUtil.fromString(kv.getJsonV(), JsonNode.class); + if (arrayNode.isArray()) { + return validatedValueJsonByTypeMetric(kv.getJsonV(), metricDataType); + } + } else { + throw new ThingsboardException("Invalid type KeyValueProto " + kv.toString() + " for MetricDataType " + metricDataType.name(), ThingsboardErrorCode.INVALID_ARGUMENTS); + } + return Optional.empty(); + } + + public static Optional validatedValuePrimitiveByTypeMetric(TransportProtos.KeyValueProto kv, MetricDataType metricDataType) throws ThingsboardException { + Optional valueOpt = getValueKvProtoPrimitive(kv); + if (valueOpt.isPresent()) { + try { + switch (metricDataType) { + // int + case Int8: + case Int16: + case UInt8: + case UInt16: + case Int32: + Optional boolInt8 = booleanStringToInt(valueOpt.get()); + if (boolInt8.isPresent()) { + return Optional.of(boolInt8.get()); + } + try { + return Optional.of(Integer.valueOf(valueOpt.get())); + } catch (NumberFormatException eInt) { + var i = new BigDecimal(valueOpt.get()); + if (i.longValue() <= Integer.MAX_VALUE) { + return Optional.of(i.intValue()); + } + throw new ThingsboardException("Invalid type value " + kv.toString() + " for MetricDataType " + + metricDataType.name(), eInt, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + // long + case UInt32: + case Int64: + case UInt64: + case DateTime: + Optional boolInt64 = booleanStringToInt(valueOpt.get()); + if (boolInt64.isPresent()) { + return Optional.of(Long.valueOf(boolInt64.get())); + } + var l = new BigDecimal(valueOpt.get()); + return Optional.of(l.longValue()); + // float + case Float: + Optional boolFloat = booleanStringToInt(valueOpt.get()); + if (boolFloat.isPresent()) { + var fb = new BigDecimal(boolFloat.get()); + return Optional.of(fb.floatValue()); + } + var f = new BigDecimal(valueOpt.get()); + return Optional.of(f.floatValue()); + // double + case Double: + Optional boolDouble = booleanStringToInt(valueOpt.get()); + if (boolDouble.isPresent()) { + return Optional.of(Double.valueOf(boolDouble.get())); + } + var dd = new BigDecimal(valueOpt.get()); + return Optional.of(dd.doubleValue()); + case Boolean: + if ("true".equals(valueOpt.get())) { + return Optional.of(true); + } else if ("false".equals(valueOpt.get())) { + return Optional.of(false); + } else { + Number number = NumberFormat.getInstance().parse(valueOpt.get()); + if (StringUtils.isBlank(number.toString()) || "0".equals(number.toString())) { // ok 0 + return Optional.of(false); + } else { + return Optional.of(true); + } + } + case String: + case Text: + case UUID: + return Optional.of(valueOpt.get()); + } + } catch (Exception e) { + log.trace("Invalid type value [{}] for MetricDataType [{}] [{}]", kv, metricDataType.name(), e.getMessage()); + throw new ThingsboardException("Invalid type value " + kv.toString() + " for MetricDataType " + metricDataType.name(), e, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + } + return Optional.empty(); + } + + public static Optional validatedValueJsonByTypeMetric(String arrayNodeStr, MetricDataType metricDataType) { + try { + Optional valueOpt; + switch (metricDataType) { + // byte[] + case Bytes: + List listBytes = JacksonUtil.fromString(arrayNodeStr, new TypeReference<>() { + }); + byte[] bytes = new byte[listBytes.size()]; + for (int i = 0; i < listBytes.size(); i++) { + bytes[i] = listBytes.get(i).byteValue(); + } + return Optional.of(bytes); + case DataSet: + case File: + case Template: + log.error("Invalid type value [{}] for MetricDataType [{}]", arrayNodeStr, metricDataType.name()); + return Optional.empty(); + case Unknown: + default: + log.error("Invalid MetricDataType [{}] type, value [{}]", arrayNodeStr, metricDataType.name()); + return Optional.empty(); + } + } catch (Exception e) { + log.error("Invalid type value [{}] for MetricDataType [{}] [{}]", arrayNodeStr, metricDataType.name(), e.getMessage()); + return Optional.empty(); + } + } + + private static Optional getValueKvProtoPrimitive(TransportProtos.KeyValueProto kv) { + if (kv.getTypeValue() == 0) { // boolean + return Optional.of(String.valueOf(kv.getBoolV())); + } else if (kv.getTypeValue() == 1) { // kvLong + return Optional.of(String.valueOf(kv.getLongV())); + } else if (kv.getTypeValue() == 2) { // kvDouble/float + return Optional.of(String.valueOf(kv.getDoubleV())); + } else if (kv.getTypeValue() == 3) { // kvString + return Optional.of(kv.getStringV()); + } else { + return Optional.empty(); + } + } + + private static Optional booleanStringToInt(String booleanStr) { + if ("true".equals(booleanStr)) { + return Optional.of(1); + } else if ("false".equals(booleanStr)) { + return Optional.of(0); + } else { + return Optional.empty(); + } + } + + @JsonIgnoreProperties( + value = {"fileName"}) + @JsonSerialize( + using = FileSerializer.class) + public class File { + + private String fileName; + private byte[] bytes; + + /** + * Default Constructor + */ + public File() { + super(); + } + + /** + * Constructor + * + * @param fileName the full file name path + * @param bytes the array of bytes that represent the contents of the file + */ + public File(String fileName, byte[] bytes) { + super(); + this.fileName = fileName == null + ? null + : fileName.replace("/", System.getProperty("file.separator")).replace("\\", + System.getProperty("file.separator")); + this.bytes = Arrays.copyOf(bytes, bytes.length); + } + + /** + * Gets the full filename path + * + * @return the full filename path + */ + public String getFileName() { + return fileName; + } + + /** + * Sets the full filename path + * + * @param fileName the full filename path + */ + public void setFileName(String fileName) { + this.fileName = fileName; + } + + /** + * Gets the bytes that represent the contents of the file + * + * @return the bytes that represent the contents of the file + */ + public byte[] getBytes() { + return bytes; + } + + /** + * Sets the bytes that represent the contents of the file + * + * @param bytes the bytes that represent the contents of the file + */ + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("File [fileName="); + builder.append(fileName); + builder.append(", bytes="); + builder.append(Arrays.toString(bytes)); + builder.append("]"); + return builder.toString(); + } + } + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcRequestHeader.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcRequestHeader.java new file mode 100644 index 0000000000..1a9ed987f7 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcRequestHeader.java @@ -0,0 +1,29 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Data; + +@Data +@JsonIgnoreProperties(ignoreUnknown = true) +public class SparkplugRpcRequestHeader { + + private String messageType; + private String metricName; + private Object value; + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcResponseBody.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcResponseBody.java new file mode 100644 index 0000000000..8f39a526c4 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugRpcResponseBody.java @@ -0,0 +1,31 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SparkplugRpcResponseBody { + + private String result; + private String value; + private String error; + +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java new file mode 100644 index 0000000000..be721f11bb --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopic.java @@ -0,0 +1,164 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Created by nickAS21 on 12.12.22 + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SparkplugTopic { + + /** + * The Sparkplug namespace version. + * For the Sparkplug™ B version of the specification, the UTF-8 string constant for the namespace element will be: “spBv1.0” + */ + private String namespace; + + /** + * The ID of the logical grouping of Edge of Network (EoN) Nodes and devices. + */ + private String groupId; + + /** + * The ID of the Edge of Network (EoN) Node. + */ + private String edgeNodeId; + + /** + * The ID of the device. + */ + private String deviceId; + + /** + * The message type. + */ + private SparkplugMessageType type; + + /** + * Constructor (device). + * + * @param namespace the namespace. + * @param groupId the group ID. + * @param edgeNodeId the edge node ID. + * @param deviceId the device ID. + * @param type the message type. + */ + public SparkplugTopic(String namespace, String groupId, String edgeNodeId, String deviceId, SparkplugMessageType type) { + super(); + this.namespace = namespace; + this.groupId = groupId; + this.edgeNodeId = edgeNodeId; + this.deviceId = deviceId; + this.type = type; + } + + /** + * Constructor (node). + * + * @param namespace the namespace. + * @param groupId the group ID. + * @param edgeNodeId the edge node ID. + * @param type the message type. + */ + public SparkplugTopic(String namespace, String groupId, String edgeNodeId, SparkplugMessageType type) { + super(); + this.namespace = namespace; + this.groupId = groupId; + this.edgeNodeId = edgeNodeId; + this.deviceId = null; + this.type = type; + } + + public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type) { + super(); + this.namespace = sparkplugTopic.namespace; + this.groupId = sparkplugTopic.groupId; + this.edgeNodeId = sparkplugTopic.edgeNodeId; + this.deviceId = null; + this.type = type; + } + public SparkplugTopic(SparkplugTopic sparkplugTopic, SparkplugMessageType type, String deviceId) { + super(); + this.namespace = sparkplugTopic.namespace; + this.groupId = sparkplugTopic.groupId; + this.edgeNodeId = sparkplugTopic.edgeNodeId; + this.deviceId = deviceId; + this.type = type; + } + + /** + * @return the Sparkplug namespace version + */ + public String getNamespace() { + return namespace; + } + + /** + * Returns the ID of the logical grouping of Edge of Network (EoN) Nodes and devices. + * + * @return the group ID + */ + public String getGroupId() { + return groupId; + } + + /** + * @return the ID of the Edge of Network (EoN) Node + */ + public String getEdgeNodeId() { + return edgeNodeId; + } + + /** + * @return the device ID + */ + public String getDeviceId() { + return deviceId; + } + + /** + * @return the message type + */ + public SparkplugMessageType getType() { + return type; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(getNamespace()).append("/") + .append(getGroupId()).append("/") + .append(getType()).append("/") + .append(getEdgeNodeId()); + if (getDeviceId() != null) { + sb.append("/").append(getDeviceId()); + } + return sb.toString(); + } + + /** + * @param type the type to check + * @return true if this topic's type matches the passes in type, false otherwise + */ + public boolean isType(SparkplugMessageType type) { + return this.type != null && this.type.equals(type); + } + + public boolean isNode() { + return this.deviceId == null; + } +} diff --git a/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java new file mode 100644 index 0000000000..58b4319781 --- /dev/null +++ b/common/transport/mqtt/src/main/java/org/thingsboard/server/transport/mqtt/util/sparkplug/SparkplugTopicUtil.java @@ -0,0 +1,116 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.transport.mqtt.util.sparkplug; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; +import org.thingsboard.server.common.data.exception.ThingsboardException; + +import java.util.HashMap; +import java.util.Map; + +/** + * Provides utility methods for handling Sparkplug MQTT message topics. + */ +public class SparkplugTopicUtil { + + private static final Map SPLIT_TOPIC_CACHE = new HashMap(); + private static final String TOPIC_INVALID_NUMBER = "Invalid number of topic elements: "; + public static final String NAMESPACE = "spBv1.0"; + + public static String[] getSplitTopic(String topic) { + String[] splitTopic = SPLIT_TOPIC_CACHE.get(topic); + if (splitTopic == null) { + splitTopic = topic.split("/"); + SPLIT_TOPIC_CACHE.put(topic, splitTopic); + } + + return splitTopic; + } + + /** + * Serializes a {@link SparkplugTopic} instance in to a JSON string. + * + * @param topic a {@link SparkplugTopic} instance + * @return a JSON string + * @throws JsonProcessingException + */ + public static String sparkplugTopicToString(SparkplugTopic topic) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(topic); + } + + /** + * Parses a Sparkplug MQTT message topic string and returns a {@link SparkplugTopic} instance. + * + * @param topic a topic string + * @return a {@link SparkplugTopic} instance + * @throws ThingsboardException if an error occurs while parsing + */ + public static SparkplugTopic parseTopicSubscribe(String topic) throws ThingsboardException { + // TODO "+", "$" + topic = topic.indexOf("#") > 0 ? topic.substring(0, topic.indexOf("#")) : topic; + return parseTopic(SparkplugTopicUtil.getSplitTopic(topic)); + } + + public static SparkplugTopic parseTopicPublish(String topic) throws ThingsboardException { + if (topic.contains("#") || topic.contains("$") || topic.contains("+")) { + throw new ThingsboardException("Invalid of topic elements for Publish", ThingsboardErrorCode.INVALID_ARGUMENTS); + } else { + String[] splitTopic = SparkplugTopicUtil.getSplitTopic(topic); + if (splitTopic.length < 4 || splitTopic.length > 5) { + throw new ThingsboardException(TOPIC_INVALID_NUMBER + splitTopic.length, ThingsboardErrorCode.INVALID_ARGUMENTS); + } + return parseTopic(splitTopic); + } + } + + /** + * Parses a Sparkplug MQTT message topic string and returns a {@link SparkplugTopic} instance. + * + * @param splitTopic a topic split into tokens + * @return a {@link SparkplugTopic} instance + * @throws Exception if an error occurs while parsing + */ + @SuppressWarnings("incomplete-switch") + public static SparkplugTopic parseTopic(String[] splitTopic) throws ThingsboardException { + int length = splitTopic.length; + if (length == 0) { + throw new ThingsboardException(TOPIC_INVALID_NUMBER + length, ThingsboardErrorCode.INVALID_ARGUMENTS); + } else { + SparkplugMessageType type; + String namespace, edgeNodeId, groupId, deviceId; + namespace = validateNameSpace(splitTopic[0]); + groupId = length > 1 ? splitTopic[1] : null; + type = length > 2 ? SparkplugMessageType.parseMessageType(splitTopic[2]) : null; + edgeNodeId = length > 3 ? splitTopic[3] : null; + deviceId = length > 4 ? splitTopic[4] : null; + return new SparkplugTopic(namespace, groupId, edgeNodeId, deviceId, type); + } + } + + /** + * For the Sparkplug™ B version of the specification, the UTF-8 string constant for the namespace element will be: "spBv1.0" + * @param nameSpace + * @return + */ + private static String validateNameSpace(String nameSpace) throws ThingsboardException { + if (NAMESPACE.equals(nameSpace)) return nameSpace; + throw new ThingsboardException("The namespace [" + nameSpace + "] is not valid and must be [" + NAMESPACE + "] for the Sparkplug™ B version.", ThingsboardErrorCode.INVALID_ARGUMENTS); + } + +} diff --git a/common/transport/mqtt/src/main/proto/sparkplug.proto b/common/transport/mqtt/src/main/proto/sparkplug.proto new file mode 100644 index 0000000000..0d757391ff --- /dev/null +++ b/common/transport/mqtt/src/main/proto/sparkplug.proto @@ -0,0 +1,204 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +syntax = "proto3"; + +import "google/protobuf/any.proto"; + + +option java_package = "org.thingsboard.server.gen.transport.mqtt"; +option java_outer_classname = "SparkplugBProto"; + +message Payload { + /* + // Indexes of Data Types + // Unknown placeholder for future expansion. + Unknown = 0; + // Basic Types + Int8 = 1; + Int16 = 2; + Int32 = 3; + Int64 = 4; + UInt8 = 5; + UInt16 = 6; + UInt32 = 7; + UInt64 = 8; + Float = 9; + Double = 10; + Boolean = 11; + String = 12; + DateTime = 13; + Text = 14; + // Additional Metric Types + UUID = 15; + DataSet = 16; + Bytes = 17; + File = 18; + Template = 19; + + // Additional PropertyValue Types + PropertySet = 20; + PropertySetList = 21; + */ + + message Template { + + message Parameter { + optional string name = 1; + optional uint32 type = 2; + + oneof value { + uint32 int_value = 3; + uint64 long_value = 4; + float float_value = 5; + double double_value = 6; + bool boolean_value = 7; + string string_value = 8; + ParameterValueExtension extension_value = 9; + } + + message ParameterValueExtension { + google.protobuf.Any extensions = 1; + } + } + + optional string version = 1; // The version of the Template to prevent mismatches + repeated Metric metrics = 2; // Each metric is the name of the metric and the datatype of the member but does not contain a value + repeated Parameter parameters = 3; + optional string template_ref = 4; // Reference to a template if this is extending a Template or an instance - must exist if an instance + optional bool is_definition = 5; + google.protobuf.Any extensions = 6; + } + + message DataSet { + + message DataSetValue { + + oneof value { + uint32 int_value = 1; + uint64 long_value = 2; + float float_value = 3; + double double_value = 4; + bool boolean_value = 5; + string string_value = 6; + DataSetValueExtension extension_value = 7; + } + + message DataSetValueExtension { + google.protobuf.Any extensions = 1; + } + } + + message Row { + repeated DataSetValue elements = 1; + google.protobuf.Any extensions = 2; // For third party extensions + } + + optional uint64 num_of_columns = 1; + repeated string columns = 2; + repeated uint32 types = 3; + repeated Row rows = 4; + google.protobuf.Any extensions = 5; // For third party extensions + } + + message PropertyValue { + + optional uint32 type = 1; + optional bool is_null = 2; + + oneof value { + uint32 int_value = 3; + uint64 long_value = 4; + float float_value = 5; + double double_value = 6; + bool boolean_value = 7; + string string_value = 8; + PropertySet propertyset_value = 9; + PropertySetList propertysets_value = 10; // List of Property Values + PropertyValueExtension extension_value = 11; + } + + message PropertyValueExtension { + google.protobuf.Any extensions = 1; + } + } + + message PropertySet { + repeated string keys = 1; // Names of the properties + repeated PropertyValue values = 2; + google.protobuf.Any extensions = 3; + } + + message PropertySetList { + repeated PropertySet propertyset = 1; + google.protobuf.Any extensions = 2; + } + + message MetaData { + // Bytes specific metadata + optional bool is_multi_part = 1; + + // General metadata + optional string content_type = 2; // Content/Media type + optional uint64 size = 3; // File size, String size, Multi-part size, etc + optional uint64 seq = 4; // Sequence number for multi-part messages + + // File metadata + optional string file_name = 5; // File name + optional string file_type = 6; // File type (i.e. xml, json, txt, cpp, etc) + optional string md5 = 7; // md5 of data + + // Catchalls and future expansion + optional string description = 8; // Could be anything such as json or xml of custom properties + google.protobuf.Any extensions = 9; + } + + message Metric { + + optional string name = 1; // Metric name - should only be included on birth + optional uint64 alias = 2; // Metric alias - tied to name on birth and included in all later DATA messages + optional uint64 timestamp = 3; // Timestamp associated with data acquisition time + optional uint32 datatype = 4; // DataType of the metric/tag value + optional bool is_historical = 5; // If this is historical data and should not update real time tag + optional bool is_transient = 6; // Tells consuming clients such as MQTT Engine to not store this as a tag + optional bool is_null = 7; // If this is null - explicitly say so rather than using -1, false, etc for some datatypes. + optional MetaData metadata = 8; // Metadata for the payload + optional PropertySet properties = 9; + + oneof value { + uint32 int_value = 10; + uint64 long_value = 11; + float float_value = 12; + double double_value = 13; + bool boolean_value = 14; + string string_value = 15; + bytes bytes_value = 16; // Bytes, File + DataSet dataset_value = 17; + Template template_value = 18; + MetricValueExtension extension_value = 19; + } + + message MetricValueExtension { + google.protobuf.Any extensions = 1; + } + } + + optional uint64 timestamp = 1; // Timestamp at message sending time + repeated Metric metrics = 2; // Repeated forever - no limit in Google Protobufs + optional uint64 seq = 3; // Sequence number + optional string uuid = 4; // UUID to track message type in terms of schema definitions + optional bytes body = 5; // To optionally bypass the whole definition above + google.protobuf.Any extensions = 6; +} \ No newline at end of file diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java index 04bf2b964d..dc2ad73cc1 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/service/DefaultTransportService.java @@ -301,8 +301,8 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetEntityProfileResponseMsg getEntityProfile(TransportProtos.GetEntityProfileRequestMsg msg) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setEntityProfileRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setEntityProfileRequestMsg(msg).build()); try { TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); return response.getValue().getEntityProfileResponseMsg(); @@ -313,8 +313,8 @@ public class DefaultTransportService implements TransportService { @Override public List getQueueRoutingInfo(TransportProtos.GetAllQueueRoutingInfoRequestMsg msg) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setGetAllQueueRoutingInfoRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setGetAllQueueRoutingInfoRequestMsg(msg).build()); try { TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); return response.getValue().getGetQueueRoutingInfoResponseMsgsList(); @@ -325,8 +325,8 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetResourceResponseMsg getResource(TransportProtos.GetResourceRequestMsg msg) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setResourceRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setResourceRequestMsg(msg).build()); try { TbProtoQueueMsg response = transportApiRequestTemplate.send(protoMsg).get(); return response.getValue().getResourceResponseMsg(); @@ -337,8 +337,8 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetSnmpDevicesResponseMsg getSnmpDevicesIds(TransportProtos.GetSnmpDevicesRequestMsg requestMsg) { - TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>( - UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder() + TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>( + UUID.randomUUID(), TransportApiRequestMsg.newBuilder() .setSnmpDevicesRequestMsg(requestMsg) .build() ); @@ -354,7 +354,7 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetDeviceResponseMsg getDevice(TransportProtos.GetDeviceRequestMsg requestMsg) { TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>( - UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder() + UUID.randomUUID(), TransportApiRequestMsg.newBuilder() .setDeviceRequestMsg(requestMsg) .build() ); @@ -374,7 +374,7 @@ public class DefaultTransportService implements TransportService { @Override public TransportProtos.GetDeviceCredentialsResponseMsg getDeviceCredentials(TransportProtos.GetDeviceCredentialsRequestMsg requestMsg) { TbProtoQueueMsg protoMsg = new TbProtoQueueMsg<>( - UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder() + UUID.randomUUID(), TransportApiRequestMsg.newBuilder() .setDeviceCredentialsRequestMsg(requestMsg) .build() ); @@ -720,8 +720,8 @@ public class DefaultTransportService implements TransportService { @Override public void process(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.GetOtaPackageRequestMsg msg, TransportServiceCallback callback) { if (checkLimits(sessionInfo, msg, callback)) { - TbProtoQueueMsg protoMsg = - new TbProtoQueueMsg<>(UUID.randomUUID(), TransportProtos.TransportApiRequestMsg.newBuilder().setOtaPackageRequestMsg(msg).build()); + TbProtoQueueMsg protoMsg = + new TbProtoQueueMsg<>(UUID.randomUUID(), TransportApiRequestMsg.newBuilder().setOtaPackageRequestMsg(msg).build()); AsyncCallbackTemplate.withCallback(transportApiRequestTemplate.send(protoMsg), response -> { callback.onSuccess(response.getValue().getOtaPackageResponseMsg()); @@ -864,7 +864,7 @@ public class DefaultTransportService implements TransportService { } } - protected void processToTransportMsg(TransportProtos.ToTransportMsg toSessionMsg) { + protected void processToTransportMsg(ToTransportMsg toSessionMsg) { UUID sessionId = new UUID(toSessionMsg.getSessionIdMSB(), toSessionMsg.getSessionIdLSB()); SessionMetaData md = sessions.get(sessionId); if (md != null) { diff --git a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java index e226067042..4870cfc3ca 100644 --- a/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java +++ b/common/transport/transport-api/src/main/java/org/thingsboard/server/common/transport/session/DeviceAwareSessionContext.java @@ -20,6 +20,8 @@ import lombok.Getter; import lombok.Setter; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; +import org.thingsboard.server.common.data.device.profile.DeviceProfileTransportConfiguration; +import org.thingsboard.server.common.data.device.profile.MqttDeviceProfileTransportConfiguration; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.transport.auth.TransportDeviceInfo; import org.thingsboard.server.gen.transport.TransportProtos; @@ -44,7 +46,7 @@ public abstract class DeviceAwareSessionContext implements SessionContext { protected volatile DeviceProfile deviceProfile; @Getter @Setter - private volatile TransportProtos.SessionInfoProto sessionInfo; + protected volatile TransportProtos.SessionInfoProto sessionInfo; @Setter private volatile boolean connected; @@ -81,4 +83,14 @@ public abstract class DeviceAwareSessionContext implements SessionContext { public void setDisconnected() { this.connected = false; } + + public boolean isSparkplug() { + DeviceProfileTransportConfiguration transportConfiguration = this.deviceProfile.getProfileData().getTransportConfiguration(); + if (transportConfiguration instanceof MqttDeviceProfileTransportConfiguration) { + return ((MqttDeviceProfileTransportConfiguration) transportConfiguration).isSparkplug(); + } else { + return false; + } + } + } diff --git a/common/transport/transport-api/src/test/java/JsonConverterTest.java b/common/transport/transport-api/src/test/java/JsonConverterTest.java index a60516e784..39ed04a29a 100644 --- a/common/transport/transport-api/src/test/java/JsonConverterTest.java +++ b/common/transport/transport-api/src/test/java/JsonConverterTest.java @@ -16,20 +16,18 @@ import com.google.gson.JsonParser; import com.google.gson.JsonSyntaxException; import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.junit.MockitoJUnitRunner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.thingsboard.server.common.transport.adaptor.JsonConverter; import java.util.ArrayList; -@RunWith(MockitoJUnitRunner.class) public class JsonConverterTest { - private static final JsonParser JSON_PARSER = new JsonParser(); + private final JsonParser JSON_PARSER = new JsonParser(); - @Before + @BeforeEach public void before() { JsonConverter.setTypeCastEnabled(true); } @@ -88,15 +86,19 @@ public class JsonConverterTest { Assert.assertEquals("10000000000000000000", result.get(0L).get(0).getStrValue().get()); } - @Test(expected = JsonSyntaxException.class) + @Test public void testParseBigDecimalOutOfLongRangeWithoutParsing() { JsonConverter.setTypeCastEnabled(false); - JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 89701010051400054084}"), 0L); + Assertions.assertThrows(JsonSyntaxException.class, () -> { + JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 89701010051400054084}"), 0L); + }); } - @Test(expected = JsonSyntaxException.class) + @Test public void testParseBigDecimalOutOfLongRangeWithoutParsing2() { JsonConverter.setTypeCastEnabled(false); - JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); + Assertions.assertThrows(JsonSyntaxException.class, () -> { + JsonConverter.convertToTelemetry(JSON_PARSER.parse("{\"meterReadingDelta\": 9.9701010061400066E19}"), 0L); + }); } } diff --git a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java index 9120b45b6b..4638b81958 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java +++ b/common/util/src/main/java/org/thingsboard/common/util/JacksonUtil.java @@ -155,6 +155,14 @@ public class JacksonUtil { return mapper.createObjectNode(); } + public static ArrayNode newArrayNode() { + return newArrayNode(OBJECT_MAPPER); + } + + public static ArrayNode newArrayNode(ObjectMapper mapper) { + return mapper.createArrayNode(); + } + public static T clone(T value) { @SuppressWarnings("unchecked") Class valueClass = (Class) value.getClass(); diff --git a/dao/pom.xml b/dao/pom.xml index 30dc1adcf0..4fc8a3c08d 100644 --- a/dao/pom.xml +++ b/dao/pom.xml @@ -162,26 +162,6 @@ com.google.guava guava - - org.cassandraunit - cassandra-unit - - - org.slf4j - slf4j-log4j12 - - - org.hibernate - hibernate-validator - - - test - - - org.apache.cassandra - cassandra-thrift - test - com.google.protobuf protobuf-java @@ -211,6 +191,11 @@ spring-test test + + org.testcontainers + cassandra + test + org.testcontainers postgresql diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java index 103f25a4ac..3d2e7078a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/AlarmDao.java @@ -15,17 +15,21 @@ */ package org.thingsboard.server.dao.alarm; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; @@ -44,12 +48,16 @@ public interface AlarmDao extends Dao { Alarm findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + ListenableFuture findLatestByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type); Alarm findAlarmById(TenantId tenantId, UUID key); ListenableFuture findAlarmByIdAsync(TenantId tenantId, UUID key); + AlarmInfo findAlarmInfoById(TenantId tenantId, UUID key); + Alarm save(TenantId tenantId, Alarm alarm); PageData findAlarms(TenantId tenantId, AlarmQuery query); @@ -58,7 +66,7 @@ public interface AlarmDao extends Dao { PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); - Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set status); + Set findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId); PageData findAlarmsIdsByEndTsBeforeAndTenantId(Long time, TenantId tenantId, PageLink pageLink); @@ -67,4 +75,17 @@ public interface AlarmDao extends Dao { List findEntityAlarmRecords(TenantId tenantId, AlarmId id); void deleteEntityAlarmRecords(TenantId tenantId, EntityId entityId); + + AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled); + + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); + + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs); + + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime); + + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java index 606eee468d..0dd2eebf87 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/alarm/BaseAlarmService.java @@ -20,20 +20,22 @@ import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.MoreExecutors; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.CollectionUtils; -import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmModificationRequest; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.exception.ApiUsageLimitsExceededException; import org.thingsboard.server.common.data.id.AlarmId; @@ -41,6 +43,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; @@ -50,11 +53,11 @@ import org.thingsboard.server.common.data.relation.EntitySearchDirection; import org.thingsboard.server.common.data.relation.RelationsSearchParameters; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.entity.EntityService; +import org.thingsboard.server.dao.exception.DataValidationException; +import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.dao.tenant.TenantService; -import javax.annotation.Nullable; -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -63,8 +66,6 @@ import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -73,32 +74,52 @@ import static org.thingsboard.server.dao.service.Validator.validateId; @Service("AlarmDaoService") @Slf4j +@RequiredArgsConstructor public class BaseAlarmService extends AbstractEntityService implements AlarmService { public static final String INCORRECT_TENANT_ID = "Incorrect tenantId "; - public static final String INCORRECT_CUSTOMER_ID = "Incorrect customerId "; - @Autowired - private AlarmDao alarmDao; + private final TenantService tenantService; + private final AlarmDao alarmDao; + private final EntityService entityService; + private final DataValidator alarmDataValidator; - @Autowired - private EntityService entityService; + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + validateAlarmRequest(request); + return withPropagated(alarmDao.updateAlarm(request)); + } - @Autowired - private DataValidator alarmDataValidator; + @Override + public AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request) { + return createAlarm(request, true); + } - protected ExecutorService readResultsProcessingExecutor; + @Override + public AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) { + validateAlarmRequest(request); + CustomerId customerId = entityService.fetchEntityCustomerId(request.getTenantId(), request.getOriginator()).orElse(null); + if (customerId == null && request.getCustomerId() != null) { + throw new DataValidationException("Can't assign alarm to customer. Originator is not assigned to customer!"); + } else if (customerId != null && request.getCustomerId() != null && !customerId.equals(request.getCustomerId())) { + throw new DataValidationException("Can't assign alarm to customer. Originator belongs to different customer!"); + } + request.setCustomerId(customerId); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, alarmCreationEnabled); + if (!result.isSuccessful() && !alarmCreationEnabled) { + throw new ApiUsageLimitsExceededException("Alarms creation is disabled"); + } + return withPropagated(result); + } - @PostConstruct - public void startExecutor() { - readResultsProcessingExecutor = Executors.newCachedThreadPool(ThingsBoardThreadFactory.forName("alarm-service")); + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs) { + return withPropagated(alarmDao.acknowledgeAlarm(tenantId, alarmId, ackTs)); } - @PreDestroy - public void stopExecutor() { - if (readResultsProcessingExecutor != null) { - readResultsProcessingExecutor.shutdownNow(); - } + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details) { + return withPropagated(alarmDao.clearAlarm(tenantId, alarmId, clearTs, details)); } @Override @@ -116,8 +137,9 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (alarm.getEndTs() == 0L) { alarm.setEndTs(alarm.getStartTs()); } - alarm.setCustomerId(entityService.fetchEntityCustomerId(alarm.getTenantId(), alarm.getOriginator()).get()); + alarm.setCustomerId(entityService.fetchEntityCustomerId(alarm.getTenantId(), alarm.getOriginator()).orElse(null)); if (alarm.getId() == null) { + // Atomic update and return alarm + assignee. Alarm existing = alarmDao.findLatestByOriginatorAndType(alarm.getTenantId(), alarm.getOriginator(), alarm.getType()); if (existing == null || existing.getStatus().isCleared()) { if (!alarmCreationEnabled) { @@ -135,6 +157,12 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } } + @Override + public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + return alarmDao.findLatestActiveByOriginatorAndType(tenantId, originator, type); + } + + @Override public ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { return alarmDao.findLatestByOriginatorAndTypeAsync(tenantId, originator, type); } @@ -147,6 +175,20 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return alarmDao.findAlarmDataByQueryForEntities(tenantId, query, orderedEntityIds); } + @Override + @Transactional + public AlarmApiCallResult delAlarm(TenantId tenantId, AlarmId alarmId) { + log.debug("Deleting Alarm Id: {}", alarmId); + AlarmInfo alarm = alarmDao.findAlarmInfoById(tenantId, alarmId.getId()); + if (alarm == null) { + return AlarmApiCallResult.builder().successful(false).build(); + } else { + deleteEntityRelations(tenantId, alarm.getId()); + alarmDao.removeById(tenantId, alarm.getUuidId()); + return AlarmApiCallResult.builder().alarm(alarm).successful(true).build(); + } + } + @Override @Transactional public AlarmOperationResult deleteAlarm(TenantId tenantId, AlarmId alarmId) { @@ -165,10 +207,10 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ log.debug("New Alarm : {}", alarm); Alarm saved = alarmDao.save(alarm.getTenantId(), alarm); List propagatedEntitiesList = createEntityAlarmRecords(saved); - return new AlarmOperationResult(saved, true, true, null, propagatedEntitiesList); + return new AlarmOperationResult(saved, true, true, propagatedEntitiesList); } - private List createEntityAlarmRecords(Alarm alarm) throws InterruptedException, ExecutionException { + private List createEntityAlarmRecords(Alarm alarm) throws ExecutionException, InterruptedException { Set propagatedEntitiesSet = new LinkedHashSet<>(); propagatedEntitiesSet.add(alarm.getOriginator()); if (alarm.isPropagate()) { @@ -226,45 +268,41 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ @Override public ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTime) { - return getAndUpdateAsync(tenantId, alarmId, new Function() { - @Nullable - @Override - public AlarmOperationResult apply(@Nullable Alarm alarm) { - if (alarm == null || alarm.getStatus().isAck()) { - return new AlarmOperationResult(alarm, false); - } else { - AlarmStatus oldStatus = alarm.getStatus(); - AlarmStatus newStatus = oldStatus.isCleared() ? AlarmStatus.CLEARED_ACK : AlarmStatus.ACTIVE_ACK; - alarm.setStatus(newStatus); - alarm.setAckTs(ackTime); - alarm = alarmDao.save(alarm.getTenantId(), alarm); - return new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); - } - } - }); + Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId()); + if (alarm == null || alarm.getStatus().isAck()) { + return Futures.immediateFuture(new AlarmOperationResult(alarm, false)); + } else { + alarm.setAcknowledged(true); + alarm.setAckTs(ackTime); + alarm = alarmDao.save(alarm.getTenantId(), alarm); + return Futures.immediateFuture(new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm)))); + } } @Override public ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTime) { - return getAndUpdateAsync(tenantId, alarmId, new Function() { - @Nullable - @Override - public AlarmOperationResult apply(@Nullable Alarm alarm) { - if (alarm == null || alarm.getStatus().isCleared()) { - return new AlarmOperationResult(alarm, false); - } else { - AlarmStatus oldStatus = alarm.getStatus(); - AlarmStatus newStatus = oldStatus.isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK; - alarm.setStatus(newStatus); - alarm.setClearTs(clearTime); - if (details != null) { - alarm.setDetails(details); - } - alarm = alarmDao.save(alarm.getTenantId(), alarm); - return new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm))); - } + Alarm alarm = alarmDao.findAlarmById(tenantId, alarmId.getId()); + if (alarm == null || alarm.getStatus().isCleared()) { + return Futures.immediateFuture(new AlarmOperationResult(alarm, false)); + } else { + alarm.setCleared(true); + alarm.setClearTs(clearTime); + if (details != null) { + alarm.setDetails(details); } - }); + alarm = alarmDao.save(alarm.getTenantId(), alarm); + return Futures.immediateFuture(new AlarmOperationResult(alarm, true, new ArrayList<>(getPropagationEntityIds(alarm)))); + } + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTime) { + return withPropagated(alarmDao.assignAlarm(tenantId, alarmId, assigneeId, assignTime)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long unassignTime) { + return withPropagated(alarmDao.unassignAlarm(tenantId, alarmId, unassignTime)); } @Override @@ -282,60 +320,35 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } @Override - public ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { + public AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId) { log.trace("Executing findAlarmInfoByIdAsync [{}]", alarmId); validateId(alarmId, "Incorrect alarmId " + alarmId); - return Futures.transform(alarmDao.findAlarmByIdAsync(tenantId, alarmId.getId()), - a -> { - AlarmInfo alarmInfo = new AlarmInfo(a); - alarmInfo.setOriginatorName( - entityService.fetchEntityName(tenantId, alarmInfo.getOriginator()).orElse("N/A")); - return alarmInfo; - }, MoreExecutors.directExecutor()); + return alarmDao.findAlarmInfoById(tenantId, alarmId.getId()); } @Override public ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query) { - PageData alarms = alarmDao.findAlarms(tenantId, query); - if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) { - return fetchAlarmsOriginators(tenantId, alarms); - } - return Futures.immediateFuture(alarms); + return Futures.immediateFuture(alarmDao.findAlarms(tenantId, query)); } @Override public ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) { - PageData alarms = alarmDao.findCustomerAlarms(tenantId, customerId, query); - if (query.getFetchOriginator() != null && query.getFetchOriginator().booleanValue()) { - return fetchAlarmsOriginators(tenantId, alarms); - } - return Futures.immediateFuture(alarms); - } - - private ListenableFuture> fetchAlarmsOriginators(TenantId tenantId, PageData alarms) { - List> alarmFutures = new ArrayList<>(alarms.getData().size()); - for (AlarmInfo alarmInfo : alarms.getData()) { - alarmInfo.setOriginatorName( - entityService.fetchEntityName(tenantId, alarmInfo.getOriginator()).orElse("Deleted")); - alarmFutures.add(Futures.immediateFuture(alarmInfo)); - } - return Futures.transform(Futures.successfulAsList(alarmFutures), - alarmInfos -> new PageData<>(alarmInfos, alarms.getTotalPages(), alarms.getTotalElements(), - alarms.hasNext()), MoreExecutors.directExecutor()); + return Futures.immediateFuture(alarmDao.findCustomerAlarms(tenantId, customerId, query)); } @Override public AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, - AlarmStatus alarmStatus) { - Set statusList = null; + AlarmStatus alarmStatus, String assigneeId) { + AlarmStatusFilter asf; if (alarmSearchStatus != null) { - statusList = alarmSearchStatus.getStatuses(); + asf = AlarmStatusFilter.from(alarmSearchStatus); } else if (alarmStatus != null) { - statusList = Collections.singleton(alarmStatus); + asf = AlarmStatusFilter.from(alarmStatus); + } else { + asf = AlarmStatusFilter.empty(); } - Set alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, statusList); - + Set alarmSeverities = alarmDao.findAlarmSeverities(tenantId, entityId, asf, assigneeId); return alarmSeverities.stream().min(AlarmSeverity::compareTo).orElse(null); } @@ -357,10 +370,15 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ if (alarm.getAckTs() > existing.getAckTs()) { existing.setAckTs(alarm.getAckTs()); } - existing.setStatus(alarm.getStatus()); + if (alarm.getAssignTs() > existing.getAssignTs()) { + existing.setAssignTs(alarm.getAssignTs()); + } + existing.setAcknowledged(alarm.isAcknowledged()); + existing.setCleared(alarm.isCleared()); existing.setSeverity(alarm.getSeverity()); existing.setDetails(alarm.getDetails()); existing.setCustomerId(alarm.getCustomerId()); + existing.setAssigneeId(alarm.getAssigneeId()); existing.setPropagate(existing.isPropagate() || alarm.isPropagate()); existing.setPropagateToOwner(existing.isPropagateToOwner() || alarm.isPropagateToOwner()); existing.setPropagateToTenant(existing.isPropagateToTenant() || alarm.isPropagateToTenant()); @@ -378,6 +396,10 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return existing; } + private List getPropagationEntityIdsList(Alarm alarm) { + return new ArrayList<>(getPropagationEntityIds(alarm)); + } + private Set getPropagationEntityIds(Alarm alarm) { if (alarm.isPropagate() || alarm.isPropagateToOwner() || alarm.isPropagateToTenant()) { List entityAlarms = alarmDao.findEntityAlarmRecords(alarm.getTenantId(), alarm.getId()); @@ -388,7 +410,7 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } private void createEntityAlarmRecord(TenantId tenantId, EntityId entityId, Alarm alarm) { - EntityAlarm entityAlarm = new EntityAlarm(tenantId, entityId, alarm.getCreatedTime(), alarm.getType(), alarm.getCustomerId(), alarm.getId()); + EntityAlarm entityAlarm = new EntityAlarm(tenantId, entityId, alarm.getCreatedTime(), alarm.getType(), alarm.getCustomerId(), null, alarm.getId()); try { alarmDao.createEntityAlarmRecord(entityAlarm); } catch (Exception e) { @@ -396,12 +418,6 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ } } - private ListenableFuture getAndUpdateAsync(TenantId tenantId, AlarmId alarmId, Function function) { - validateId(alarmId, "Alarm id should be specified!"); - ListenableFuture entity = alarmDao.findAlarmByIdAsync(tenantId, alarmId.getId()); - return Futures.transform(entity, function, readResultsProcessingExecutor); - } - private T getAndUpdate(TenantId tenantId, AlarmId alarmId, Function function) { validateId(alarmId, "Alarm id should be specified!"); Alarm entity = alarmDao.findAlarmById(tenantId, alarmId.getId()); @@ -418,4 +434,39 @@ public class BaseAlarmService extends AbstractEntityService implements AlarmServ return EntityType.ALARM; } + //TODO: refactor to use efficient caching. + private AlarmApiCallResult withPropagated(AlarmApiCallResult result) { + if (result.isSuccessful() && result.getAlarm() != null) { + List propagationEntities; + if (result.isPropagationChanged()) { + try { + propagationEntities = createEntityAlarmRecords(result.getAlarm()); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } else { + propagationEntities = getPropagationEntityIdsList(result.getAlarm()); + } + return new AlarmApiCallResult(result, propagationEntities); + } else { + return result; + } + } + + private void validateAlarmRequest(AlarmModificationRequest request) { + ConstraintValidator.validateFields(request); + if (request.getEndTs() > 0 && request.getStartTs() > request.getEndTs()) { + throw new DataValidationException("Alarm start ts can't be greater then alarm end ts!"); + } + if (!tenantService.tenantExists(request.getTenantId())) { + throw new DataValidationException("Alarm is referencing to non-existent tenant!"); + } + if (request.getStartTs() == 0L) { + request.setStartTs(System.currentTimeMillis()); + } + if (request.getEndTs() == 0L) { + request.setEndTs(request.getStartTs()); + } + } + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java index 584ae84531..017fe4315c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/attributes/CachedAttributesService.java @@ -66,7 +66,7 @@ public class CachedAttributesService implements AttributesService { private final TbTransactionalCache cache; private ListeningExecutorService cacheExecutor; - @Value("${cache.type}") + @Value("${cache.type:caffeine}") private String cacheType; public CachedAttributesService(AttributesDao attributesDao, diff --git a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java index e6caddd80f..7aa785107e 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java @@ -166,6 +166,8 @@ public class AuditLogServiceImpl implements AuditLogService { case UPDATED: case ALARM_ACK: case ALARM_CLEAR: + case ALARM_ASSIGN: + case ALARM_UNASSIGN: case RELATIONS_DELETED: case ASSIGNED_TO_TENANT: if (entity != null) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java index de6843e3a9..cbce9e48a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java +++ b/dao/src/main/java/org/thingsboard/server/dao/entity/BaseEntityService.java @@ -20,7 +20,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.CollectionUtils; import org.thingsboard.server.common.data.HasCustomerId; +import org.thingsboard.server.common.data.HasEmail; +import org.thingsboard.server.common.data.HasLabel; import org.thingsboard.server.common.data.HasName; +import org.thingsboard.server.common.data.HasTitle; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.id.NameLabelAndCustomerDetails; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.HasId; @@ -34,6 +39,7 @@ import org.thingsboard.server.common.data.query.RelationsQueryFilter; import org.thingsboard.server.dao.exception.IncorrectParameterException; import java.util.Optional; +import java.util.function.Function; import static org.thingsboard.server.common.data.id.EntityId.NULL_UUID; import static org.thingsboard.server.dao.service.Validator.validateEntityDataPageLink; @@ -77,35 +83,67 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe @Override public Optional fetchEntityName(TenantId tenantId, EntityId entityId) { log.trace("Executing fetchEntityName [{}]", entityId); - EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()); - Optional> hasIdOpt = entityDaoService.findEntity(tenantId, entityId); - if (hasIdOpt.isPresent()) { - HasId hasId = hasIdOpt.get(); - if (hasId instanceof HasName) { - HasName hasName = (HasName) hasId; - return Optional.ofNullable(hasName.getName()); - } - } - return Optional.empty(); + return fetchAndConvert(tenantId, entityId, this::getName); + } + + @Override + public Optional fetchEntityLabel(TenantId tenantId, EntityId entityId) { + log.trace("Executing fetchEntityLabel [{}]", entityId); + return fetchAndConvert(tenantId, entityId, this::getLabel); } @Override public Optional fetchEntityCustomerId(TenantId tenantId, EntityId entityId) { log.trace("Executing fetchEntityCustomerId [{}]", entityId); + return fetchAndConvert(tenantId, entityId, this::getCustomerId); + } + + @Override + public Optional fetchNameLabelAndCustomerDetails(TenantId tenantId, EntityId entityId) { + log.trace("Executing fetchNameLabelAndCustomerDetails [{}]", entityId); + return fetchAndConvert(tenantId, entityId, this::getNameLabelAndCustomerDetails); + } + + private Optional fetchAndConvert(TenantId tenantId, EntityId entityId, Function, T> converter) { EntityDaoService entityDaoService = entityServiceRegistry.getServiceByEntityType(entityId.getEntityType()); - Optional> hasIdOpt = entityDaoService.findEntity(tenantId, entityId); - if (hasIdOpt.isPresent()) { - HasId hasId = hasIdOpt.get(); - if (hasId instanceof HasCustomerId) { - HasCustomerId hasCustomerId = (HasCustomerId) hasId; - CustomerId customerId = hasCustomerId.getCustomerId(); - if (customerId == null) { - customerId = NULL_CUSTOMER_ID; - } - return Optional.of(customerId); + Optional> entityOpt = entityDaoService.findEntity(tenantId, entityId); + return entityOpt.map(converter); + } + + private String getName(HasId entity) { + return entity instanceof HasName ? ((HasName) entity).getName() : null; + } + + private String getLabel(HasId entity) { + if (entity instanceof HasTitle && StringUtils.isNotEmpty(((HasTitle) entity).getTitle())) { + return ((HasTitle) entity).getTitle(); + } + if (entity instanceof HasLabel && StringUtils.isNotEmpty(((HasLabel) entity).getLabel())) { + return ((HasLabel) entity).getLabel(); + } + if (entity instanceof HasEmail && StringUtils.isNotEmpty(((HasEmail) entity).getEmail())) { + return ((HasEmail) entity).getEmail(); + } + if (entity instanceof HasName && StringUtils.isNotEmpty(((HasName) entity).getName())) { + return ((HasName) entity).getName(); + } + return null; + } + + private CustomerId getCustomerId(HasId entity) { + if (entity instanceof HasCustomerId) { + HasCustomerId hasCustomerId = (HasCustomerId) entity; + CustomerId customerId = hasCustomerId.getCustomerId(); + if (customerId == null) { + customerId = NULL_CUSTOMER_ID; } + return customerId; } - return Optional.of(NULL_CUSTOMER_ID); + return NULL_CUSTOMER_ID; + } + + private NameLabelAndCustomerDetails getNameLabelAndCustomerDetails(HasId entity) { + return new NameLabelAndCustomerDetails(getName(entity), getLabel(entity), getCustomerId(entity)); } private static void validateEntityCountQuery(EntityCountQuery query) { @@ -126,7 +164,7 @@ public class BaseEntityService extends AbstractEntityService implements EntitySe } private static void validateRelationQuery(RelationsQueryFilter queryFilter) { - if (queryFilter.isMultiRoot() && queryFilter.getMultiRootEntitiesType() ==null){ + if (queryFilter.isMultiRoot() && queryFilter.getMultiRootEntitiesType() == null) { throw new IncorrectParameterException("Multi-root relation query filter should contain 'multiRootEntitiesType'"); } if (queryFilter.isMultiRoot() && CollectionUtils.isEmpty(queryFilter.getMultiRootEntityIds())) { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java index cf953583d1..0f4ee310c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java @@ -41,6 +41,7 @@ public class ModelConstants { public static final String USER_ID_PROPERTY = "user_id"; public static final String TENANT_ID_PROPERTY = "tenant_id"; public static final String CUSTOMER_ID_PROPERTY = "customer_id"; + public static final String ASSIGNEE_ID_PROPERTY = "assignee_id"; public static final String DEVICE_ID_PROPERTY = "device_id"; public static final String TITLE_PROPERTY = "title"; public static final String ALIAS_PROPERTY = "alias"; @@ -81,6 +82,7 @@ public class ModelConstants { public static final String USER_CREDENTIALS_PASSWORD_PROPERTY = "password"; //NOSONAR, the constant used to identify password column name (not password value itself) public static final String USER_CREDENTIALS_ACTIVATE_TOKEN_PROPERTY = "activate_token"; public static final String USER_CREDENTIALS_RESET_TOKEN_PROPERTY = "reset_token"; + public static final String USER_CREDENTIALS_ADDITIONAL_PROPERTY = "additional_info"; public static final String USER_CREDENTIALS_BY_USER_COLUMN_FAMILY_NAME = "user_credentials_by_user"; public static final String USER_CREDENTIALS_BY_ACTIVATE_TOKEN_COLUMN_FAMILY_NAME = "user_credentials_by_activate_token"; @@ -286,24 +288,35 @@ public class ModelConstants { */ public static final String ENTITY_ALARM_COLUMN_FAMILY_NAME = "entity_alarm"; public static final String ALARM_COLUMN_FAMILY_NAME = "alarm"; + public static final String ALARM_VIEW_NAME = "alarm_info"; public static final String ALARM_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; public static final String ALARM_CUSTOMER_ID_PROPERTY = CUSTOMER_ID_PROPERTY; public static final String ALARM_TYPE_PROPERTY = "type"; - public static final String ALARM_DETAILS_PROPERTY = "details"; + public static final String ALARM_DETAILS_PROPERTY = ADDITIONAL_INFO_PROPERTY; + public static final String ALARM_STATUS_PROPERTY = "status"; public static final String ALARM_ORIGINATOR_ID_PROPERTY = "originator_id"; public static final String ALARM_ORIGINATOR_NAME_PROPERTY = "originator_name"; + public static final String ALARM_ORIGINATOR_LABEL_PROPERTY = "originator_label"; public static final String ALARM_ORIGINATOR_TYPE_PROPERTY = "originator_type"; public static final String ALARM_SEVERITY_PROPERTY = "severity"; - public static final String ALARM_STATUS_PROPERTY = "status"; + public static final String ALARM_ASSIGNEE_ID_PROPERTY = "assignee_id"; + public static final String ALARM_ASSIGNEE_FIRST_NAME_PROPERTY = "assignee_first_name"; + public static final String ALARM_ASSIGNEE_LAST_NAME_PROPERTY = "assignee_last_name"; + public static final String ALARM_ASSIGNEE_EMAIL_PROPERTY = "assignee_email"; public static final String ALARM_START_TS_PROPERTY = "start_ts"; public static final String ALARM_END_TS_PROPERTY = "end_ts"; + public static final String ALARM_ACKNOWLEDGED_PROPERTY = "acknowledged"; public static final String ALARM_ACK_TS_PROPERTY = "ack_ts"; + public static final String ALARM_CLEARED_PROPERTY = "cleared"; public static final String ALARM_CLEAR_TS_PROPERTY = "clear_ts"; + public static final String ALARM_ASSIGN_TS_PROPERTY = "assign_ts"; public static final String ALARM_PROPAGATE_PROPERTY = "propagate"; public static final String ALARM_PROPAGATE_TO_OWNER_PROPERTY = "propagate_to_owner"; public static final String ALARM_PROPAGATE_TO_TENANT_PROPERTY = "propagate_to_tenant"; public static final String ALARM_PROPAGATE_RELATION_TYPES = "propagate_relation_types"; + public static final String ALARM_OPERATION_RESULT_PROPERTY = "operation_result"; + public static final String ALARM_BY_ID_VIEW_NAME = "alarm_by_id"; public static final String ALARM_COMMENT_TENANT_ID_PROPERTY = TENANT_ID_PROPERTY; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java index cf05ccda02..164e1518a1 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AbstractAlarmEntity.java @@ -25,11 +25,11 @@ import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.model.BaseEntity; import org.thingsboard.server.dao.model.BaseSqlEntity; import org.thingsboard.server.dao.model.ModelConstants; @@ -43,7 +43,11 @@ import java.util.Arrays; import java.util.Collections; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ACK_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_ID_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGN_TS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEARED_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CLEAR_TS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_END_TS_PROPERTY; @@ -55,7 +59,6 @@ import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_TO import static org.thingsboard.server.dao.model.ModelConstants.ALARM_PROPAGATE_TO_TENANT_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_SEVERITY_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_START_TS_PROPERTY; -import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TENANT_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ALARM_TYPE_PROPERTY; @@ -84,9 +87,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity @Column(name = ALARM_SEVERITY_PROPERTY) private AlarmSeverity severity; - @Enumerated(EnumType.STRING) - @Column(name = ALARM_STATUS_PROPERTY) - private AlarmStatus status; + @Type(type="pg-uuid") + @Column(name = ALARM_ASSIGNEE_ID_PROPERTY) + private UUID assigneeId; @Column(name = ALARM_START_TS_PROPERTY) private Long startTs; @@ -94,14 +97,23 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity @Column(name = ALARM_END_TS_PROPERTY) private Long endTs; + @Column(name = ALARM_ACKNOWLEDGED_PROPERTY) + private boolean acknowledged; + @Column(name = ALARM_ACK_TS_PROPERTY) private Long ackTs; + @Column(name = ALARM_CLEARED_PROPERTY) + private boolean cleared; + @Column(name = ALARM_CLEAR_TS_PROPERTY) private Long clearTs; + @Column(name = ALARM_ASSIGN_TS_PROPERTY) + private Long assignTs; + @Type(type = "json") - @Column(name = ModelConstants.ASSET_ADDITIONAL_INFO_PROPERTY) + @Column(name = ModelConstants.ALARM_DETAILS_PROPERTY) private JsonNode details; @Column(name = ALARM_PROPAGATE_PROPERTY) @@ -136,7 +148,11 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.originatorType = alarm.getOriginator().getEntityType(); this.type = alarm.getType(); this.severity = alarm.getSeverity(); - this.status = alarm.getStatus(); + this.acknowledged = alarm.isAcknowledged(); + this.cleared = alarm.isCleared(); + if (alarm.getAssigneeId() != null) { + this.assigneeId = alarm.getAssigneeId().getId(); + } this.propagate = alarm.isPropagate(); this.propagateToOwner = alarm.isPropagateToOwner(); this.propagateToTenant = alarm.isPropagateToTenant(); @@ -144,11 +160,12 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.endTs = alarm.getEndTs(); this.ackTs = alarm.getAckTs(); this.clearTs = alarm.getClearTs(); + this.assignTs = alarm.getAssignTs(); this.details = alarm.getDetails(); if (!CollectionUtils.isEmpty(alarm.getPropagateRelationTypes())) { this.propagateRelationTypes = String.join(",", alarm.getPropagateRelationTypes()); } else { - this.propagateRelationTypes = null; + this.propagateRelationTypes = ""; } } @@ -162,7 +179,9 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.originatorType = alarmEntity.getOriginatorType(); this.type = alarmEntity.getType(); this.severity = alarmEntity.getSeverity(); - this.status = alarmEntity.getStatus(); + this.acknowledged = alarmEntity.isAcknowledged(); + this.cleared = alarmEntity.isCleared(); + this.assigneeId = alarmEntity.getAssigneeId(); this.propagate = alarmEntity.getPropagate(); this.propagateToOwner = alarmEntity.getPropagateToOwner(); this.propagateToTenant = alarmEntity.getPropagateToTenant(); @@ -170,6 +189,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity this.endTs = alarmEntity.getEndTs(); this.ackTs = alarmEntity.getAckTs(); this.clearTs = alarmEntity.getClearTs(); + this.assignTs = alarmEntity.getAssignTs(); this.details = alarmEntity.getDetails(); this.propagateRelationTypes = alarmEntity.getPropagateRelationTypes(); } @@ -186,7 +206,11 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); alarm.setType(type); alarm.setSeverity(severity); - alarm.setStatus(status); + alarm.setAcknowledged(acknowledged); + alarm.setCleared(cleared); + if (assigneeId != null) { + alarm.setAssigneeId(new UserId(assigneeId)); + } alarm.setPropagate(propagate); alarm.setPropagateToOwner(propagateToOwner); alarm.setPropagateToTenant(propagateToTenant); @@ -194,6 +218,7 @@ public abstract class AbstractAlarmEntity extends BaseSqlEntity alarm.setEndTs(endTs); alarm.setAckTs(ackTs); alarm.setClearTs(clearTs); + alarm.setAssignTs(assignTs); alarm.setDetails(details); if (!StringUtils.isEmpty(propagateRelationTypes)) { alarm.setPropagateRelationTypes(Arrays.asList(propagateRelationTypes.split(","))); diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java index 1218613e2a..40ecd19a02 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/AlarmInfoEntity.java @@ -17,24 +17,63 @@ package org.thingsboard.server.dao.model.sql; import lombok.Data; import lombok.EqualsAndHashCode; +import org.hibernate.annotations.TypeDef; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.dao.model.sqlts.latest.TsKvLatestEntity; +import org.thingsboard.server.dao.sqlts.latest.SearchTsKvLatestRepository; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.NamedNativeQueries; +import javax.persistence.NamedNativeQuery; +import javax.persistence.NamedStoredProcedureQuery; +import javax.persistence.ParameterMode; +import javax.persistence.StoredProcedureParameter; +import javax.persistence.Table; + +import java.util.UUID; + +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_STATUS_PROPERTY; +import static org.thingsboard.server.dao.model.ModelConstants.ALARM_VIEW_NAME; @Data @EqualsAndHashCode(callSuper = true) +@Entity +@Table(name = ALARM_VIEW_NAME) public class AlarmInfoEntity extends AbstractAlarmEntity { + @Column(name = ALARM_ORIGINATOR_NAME_PROPERTY) private String originatorName; + @Column(name = ALARM_ORIGINATOR_LABEL_PROPERTY) + private String originatorLabel; + @Column(name = ALARM_ASSIGNEE_FIRST_NAME_PROPERTY) + private String assigneeFirstName; + @Column(name = ALARM_ASSIGNEE_LAST_NAME_PROPERTY) + private String assigneeLastName; + @Column(name = ALARM_ASSIGNEE_EMAIL_PROPERTY) + private String assigneeEmail; + @Column(name = ALARM_STATUS_PROPERTY) + private String status; public AlarmInfoEntity() { super(); } - public AlarmInfoEntity(AlarmEntity alarmEntity) { - super(alarmEntity); - } - @Override public AlarmInfo toData() { - return new AlarmInfo(super.toAlarm(), this.originatorName); + AlarmInfo alarmInfo = new AlarmInfo(super.toAlarm()); + alarmInfo.setOriginatorName(originatorName); + alarmInfo.setOriginatorLabel(originatorLabel); + if (getAssigneeId() != null) { + alarmInfo.setAssignee(new AlarmAssignee(new UserId(getAssigneeId()), assigneeFirstName, assigneeLastName, assigneeEmail)); + } + return alarmInfo; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java index ff16525235..282bca89e8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/EntityAlarmEntity.java @@ -21,6 +21,7 @@ import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.model.ToData; import javax.persistence.Column; @@ -30,6 +31,7 @@ import javax.persistence.IdClass; import javax.persistence.Table; import java.util.UUID; +import static org.thingsboard.server.dao.model.ModelConstants.ASSIGNEE_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.CREATED_TIME_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.CUSTOMER_ID_PROPERTY; import static org.thingsboard.server.dao.model.ModelConstants.ENTITY_ALARM_COLUMN_FAMILY_NAME; diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java index 97868a1e07..5229c2cad6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserCredentialsEntity.java @@ -15,8 +15,10 @@ */ package org.thingsboard.server.dao.model.sql; +import com.fasterxml.jackson.databind.JsonNode; import lombok.Data; import lombok.EqualsAndHashCode; +import org.hibernate.annotations.Type; import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; @@ -50,6 +52,10 @@ public final class UserCredentialsEntity extends BaseSqlEntity @Column(name = ModelConstants.USER_CREDENTIALS_RESET_TOKEN_PROPERTY, unique = true) private String resetToken; + @Type(type = "json") + @Column(name = ModelConstants.USER_CREDENTIALS_ADDITIONAL_PROPERTY) + private JsonNode additionalInfo; + public UserCredentialsEntity() { super(); } @@ -66,6 +72,7 @@ public final class UserCredentialsEntity extends BaseSqlEntity this.password = userCredentials.getPassword(); this.activateToken = userCredentials.getActivateToken(); this.resetToken = userCredentials.getResetToken(); + this.additionalInfo = userCredentials.getAdditionalInfo(); } @Override @@ -79,6 +86,7 @@ public final class UserCredentialsEntity extends BaseSqlEntity userCredentials.setPassword(password); userCredentials.setActivateToken(activateToken); userCredentials.setResetToken(resetToken); + userCredentials.setAdditionalInfo(additionalInfo); return userCredentials; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java index 7ffe7e7b45..8235a773e9 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/nosql/CassandraAbstractDao.java @@ -94,7 +94,7 @@ public abstract class CassandraAbstractDao { log.debug("Execute cassandra async statement {}", statementToString(statement)); } if (statement.getConsistencyLevel() == null) { - statement.setConsistencyLevel(level); + statement = statement.setConsistencyLevel(level); } return rateExecutor.submit(new CassandraStatementTask(tenantId, getSession(), statement)); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java index 43e9069fac..49e0257daf 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/AlarmRepository.java @@ -18,10 +18,11 @@ package org.thingsboard.server.dao.sql.alarm; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.query.Procedure; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.AlarmInfoEntity; @@ -39,7 +40,13 @@ public interface AlarmRepository extends JpaRepository { @Param("alarmType") String alarmType, Pageable pageable); - @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + @Query("SELECT a FROM AlarmEntity a WHERE a.originatorId = :originatorId AND a.type = :alarmType AND a.cleared = FALSE ORDER BY a.createdTime DESC") + List findLatestActiveByOriginatorAndType(@Param("originatorId") UUID originatorId, + @Param("alarmType") String alarmType, + Pageable pageable); + + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "WHERE a.tenantId = :tenantId " + "AND ea.tenantId = :tenantId " + @@ -47,14 +54,16 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) " , countQuery = "" + "SELECT count(a) " + //alarms with relations only - "FROM AlarmEntity a " + + "FROM AlarmInfoEntity a " + "LEFT JOIN EntityAlarmEntity ea ON a.id = ea.alarmId " + "WHERE a.tenantId = :tenantId " + "AND ea.tenantId = :tenantId " + @@ -62,7 +71,9 @@ public interface AlarmRepository extends JpaRepository { "AND ea.entityType = :affectedEntityType " + "AND (:startTime IS NULL OR (a.createdTime >= :startTime AND ea.createdTime >= :startTime)) " + "AND (:endTime IS NULL OR (a.createdTime <= :endTime AND ea.createdTime <= :endTime)) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") @@ -71,51 +82,69 @@ public interface AlarmRepository extends JpaRepository { @Param("affectedEntityType") String affectedEntityType, @Param("startTime") Long startTime, @Param("endTime") Long endTime, - @Param("alarmStatuses") Set alarmStatuses, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, @Param("searchText") String searchText, Pageable pageable); - @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ", countQuery = "" + "SELECT count(a) " + - "FROM AlarmEntity a " + + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") Page findAllAlarms(@Param("tenantId") UUID tenantId, @Param("startTime") Long startTime, @Param("endTime") Long endTime, - @Param("alarmStatuses") Set alarmStatuses, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, @Param("searchText") String searchText, Pageable pageable); - @Query(value = "SELECT new org.thingsboard.server.dao.model.sql.AlarmInfoEntity(a) FROM AlarmEntity a " + + @Query(value = "SELECT a " + + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) " , countQuery = "" + "SELECT count(a) " + - "FROM AlarmEntity a " + + "FROM AlarmInfoEntity a " + "WHERE a.tenantId = :tenantId AND a.customerId = :customerId " + "AND (:startTime IS NULL OR a.createdTime >= :startTime) " + "AND (:endTime IS NULL OR a.createdTime <= :endTime) " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses)) " + + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId)) " + "AND (LOWER(a.type) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.severity) LIKE LOWER(CONCAT('%', :searchText, '%')) " + " OR LOWER(a.status) LIKE LOWER(CONCAT('%', :searchText, '%'))) ") @@ -123,7 +152,11 @@ public interface AlarmRepository extends JpaRepository { @Param("customerId") UUID customerId, @Param("startTime") Long startTime, @Param("endTime") Long endTime, - @Param("alarmStatuses") Set alarmStatuses, + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId, @Param("searchText") String searchText, Pageable pageable); @@ -133,13 +166,49 @@ public interface AlarmRepository extends JpaRepository { "AND ea.tenantId = :tenantId " + "AND ea.entityId = :affectedEntityId " + "AND ea.entityType = :affectedEntityType " + - "AND ((:alarmStatuses) IS NULL OR a.status in (:alarmStatuses))") + "AND ((:clearFilterEnabled) IS FALSE OR a.cleared = :clearFilter) " + + "AND ((:ackFilterEnabled) IS FALSE OR a.acknowledged = :ackFilter) " + + "AND (:assigneeId IS NULL OR a.assigneeId = uuid(:assigneeId))") Set findAlarmSeverities(@Param("tenantId") UUID tenantId, @Param("affectedEntityId") UUID affectedEntityId, @Param("affectedEntityType") String affectedEntityType, - @Param("alarmStatuses") Set alarmStatuses); + @Param("clearFilterEnabled") boolean clearFilterEnabled, + @Param("clearFilter") boolean clearFilter, + @Param("ackFilterEnabled") boolean ackFilterEnabled, + @Param("ackFilter") boolean ackFilter, + @Param("assigneeId") String assigneeId); @Query("SELECT a.id FROM AlarmEntity a WHERE a.tenantId = :tenantId AND a.createdTime < :time AND a.endTs < :time") Page findAlarmsIdsByEndTsBeforeAndTenantId(@Param("time") Long time, @Param("tenantId") UUID tenantId, Pageable pageable); + @Query(value = "SELECT a FROM AlarmInfoEntity a WHERE a.tenantId = :tenantId AND a.id = :alarmId") + AlarmInfoEntity findAlarmInfoById(@Param("tenantId") UUID tenantId, @Param("alarmId") UUID alarmId); + + @Procedure(procedureName = "create_or_update_active_alarm") + String createOrUpdateActiveAlarm(@Param("t_id") UUID tenantId, @Param("c_id") UUID customerId, + @Param("a_id") UUID alarmId, @Param("a_created_ts") long createdTime, + @Param("a_o_id") UUID originatorId, @Param("a_o_type") int originatorType, + @Param("a_type") String type, @Param("a_severity") String severity, + @Param("a_start_ts") long startTs, @Param("a_end_ts") long endTs, @Param("a_details") String detailsAsString, + @Param("a_propagate") boolean propagate, @Param("a_propagate_to_owner") boolean propagateToOwner, + @Param("a_propagate_to_tenant") boolean propagateToTenant, @Param("a_propagation_types") String propagationTypes, + @Param("a_creation_enabled") boolean alarmCreationEnabled); + + @Procedure(procedureName = "update_alarm") + String updateAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_severity") String severity, + @Param("a_start_ts") long startTs, @Param("a_end_ts") long endTs, @Param("a_details") String detailsAsString, + @Param("a_propagate") boolean propagate, @Param("a_propagate_to_owner") boolean propagateToOwner, + @Param("a_propagate_to_tenant") boolean propagateToTenant, @Param("a_propagation_types") String propagationTypes); + + @Procedure(procedureName = "acknowledge_alarm") + String acknowledgeAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long ts); + + @Procedure(procedureName = "clear_alarm") + String clearAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long ts, @Param("a_details") String details); + + @Procedure(procedureName = "assign_alarm") + String assignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("u_id") UUID userId, @Param("a_ts") long assignTime); + + @Procedure(procedureName = "unassign_alarm") + String unassignAlarm(@Param("t_id") UUID tenantId, @Param("a_id") UUID alarmId, @Param("a_ts") long unassignTime); } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java index b8f508fef8..f1abd07d95 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDao.java @@ -15,39 +15,54 @@ */ package org.thingsboard.server.dao.sql.alarm; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.EntityAlarm; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; +import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; import org.thingsboard.server.dao.DaoUtil; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmDao; +import org.thingsboard.server.dao.model.ModelConstants; import org.thingsboard.server.dao.model.sql.AlarmEntity; import org.thingsboard.server.dao.model.sql.EntityAlarmEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; import org.thingsboard.server.dao.sql.query.AlarmQueryRepository; import org.thingsboard.server.dao.util.SqlDao; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -87,6 +102,15 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return latest.isEmpty() ? null : DaoUtil.getData(latest.get(0)); } + @Override + public Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type) { + List latest = alarmRepository.findLatestActiveByOriginatorAndType( + originator.getId(), + type, + PageRequest.of(0, 1)); + return latest.isEmpty() ? null : DaoUtil.getData(latest.get(0)); + } + @Override public ListenableFuture findLatestByOriginatorAndTypeAsync(TenantId tenantId, EntityId originator, String type) { return service.submit(() -> findLatestByOriginatorAndType(tenantId, originator, type)); @@ -97,6 +121,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A return findById(tenantId, key); } + @Override + public AlarmInfo findAlarmInfoById(TenantId tenantId, UUID key) { + return DaoUtil.getData(alarmRepository.findAlarmInfoById(tenantId.getId(), key)); + } + @Override public ListenableFuture findAlarmByIdAsync(TenantId tenantId, UUID key) { return findByIdAsync(tenantId, key); @@ -106,11 +135,10 @@ public class JpaAlarmDao extends JpaAbstractDao implements A public PageData findAlarms(TenantId tenantId, AlarmQuery query) { log.trace("Try to find alarms by entity [{}], status [{}] and pageLink [{}]", query.getAffectedEntityId(), query.getStatus(), query.getPageLink()); EntityId affectedEntity = query.getAffectedEntityId(); - Set statusSet = null; - if (query.getSearchStatus() != null) { - statusSet = query.getSearchStatus().getStatuses(); - } else if (query.getStatus() != null) { - statusSet = Collections.singleton(query.getStatus()); + AlarmStatusFilter asf = AlarmStatusFilter.from(query); + String assigneeId = null; + if (query.getAssigneeId() != null) { + assigneeId = query.getAssigneeId().toString(); } if (affectedEntity != null) { return DaoUtil.toPageData( @@ -120,7 +148,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A affectedEntity.getEntityType().name(), query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), - statusSet, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -131,7 +163,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A tenantId.getId(), query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), - statusSet, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -142,11 +178,10 @@ public class JpaAlarmDao extends JpaAbstractDao implements A @Override public PageData findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query) { log.trace("Try to find customer alarms by status [{}] and pageLink [{}]", query.getStatus(), query.getPageLink()); - Set statusSet = null; - if (query.getSearchStatus() != null) { - statusSet = query.getSearchStatus().getStatuses(); - } else if (query.getStatus() != null) { - statusSet = Collections.singleton(query.getStatus()); + AlarmStatusFilter asf = AlarmStatusFilter.from(query); + String assigneeId = null; + if (query.getAssigneeId() != null) { + assigneeId = query.getAssigneeId().toString(); } return DaoUtil.toPageData( alarmRepository.findCustomerAlarms( @@ -154,7 +189,11 @@ public class JpaAlarmDao extends JpaAbstractDao implements A customerId.getId(), query.getPageLink().getStartTime(), query.getPageLink().getEndTime(), - statusSet, + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId, Objects.toString(query.getPageLink().getTextSearch(), ""), DaoUtil.toPageable(query.getPageLink()) ) @@ -167,8 +206,13 @@ public class JpaAlarmDao extends JpaAbstractDao implements A } @Override - public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, Set statuses) { - return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), statuses); + public Set findAlarmSeverities(TenantId tenantId, EntityId entityId, AlarmStatusFilter asf, String assigneeId) { + return alarmRepository.findAlarmSeverities(tenantId.getId(), entityId.getId(), entityId.getEntityType().name(), + asf.hasClearFilter(), + asf.hasClearFilter() && asf.getClearFilter(), + asf.hasAckFilter(), + asf.hasAckFilter() && asf.getAckFilter(), + assigneeId); } @Override @@ -195,6 +239,174 @@ public class JpaAlarmDao extends JpaAbstractDao implements A entityAlarmRepository.deleteByEntityId(entityId.getId()); } + @Override + public AlarmApiCallResult createOrUpdateActiveAlarm(AlarmCreateOrUpdateActiveRequest request, boolean alarmCreationEnabled) { + AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); + return toAlarmApiResult(alarmRepository.createOrUpdateActiveAlarm( + request.getTenantId().getId(), + request.getCustomerId() != null ? request.getCustomerId().getId() : CustomerId.NULL_UUID, + UUID.randomUUID(), + System.currentTimeMillis(), + request.getOriginator().getId(), + request.getOriginator().getEntityType().ordinal(), + request.getType(), + request.getSeverity().name(), + request.getStartTs(), request.getEndTs(), + getDetailsAsString(request.getDetails()), + ap.isPropagate(), + ap.isPropagateToOwner(), + ap.isPropagateToTenant(), + getPropagationTypes(ap), + alarmCreationEnabled + )); + } + + @Override + public AlarmApiCallResult updateAlarm(AlarmUpdateRequest request) { + AlarmPropagationInfo ap = getSafePropagationInfo(request.getPropagation()); + return toAlarmApiResult(alarmRepository.updateAlarm( + request.getTenantId().getId(), + request.getAlarmId().getId(), + request.getSeverity().name(), + request.getStartTs(), request.getEndTs(), + getDetailsAsString(request.getDetails()), + ap.isPropagate(), + ap.isPropagateToOwner(), + ap.isPropagateToTenant(), + getPropagationTypes(ap) + )); + } + + @Override + public AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId id, long ackTs) { + return toAlarmApiResult(alarmRepository.acknowledgeAlarm(tenantId.getId(), id.getId(), ackTs)); + } + + @Override + public AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId id, long clearTs, JsonNode details) { + return toAlarmApiResult(alarmRepository.clearAlarm(tenantId.getId(), id.getId(), clearTs, getDetailsAsString(details))); + } + + @Override + public AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId id, UserId assigneeId, long assignTime) { + return toAlarmApiResult(alarmRepository.assignAlarm(tenantId.getId(), id.getId(), assigneeId.getId(), assignTime)); + } + + @Override + public AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId id, long unassignTime) { + return toAlarmApiResult(alarmRepository.unassignAlarm(tenantId.getId(), id.getId(), unassignTime)); + } + + @NotNull + private static String getPropagationTypes(AlarmPropagationInfo ap) { + String propagateRelationTypes; + if (!CollectionUtils.isEmpty(ap.getPropagateRelationTypes())) { + propagateRelationTypes = String.join(",", ap.getPropagateRelationTypes()); + } else { + propagateRelationTypes = ""; + } + return propagateRelationTypes; + } + + private static AlarmPropagationInfo getSafePropagationInfo(AlarmPropagationInfo ap) { + return ap != null ? ap : AlarmPropagationInfo.EMPTY; + } + + private static String getDetailsAsString(JsonNode details) { + var detailsStr = JacksonUtil.toString(details); + if (StringUtils.isEmpty(detailsStr)) { + detailsStr = "{}"; + } + return detailsStr; + } + + private AlarmApiCallResult toAlarmApiResult(String str) { + var json = JacksonUtil.toJsonNode(str); + var result = AlarmApiCallResult.builder(); + boolean success = json.get("success").asBoolean(); + result.successful(success); + if (success) { + boolean modified = false; + boolean created = false; + boolean cleared = false; + if (json.has("modified")) { + modified = json.get("modified").asBoolean(); + } + + if (json.has("created")) { + created = json.get("created").asBoolean(); + } + + if (json.has("cleared")) { + cleared = json.get("cleared").asBoolean(); + } + result.created(created); + result.cleared(cleared); + result.modified(created || cleared || modified); + if (json.has("alarm") && !json.get("alarm").isNull()) { + result.alarm(toAlarmInfo(json.get("alarm"))); + } + if (json.has("old") && !json.get("old").isNull()) { + result.old(toAlarm(json.get("old"))); + } + } + return result.build(); + } + + private AlarmInfo toAlarmInfo(JsonNode json) { + AlarmInfo alarmInfo = new AlarmInfo(toAlarm(json)); + getSafe(json, ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY).ifPresent(alarmInfo::setOriginatorName); + getSafe(json, ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY).ifPresent(alarmInfo::setOriginatorLabel); + if (alarmInfo.getAssigneeId() != null) { + var assigneeBuilder = AlarmAssignee.builder().id(alarmInfo.getAssigneeId()); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY).ifPresent(assigneeBuilder::firstName); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY).ifPresent(assigneeBuilder::lastName); + getSafe(json, ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY).ifPresent(assigneeBuilder::email); + alarmInfo.setAssignee(assigneeBuilder.build()); + } + return alarmInfo; + } + + private Alarm toAlarm(JsonNode json) { + Alarm alarm = new Alarm(new AlarmId(UUID.fromString(json.get(ModelConstants.ID_PROPERTY).asText()))); + alarm.setCreatedTime(json.get(ModelConstants.CREATED_TIME_PROPERTY).asLong()); + getSafe(json, ModelConstants.TENANT_ID_COLUMN).ifPresent(s -> alarm.setTenantId(TenantId.fromUUID(UUID.fromString(s)))); + getSafe(json, ModelConstants.CUSTOMER_ID_PROPERTY).ifPresent(s -> alarm.setCustomerId(new CustomerId(UUID.fromString(s)))); + getSafe(json, ModelConstants.ASSIGNEE_ID_PROPERTY).ifPresent(s -> alarm.setAssigneeId(new UserId(UUID.fromString(s)))); + alarm.setOriginator(EntityIdFactory.getByTypeAndUuid( + json.get(ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY).asInt(), + json.get(ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY).asText())); + getSafe(json, ModelConstants.ALARM_TYPE_PROPERTY).ifPresent(alarm::setType); + getSafe(json, ModelConstants.ALARM_SEVERITY_PROPERTY).map(AlarmSeverity::valueOf).ifPresent(alarm::setSeverity); + alarm.setAcknowledged(json.get(ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY).asBoolean()); + alarm.setCleared(json.get(ModelConstants.ALARM_CLEARED_PROPERTY).asBoolean()); + alarm.setPropagate(json.get(ModelConstants.ALARM_PROPAGATE_PROPERTY).asBoolean()); + alarm.setPropagateToOwner(json.get(ModelConstants.ALARM_PROPAGATE_TO_OWNER_PROPERTY).asBoolean()); + alarm.setPropagateToTenant(json.get(ModelConstants.ALARM_PROPAGATE_TO_TENANT_PROPERTY).asBoolean()); + alarm.setStartTs(json.get(ModelConstants.ALARM_START_TS_PROPERTY).asLong()); + alarm.setEndTs(json.get(ModelConstants.ALARM_END_TS_PROPERTY).asLong()); + alarm.setAckTs(json.get(ModelConstants.ALARM_ACK_TS_PROPERTY).asLong()); + alarm.setClearTs(json.get(ModelConstants.ALARM_CLEAR_TS_PROPERTY).asLong()); + alarm.setAssignTs(json.get(ModelConstants.ALARM_ASSIGN_TS_PROPERTY).asLong()); + getSafe(json, ModelConstants.ALARM_DETAILS_PROPERTY).map(JacksonUtil::toJsonNode).ifPresent(alarm::setDetails); + alarm.setPropagateRelationTypes(getSafe(json, ModelConstants.ALARM_PROPAGATE_RELATION_TYPES).filter(StringUtils::isNoneEmpty) + .map(s -> Arrays.asList(s.split(","))).orElse(Collections.emptyList())); + return alarm; + } + + private static Optional getSafe(JsonNode json, String fieldName) { + if (json.has(fieldName)) { + var element = json.get(fieldName); + if (element.isNull() || !element.isTextual()) { + return Optional.empty(); + } else { + return Optional.of(element.asText()); + } + } else { + return Optional.empty(); + } + } + @Override public EntityType getEntityType() { return EntityType.ALARM; diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java index 7943ce5178..a07887fb57 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/AlarmDataAdapter.java @@ -21,6 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmAssignee; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.EntityIdFactory; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.EntityDataPageLink; @@ -68,6 +70,7 @@ public class AlarmDataAdapter { alarm.setCreatedTime((long) row.get(ModelConstants.CREATED_TIME_PROPERTY)); alarm.setAckTs((long) row.get(ModelConstants.ALARM_ACK_TS_PROPERTY)); alarm.setClearTs((long) row.get(ModelConstants.ALARM_CLEAR_TS_PROPERTY)); + alarm.setAssignTs((long) row.get(ModelConstants.ALARM_ASSIGN_TS_PROPERTY)); alarm.setStartTs((long) row.get(ModelConstants.ALARM_START_TS_PROPERTY)); alarm.setEndTs((long) row.get(ModelConstants.ALARM_END_TS_PROPERTY)); Object additionalInfo = row.get(ModelConstants.ADDITIONAL_INFO_PROPERTY); @@ -81,12 +84,23 @@ public class AlarmDataAdapter { EntityType originatorType = EntityType.values()[(int) row.get(ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY)]; UUID originatorId = (UUID) row.get(ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY); alarm.setOriginator(EntityIdFactory.getByTypeAndUuid(originatorType, originatorId)); + Object assigneeIdObj = row.get(ModelConstants.ASSIGNEE_ID_PROPERTY); + String assigneeFirstName = null; + String assigneeLastName = null; + String assigneeEmail = null; + if (assigneeIdObj != null) { + alarm.setAssigneeId(new UserId((UUID) row.get(ModelConstants.ALARM_ASSIGNEE_ID_PROPERTY))); + assigneeFirstName = (String) row.get(ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY); + assigneeLastName = (String) row.get(ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY); + assigneeEmail = (String) row.get(ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY); + } alarm.setPropagate((boolean) row.get(ModelConstants.ALARM_PROPAGATE_PROPERTY)); alarm.setPropagateToOwner((boolean) row.get(ModelConstants.ALARM_PROPAGATE_TO_OWNER_PROPERTY)); alarm.setPropagateToTenant((boolean) row.get(ModelConstants.ALARM_PROPAGATE_TO_TENANT_PROPERTY)); alarm.setType(row.get(ModelConstants.ALARM_TYPE_PROPERTY).toString()); alarm.setSeverity(AlarmSeverity.valueOf(row.get(ModelConstants.ALARM_SEVERITY_PROPERTY).toString())); - alarm.setStatus(AlarmStatus.valueOf(row.get(ModelConstants.ALARM_STATUS_PROPERTY).toString())); + alarm.setAcknowledged((boolean) row.get(ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY)); + alarm.setCleared((boolean) row.get(ModelConstants.ALARM_CLEARED_PROPERTY)); alarm.setTenantId(TenantId.fromUUID((UUID) row.get(ModelConstants.TENANT_ID_PROPERTY))); Object customerIdObj = row.get(ModelConstants.CUSTOMER_ID_PROPERTY); CustomerId customerId = customerIdObj != null ? new CustomerId((UUID) customerIdObj) : null; @@ -105,7 +119,17 @@ public class AlarmDataAdapter { EntityId entityId = entityIdMap.get(entityUuid); Object originatorNameObj = row.get(ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY); String originatorName = originatorNameObj != null ? originatorNameObj.toString() : null; - return new AlarmData(alarm, originatorName, entityId); + Object originatorLabelObj = row.get(ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY); + String originatorLabel = originatorLabelObj != null ? originatorLabelObj.toString() : null; + + AlarmData alarmData = new AlarmData(alarm, entityId); + alarmData.setOriginatorName(originatorName); + alarmData.setOriginatorLabel(originatorLabel); + if (alarm.getAssigneeId() != null) { + alarmData.setAssignee(new AlarmAssignee(alarm.getAssigneeId(), assigneeFirstName, assigneeLastName, assigneeEmail)); + } + + return alarmData; } } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java index 543b8549c4..ecf9c1c95f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/query/DefaultAlarmQueryRepository.java @@ -19,11 +19,13 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Repository; import org.springframework.transaction.support.TransactionTemplate; +import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmStatusFilter; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -35,6 +37,7 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.dao.model.ModelConstants; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -51,46 +54,44 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { private static final Map alarmFieldColumnMap = new HashMap<>(); + private static final String ASSIGNEE_EMAIL_KEY = "assigneeEmail"; + private static final String ASSIGNEE_LAST_NAME_KEY = "assigneeLastName"; + private static final String ASSIGNEE_FIRST_NAME_KEY = "assigneeFirstName"; + private static final String ASSIGNEE_ID_KEY = "assigneeId"; + private static final String ASSIGNEE_KEY = "assignee"; + static { alarmFieldColumnMap.put("createdTime", ModelConstants.CREATED_TIME_PROPERTY); alarmFieldColumnMap.put("ackTs", ModelConstants.ALARM_ACK_TS_PROPERTY); alarmFieldColumnMap.put("ackTime", ModelConstants.ALARM_ACK_TS_PROPERTY); alarmFieldColumnMap.put("clearTs", ModelConstants.ALARM_CLEAR_TS_PROPERTY); alarmFieldColumnMap.put("clearTime", ModelConstants.ALARM_CLEAR_TS_PROPERTY); + alarmFieldColumnMap.put("assignTime", ModelConstants.ALARM_ASSIGN_TS_PROPERTY); alarmFieldColumnMap.put("details", ModelConstants.ADDITIONAL_INFO_PROPERTY); alarmFieldColumnMap.put("endTs", ModelConstants.ALARM_END_TS_PROPERTY); alarmFieldColumnMap.put("endTime", ModelConstants.ALARM_END_TS_PROPERTY); alarmFieldColumnMap.put("startTs", ModelConstants.ALARM_START_TS_PROPERTY); alarmFieldColumnMap.put("startTime", ModelConstants.ALARM_START_TS_PROPERTY); - alarmFieldColumnMap.put("status", ModelConstants.ALARM_STATUS_PROPERTY); + alarmFieldColumnMap.put("acknowledged", ModelConstants.ALARM_ACKNOWLEDGED_PROPERTY); + alarmFieldColumnMap.put("cleared", ModelConstants.ALARM_CLEARED_PROPERTY); alarmFieldColumnMap.put("type", ModelConstants.ALARM_TYPE_PROPERTY); alarmFieldColumnMap.put("severity", ModelConstants.ALARM_SEVERITY_PROPERTY); alarmFieldColumnMap.put("originatorId", ModelConstants.ALARM_ORIGINATOR_ID_PROPERTY); alarmFieldColumnMap.put("originatorType", ModelConstants.ALARM_ORIGINATOR_TYPE_PROPERTY); - alarmFieldColumnMap.put("originator", "originator_name"); + alarmFieldColumnMap.put(ASSIGNEE_ID_KEY, ModelConstants.ALARM_ASSIGNEE_ID_PROPERTY); + alarmFieldColumnMap.put("originator", ModelConstants.ALARM_ORIGINATOR_NAME_PROPERTY); + alarmFieldColumnMap.put("originatorLabel", ModelConstants.ALARM_ORIGINATOR_LABEL_PROPERTY); + alarmFieldColumnMap.put(ASSIGNEE_FIRST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_FIRST_NAME_PROPERTY); + alarmFieldColumnMap.put(ASSIGNEE_LAST_NAME_KEY, ModelConstants.ALARM_ASSIGNEE_LAST_NAME_PROPERTY); + alarmFieldColumnMap.put(ASSIGNEE_EMAIL_KEY, ModelConstants.ALARM_ASSIGNEE_EMAIL_PROPERTY); } - private static final String SELECT_ORIGINATOR_NAME = " COALESCE(CASE" + - " WHEN a.originator_type = " + EntityType.TENANT.ordinal() + - " THEN (select title from tenant where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.CUSTOMER.ordinal() + - " THEN (select title from customer where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.USER.ordinal() + - " THEN (select email from tb_user where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.DASHBOARD.ordinal() + - " THEN (select title from dashboard where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.ASSET.ordinal() + - " THEN (select name from asset where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.DEVICE.ordinal() + - " THEN (select name from device where id = a.originator_id)" + - " WHEN a.originator_type = " + EntityType.ENTITY_VIEW.ordinal() + - " THEN (select name from entity_view where id = a.originator_id)" + - " END, 'Deleted') as originator_name"; - private static final String FIELDS_SELECTION = "select a.id as id," + " a.created_time as created_time," + " a.ack_ts as ack_ts," + " a.clear_ts as clear_ts," + + " a.assign_ts as assign_ts," + + " a.assignee_id as assignee_id," + " a.additional_info as additional_info," + " a.end_ts as end_ts," + " a.originator_id as originator_id," + @@ -100,13 +101,19 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { " a.propagate_to_tenant as propagate_to_tenant," + " a.severity as severity," + " a.start_ts as start_ts," + - " a.status as status, " + " a.tenant_id as tenant_id, " + " a.customer_id as customer_id, " + " a.propagate_relation_types as propagate_relation_types, " + - " a.type as type," + SELECT_ORIGINATOR_NAME + ", "; + " a.type as type, " + + " a.originator_name as originator_name, " + + " a.originator_label as originator_label, " + + " a.assignee_first_name as assignee_first_name, " + + " a.assignee_last_name as assignee_last_name, " + + " a.assignee_email as assignee_email, " + + " a.cleared as cleared, " + + " a.acknowledged as acknowledged, "; - private static final String JOIN_ENTITY_ALARMS = "inner join entity_alarm ea on a.id = ea.alarm_id"; + private static final String JOIN_ENTITY_ALARMS = "inner join entity_alarm ea on a.id = ea.alarm_id "; protected final NamedParameterJdbcTemplate jdbcTemplate; private final TransactionTemplate transactionTemplate; @@ -121,12 +128,12 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { @Override public PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds) { - return transactionTemplate.execute(status -> { + return transactionTemplate.execute(trStatus -> { AlarmDataPageLink pageLink = query.getPageLink(); QueryContext ctx = new QueryContext(new QuerySecurityContext(tenantId, null, EntityType.ALARM)); ctx.addUuidListParameter("entity_ids", orderedEntityIds.stream().map(EntityId::getId).collect(Collectors.toList())); StringBuilder selectPart = new StringBuilder(FIELDS_SELECTION); - StringBuilder fromPart = new StringBuilder(" from alarm a "); + StringBuilder fromPart = new StringBuilder(" from alarm_info a "); StringBuilder wherePart = new StringBuilder(" where "); StringBuilder sortPart = new StringBuilder(" order by "); StringBuilder joinPart = new StringBuilder(); @@ -140,9 +147,25 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { selectPart.append(" a.originator_id as entity_id "); } EntityDataSortOrder sortOrder = pageLink.getSortOrder(); - String textSearchQuery = buildTextSearchQuery(ctx, query.getAlarmFields(), pageLink.getTextSearch()); + + List alarmFields = new ArrayList<>(); + for (EntityKey key : query.getAlarmFields()) { + if (EntityKeyType.ALARM_FIELD.equals(key.getType()) && ASSIGNEE_KEY.equalsIgnoreCase(key.getKey())) { + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, ASSIGNEE_ID_KEY)); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, ASSIGNEE_FIRST_NAME_KEY)); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, ASSIGNEE_LAST_NAME_KEY)); + alarmFields.add(new EntityKey(EntityKeyType.ALARM_FIELD, ASSIGNEE_EMAIL_KEY)); + } else { + alarmFields.add(key); + } + } + + String textSearchQuery = buildTextSearchQuery(ctx, alarmFields, pageLink.getTextSearch()); if (sortOrder != null && sortOrder.getKey().getType().equals(EntityKeyType.ALARM_FIELD)) { String sortOrderKey = sortOrder.getKey().getKey(); + if ("status".equalsIgnoreCase(sortOrderKey)) { + selectPart.append(", a.status as status "); + } sortPart.append(alarmFieldColumnMap.getOrDefault(sortOrderKey, sortOrderKey)) .append(" ").append(sortOrder.getDirection().name()); if (pageLink.isSearchPropagatedAlarms()) { @@ -225,14 +248,25 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { wherePart.append("a.severity in (:alarmSeverities)"); } - if (pageLink.getStatusList() != null && !pageLink.getStatusList().isEmpty()) { - Set statusSet = toStatusSet(pageLink.getStatusList()); - if (!statusSet.isEmpty()) { + AlarmStatusFilter asf = AlarmStatusFilter.fromList(pageLink.getStatusList()); + if (asf.hasAnyFilter()) { + if (asf.hasAckFilter()) { addAndIfNeeded(wherePart, addAnd); addAnd = true; - ctx.addStringListParameter("alarmStatuses", statusSet.stream().map(AlarmStatus::name).collect(Collectors.toList())); - wherePart.append(" a.status in (:alarmStatuses)"); + ctx.addBooleanParameter("ackStatus", asf.getAckFilter()); + wherePart.append(" a.acknowledged = :ackStatus"); } + if (asf.hasClearFilter()) { + addAndIfNeeded(wherePart, addAnd); + // addAnd = true; // not needed but stored as an example if someone adds new conditions + ctx.addBooleanParameter("clearStatus", asf.getClearFilter()); + wherePart.append(" a.cleared = :clearStatus"); + } + } + + if (pageLink.getAssigneeId() != null) { + ctx.addUuidParameter("assigneeId", pageLink.getAssigneeId().getId()); + wherePart.append(" a.assignee_id = :assigneeId"); } String mainQuery = String.format("%s%s", selectPart, fromPart); @@ -295,37 +329,6 @@ public class DefaultAlarmQueryRepository implements AlarmQueryRepository { return permissionsQuery.toString(); } - private Set toStatusSet(List statusList) { - Set result = new HashSet<>(); - for (AlarmSearchStatus searchStatus : statusList) { - switch (searchStatus) { - case ACK: - result.add(AlarmStatus.ACTIVE_ACK); - result.add(AlarmStatus.CLEARED_ACK); - break; - case UNACK: - result.add(AlarmStatus.ACTIVE_UNACK); - result.add(AlarmStatus.CLEARED_UNACK); - break; - case CLEARED: - result.add(AlarmStatus.CLEARED_ACK); - result.add(AlarmStatus.CLEARED_UNACK); - break; - case ACTIVE: - result.add(AlarmStatus.ACTIVE_ACK); - result.add(AlarmStatus.ACTIVE_UNACK); - break; - default: - break; - } - if (searchStatus == AlarmSearchStatus.ANY || result.size() == AlarmStatus.values().length) { - result.clear(); - return result; - } - } - return result; - } - private void addAndIfNeeded(StringBuilder wherePart, boolean addAnd) { if (addAnd) { wherePart.append(" and "); diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index c8c5175e5f..b99883c52f 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -29,7 +29,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EntityType; -import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -40,7 +39,6 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.common.data.security.UserSettings; import org.thingsboard.server.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.dao.entity.AbstractEntityService; import org.thingsboard.server.dao.exception.IncorrectParameterException; @@ -51,6 +49,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; import static org.thingsboard.server.dao.service.Validator.validateId; import static org.thingsboard.server.dao.service.Validator.validatePageLink; import static org.thingsboard.server.dao.service.Validator.validateString; @@ -126,9 +125,10 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic if (user.getId() == null) { UserCredentials userCredentials = new UserCredentials(); userCredentials.setEnabled(false); - userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + userCredentials.setActivateToken(generateSafeToken(DEFAULT_TOKEN_LENGTH)); userCredentials.setUserId(new UserId(savedUser.getUuidId())); - saveUserCredentialsAndPasswordHistory(user.getTenantId(), userCredentials); + userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + userCredentialsDao.save(user.getTenantId(), userCredentials); } return savedUser; } @@ -158,7 +158,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic public UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials) { log.trace("Executing saveUserCredentials [{}]", userCredentials); userCredentialsValidator.validate(userCredentials, data -> tenantId); - return saveUserCredentialsAndPasswordHistory(tenantId, userCredentials); + return userCredentialsDao.save(tenantId, userCredentials); } @Override @@ -176,7 +176,9 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userCredentials.setEnabled(true); userCredentials.setActivateToken(null); userCredentials.setPassword(password); - + if (userCredentials.getPassword() != null) { + updatePasswordHistory(userCredentials); + } return saveUserCredentials(tenantId, userCredentials); } @@ -192,7 +194,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic if (!userCredentials.isEnabled()) { throw new DisabledException(String.format("User credentials not enabled [%s]", email)); } - userCredentials.setResetToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH)); return saveUserCredentials(tenantId, userCredentials); } @@ -202,7 +204,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic if (!userCredentials.isEnabled()) { throw new IncorrectParameterException("Unable to reset password for inactive user"); } - userCredentials.setResetToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + userCredentials.setResetToken(generateSafeToken(DEFAULT_TOKEN_LENGTH)); return saveUserCredentials(tenantId, userCredentials); } @@ -212,7 +214,10 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userCredentialsValidator.validate(userCredentials, data -> tenantId); userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); userCredentials.setId(null); - return saveUserCredentialsAndPasswordHistory(tenantId, userCredentials); + if (userCredentials.getPassword() != null) { + updatePasswordHistory(userCredentials); + } + return userCredentialsDao.save(tenantId, userCredentials); } @Override @@ -343,17 +348,8 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic return failedLoginAttempts; } - private UserCredentials saveUserCredentialsAndPasswordHistory(TenantId tenantId, UserCredentials userCredentials) { - UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); - User user = findUserById(tenantId, userCredentials.getUserId()); - if (userCredentials.getPassword() != null) { - updatePasswordHistory(user, userCredentials); - } - return result; - } - - private void updatePasswordHistory(User user, UserCredentials userCredentials) { - JsonNode additionalInfo = user.getAdditionalInfo(); + private void updatePasswordHistory(UserCredentials userCredentials) { + JsonNode additionalInfo = userCredentials.getAdditionalInfo(); if (!(additionalInfo instanceof ObjectNode)) { additionalInfo = JacksonUtil.newObjectNode(); } @@ -374,8 +370,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic userPasswordHistoryJson = JacksonUtil.valueToTree(userPasswordHistoryMap); ((ObjectNode) additionalInfo).set(USER_PASSWORD_HISTORY, userPasswordHistoryJson); } - user.setAdditionalInfo(additionalInfo); - saveUser(user); + userCredentials.setAdditionalInfo(additionalInfo); } private final PaginatedRemover tenantAdminsRemover = new PaginatedRemover<>() { diff --git a/dao/src/main/resources/cassandra/schema-ts-latest.cql b/dao/src/main/resources/cassandra/schema-ts-latest.cql index e3c72ab6da..6b76c6187c 100644 --- a/dao/src/main/resources/cassandra/schema-ts-latest.cql +++ b/dao/src/main/resources/cassandra/schema-ts-latest.cql @@ -15,7 +15,7 @@ -- CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_latest_cf ( - entity_type text, // (DEVICE, CUSTOMER, TENANT) + entity_type text, -- (DEVICE, CUSTOMER, TENANT) entity_id timeuuid, key text, ts bigint, diff --git a/dao/src/main/resources/cassandra/schema-ts.cql b/dao/src/main/resources/cassandra/schema-ts.cql index fb60ed9e78..ae8c11a703 100644 --- a/dao/src/main/resources/cassandra/schema-ts.cql +++ b/dao/src/main/resources/cassandra/schema-ts.cql @@ -15,7 +15,7 @@ -- CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf ( - entity_type text, // (DEVICE, CUSTOMER, TENANT) + entity_type text, -- (DEVICE, CUSTOMER, TENANT) entity_id timeuuid, key text, partition bigint, @@ -29,7 +29,7 @@ CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_cf ( ); CREATE TABLE IF NOT EXISTS thingsboard.ts_kv_partitions_cf ( - entity_type text, // (DEVICE, CUSTOMER, TENANT) + entity_type text, -- (DEVICE, CUSTOMER, TENANT) entity_id timeuuid, key text, partition bigint, diff --git a/dao/src/main/resources/sql/schema-entities-idx.sql b/dao/src/main/resources/sql/schema-entities-idx.sql index a5b1261d40..518baf8df4 100644 --- a/dao/src/main/resources/sql/schema-entities-idx.sql +++ b/dao/src/main/resources/sql/schema-entities-idx.sql @@ -20,12 +20,20 @@ CREATE INDEX IF NOT EXISTS idx_alarm_originator_created_time ON alarm(originator CREATE INDEX IF NOT EXISTS idx_alarm_tenant_created_time ON alarm(tenant_id, created_time DESC); -CREATE INDEX IF NOT EXISTS idx_alarm_tenant_status_created_time ON alarm(tenant_id, status, created_time DESC); +-- Drop index by 'status' column and replace with new one that has only active alarms; +CREATE INDEX IF NOT EXISTS idx_alarm_originator_alarm_type_active + ON alarm USING btree (originator_id, type) WHERE cleared = false; CREATE INDEX IF NOT EXISTS idx_alarm_tenant_alarm_type_created_time ON alarm(tenant_id, type, created_time DESC); +CREATE INDEX IF NOT EXISTS idx_alarm_tenant_assignee_created_time ON alarm(tenant_id, assignee_id, created_time DESC); + CREATE INDEX IF NOT EXISTS idx_entity_alarm_created_time ON entity_alarm(tenant_id, entity_id, created_time DESC); +-- Cover index by alarm type to optimize propagated alarm queries; +CREATE INDEX IF NOT EXISTS idx_entity_alarm_entity_id_alarm_type_created_time_alarm_id ON entity_alarm +USING btree (tenant_id, entity_id, alarm_type, created_time DESC) INCLUDE(alarm_id); + CREATE INDEX IF NOT EXISTS idx_entity_alarm_alarm_id ON entity_alarm(alarm_id); CREATE INDEX IF NOT EXISTS idx_relation_to_id ON relation(relation_type_group, to_type, to_id); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index b222cf4b96..9a975e1f5b 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -52,13 +52,16 @@ CREATE TABLE IF NOT EXISTS alarm ( propagate boolean, severity varchar(255), start_ts bigint, - status varchar(255), + assign_ts bigint DEFAULT 0, + assignee_id uuid, tenant_id uuid, customer_id uuid, propagate_relation_types varchar, type varchar(255), propagate_to_owner boolean, - propagate_to_tenant boolean + propagate_to_tenant boolean, + acknowledged boolean, + cleared boolean ); CREATE TABLE IF NOT EXISTS alarm_comment ( @@ -427,13 +430,6 @@ CREATE TABLE IF NOT EXISTS relation ( additional_info varchar, CONSTRAINT relation_pkey PRIMARY KEY (from_id, from_type, relation_type_group, relation_type, to_id, to_type) ); --- ) PARTITION BY LIST (relation_type_group); --- --- CREATE TABLE other_relations PARTITION OF relation DEFAULT; --- CREATE TABLE common_relations PARTITION OF relation FOR VALUES IN ('COMMON'); --- CREATE TABLE alarm_relations PARTITION OF relation FOR VALUES IN ('ALARM'); --- CREATE TABLE dashboard_relations PARTITION OF relation FOR VALUES IN ('DASHBOARD'); --- CREATE TABLE rule_relations PARTITION OF relation FOR VALUES IN ('RULE_CHAIN', 'RULE_NODE'); CREATE TABLE IF NOT EXISTS tb_user ( id uuid NOT NULL CONSTRAINT tb_user_pkey PRIMARY KEY, @@ -487,7 +483,8 @@ CREATE TABLE IF NOT EXISTS user_credentials ( enabled boolean, password varchar(255), reset_token varchar(255) UNIQUE, - user_id uuid UNIQUE + user_id uuid UNIQUE, + additional_info varchar DEFAULT '{}' ); CREATE TABLE IF NOT EXISTS widget_type ( @@ -795,4 +792,246 @@ CREATE TABLE IF NOT EXISTS user_settings ( user_id uuid NOT NULL CONSTRAINT user_settings_pkey PRIMARY KEY, settings varchar(10000), CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE -); \ No newline at end of file +); + +DROP VIEW IF EXISTS alarm_info CASCADE; +CREATE VIEW alarm_info AS +SELECT a.*, +(CASE WHEN a.acknowledged AND a.cleared THEN 'CLEARED_ACK' + WHEN NOT a.acknowledged AND a.cleared THEN 'CLEARED_UNACK' + WHEN a.acknowledged AND NOT a.cleared THEN 'ACTIVE_ACK' + WHEN NOT a.acknowledged AND NOT a.cleared THEN 'ACTIVE_UNACK' END) as status, +COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id) + WHEN a.originator_type = 1 THEN (select title from customer where id = a.originator_id) + WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id) + WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id) + WHEN a.originator_type = 4 THEN (select name from asset where id = a.originator_id) + WHEN a.originator_type = 5 THEN (select name from device where id = a.originator_id) + WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id) + WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id) + WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id) + WHEN a.originator_type = 18 THEN (select name from edge where id = a.originator_id) END + , 'Deleted') originator_name, +COALESCE(CASE WHEN a.originator_type = 0 THEN (select title from tenant where id = a.originator_id) + WHEN a.originator_type = 1 THEN (select COALESCE(title, email) from customer where id = a.originator_id) + WHEN a.originator_type = 2 THEN (select email from tb_user where id = a.originator_id) + WHEN a.originator_type = 3 THEN (select title from dashboard where id = a.originator_id) + WHEN a.originator_type = 4 THEN (select COALESCE(label, name) from asset where id = a.originator_id) + WHEN a.originator_type = 5 THEN (select COALESCE(label, name) from device where id = a.originator_id) + WHEN a.originator_type = 9 THEN (select name from entity_view where id = a.originator_id) + WHEN a.originator_type = 13 THEN (select name from device_profile where id = a.originator_id) + WHEN a.originator_type = 14 THEN (select name from asset_profile where id = a.originator_id) + WHEN a.originator_type = 18 THEN (select COALESCE(label, name) from edge where id = a.originator_id) END + , 'Deleted') as originator_label, +u.first_name as assignee_first_name, u.last_name as assignee_last_name, u.email as assignee_email +FROM alarm a +LEFT JOIN tb_user u ON u.id = a.assignee_id; + +CREATE OR REPLACE FUNCTION create_or_update_active_alarm( + t_id uuid, c_id uuid, a_id uuid, a_created_ts bigint, + a_o_id uuid, a_o_type integer, a_type varchar, + a_severity varchar, a_start_ts bigint, a_end_ts bigint, + a_details varchar, + a_propagate boolean, a_propagate_to_owner boolean, + a_propagate_to_tenant boolean, a_propagation_types varchar, + a_creation_enabled boolean) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + null_id constant uuid = '13814000-1dd2-11b2-8080-808080808080'::uuid; + existing alarm; + result alarm_info; + row_count integer; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.originator_id = a_o_id AND a.type = a_type AND a.cleared = false ORDER BY a.start_ts DESC FOR UPDATE; + IF existing.id IS NULL THEN + IF a_creation_enabled = FALSE THEN + RETURN json_build_object('success', false)::text; + END IF; + IF c_id = null_id THEN + c_id = NULL; + end if; + INSERT INTO alarm + (tenant_id, customer_id, id, created_time, + originator_id, originator_type, type, + severity, start_ts, end_ts, + additional_info, + propagate, propagate_to_owner, propagate_to_tenant, propagate_relation_types, + acknowledged, ack_ts, + cleared, clear_ts, + assignee_id, assign_ts) + VALUES + (t_id, c_id, a_id, a_created_ts, + a_o_id, a_o_type, a_type, + a_severity, a_start_ts, a_end_ts, + a_details, + a_propagate, a_propagate_to_owner, a_propagate_to_tenant, a_propagation_types, + false, 0, false, 0, NULL, 0); + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'created', true, 'modified', true, 'alarm', row_to_json(result))::text; + ELSE + UPDATE alarm a + SET severity = a_severity, + start_ts = a_start_ts, + end_ts = a_end_ts, + additional_info = a_details, + propagate = a_propagate, + propagate_to_owner = a_propagate_to_owner, + propagate_to_tenant = a_propagate_to_tenant, + propagate_relation_types = a_propagation_types + WHERE a.id = existing.id + AND a.tenant_id = t_id + AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details + OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR + propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types); + GET DIAGNOSTICS row_count = ROW_COUNT; + SELECT * INTO result FROM alarm_info a WHERE a.id = existing.id AND a.tenant_id = t_id; + IF row_count > 0 THEN + RETURN json_build_object('success', true, 'modified', true, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text; + ELSE + RETURN json_build_object('success', true, 'modified', false, 'alarm', row_to_json(result))::text; + END IF; + END IF; +END +$$; + +DROP FUNCTION IF EXISTS update_alarm; +CREATE OR REPLACE FUNCTION update_alarm(t_id uuid, a_id uuid, a_severity varchar, a_start_ts bigint, a_end_ts bigint, + a_details varchar, + a_propagate boolean, a_propagate_to_owner boolean, + a_propagate_to_tenant boolean, a_propagation_types varchar) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + row_count integer; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + UPDATE alarm a + SET severity = a_severity, + start_ts = a_start_ts, + end_ts = a_end_ts, + additional_info = a_details, + propagate = a_propagate, + propagate_to_owner = a_propagate_to_owner, + propagate_to_tenant = a_propagate_to_tenant, + propagate_relation_types = a_propagation_types + WHERE a.id = a_id + AND a.tenant_id = t_id + AND (severity != a_severity OR start_ts != a_start_ts OR end_ts != a_end_ts OR additional_info != a_details + OR propagate != a_propagate OR propagate_to_owner != a_propagate_to_owner OR + propagate_to_tenant != a_propagate_to_tenant OR propagate_relation_types != a_propagation_types); + GET DIAGNOSTICS row_count = ROW_COUNT; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + IF row_count > 0 THEN + RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result), 'old', row_to_json(existing))::text; + ELSE + RETURN json_build_object('success', true, 'modified', row_count > 0, 'alarm', row_to_json(result))::text; + END IF; +END +$$; + +DROP FUNCTION IF EXISTS acknowledge_alarm; +CREATE OR REPLACE FUNCTION acknowledge_alarm(t_id uuid, a_id uuid, a_ts bigint) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + + IF NOT (existing.acknowledged) THEN + modified = TRUE; + UPDATE alarm a SET acknowledged = true, ack_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END +$$; + +DROP FUNCTION IF EXISTS clear_alarm; +CREATE OR REPLACE FUNCTION clear_alarm(t_id uuid, a_id uuid, a_ts bigint, a_details varchar) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + cleared boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF NOT(existing.cleared) THEN + cleared = TRUE; + UPDATE alarm a SET cleared = true, clear_ts = a_ts, additional_info = a_details WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'cleared', cleared, 'alarm', row_to_json(result))::text; +END +$$; + +DROP FUNCTION IF EXISTS assign_alarm; +CREATE OR REPLACE FUNCTION assign_alarm(t_id uuid, a_id uuid, u_id uuid, a_ts bigint) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF existing.assignee_id IS NULL OR existing.assignee_id != u_id THEN + modified = TRUE; + UPDATE alarm a SET assignee_id = u_id, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END +$$; + +DROP FUNCTION IF EXISTS unassign_alarm; +CREATE OR REPLACE FUNCTION unassign_alarm(t_id uuid, a_id uuid, a_ts bigint) + RETURNS varchar + LANGUAGE plpgsql +AS +$$ +DECLARE + existing alarm; + result alarm_info; + modified boolean = FALSE; +BEGIN + SELECT * INTO existing FROM alarm a WHERE a.id = a_id AND a.tenant_id = t_id FOR UPDATE; + IF existing IS NULL THEN + RETURN json_build_object('success', false)::text; + END IF; + IF existing.assignee_id IS NOT NULL THEN + modified = TRUE; + UPDATE alarm a SET assignee_id = NULL, assign_ts = a_ts WHERE a.id = a_id AND a.tenant_id = t_id; + END IF; + SELECT * INTO result FROM alarm_info a WHERE a.id = a_id AND a.tenant_id = t_id; + RETURN json_build_object('success', true, 'modified', modified, 'alarm', row_to_json(result))::text; +END +$$; \ No newline at end of file diff --git a/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java b/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java deleted file mode 100644 index 69a164f729..0000000000 --- a/dao/src/test/java/org/apache/cassandra/io/sstable/Descriptor.java +++ /dev/null @@ -1,366 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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.apache.cassandra.io.sstable; - -import java.io.File; -import java.io.IOError; -import java.io.IOException; -import java.util.*; -import java.util.regex.Pattern; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.CharMatcher; -import com.google.common.base.Objects; - -import org.apache.cassandra.db.Directories; -import org.apache.cassandra.io.sstable.format.SSTableFormat; -import org.apache.cassandra.io.sstable.format.Version; -import org.apache.cassandra.io.sstable.metadata.IMetadataSerializer; -import org.apache.cassandra.io.sstable.metadata.LegacyMetadataSerializer; -import org.apache.cassandra.io.sstable.metadata.MetadataSerializer; -import org.apache.cassandra.utils.Pair; - -import static org.apache.cassandra.io.sstable.Component.separator; - -/** - * A SSTable is described by the keyspace and column family it contains data - * for, a generation (where higher generations contain more recent data) and - * an alphabetic version string. - * - * A descriptor can be marked as temporary, which influences generated filenames. - */ -public class Descriptor -{ - public static String TMP_EXT = ".tmp"; - - /** canonicalized path to the directory where SSTable resides */ - public final File directory; - /** version has the following format: [a-z]+ */ - public final Version version; - public final String ksname; - public final String cfname; - public final int generation; - public final SSTableFormat.Type formatType; - /** digest component - might be {@code null} for old, legacy sstables */ - public final Component digestComponent; - private final int hashCode; - - /** - * A descriptor that assumes CURRENT_VERSION. - */ - @VisibleForTesting - public Descriptor(File directory, String ksname, String cfname, int generation) - { - this(SSTableFormat.Type.current().info.getLatestVersion(), directory, ksname, cfname, generation, SSTableFormat.Type.current(), null); - } - - /** - * Constructor for sstable writers only. - */ - public Descriptor(File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType) - { - this(formatType.info.getLatestVersion(), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType())); - } - - @VisibleForTesting - public Descriptor(String version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType) - { - this(formatType.info.getVersion(version), directory, ksname, cfname, generation, formatType, Component.digestFor(formatType.info.getLatestVersion().uncompressedChecksumType())); - } - - public Descriptor(Version version, File directory, String ksname, String cfname, int generation, SSTableFormat.Type formatType, Component digestComponent) - { - assert version != null && directory != null && ksname != null && cfname != null && formatType.info.getLatestVersion().getClass().equals(version.getClass()); - this.version = version; - try - { - this.directory = directory.getCanonicalFile(); - } - catch (IOException e) - { - throw new IOError(e); - } - this.ksname = ksname; - this.cfname = cfname; - this.generation = generation; - this.formatType = formatType; - this.digestComponent = digestComponent; - - hashCode = Objects.hashCode(version, this.directory, generation, ksname, cfname, formatType); - } - - public Descriptor withGeneration(int newGeneration) - { - return new Descriptor(version, directory, ksname, cfname, newGeneration, formatType, digestComponent); - } - - public Descriptor withFormatType(SSTableFormat.Type newType) - { - return new Descriptor(newType.info.getLatestVersion(), directory, ksname, cfname, generation, newType, digestComponent); - } - - public Descriptor withDigestComponent(Component newDigestComponent) - { - return new Descriptor(version, directory, ksname, cfname, generation, formatType, newDigestComponent); - } - - public String tmpFilenameFor(Component component) - { - return filenameFor(component) + TMP_EXT; - } - - public String filenameFor(Component component) - { - return baseFilename() + separator + component.name(); - } - - public String baseFilename() - { - StringBuilder buff = new StringBuilder(); - buff.append(directory).append(File.separatorChar); - appendFileName(buff); - return buff.toString(); - } - - private void appendFileName(StringBuilder buff) - { - if (!version.hasNewFileName()) - { - buff.append(ksname).append(separator); - buff.append(cfname).append(separator); - } - buff.append(version).append(separator); - buff.append(generation); - if (formatType != SSTableFormat.Type.LEGACY) - buff.append(separator).append(formatType.name); - } - - public String relativeFilenameFor(Component component) - { - final StringBuilder buff = new StringBuilder(); - appendFileName(buff); - buff.append(separator).append(component.name()); - return buff.toString(); - } - - public SSTableFormat getFormat() - { - return formatType.info; - } - - /** Return any temporary files found in the directory */ - public List getTemporaryFiles() - { - List ret = new ArrayList<>(); - File[] tmpFiles = directory.listFiles((dir, name) -> - name.endsWith(Descriptor.TMP_EXT)); - - for (File tmpFile : tmpFiles) - ret.add(tmpFile); - - return ret; - } - - /** - * Files obsoleted by CASSANDRA-7066 : temporary files and compactions_in_progress. We support - * versions 2.1 (ka) and 2.2 (la). - * Temporary files have tmp- or tmplink- at the beginning for 2.2 sstables or after ks-cf- for 2.1 sstables - */ - - private final static String LEGACY_COMP_IN_PROG_REGEX_STR = "^compactions_in_progress(\\-[\\d,a-f]{32})?$"; - private final static Pattern LEGACY_COMP_IN_PROG_REGEX = Pattern.compile(LEGACY_COMP_IN_PROG_REGEX_STR); - private final static String LEGACY_TMP_REGEX_STR = "^((.*)\\-(.*)\\-)?tmp(link)?\\-((?:l|k).)\\-(\\d)*\\-(.*)$"; - private final static Pattern LEGACY_TMP_REGEX = Pattern.compile(LEGACY_TMP_REGEX_STR); - - public static boolean isLegacyFile(File file) - { - if (file.isDirectory()) - return file.getParentFile() != null && - file.getParentFile().getName().equalsIgnoreCase("system") && - LEGACY_COMP_IN_PROG_REGEX.matcher(file.getName()).matches(); - else - return LEGACY_TMP_REGEX.matcher(file.getName()).matches(); - } - - public static boolean isValidFile(String fileName) - { - return fileName.endsWith(".db") && !LEGACY_TMP_REGEX.matcher(fileName).matches(); - } - - /** - * @see #fromFilename(File directory, String name) - * @param filename The SSTable filename - * @return Descriptor of the SSTable initialized from filename - */ - public static Descriptor fromFilename(String filename) - { - return fromFilename(filename, false); - } - - public static Descriptor fromFilename(String filename, SSTableFormat.Type formatType) - { - return fromFilename(filename).withFormatType(formatType); - } - - public static Descriptor fromFilename(String filename, boolean skipComponent) - { - File file = new File(filename).getAbsoluteFile(); - return fromFilename(file.getParentFile(), file.getName(), skipComponent).left; - } - - public static Pair fromFilename(File directory, String name) - { - return fromFilename(directory, name, false); - } - - /** - * Filename of the form is vary by version: - * - *
    - *
  • <ksname>-<cfname>-(tmp-)?<version>-<gen>-<component> for cassandra 2.0 and before
  • - *
  • (<tmp marker>-)?<version>-<gen>-<component> for cassandra 3.0 and later
  • - *
- * - * If this is for SSTable of secondary index, directory should ends with index name for 2.1+. - * - * @param directory The directory of the SSTable files - * @param name The name of the SSTable file - * @param skipComponent true if the name param should not be parsed for a component tag - * - * @return A Descriptor for the SSTable, and the Component remainder. - */ - @SuppressWarnings("deprecation") - public static Pair fromFilename(File directory, String name, boolean skipComponent) - { - File parentDirectory = directory != null ? directory : new File("."); - - // tokenize the filename - StringTokenizer st = new StringTokenizer(name, String.valueOf(separator)); - String nexttok; - - // read tokens backwards to determine version - Deque tokenStack = new ArrayDeque<>(); - while (st.hasMoreTokens()) - { - tokenStack.push(st.nextToken()); - } - - // component suffix - String component = skipComponent ? null : tokenStack.pop(); - - nexttok = tokenStack.pop(); - // generation OR format type - SSTableFormat.Type fmt = SSTableFormat.Type.LEGACY; - if (!CharMatcher.digit().matchesAllOf(nexttok)) - { - fmt = SSTableFormat.Type.validate(nexttok); - nexttok = tokenStack.pop(); - } - - // generation - int generation = Integer.parseInt(nexttok); - - // version - nexttok = tokenStack.pop(); - - if (!Version.validate(nexttok)) - throw new UnsupportedOperationException("SSTable " + name + " is too old to open. Upgrade to 2.0 first, and run upgradesstables"); - - Version version = fmt.info.getVersion(nexttok); - - // ks/cf names - String ksname, cfname; - if (version.hasNewFileName()) - { - // for 2.1+ read ks and cf names from directory - File cfDirectory = parentDirectory; - // check if this is secondary index - String indexName = ""; - if (cfDirectory.getName().startsWith(Directories.SECONDARY_INDEX_NAME_SEPARATOR)) - { - indexName = cfDirectory.getName(); - cfDirectory = cfDirectory.getParentFile(); - } - if (cfDirectory.getName().equals(Directories.BACKUPS_SUBDIR)) - { - cfDirectory = cfDirectory.getParentFile(); - } - else if (cfDirectory.getParentFile().getName().equals(Directories.SNAPSHOT_SUBDIR)) - { - cfDirectory = cfDirectory.getParentFile().getParentFile(); - } - cfname = cfDirectory.getName().split("-")[0] + indexName; - ksname = cfDirectory.getParentFile().getName(); - } - else - { - cfname = tokenStack.pop(); - ksname = tokenStack.pop(); - } - assert tokenStack.isEmpty() : "Invalid file name " + name + " in " + directory; - - return Pair.create(new Descriptor(version, parentDirectory, ksname, cfname, generation, fmt, - // _assume_ version from version - Component.digestFor(version.uncompressedChecksumType())), - component); - } - - @SuppressWarnings("deprecation") - public IMetadataSerializer getMetadataSerializer() - { - if (version.hasNewStatsFile()) - return new MetadataSerializer(); - else - return new LegacyMetadataSerializer(); - } - - /** - * @return true if the current Cassandra version can read the given sstable version - */ - public boolean isCompatible() - { - return version.isCompatible(); - } - - @Override - public String toString() - { - return baseFilename(); - } - - @Override - public boolean equals(Object o) - { - if (o == this) - return true; - if (!(o instanceof Descriptor)) - return false; - Descriptor that = (Descriptor)o; - return that.directory.equals(this.directory) - && that.generation == this.generation - && that.ksname.equals(this.ksname) - && that.cfname.equals(this.cfname) - && that.formatType == this.formatType; - } - - @Override - public int hashCode() - { - return hashCode; - } -} diff --git a/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java b/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java deleted file mode 100644 index 350d27591f..0000000000 --- a/dao/src/test/java/org/apache/cassandra/io/sstable/format/SSTableFormat.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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.apache.cassandra.io.sstable.format; - -import com.google.common.base.CharMatcher; -import org.apache.cassandra.config.CFMetaData; -import org.apache.cassandra.db.RowIndexEntry; -import org.apache.cassandra.db.SerializationHeader; -import org.apache.cassandra.io.sstable.format.big.BigFormat; - -/** - * Provides the accessors to data on disk. - */ -public interface SSTableFormat -{ - static boolean enableSSTableDevelopmentTestMode = Boolean.getBoolean("cassandra.test.sstableformatdevelopment"); - - - Version getLatestVersion(); - Version getVersion(String version); - - SSTableWriter.Factory getWriterFactory(); - SSTableReader.Factory getReaderFactory(); - - RowIndexEntry.IndexSerializer getIndexSerializer(CFMetaData cfm, Version version, SerializationHeader header); - - public static enum Type - { - //Used internally to refer to files with no - //format flag in the filename - LEGACY("big", BigFormat.instance), - - //The original sstable format - BIG("big", BigFormat.instance); - - public final SSTableFormat info; - public final String name; - - public static Type current() - { - return BIG; - } - - @SuppressWarnings("deprecation") - private Type(String name, SSTableFormat info) - { - //Since format comes right after generation - //we disallow formats with numeric names - // We have removed this check for compatibility with the embedded cassandra used for tests. - assert !CharMatcher.digit().matchesAllOf(name); - - this.name = name; - this.info = info; - } - - public static Type validate(String name) - { - for (Type valid : Type.values()) - { - //This is used internally for old sstables - if (valid == LEGACY) - continue; - - if (valid.name.equalsIgnoreCase(name)) - return valid; - } - - throw new IllegalArgumentException("No Type constant " + name); - } - } -} diff --git a/dao/src/test/java/org/apache/cassandra/io/util/FileUtils.java b/dao/src/test/java/org/apache/cassandra/io/util/FileUtils.java deleted file mode 100644 index 2609a2c3d0..0000000000 --- a/dao/src/test/java/org/apache/cassandra/io/util/FileUtils.java +++ /dev/null @@ -1,760 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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.apache.cassandra.io.util; - -import java.io.*; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; -import java.nio.file.attribute.BasicFileAttributes; -import java.nio.file.attribute.FileAttributeView; -import java.nio.file.attribute.FileStoreAttributeView; -import java.text.DecimalFormat; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.function.Predicate; -import java.util.stream.StreamSupport; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import org.apache.cassandra.concurrent.ScheduledExecutors; -import org.apache.cassandra.io.FSError; -import org.apache.cassandra.io.FSErrorHandler; -import org.apache.cassandra.io.FSReadError; -import org.apache.cassandra.io.FSWriteError; -import org.apache.cassandra.io.sstable.CorruptSSTableException; -import org.apache.cassandra.utils.JVMStabilityInspector; - -import static com.google.common.base.Throwables.throwIfUnchecked; -import static org.apache.cassandra.utils.Throwables.maybeFail; -import static org.apache.cassandra.utils.Throwables.merge; - -public final class FileUtils -{ - public static final Charset CHARSET = StandardCharsets.UTF_8; - - private static final Logger logger = LoggerFactory.getLogger(FileUtils.class); - public static final long ONE_KB = 1024; - public static final long ONE_MB = 1024 * ONE_KB; - public static final long ONE_GB = 1024 * ONE_MB; - public static final long ONE_TB = 1024 * ONE_GB; - - private static final DecimalFormat df = new DecimalFormat("#.##"); - public static final boolean isCleanerAvailable = false; - private static final AtomicReference> fsErrorHandler = new AtomicReference<>(Optional.empty()); - - public static void createHardLink(String from, String to) - { - createHardLink(new File(from), new File(to)); - } - - public static void createHardLink(File from, File to) - { - if (to.exists()) - throw new RuntimeException("Tried to create duplicate hard link to " + to); - if (!from.exists()) - throw new RuntimeException("Tried to hard link to file that does not exist " + from); - - try - { - Files.createLink(to.toPath(), from.toPath()); - } - catch (IOException e) - { - throw new FSWriteError(e, to); - } - } - - public static File createTempFile(String prefix, String suffix, File directory) - { - try - { - return File.createTempFile(prefix, suffix, directory); - } - catch (IOException e) - { - throw new FSWriteError(e, directory); - } - } - - public static File createTempFile(String prefix, String suffix) - { - return createTempFile(prefix, suffix, new File(System.getProperty("java.io.tmpdir"))); - } - - public static Throwable deleteWithConfirm(String filePath, boolean expect, Throwable accumulate) - { - return deleteWithConfirm(new File(filePath), expect, accumulate); - } - - public static Throwable deleteWithConfirm(File file, boolean expect, Throwable accumulate) - { - boolean exists = file.exists(); - assert exists || !expect : "attempted to delete non-existing file " + file.getName(); - try - { - if (exists) - Files.delete(file.toPath()); - } - catch (Throwable t) - { - try - { - throw new FSWriteError(t, file); - } - catch (Throwable t2) - { - accumulate = merge(accumulate, t2); - } - } - return accumulate; - } - - public static void deleteWithConfirm(String file) - { - deleteWithConfirm(new File(file)); - } - - public static void deleteWithConfirm(File file) - { - maybeFail(deleteWithConfirm(file, true, null)); - } - - public static void renameWithOutConfirm(String from, String to) - { - try - { - atomicMoveWithFallback(new File(from).toPath(), new File(to).toPath()); - } - catch (IOException e) - { - if (logger.isTraceEnabled()) - logger.trace("Could not move file "+from+" to "+to, e); - } - } - - public static void renameWithConfirm(String from, String to) - { - renameWithConfirm(new File(from), new File(to)); - } - - public static void renameWithConfirm(File from, File to) - { - assert from.exists(); - if (logger.isTraceEnabled()) - logger.trace("Renaming {} to {}", from.getPath(), to.getPath()); - // this is not FSWE because usually when we see it it's because we didn't close the file before renaming it, - // and Windows is picky about that. - try - { - atomicMoveWithFallback(from.toPath(), to.toPath()); - } - catch (IOException e) - { - throw new RuntimeException(String.format("Failed to rename %s to %s", from.getPath(), to.getPath()), e); - } - } - - /** - * Move a file atomically, if it fails, it falls back to a non-atomic operation - * @param from - * @param to - * @throws IOException - */ - private static void atomicMoveWithFallback(Path from, Path to) throws IOException - { - try - { - Files.move(from, to, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); - } - catch (AtomicMoveNotSupportedException e) - { - logger.trace("Could not do an atomic move", e); - Files.move(from, to, StandardCopyOption.REPLACE_EXISTING); - } - - } - public static void truncate(String path, long size) - { - try(FileChannel channel = FileChannel.open(Paths.get(path), StandardOpenOption.READ, StandardOpenOption.WRITE)) - { - channel.truncate(size); - } - catch (IOException e) - { - throw new RuntimeException(e); - } - } - - public static void closeQuietly(Closeable c) - { - try - { - if (c != null) - c.close(); - } - catch (Exception e) - { - logger.warn("Failed closing {}", c, e); - } - } - - public static void closeQuietly(AutoCloseable c) - { - try - { - if (c != null) - c.close(); - } - catch (Exception e) - { - logger.warn("Failed closing {}", c, e); - } - } - - public static void close(Closeable... cs) throws IOException - { - close(Arrays.asList(cs)); - } - - public static void close(Iterable cs) throws IOException - { - Throwable e = null; - for (Closeable c : cs) - { - try - { - if (c != null) - c.close(); - } - catch (Throwable ex) - { - if (e == null) e = ex; - else e.addSuppressed(ex); - logger.warn("Failed closing stream {}", c, ex); - } - } - maybeFail(e, IOException.class); - } - - public static void closeQuietly(Iterable cs) - { - for (AutoCloseable c : cs) - { - try - { - if (c != null) - c.close(); - } - catch (Exception ex) - { - logger.warn("Failed closing {}", c, ex); - } - } - } - - public static String getCanonicalPath(String filename) - { - try - { - return new File(filename).getCanonicalPath(); - } - catch (IOException e) - { - throw new FSReadError(e, filename); - } - } - - public static String getCanonicalPath(File file) - { - try - { - return file.getCanonicalPath(); - } - catch (IOException e) - { - throw new FSReadError(e, file); - } - } - - /** Return true if file is contained in folder */ - public static boolean isContained(File folder, File file) - { - Path folderPath = Paths.get(getCanonicalPath(folder)); - Path filePath = Paths.get(getCanonicalPath(file)); - - return filePath.startsWith(folderPath); - } - - /** Convert absolute path into a path relative to the base path */ - public static String getRelativePath(String basePath, String path) - { - try - { - return Paths.get(basePath).relativize(Paths.get(path)).toString(); - } - catch(Exception ex) - { - String absDataPath = FileUtils.getCanonicalPath(basePath); - return Paths.get(absDataPath).relativize(Paths.get(path)).toString(); - } - } - - public static void clean(ByteBuffer buffer) - { - if (buffer == null) - return; - } - - public static void createDirectory(String directory) - { - createDirectory(new File(directory)); - } - - public static void createDirectory(File directory) - { - if (!directory.exists()) - { - if (!directory.mkdirs()) - throw new FSWriteError(new IOException("Failed to mkdirs " + directory), directory); - } - } - - public static boolean delete(String file) - { - File f = new File(file); - return f.delete(); - } - - public static void delete(File... files) - { - if (files == null) - { - // CASSANDRA-13389: some callers use Files.listFiles() which, on error, silently returns null - logger.debug("Received null list of files to delete"); - return; - } - - for ( File file : files ) - { - file.delete(); - } - } - - public static void deleteAsync(final String file) - { - Runnable runnable = new Runnable() - { - public void run() - { - deleteWithConfirm(new File(file)); - } - }; - ScheduledExecutors.nonPeriodicTasks.execute(runnable); - } - - public static void visitDirectory(Path dir, Predicate filter, Consumer consumer) - { - try (DirectoryStream stream = Files.newDirectoryStream(dir)) - { - StreamSupport.stream(stream.spliterator(), false) - .map(Path::toFile) - // stream directories are weakly consistent so we always check if the file still exists - .filter(f -> f.exists() && (filter == null || filter.test(f))) - .forEach(consumer); - } - catch (IOException|DirectoryIteratorException ex) - { - logger.error("Failed to list files in {} with exception: {}", dir, ex.getMessage(), ex); - } - } - - public static String stringifyFileSize(double value) - { - double d; - if ( value >= ONE_TB ) - { - d = value / ONE_TB; - String val = df.format(d); - return val + " TiB"; - } - else if ( value >= ONE_GB ) - { - d = value / ONE_GB; - String val = df.format(d); - return val + " GiB"; - } - else if ( value >= ONE_MB ) - { - d = value / ONE_MB; - String val = df.format(d); - return val + " MiB"; - } - else if ( value >= ONE_KB ) - { - d = value / ONE_KB; - String val = df.format(d); - return val + " KiB"; - } - else - { - String val = df.format(value); - return val + " bytes"; - } - } - - /** - * Deletes all files and subdirectories under "dir". - * @param dir Directory to be deleted - * @throws FSWriteError if any part of the tree cannot be deleted - */ - public static void deleteRecursive(File dir) - { - if (dir.isDirectory()) - { - String[] children = dir.list(); - for (String child : children) - deleteRecursive(new File(dir, child)); - } - - // The directory is now empty so now it can be smoked - deleteWithConfirm(dir); - } - - /** - * Schedules deletion of all file and subdirectories under "dir" on JVM shutdown. - * @param dir Directory to be deleted - */ - public static void deleteRecursiveOnExit(File dir) - { - if (dir.isDirectory()) - { - String[] children = dir.list(); - for (String child : children) - deleteRecursiveOnExit(new File(dir, child)); - } - - logger.trace("Scheduling deferred deletion of file: {}", dir); - dir.deleteOnExit(); - } - - public static void handleCorruptSSTable(CorruptSSTableException e) - { - fsErrorHandler.get().ifPresent(handler -> handler.handleCorruptSSTable(e)); - } - - public static void handleFSError(FSError e) - { - fsErrorHandler.get().ifPresent(handler -> handler.handleFSError(e)); - } - - /** - * handleFSErrorAndPropagate will invoke the disk failure policy error handler, - * which may or may not stop the daemon or transports. However, if we don't exit, - * we still want to propagate the exception to the caller in case they have custom - * exception handling - * - * @param e A filesystem error - */ - public static void handleFSErrorAndPropagate(FSError e) - { - JVMStabilityInspector.inspectThrowable(e); - throwIfUnchecked(e); - throw new RuntimeException(e); - } - - /** - * Get the size of a directory in bytes - * @param folder The directory for which we need size. - * @return The size of the directory - */ - public static long folderSize(File folder) - { - final long [] sizeArr = {0L}; - try - { - Files.walkFileTree(folder.toPath(), new SimpleFileVisitor() - { - @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) - { - sizeArr[0] += attrs.size(); - return FileVisitResult.CONTINUE; - } - }); - } - catch (IOException e) - { - logger.error("Error while getting {} folder size. {}", folder, e); - } - return sizeArr[0]; - } - - public static void copyTo(DataInput in, OutputStream out, int length) throws IOException - { - byte[] buffer = new byte[64 * 1024]; - int copiedBytes = 0; - - while (copiedBytes + buffer.length < length) - { - in.readFully(buffer); - out.write(buffer); - copiedBytes += buffer.length; - } - - if (copiedBytes < length) - { - int left = length - copiedBytes; - in.readFully(buffer, 0, left); - out.write(buffer, 0, left); - } - } - - public static boolean isSubDirectory(File parent, File child) throws IOException - { - parent = parent.getCanonicalFile(); - child = child.getCanonicalFile(); - - File toCheck = child; - while (toCheck != null) - { - if (parent.equals(toCheck)) - return true; - toCheck = toCheck.getParentFile(); - } - return false; - } - - public static void append(File file, String ... lines) - { - if (file.exists()) - write(file, Arrays.asList(lines), StandardOpenOption.APPEND); - else - write(file, Arrays.asList(lines), StandardOpenOption.CREATE); - } - - public static void appendAndSync(File file, String ... lines) - { - if (file.exists()) - write(file, Arrays.asList(lines), StandardOpenOption.APPEND, StandardOpenOption.SYNC); - else - write(file, Arrays.asList(lines), StandardOpenOption.CREATE, StandardOpenOption.SYNC); - } - - public static void replace(File file, String ... lines) - { - write(file, Arrays.asList(lines), StandardOpenOption.TRUNCATE_EXISTING); - } - - public static void write(File file, List lines, StandardOpenOption ... options) - { - try - { - Files.write(file.toPath(), - lines, - CHARSET, - options); - } - catch (IOException ex) - { - throw new RuntimeException(ex); - } - } - - public static List readLines(File file) - { - try - { - return Files.readAllLines(file.toPath(), CHARSET); - } - catch (IOException ex) - { - if (ex instanceof NoSuchFileException) - return Collections.emptyList(); - - throw new RuntimeException(ex); - } - } - - public static void setFSErrorHandler(FSErrorHandler handler) - { - fsErrorHandler.getAndSet(Optional.ofNullable(handler)); - } - - /** - * Returns the size of the specified partition. - *

This method handles large file system by returning {@code Long.MAX_VALUE} if the size overflow. - * See JDK-8179320 for more information.

- * - * @param file the partition - * @return the size, in bytes, of the partition or {@code 0L} if the abstract pathname does not name a partition - */ - public static long getTotalSpace(File file) - { - return handleLargeFileSystem(file.getTotalSpace()); - } - - /** - * Returns the number of unallocated bytes on the specified partition. - *

This method handles large file system by returning {@code Long.MAX_VALUE} if the number of unallocated bytes - * overflow. See JDK-8179320 for more information

- * - * @param file the partition - * @return the number of unallocated bytes on the partition or {@code 0L} - * if the abstract pathname does not name a partition. - */ - public static long getFreeSpace(File file) - { - return handleLargeFileSystem(file.getFreeSpace()); - } - - /** - * Returns the number of available bytes on the specified partition. - *

This method handles large file system by returning {@code Long.MAX_VALUE} if the number of available bytes - * overflow. See JDK-8179320 for more information

- * - * @param file the partition - * @return the number of available bytes on the partition or {@code 0L} - * if the abstract pathname does not name a partition. - */ - public static long getUsableSpace(File file) - { - return handleLargeFileSystem(file.getUsableSpace()); - } - - /** - * Returns the {@link FileStore} representing the file store where a file - * is located. This {@link FileStore} handles large file system by returning {@code Long.MAX_VALUE} - * from {@code FileStore#getTotalSpace()}, {@code FileStore#getUnallocatedSpace()} and {@code FileStore#getUsableSpace()} - * it the value is bigger than {@code Long.MAX_VALUE}. See JDK-8162520 - * for more information. - * - * @param path the path to the file - * @return the file store where the file is stored - */ - public static FileStore getFileStore(Path path) throws IOException - { - return new SafeFileStore(Files.getFileStore(path)); - } - - /** - * Handle large file system by returning {@code Long.MAX_VALUE} when the size overflows. - * @param size returned by the Java's FileStore methods - * @return the size or {@code Long.MAX_VALUE} if the size was bigger than {@code Long.MAX_VALUE} - */ - private static long handleLargeFileSystem(long size) - { - return size < 0 ? Long.MAX_VALUE : size; - } - - /** - * Private constructor as the class contains only static methods. - */ - private FileUtils() - { - } - - /** - * FileStore decorator used to safely handle large file system. - * - *

Java's FileStore methods (getTotalSpace/getUnallocatedSpace/getUsableSpace) are limited to reporting bytes as - * signed long (2^63-1), if the filesystem is any bigger, then the size overflows. {@code SafeFileStore} will - * return {@code Long.MAX_VALUE} if the size overflow.

- * - * @see https://bugs.openjdk.java.net/browse/JDK-8162520. - */ - private static final class SafeFileStore extends FileStore - { - /** - * The decorated {@code FileStore} - */ - private final FileStore fileStore; - - public SafeFileStore(FileStore fileStore) - { - this.fileStore = fileStore; - } - - @Override - public String name() - { - return fileStore.name(); - } - - @Override - public String type() - { - return fileStore.type(); - } - - @Override - public boolean isReadOnly() - { - return fileStore.isReadOnly(); - } - - @Override - public long getTotalSpace() throws IOException - { - return handleLargeFileSystem(fileStore.getTotalSpace()); - } - - @Override - public long getUsableSpace() throws IOException - { - return handleLargeFileSystem(fileStore.getUsableSpace()); - } - - @Override - public long getUnallocatedSpace() throws IOException - { - return handleLargeFileSystem(fileStore.getUnallocatedSpace()); - } - - @Override - public boolean supportsFileAttributeView(Class type) - { - return fileStore.supportsFileAttributeView(type); - } - - @Override - public boolean supportsFileAttributeView(String name) - { - return fileStore.supportsFileAttributeView(name); - } - - @Override - public V getFileStoreAttributeView(Class type) - { - return fileStore.getFileStoreAttributeView(type); - } - - @Override - public Object getAttribute(String attribute) throws IOException - { - return fileStore.getAttribute(attribute); - } - } -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractNoSqlContainer.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractNoSqlContainer.java new file mode 100644 index 0000000000..3f7c2c779a --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractNoSqlContainer.java @@ -0,0 +1,96 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.thingsboard.server.dao; + +import com.github.dockerjava.api.command.InspectContainerResponse; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.junit.ClassRule; +import org.junit.rules.ExternalResource; +import org.testcontainers.containers.CassandraContainer; +import org.testcontainers.containers.delegate.CassandraDatabaseDelegate; +import org.testcontainers.delegate.DatabaseDelegate; +import org.testcontainers.ext.ScriptUtils; + +import javax.script.ScriptException; +import java.io.IOException; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@Slf4j +public abstract class AbstractNoSqlContainer { + + public static final List INIT_SCRIPTS = List.of( + "cassandra/schema-keyspace.cql", + "cassandra/schema-ts.cql", + "cassandra/schema-ts-latest.cql" + ); + + @ClassRule(order = 0) + public static final CassandraContainer cassandra = (CassandraContainer) new CassandraContainer("cassandra:4.1") { + @Override + protected void containerIsStarted(InspectContainerResponse containerInfo) { + super.containerIsStarted(containerInfo); + DatabaseDelegate db = new CassandraDatabaseDelegate(this); + INIT_SCRIPTS.forEach(script -> runInitScriptIfRequired(db, script)); + } + + private void runInitScriptIfRequired(DatabaseDelegate db, String initScriptPath) { + logger().info("Init script [{}]", initScriptPath); + if (initScriptPath != null) { + try { + URL resource = Thread.currentThread().getContextClassLoader().getResource(initScriptPath); + if (resource == null) { + logger().warn("Could not load classpath init script: {}", initScriptPath); + throw new ScriptUtils.ScriptLoadException("Could not load classpath init script: " + initScriptPath + ". Resource not found."); + } + String cql = IOUtils.toString(resource, StandardCharsets.UTF_8); + ScriptUtils.executeDatabaseScript(db, initScriptPath, cql); + } catch (IOException e) { + logger().warn("Could not load classpath init script: {}", initScriptPath); + throw new ScriptUtils.ScriptLoadException("Could not load classpath init script: " + initScriptPath, e); + } catch (ScriptException e) { + logger().error("Error while executing init script: {}", initScriptPath, e); + throw new ScriptUtils.UncategorizedScriptException("Error while executing init script: " + initScriptPath, e); + } + } + } + } + .withEnv("HEAP_NEWSIZE", "64M") + .withEnv("MAX_HEAP_SIZE", "512M") + .withEnv("CASSANDRA_CLUSTER_NAME", "ThingsBoard Cluster"); + + @ClassRule(order = 1) + public static ExternalResource resource = new ExternalResource() { + @Override + protected void before() throws Throwable { + cassandra.start(); + String cassandraUrl = String.format("%s:%s", cassandra.getHost(), cassandra.getMappedPort(9042)); + log.debug("Cassandra url [{}]", cassandraUrl); + System.setProperty("cassandra.url", cassandraUrl); + } + + @Override + protected void after() { + cassandra.stop(); + List.of("cassandra.url") + .forEach(System.getProperties()::remove); + } + }; + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java new file mode 100644 index 0000000000..e9a6cb8641 --- /dev/null +++ b/dao/src/test/java/org/thingsboard/server/dao/AbstractRedisContainer.java @@ -0,0 +1,51 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao; + +import lombok.extern.slf4j.Slf4j; +import org.junit.ClassRule; +import org.junit.rules.ExternalResource; +import org.testcontainers.containers.GenericContainer; + +import java.util.List; + +@Slf4j +public class AbstractRedisContainer { + + @ClassRule(order = 0) + public static GenericContainer redis = new GenericContainer("redis:7.0") + .withExposedPorts(6379); + + @ClassRule(order = 1) + public static ExternalResource resource = new ExternalResource() { + @Override + protected void before() throws Throwable { + redis.start(); + System.setProperty("cache.type", "redis"); + System.setProperty("redis.connection.type", "standalone"); + System.setProperty("redis.standalone.host", redis.getHost()); + System.setProperty("redis.standalone.port", String.valueOf(redis.getMappedPort(6379))); + } + + @Override + protected void after() { + redis.stop(); + List.of("cache.type", "redis.connection.type", "redis.standalone.host", "redis.standalone.port") + .forEach(System.getProperties()::remove); + } + }; + +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java b/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java deleted file mode 100644 index 7bab5e1138..0000000000 --- a/dao/src/test/java/org/thingsboard/server/dao/CustomCassandraCQLUnit.java +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright © 2016-2023 The Thingsboard Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.thingsboard.server.dao; - -import com.datastax.oss.driver.api.core.CqlSession; -import org.cassandraunit.BaseCassandraUnit; -import org.cassandraunit.CQLDataLoader; -import org.cassandraunit.dataset.CQLDataSet; -import org.cassandraunit.utils.EmbeddedCassandraServerHelper; - -import java.util.List; - -public class CustomCassandraCQLUnit extends BaseCassandraUnit { - protected List dataSets; - - public CqlSession session; - - public CustomCassandraCQLUnit(List dataSets) { - this.dataSets = dataSets; - } - - public CustomCassandraCQLUnit(List dataSets, int readTimeoutMillis) { - this.dataSets = dataSets; - this.readTimeoutMillis = readTimeoutMillis; - } - - public CustomCassandraCQLUnit(List dataSets, String configurationFileName) { - this(dataSets); - this.configurationFileName = configurationFileName; - } - - public CustomCassandraCQLUnit(List dataSets, String configurationFileName, int readTimeoutMillis) { - this(dataSets); - this.configurationFileName = configurationFileName; - this.readTimeoutMillis = readTimeoutMillis; - } - - public CustomCassandraCQLUnit(List dataSets, String configurationFileName, long startUpTimeoutMillis) { - super(startUpTimeoutMillis); - this.dataSets = dataSets; - this.configurationFileName = configurationFileName; - } - - public CustomCassandraCQLUnit(List dataSets, String configurationFileName, long startUpTimeoutMillis, int readTimeoutMillis) { - super(startUpTimeoutMillis); - this.dataSets = dataSets; - this.configurationFileName = configurationFileName; - this.readTimeoutMillis = readTimeoutMillis; - } - - @Override - protected void load() { - session = EmbeddedCassandraServerHelper.getSession(); - CQLDataLoader dataLoader = new CQLDataLoader(session); - dataSets.forEach(dataLoader::load); - session = dataLoader.getSession(); - System.setSecurityManager(null); - } - - @Override - protected void after() { - super.after(); - try (CqlSession s = session) { - session = null; - } - System.setSecurityManager(null); - } - - // Getters for those who do not like to directly access fields - - public CqlSession getSession() { - return session; - } - -} diff --git a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java index 1cc88f5734..9f82b8735a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/NoSqlDaoServiceTestSuite.java @@ -15,28 +15,14 @@ */ package org.thingsboard.server.dao; -import org.cassandraunit.dataset.cql.ClassPathCQLDataSet; -import org.junit.ClassRule; import org.junit.extensions.cpsuite.ClasspathSuite; import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; import org.junit.runner.RunWith; -import java.util.Arrays; - @RunWith(ClasspathSuite.class) @ClassnameFilters({ "org.thingsboard.server.dao.service.*.nosql.*ServiceNoSqlTest", }) -public class NoSqlDaoServiceTestSuite { - - @ClassRule - public static CustomCassandraCQLUnit cassandraUnit = - new CustomCassandraCQLUnit( - Arrays.asList( - new ClassPathCQLDataSet("cassandra/schema-keyspace.cql", false, false), - new ClassPathCQLDataSet("cassandra/schema-ts.cql", false, false), - new ClassPathCQLDataSet("cassandra/schema-ts-latest.cql", false, false) - ), - "cassandra-test.yaml", 30000L); +public class NoSqlDaoServiceTestSuite extends AbstractNoSqlContainer { } diff --git a/dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java b/dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java index 2fa63f2387..61f77ee1b7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java +++ b/dao/src/test/java/org/thingsboard/server/dao/RedisSqlTestSuite.java @@ -15,37 +15,15 @@ */ package org.thingsboard.server.dao; -import org.junit.ClassRule; import org.junit.extensions.cpsuite.ClasspathSuite; import org.junit.extensions.cpsuite.ClasspathSuite.ClassnameFilters; import org.junit.runner.RunWith; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.context.support.TestPropertySourceUtils; -import org.testcontainers.containers.GenericContainer; -@ContextConfiguration(initializers = RedisSqlTestSuite.class) @RunWith(ClasspathSuite.class) @ClassnameFilters( //All the same tests using redis instead of caffeine. "org.thingsboard.server.dao.service.*ServiceSqlTest" ) -public class RedisSqlTestSuite implements ApplicationContextInitializer { - - @ClassRule - public static GenericContainer redis = new GenericContainer("redis:4.0").withExposedPorts(6379); - - @Override - public void initialize(ConfigurableApplicationContext applicationContext) { - TestPropertySourceUtils.addInlinedPropertiesToEnvironment( - applicationContext, "cache.type=redis"); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment( - applicationContext, "redis.connection.type=standalone"); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment( - applicationContext, "redis.standalone.host=localhost"); - TestPropertySourceUtils.addInlinedPropertiesToEnvironment( - applicationContext, "redis.standalone.port=" + redis.getMappedPort(6379)); - } +public class RedisSqlTestSuite extends AbstractRedisContainer { } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java index 1905a0b784..c839b8ed69 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAdminSettingsServiceTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.dao.exception.DataValidationException; @@ -54,17 +55,21 @@ public abstract class BaseAdminSettingsServiceTest extends AbstractServiceTest { Assert.assertEquals(adminSettings.getJsonValue(), savedAdminSettings.getJsonValue()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAdminSettingsWithEmptyKey() { AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(SYSTEM_TENANT_ID, "mail"); adminSettings.setKey(null); - adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); + Assertions.assertThrows(DataValidationException.class, () -> { + adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); + }); } - @Test(expected = DataValidationException.class) + @Test public void testChangeAdminSettingsKey() { AdminSettings adminSettings = adminSettingsService.findAdminSettingsByKey(SYSTEM_TENANT_ID, "mail"); adminSettings.setKey("newKey"); - adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); + Assertions.assertThrows(DataValidationException.class, () -> { + adminSettingsService.saveAdminSettings(SYSTEM_TENANT_ID, adminSettings); + }); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java index 00eba295ae..688793d6c6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmCommentServiceTest.java @@ -58,7 +58,7 @@ public abstract class BaseAlarmCommentServiceTest extends AbstractServiceTest { alarm = Alarm.builder().tenantId(tenantId).originator(new AssetId(Uuids.timeBased())) .type(TEST_ALARM) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) + .severity(AlarmSeverity.CRITICAL) .startTs(System.currentTimeMillis()).build(); alarm = alarmService.createOrUpdateAlarm(alarm).getAlarm(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java index dca45f100e..b7f5135533 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAlarmServiceTest.java @@ -20,20 +20,25 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmPropagationInfo; import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.id.AssetId; -import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; +import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.page.SortOrder; import org.thingsboard.server.common.data.page.TimePageLink; import org.thingsboard.server.common.data.query.AlarmData; @@ -45,8 +50,9 @@ import org.thingsboard.server.common.data.query.EntityKey; import org.thingsboard.server.common.data.query.EntityKeyType; import org.thingsboard.server.common.data.relation.EntityRelation; import org.thingsboard.server.common.data.relation.RelationTypeGroup; +import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult; -import org.thingsboard.common.util.JacksonUtil; import java.util.Arrays; import java.util.Collections; @@ -56,6 +62,11 @@ import java.util.concurrent.ExecutionException; public abstract class BaseAlarmServiceTest extends AbstractServiceTest { public static final String TEST_ALARM = "TEST_ALARM"; + + private static final String TEST_TENANT_EMAIL = "testtenant@thingsboard.org"; + private static final String TEST_TENANT_FIRST_NAME = "testtenantfirstname"; + private static final String TEST_TENANT_LAST_NAME = "testtenantlastname"; + private TenantId tenantId; @Before @@ -83,12 +94,12 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); long ts = System.currentTimeMillis(); - Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) .type(TEST_ALARM) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); Alarm created = result.getAlarm(); Assert.assertNotNull(created); @@ -107,7 +118,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(0L, created.getAckTs()); Assert.assertEquals(0L, created.getClearTs()); - Alarm fetched = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + Alarm fetched = alarmService.findAlarmInfoById(tenantId, created.getId()); Assert.assertEquals(created, fetched); } @@ -121,14 +132,13 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); long ts = System.currentTimeMillis(); - Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) .type(TEST_ALARM) - .propagate(false) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); - Alarm created = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); // Check child relation PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -139,7 +149,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check parent relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -152,7 +162,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertEquals(0, alarms.getData().size()); created.setPropagate(true); - result = alarmService.createOrUpdateAlarm(created); + result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created)); created = result.getAlarm(); // Check child relation @@ -164,7 +174,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check parent relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -175,10 +185,10 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); - alarmService.ackAlarm(tenantId, created.getId(), System.currentTimeMillis()).get(); - created = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()); + created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) @@ -188,7 +198,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check not existing relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -200,8 +210,8 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(0, alarms.getData().size()); - alarmService.clearAlarm(tenantId, created.getId(), null, System.currentTimeMillis()).get(); - created = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + alarmService.clearAlarm(tenantId, created.getId(), System.currentTimeMillis(), null); + created = alarmService.findAlarmInfoById(tenantId, created.getId()); alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) @@ -211,7 +221,77 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); + } + + @Test + public void testFindAssignedAlarm() throws ExecutionException, InterruptedException { + + AssetId parentId = new AssetId(Uuids.timeBased()); + AssetId childId = new AssetId(Uuids.timeBased()); + + EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); + + Assert.assertTrue(relationService.saveRelation(tenantId, relation)); + + long ts = System.currentTimeMillis(); + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) + .type(TEST_ALARM) + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); + + AlarmInfo created = result.getAlarm(); + + User tenantUser = new User(); + tenantUser.setTenantId(tenantId); + tenantUser.setAuthority(Authority.TENANT_ADMIN); + tenantUser.setEmail(TEST_TENANT_EMAIL); + tenantUser.setFirstName(TEST_TENANT_FIRST_NAME); + tenantUser.setLastName(TEST_TENANT_LAST_NAME); + tenantUser = userService.saveUser(tenantUser); + + Assert.assertNotNull(tenantUser); + + AlarmApiCallResult assignmentResult = alarmService.assignAlarm(tenantId, created.getId(), tenantUser.getId(), ts); + created = assignmentResult.getAlarm(); + + PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() + .assigneeId(tenantUser.getId()) + .fetchOriginator(true) + .pageLink(new TimePageLink(1, 0, "", + new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) + ).build()).get(); + Assert.assertNotNull(alarms.getData()); + Assert.assertEquals(1, alarms.getData().size()); Assert.assertEquals(created, alarms.getData().get(0)); + + AlarmDataPageLink pageLink = new AlarmDataPageLink(); + pageLink.setPage(0); + pageLink.setPageSize(10); + pageLink.setAssigneeId(tenantUser.getId()); + + PageData assignedAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(created.getOriginator())); + Assert.assertNotNull(assignedAlarms.getData()); + Assert.assertEquals(1, assignedAlarms.getData().size()); + Assert.assertEquals(created, new AlarmInfo(assignedAlarms.getData().get(0))); + + User tenantUser2 = new User(); + tenantUser2.setTenantId(tenantId); + tenantUser2.setAuthority(Authority.TENANT_ADMIN); + tenantUser2.setEmail(2 + TEST_TENANT_EMAIL); + tenantUser2.setFirstName(TEST_TENANT_FIRST_NAME); + tenantUser2.setLastName(TEST_TENANT_LAST_NAME); + tenantUser2 = userService.saveUser(tenantUser2); + + Assert.assertNotNull(tenantUser2); + pageLink.setAssigneeId(tenantUser2.getId()); + + PageData assignedToNonExistingUserAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(created.getOriginator())); + Assert.assertNotNull(assignedToNonExistingUserAlarms.getData()); + Assert.assertTrue(assignedToNonExistingUserAlarms.getData().isEmpty()); + } @Test @@ -235,23 +315,23 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { customerDevice = deviceService.saveDevice(customerDevice); long ts = System.currentTimeMillis(); - Alarm tenantAlarm = Alarm.builder().tenantId(tenantId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(tenantDevice.getId()) .type(TEST_ALARM) - .propagate(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - AlarmOperationResult result = alarmService.createOrUpdateAlarm(tenantAlarm); - tenantAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagate(true).build()) + .startTs(ts).build()); + AlarmInfo tenantAlarm = result.getAlarm(); - Alarm deviceAlarm = Alarm.builder().tenantId(tenantId) + result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(customerDevice.getId()) .type(TEST_ALARM) - .propagate(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - result = alarmService.createOrUpdateAlarm(deviceAlarm); - deviceAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagate(true).build()) + .startTs(ts).build()); + AlarmInfo deviceAlarm = result.getAlarm(); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); @@ -269,7 +349,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(customerDevice.getId())); Assert.assertEquals(1, customerAlarms.getData().size()); - Assert.assertEquals(deviceAlarm, customerAlarms.getData().get(0)); + Assert.assertEquals(deviceAlarm, new AlarmInfo(customerAlarms.getData().get(0))); PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(tenantDevice.getId()) @@ -279,7 +359,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(tenantAlarm, alarms.getData().get(0)); + Assert.assertEquals(tenantAlarm, new AlarmInfo(alarms.getData().get(0))); } @Test @@ -311,23 +391,21 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { relationService.saveRelation(tenantId, relation); long ts = System.currentTimeMillis(); - Alarm tenantAlarm = Alarm.builder().tenantId(tenantId) + alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(tenantDevice.getId()) .type("Not Propagated") - .propagate(false) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - AlarmOperationResult result = alarmService.createOrUpdateAlarm(tenantAlarm); - tenantAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .startTs(ts).build()); - Alarm customerAlarm = Alarm.builder().tenantId(tenantId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(tenantDevice.getId()) .type("Propagated") - .propagate(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - result = alarmService.createOrUpdateAlarm(customerAlarm); - customerAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagate(true).build()) + .startTs(ts).build()); + AlarmInfo customerAlarm = result.getAlarm(); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); @@ -343,7 +421,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { //TEST that propagated alarms are visible on the asset level. PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(customerAsset.getId())); Assert.assertEquals(1, customerAlarms.getData().size()); - Assert.assertEquals(customerAlarm, customerAlarms.getData().get(0)); + Assert.assertEquals(customerAlarm, new AlarmInfo(customerAlarms.getData().get(0))); } @Test @@ -361,24 +439,24 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { device = deviceService.saveDevice(device); long ts = System.currentTimeMillis(); - Alarm tenantAlarm = Alarm.builder().tenantId(tenantId) + + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(device.getId()) .type("Propagated To Tenant") - .propagateToTenant(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - AlarmOperationResult result = alarmService.createOrUpdateAlarm(tenantAlarm); - tenantAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagateToTenant(true).build()) + .startTs(ts).build()); + AlarmInfo tenantAlarm = result.getAlarm(); - Alarm customerAlarm = Alarm.builder().tenantId(tenantId) + result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) .originator(device.getId()) .type("Propagated to Customer") - .propagate(false) - .propagateToOwner(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - result = alarmService.createOrUpdateAlarm(customerAlarm); - customerAlarm = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagateToOwner(true).build()) + .startTs(ts).build()); + AlarmInfo customerAlarm = result.getAlarm(); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); @@ -389,24 +467,24 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { pageLink.setEndTs(System.currentTimeMillis()); pageLink.setSearchPropagatedAlarms(true); pageLink.setSeverityList(Arrays.asList(AlarmSeverity.CRITICAL, AlarmSeverity.WARNING)); - pageLink.setStatusList(Arrays.asList(AlarmSearchStatus.ACTIVE)); + pageLink.setStatusList(Collections.singletonList(AlarmSearchStatus.ACTIVE)); //TEST that propagated alarms are visible on the asset level. PageData tenantAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(tenantId)); Assert.assertEquals(1, tenantAlarms.getData().size()); - Assert.assertEquals(tenantAlarm, tenantAlarms.getData().get(0)); + Assert.assertEquals(tenantAlarm, new AlarmInfo(tenantAlarms.getData().get(0))); //TEST that propagated alarms are visible on the asset level. PageData customerAlarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(customer.getId())); Assert.assertEquals(1, customerAlarms.getData().size()); - Assert.assertEquals(customerAlarm, customerAlarms.getData().get(0)); + Assert.assertEquals(customerAlarm, new AlarmInfo(customerAlarms.getData().get(0))); } - private AlarmDataQuery toQuery(AlarmDataPageLink pageLink){ + private AlarmDataQuery toQuery(AlarmDataPageLink pageLink) { return toQuery(pageLink, Collections.emptyList()); } - private AlarmDataQuery toQuery(AlarmDataPageLink pageLink, List alarmFields){ + private AlarmDataQuery toQuery(AlarmDataPageLink pageLink, List alarmFields) { return new AlarmDataQuery(new DeviceTypeFilter(), pageLink, null, null, null, alarmFields); } @@ -425,45 +503,41 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { customerDevice = deviceService.saveDevice(customerDevice); // no one alarms was created - Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null)); + Assert.assertNull(alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null, null)); - Alarm alarm1 = Alarm.builder() + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) .originator(customerDevice.getId()) .type(TEST_ALARM) .severity(AlarmSeverity.MAJOR) - .status(AlarmStatus.ACTIVE_UNACK) - .startTs(System.currentTimeMillis()) - .build(); - alarm1 = alarmService.createOrUpdateAlarm(alarm1).getAlarm(); - alarmService.clearAlarm(tenantId, alarm1.getId(), null, System.currentTimeMillis()).get(); + .startTs(System.currentTimeMillis()).build()); + AlarmInfo alarm1 = result.getAlarm(); + alarmService.clearAlarm(tenantId, alarm1.getId(), System.currentTimeMillis(), null); - Alarm alarm2 = Alarm.builder() + result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) .originator(customerDevice.getId()) .type(TEST_ALARM) .severity(AlarmSeverity.MINOR) - .status(AlarmStatus.ACTIVE_ACK) - .startTs(System.currentTimeMillis()) - .build(); - alarm2 = alarmService.createOrUpdateAlarm(alarm2).getAlarm(); - alarmService.clearAlarm(tenantId, alarm2.getId(), null, System.currentTimeMillis()).get(); + .startTs(System.currentTimeMillis()).build()); + AlarmInfo alarm2 = result.getAlarm(); + alarmService.acknowledgeAlarm(tenantId, alarm2.getId(), System.currentTimeMillis()); + alarmService.clearAlarm(tenantId, alarm2.getId(), System.currentTimeMillis(), null); - Alarm alarm3 = Alarm.builder() + result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() .tenantId(tenantId) .originator(customerDevice.getId()) .type(TEST_ALARM) .severity(AlarmSeverity.CRITICAL) - .status(AlarmStatus.ACTIVE_ACK) - .startTs(System.currentTimeMillis()) - .build(); - alarm3 = alarmService.createOrUpdateAlarm(alarm3).getAlarm(); - - Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null)); - Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null)); - Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK)); - Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null)); - Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK)); + .startTs(System.currentTimeMillis()).build()); + AlarmInfo alarm3 = result.getAlarm(); + alarmService.acknowledgeAlarm(tenantId, alarm3.getId(), System.currentTimeMillis()); + + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.UNACK, null, null)); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, null, null)); + Assert.assertEquals(AlarmSeverity.MAJOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_UNACK, null)); + Assert.assertEquals(AlarmSeverity.CRITICAL, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), AlarmSearchStatus.ACTIVE, null, null)); + Assert.assertEquals(AlarmSeverity.MINOR, alarmService.findHighestAlarmSeverity(tenantId, customerDevice.getId(), null, AlarmStatus.CLEARED_ACK, null)); } @Test @@ -479,15 +553,13 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation2).get()); long ts = System.currentTimeMillis(); - Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) .type(TEST_ALARM) - .propagate(false) .severity(AlarmSeverity.CRITICAL) - .status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); - Alarm created = result.getAlarm(); + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); AlarmDataPageLink pageLink = new AlarmDataPageLink(); pageLink.setPage(0); @@ -504,7 +576,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); pageLink.setPage(0); pageLink.setPageSize(10); @@ -519,17 +591,17 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, new Alarm(alarms.getData().get(0))); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); pageLink.setSearchPropagatedAlarms(true); alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, new Alarm(alarms.getData().get(0))); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check child relation created.setPropagate(true); - result = alarmService.createOrUpdateAlarm(created); + result = alarmService.updateAlarm(AlarmUpdateRequest.fromAlarm(created)); created = result.getAlarm(); // Check child relation @@ -546,7 +618,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check parent relation pageLink.setPage(0); @@ -562,37 +634,40 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(parentId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); PageData alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) + .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarmsInfoData.getData()); Assert.assertEquals(1, alarmsInfoData.getData().size()); - Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0))); alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId) + .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarmsInfoData.getData()); Assert.assertEquals(1, alarmsInfoData.getData().size()); - Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0))); alarmsInfoData = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(parentId2) + .fetchOriginator(true) .status(AlarmStatus.ACTIVE_UNACK).pageLink( new TimePageLink(10, 0, "", new SortOrder("createdTime", SortOrder.Direction.DESC), 0L, System.currentTimeMillis()) ).build()).get(); Assert.assertNotNull(alarmsInfoData.getData()); Assert.assertEquals(1, alarmsInfoData.getData().size()); - Assert.assertEquals(created, alarmsInfoData.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarmsInfoData.getData().get(0))); pageLink.setPage(0); pageLink.setPageSize(10); @@ -607,10 +682,9 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(parentId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); - alarmService.ackAlarm(tenantId, created.getId(), System.currentTimeMillis()).get(); - created = alarmService.findAlarmByIdAsync(tenantId, created.getId()).get(); + created = alarmService.acknowledgeAlarm(tenantId, created.getId(), System.currentTimeMillis()).getAlarm(); pageLink.setPage(0); pageLink.setPageSize(10); @@ -625,7 +699,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { alarms = alarmService.findAlarmDataByQueryForEntities(tenantId, toQuery(pageLink), Collections.singletonList(childId)); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); } @Test @@ -635,17 +709,17 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { EntityRelation relation = new EntityRelation(parentId, childId, EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(relationService.saveRelationAsync(tenantId, relation).get()); + Assert.assertTrue(relationService.saveRelation(tenantId, relation)); long ts = System.currentTimeMillis(); - Alarm alarm = Alarm.builder().tenantId(tenantId).originator(childId) + AlarmApiCallResult result = alarmService.createAlarm(AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(childId) .type(TEST_ALARM) - .propagate(true) - .severity(AlarmSeverity.CRITICAL).status(AlarmStatus.ACTIVE_UNACK) - .startTs(ts).build(); - - AlarmOperationResult result = alarmService.createOrUpdateAlarm(alarm); - Alarm created = result.getAlarm(); + .severity(AlarmSeverity.CRITICAL) + .propagation(AlarmPropagationInfo.builder().propagate(true).build()) + .startTs(ts).build()); + AlarmInfo created = result.getAlarm(); PageData alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() .affectedEntityId(childId) @@ -655,7 +729,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); // Check parent relation alarms = alarmService.findAlarms(tenantId, AlarmQuery.builder() @@ -666,7 +740,7 @@ public abstract class BaseAlarmServiceTest extends AbstractServiceTest { ).build()).get(); Assert.assertNotNull(alarms.getData()); Assert.assertEquals(1, alarms.getData().size()); - Assert.assertEquals(created, alarms.getData().get(0)); + Assert.assertEquals(created, new AlarmInfo(alarms.getData().get(0))); Assert.assertTrue("Alarm was not deleted when expected", alarmService.deleteAlarm(tenantId, created.getId()).isSuccessful()); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java index ea0d9abedf..091b309269 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetProfileServiceTest.java @@ -23,6 +23,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.asset.Asset; @@ -150,22 +151,26 @@ public abstract class BaseAssetProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(savedAssetProfile2.getId(), defaultAssetProfile.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetProfileWithEmptyName() { AssetProfile assetProfile = new AssetProfile(); assetProfile.setTenantId(tenantId); - assetProfileService.saveAssetProfile(assetProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + assetProfileService.saveAssetProfile(assetProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetProfileWithSameName() { AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); assetProfileService.saveAssetProfile(assetProfile); AssetProfile assetProfile2 = this.createAssetProfile(tenantId, "Asset Profile"); - assetProfileService.saveAssetProfile(assetProfile2); + Assertions.assertThrows(DataValidationException.class, () -> { + assetProfileService.saveAssetProfile(assetProfile2); + }); } - @Test(expected = DataValidationException.class) + @Test public void testDeleteAssetProfileWithExistingAsset() { AssetProfile assetProfile = this.createAssetProfile(tenantId, "Asset Profile"); AssetProfile savedAssetProfile = assetProfileService.saveAssetProfile(assetProfile); @@ -174,7 +179,9 @@ public abstract class BaseAssetProfileServiceTest extends AbstractServiceTest { asset.setName("Test asset"); asset.setAssetProfileId(savedAssetProfile.getId()); assetService.saveAsset(asset); - assetProfileService.deleteAssetProfile(tenantId, savedAssetProfile.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + assetProfileService.deleteAssetProfile(tenantId, savedAssetProfile.getId()); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java index 9fe9752a85..108fcb70d9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseAssetServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; import org.thingsboard.server.common.data.StringUtils; @@ -83,63 +84,73 @@ public abstract class BaseAssetServiceTest extends AbstractServiceTest { assetService.deleteAsset(tenantId, savedAsset.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetWithEmptyName() { Asset asset = new Asset(); asset.setTenantId(tenantId); asset.setType("default"); - assetService.saveAsset(asset); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.saveAsset(asset); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetWithEmptyTenant() { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); - assetService.saveAsset(asset); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.saveAsset(asset); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveAssetWithInvalidTenant() { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); asset.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - assetService.saveAsset(asset); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.saveAsset(asset); + }); } - @Test(expected = DataValidationException.class) + @Test public void testAssignAssetToNonExistentCustomer() { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); asset.setTenantId(tenantId); - asset = assetService.saveAsset(asset); + Asset savedAsset = assetService.saveAsset(asset); try { - assetService.assignAssetToCustomer(tenantId, asset.getId(), new CustomerId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.assignAssetToCustomer(tenantId, savedAsset.getId(), new CustomerId(Uuids.timeBased())); + }); } finally { - assetService.deleteAsset(tenantId, asset.getId()); + assetService.deleteAsset(tenantId, savedAsset.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignAssetToCustomerFromDifferentTenant() { Asset asset = new Asset(); asset.setName("My asset"); asset.setType("default"); asset.setTenantId(tenantId); - asset = assetService.saveAsset(asset); + Asset savedAsset = assetService.saveAsset(asset); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); customer.setTitle("Test different customer"); - customer = customerService.saveCustomer(customer); + Customer savedCustomer = customerService.saveCustomer(customer); try { - assetService.assignAssetToCustomer(tenantId, asset.getId(), customer.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + assetService.assignAssetToCustomer(tenantId, savedAsset.getId(), savedCustomer.getId()); + }); } finally { - assetService.deleteAsset(tenantId, asset.getId()); + assetService.deleteAsset(tenantId, savedAsset.getId()); tenantService.deleteTenant(tenant.getId()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java index 11d82ad963..18bf036495 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseCustomerServiceTest.java @@ -24,6 +24,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.ThingsBoardExecutors; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.StringUtils; @@ -99,35 +100,43 @@ public abstract class BaseCustomerServiceTest extends AbstractServiceTest { customerService.deleteCustomer(tenantId, savedCustomer.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveCustomerWithEmptyTitle() { Customer customer = new Customer(); customer.setTenantId(tenantId); - customerService.saveCustomer(customer); + Assertions.assertThrows(DataValidationException.class, () -> { + customerService.saveCustomer(customer); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveCustomerWithEmptyTenant() { Customer customer = new Customer(); customer.setTitle("My customer"); - customerService.saveCustomer(customer); + Assertions.assertThrows(DataValidationException.class, () -> { + customerService.saveCustomer(customer); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveCustomerWithInvalidTenant() { Customer customer = new Customer(); customer.setTitle("My customer"); customer.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - customerService.saveCustomer(customer); + Assertions.assertThrows(DataValidationException.class, () -> { + customerService.saveCustomer(customer); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveCustomerWithInvalidEmail() { Customer customer = new Customer(); customer.setTenantId(tenantId); customer.setTitle("My customer"); customer.setEmail("invalid@mail"); - customerService.saveCustomer(customer); + Assertions.assertThrows(DataValidationException.class, () -> { + customerService.saveCustomer(customer); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java index 88d66e9586..4dc28a210a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDashboardServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Dashboard; import org.thingsboard.server.common.data.DashboardInfo; @@ -82,58 +83,68 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDashboardWithEmptyTitle() { Dashboard dashboard = new Dashboard(); dashboard.setTenantId(tenantId); - dashboardService.saveDashboard(dashboard); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.saveDashboard(dashboard); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDashboardWithEmptyTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); - dashboardService.saveDashboard(dashboard); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.saveDashboard(dashboard); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDashboardWithInvalidTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - dashboardService.saveDashboard(dashboard); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.saveDashboard(dashboard); + }); } - @Test(expected = DataValidationException.class) + @Test public void testAssignDashboardToNonExistentCustomer() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(tenantId); - dashboard = dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); try { - dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), new CustomerId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.assignDashboardToCustomer(tenantId, savedDashboard.getId(), new CustomerId(Uuids.timeBased())); + }); } finally { - dashboardService.deleteDashboard(tenantId, dashboard.getId()); + dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignDashboardToCustomerFromDifferentTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(tenantId); - dashboard = dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant [dashboard]"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); customer.setTitle("Test different customer"); - customer = customerService.saveCustomer(customer); + Customer savedCustomer = customerService.saveCustomer(customer); try { - dashboardService.assignDashboardToCustomer(tenantId, dashboard.getId(), customer.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.assignDashboardToCustomer(tenantId, savedDashboard.getId(), savedCustomer.getId()); + }); } finally { - dashboardService.deleteDashboard(tenantId, dashboard.getId()); + dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); tenantService.deleteTenant(tenant.getId()); } } @@ -389,25 +400,27 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { tenantService.deleteTenant(tenantId); } - @Test(expected = DataValidationException.class) + @Test public void testAssignDashboardToNonExistentEdge() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(tenantId); - dashboard = dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); try { - dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), new EdgeId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.assignDashboardToEdge(tenantId, savedDashboard.getId(), new EdgeId(Uuids.timeBased())); + }); } finally { - dashboardService.deleteDashboard(tenantId, dashboard.getId()); + dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignDashboardToEdgeFromDifferentTenant() { Dashboard dashboard = new Dashboard(); dashboard.setTitle("My dashboard"); dashboard.setTenantId(tenantId); - dashboard = dashboardService.saveDashboard(dashboard); + Dashboard savedDashboard = dashboardService.saveDashboard(dashboard); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant [edge]"); tenant = tenantService.saveTenant(tenant); @@ -418,11 +431,13 @@ public abstract class BaseDashboardServiceTest extends AbstractServiceTest { edge.setType("default"); edge.setSecret(StringUtils.randomAlphanumeric(20)); edge.setRoutingKey(StringUtils.randomAlphanumeric(20)); - edge = edgeService.saveEdge(edge); + Edge savedEdge = edgeService.saveEdge(edge); try { - dashboardService.assignDashboardToEdge(tenantId, dashboard.getId(), edge.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + dashboardService.assignDashboardToEdge(tenantId, savedDashboard.getId(), savedEdge.getId()); + }); } finally { - dashboardService.deleteDashboard(tenantId, dashboard.getId()); + dashboardService.deleteDashboard(tenantId, savedDashboard.getId()); tenantService.deleteTenant(tenant.getId()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java index 9415da99ea..7fc9f7b218 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsCacheTest.java @@ -43,8 +43,8 @@ import static org.mockito.Mockito.when; public abstract class BaseDeviceCredentialsCacheTest extends AbstractServiceTest { - private static final String CREDENTIALS_ID_1 = StringUtils.randomAlphanumeric(20); - private static final String CREDENTIALS_ID_2 = StringUtils.randomAlphanumeric(20); + private final String CREDENTIALS_ID_1 = StringUtils.randomAlphanumeric(20); + private final String CREDENTIALS_ID_2 = StringUtils.randomAlphanumeric(20); @Autowired private DeviceCredentialsService deviceCredentialsService; diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java index a1d4543c58..f3e052b3e9 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceCredentialsServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.DeviceCredentialsId; @@ -47,13 +48,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe tenantService.deleteTenant(tenantId); } - @Test(expected = DataValidationException.class) + @Test public void testCreateDeviceCredentials() { DeviceCredentials deviceCredentials = new DeviceCredentials(); - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceCredentialsWithEmptyDevice() { Device device = new Device(); device.setName("My device"); @@ -63,13 +66,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setDeviceId(null); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceCredentialsWithEmptyCredentialsType() { Device device = new Device(); device.setName("My device"); @@ -79,13 +84,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setCredentialsType(null); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceCredentialsWithEmptyCredentialsId() { Device device = new Device(); device.setName("My device"); @@ -95,13 +102,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setCredentialsId(null); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveNonExistentDeviceCredentials() { Device device = new Device(); device.setName("My device"); @@ -115,13 +124,15 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe newDeviceCredentials.setCredentialsType(deviceCredentials.getCredentialsType()); newDeviceCredentials.setCredentialsId(deviceCredentials.getCredentialsId()); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, newDeviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, newDeviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceCredentialsWithNonExistentDevice() { Device device = new Device(); device.setName("My device"); @@ -131,7 +142,9 @@ public abstract class BaseDeviceCredentialsServiceTest extends AbstractServiceTe DeviceCredentials deviceCredentials = deviceCredentialsService.findDeviceCredentialsByDeviceId(tenantId, device.getId()); deviceCredentials.setDeviceId(new DeviceId(Uuids.timeBased())); try { - deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceCredentialsService.updateDeviceCredentials(tenantId, deviceCredentials); + }); } finally { deviceService.deleteDevice(tenantId, device.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java index d669c24cd6..77a8a479f2 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceProfileServiceTest.java @@ -24,6 +24,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.ThingsBoardThreadFactory; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -196,23 +197,27 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(savedDeviceProfile2.getId(), defaultDeviceProfile.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceProfileWithEmptyName() { DeviceProfile deviceProfile = new DeviceProfile(); deviceProfile.setTenantId(tenantId); - deviceProfileService.saveDeviceProfile(deviceProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.saveDeviceProfile(deviceProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceProfileWithSameName() { DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); deviceProfileService.saveDeviceProfile(deviceProfile); DeviceProfile deviceProfile2 = this.createDeviceProfile(tenantId, "Device Profile"); - deviceProfileService.saveDeviceProfile(deviceProfile2); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.saveDeviceProfile(deviceProfile2); + }); } @Ignore - @Test(expected = DataValidationException.class) + @Test public void testChangeDeviceProfileTypeWithExistingDevices() { DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); @@ -224,10 +229,12 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { deviceService.saveDevice(device); //TODO: once we have more profile types, we should test that we can not change profile type in runtime and uncomment the @Ignore. // savedDeviceProfile.setType(DeviceProfileType.LWM2M); - deviceProfileService.saveDeviceProfile(savedDeviceProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testChangeDeviceProfileTransportTypeWithExistingDevices() { DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); @@ -238,10 +245,12 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { device.setDeviceProfileId(savedDeviceProfile.getId()); deviceService.saveDevice(device); savedDeviceProfile.setTransportType(DeviceTransportType.MQTT); - deviceProfileService.saveDeviceProfile(savedDeviceProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.saveDeviceProfile(savedDeviceProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testDeleteDeviceProfileWithExistingDevice() { DeviceProfile deviceProfile = this.createDeviceProfile(tenantId, "Device Profile"); DeviceProfile savedDeviceProfile = deviceProfileService.saveDeviceProfile(deviceProfile); @@ -251,7 +260,9 @@ public abstract class BaseDeviceProfileServiceTest extends AbstractServiceTest { device.setType("default"); device.setDeviceProfileId(savedDeviceProfile.getId()); deviceService.saveDevice(device); - deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java index ea2fa9546f..010f56235f 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseDeviceServiceTest.java @@ -21,6 +21,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.junit.rules.ExpectedException; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; @@ -91,7 +92,7 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deleteDevice(tenantId, device); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDevicesWithMaxDeviceOutOfLimit() { TenantProfile defaultTenantProfile = tenantProfileService.findDefaultTenantProfile(tenantId); defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxDevices(1).build()); @@ -102,7 +103,9 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { this.saveDevice(tenantId, "My first device"); Assert.assertEquals(1, deviceService.countByTenantId(tenantId)); - this.saveDevice(tenantId, "My second device that out of maxDeviceCount limit"); + Assertions.assertThrows(DataValidationException.class, () -> { + this.saveDevice(tenantId, "My second device that out of maxDeviceCount limit"); + }); } @Test @@ -246,63 +249,73 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { deviceService.saveDevice(savedDevice); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceWithEmptyName() { Device device = new Device(); device.setType("default"); device.setTenantId(tenantId); - deviceService.saveDevice(device); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.saveDevice(device); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceWithEmptyTenant() { Device device = new Device(); device.setName("My device"); device.setType("default"); - deviceService.saveDevice(device); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.saveDevice(device); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveDeviceWithInvalidTenant() { Device device = new Device(); device.setName("My device"); device.setType("default"); device.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - deviceService.saveDevice(device); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.saveDevice(device); + }); } - @Test(expected = DataValidationException.class) + @Test public void testAssignDeviceToNonExistentCustomer() { Device device = new Device(); device.setName("My device"); device.setType("default"); device.setTenantId(tenantId); - device = deviceService.saveDevice(device); + Device savedDevice = deviceService.saveDevice(device); try { - deviceService.assignDeviceToCustomer(tenantId, device.getId(), new CustomerId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.assignDeviceToCustomer(tenantId, savedDevice.getId(), new CustomerId(Uuids.timeBased())); + }); } finally { - deviceService.deleteDevice(tenantId, device.getId()); + deviceService.deleteDevice(tenantId, savedDevice.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignDeviceToCustomerFromDifferentTenant() { Device device = new Device(); device.setName("My device"); device.setType("default"); device.setTenantId(tenantId); - device = deviceService.saveDevice(device); + Device savedDevice = deviceService.saveDevice(device); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); customer.setTitle("Test different customer"); - customer = customerService.saveCustomer(customer); + Customer savedCustomer = customerService.saveCustomer(customer); try { - deviceService.assignDeviceToCustomer(tenantId, device.getId(), customer.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + deviceService.assignDeviceToCustomer(tenantId, savedDevice.getId(), savedCustomer.getId()); + }); } finally { - deviceService.deleteDevice(tenantId, device.getId()); + deviceService.deleteDevice(tenantId, savedDevice.getId()); tenantService.deleteTenant(tenant.getId()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java index 5aced56afd..70d41f4681 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseEdgeServiceTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.EntitySubtype; @@ -86,57 +87,67 @@ public abstract class BaseEdgeServiceTest extends AbstractServiceTest { edgeService.deleteEdge(tenantId, savedEdge.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveEdgeWithEmptyName() { Edge edge = new Edge(); edge.setType("default"); edge.setTenantId(tenantId); - edgeService.saveEdge(edge); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.saveEdge(edge); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveEdgeWithEmptyTenant() { Edge edge = new Edge(); edge.setName("My edge"); edge.setType("default"); - edgeService.saveEdge(edge); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.saveEdge(edge); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveEdgeWithInvalidTenant() { Edge edge = new Edge(); edge.setName("My edge"); edge.setType("default"); edge.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - edgeService.saveEdge(edge); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.saveEdge(edge); + }); } - @Test(expected = DataValidationException.class) + @Test public void testAssignEdgeToNonExistentCustomer() { Edge edge = constructEdge("My edge", "default"); - edge = edgeService.saveEdge(edge); + Edge savedEdge = edgeService.saveEdge(edge); try { - edgeService.assignEdgeToCustomer(tenantId, edge.getId(), new CustomerId(Uuids.timeBased())); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.assignEdgeToCustomer(tenantId, savedEdge.getId(), new CustomerId(Uuids.timeBased())); + }); } finally { - edgeService.deleteEdge(tenantId, edge.getId()); + edgeService.deleteEdge(tenantId, savedEdge.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testAssignEdgeToCustomerFromDifferentTenant() { Edge edge = constructEdge("My edge", "default"); - edge = edgeService.saveEdge(edge); + Edge savedEdge = edgeService.saveEdge(edge); Tenant tenant = new Tenant(); tenant.setTitle("Test different tenant"); tenant = tenantService.saveTenant(tenant); Customer customer = new Customer(); customer.setTenantId(tenant.getId()); customer.setTitle("Test different customer"); - customer = customerService.saveCustomer(customer); + Customer savedCustomer = customerService.saveCustomer(customer); try { - edgeService.assignEdgeToCustomer(tenantId, edge.getId(), customer.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + edgeService.assignEdgeToCustomer(tenantId, savedEdge.getId(), savedCustomer.getId()); + }); } finally { - edgeService.deleteEdge(tenantId, edge.getId()); + edgeService.deleteEdge(tenantId, savedEdge.getId()); tenantService.deleteTenant(tenant.getId()); } } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java index 3ab182519c..510ca0f2db 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ConfigTemplateServiceTest.java @@ -19,6 +19,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.oauth2.MapperType; @@ -51,12 +52,14 @@ public abstract class BaseOAuth2ConfigTemplateServiceTest extends AbstractServic } - @Test(expected = DataValidationException.class) + @Test public void testSaveDuplicateProviderId() { OAuth2ClientRegistrationTemplate first = validClientRegistrationTemplate("providerId"); OAuth2ClientRegistrationTemplate second = validClientRegistrationTemplate("providerId"); oAuth2ConfigTemplateService.saveClientRegistrationTemplate(first); - oAuth2ConfigTemplateService.saveClientRegistrationTemplate(second); + Assertions.assertThrows(DataValidationException.class, () -> { + oAuth2ConfigTemplateService.saveClientRegistrationTemplate(second); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java index 42ea18bdec..73dd08d53c 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOAuth2ServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.oauth2.MapperType; @@ -61,7 +62,7 @@ public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest { Assert.assertTrue(oAuth2Service.findOAuth2Info().getOauth2ParamsInfos().isEmpty()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveHttpAndMixedDomainsTogether() { OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( OAuth2ParamsInfo.builder() @@ -77,10 +78,12 @@ public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest { )) .build() )); - oAuth2Service.saveOAuth2Info(oAuth2Info); + Assertions.assertThrows(DataValidationException.class, () -> { + oAuth2Service.saveOAuth2Info(oAuth2Info); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveHttpsAndMixedDomainsTogether() { OAuth2Info oAuth2Info = new OAuth2Info(true, Lists.newArrayList( OAuth2ParamsInfo.builder() @@ -96,7 +99,9 @@ public abstract class BaseOAuth2ServiceTest extends AbstractServiceTest { )) .build() )); - oAuth2Service.saveOAuth2Info(oAuth2Info); + Assertions.assertThrows(DataValidationException.class, () -> { + oAuth2Service.saveOAuth2Info(oAuth2Info); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java index d4242fb1c4..af2f067c2e 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseQueueServiceTest.java @@ -19,6 +19,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; @@ -139,7 +140,7 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queueService.deleteQueue(tenantId, foundQueue.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyName() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -149,10 +150,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithInvalidName() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -163,10 +166,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyTopic() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -176,10 +181,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithInvalidTopic() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -190,10 +197,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyPollInterval() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -203,10 +212,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyPartitions() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -216,10 +227,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyPackProcessingTimeout() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -229,10 +242,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPartitions(1); queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptySubmitStrategy() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -242,10 +257,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPartitions(1); queue.setPackProcessingTimeout(2000); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyProcessingStrategy() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -255,10 +272,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setPartitions(1); queue.setPackProcessingTimeout(2000); queue.setSubmitStrategy(createTestSubmitStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptySubmitStrategyType() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -270,10 +289,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.getSubmitStrategy().setType(null); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptySubmitStrategyBatchSize() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -286,10 +307,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.getSubmitStrategy().setType(SubmitStrategyType.BATCH); queue.getSubmitStrategy().setBatchSize(0); queue.setProcessingStrategy(createTestProcessingStrategy()); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithEmptyProcessingStrategyType() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -301,10 +324,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setType(null); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithNegativeProcessingStrategyRetries() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -316,10 +341,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setRetries(-1); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithNegativeProcessingStrategyFailurePercentage() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -331,10 +358,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setFailurePercentage(-1); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithNegativeProcessingStrategyPauseBetweenRetries() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -346,10 +375,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setPauseBetweenRetries(-1); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithProcessingStrategyPauseBetweenRetriesBiggerThenMaxPauseBetweenRetries() { Queue queue = new Queue(); queue.setTenantId(tenantId); @@ -361,10 +392,12 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); queue.getProcessingStrategy().setPauseBetweenRetries(100); - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveQueueWithNotIsolatedTenant() { Tenant tenant = new Tenant(); tenant.setTitle("Not isolated tenant"); @@ -381,7 +414,9 @@ public abstract class BaseQueueServiceTest extends AbstractServiceTest { queue.setSubmitStrategy(createTestSubmitStrategy()); queue.setProcessingStrategy(createTestProcessingStrategy()); try { - queueService.saveQueue(queue); + Assertions.assertThrows(DataValidationException.class, () -> { + queueService.saveQueue(queue); + }); } finally { tenantService.deleteTenant(savedTenant.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java index 82c50f2438..c920b86bd5 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRelationServiceTest.java @@ -22,6 +22,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.id.AssetId; import org.thingsboard.server.common.data.id.DeviceId; @@ -334,28 +335,34 @@ public abstract class BaseRelationServiceTest extends AbstractServiceTest { } } - @Test(expected = DataValidationException.class) + @Test public void testSaveRelationWithEmptyFrom() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); relation.setTo(new AssetId(Uuids.timeBased())); relation.setType(EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(saveRelation(relation)); + Assertions.assertThrows(DataValidationException.class, () -> { + Assert.assertTrue(saveRelation(relation)); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveRelationWithEmptyTo() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); relation.setFrom(new AssetId(Uuids.timeBased())); relation.setType(EntityRelation.CONTAINS_TYPE); - Assert.assertTrue(saveRelation(relation)); + Assertions.assertThrows(DataValidationException.class, () -> { + Assert.assertTrue(saveRelation(relation)); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveRelationWithEmptyType() throws ExecutionException, InterruptedException { EntityRelation relation = new EntityRelation(); relation.setFrom(new AssetId(Uuids.timeBased())); relation.setTo(new AssetId(Uuids.timeBased())); - Assert.assertTrue(saveRelation(relation)); + Assertions.assertThrows(DataValidationException.class, () -> { + Assert.assertTrue(saveRelation(relation)); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java index 18c6e110d5..16901119a6 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseRuleChainServiceTest.java @@ -21,6 +21,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.edge.Edge; @@ -86,19 +87,23 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainById(tenantId, savedRuleChain.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveRuleChainWithEmptyName() { RuleChain ruleChain = new RuleChain(); ruleChain.setTenantId(tenantId); - ruleChainService.saveRuleChain(ruleChain); + Assertions.assertThrows(DataValidationException.class, () -> { + ruleChainService.saveRuleChain(ruleChain); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveRuleChainWithInvalidTenant() { RuleChain ruleChain = new RuleChain(); ruleChain.setName("My RuleChain"); ruleChain.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - ruleChainService.saveRuleChain(ruleChain); + Assertions.assertThrows(DataValidationException.class, () -> { + ruleChainService.saveRuleChain(ruleChain); + }); } @Test @@ -321,14 +326,18 @@ public abstract class BaseRuleChainServiceTest extends AbstractServiceTest { ruleChainService.deleteRuleChainById(tenantId, savedRuleChainMetaData.getRuleChainId()); } - @Test(expected = DataValidationException.class) - public void testUpdateRuleChainMetaDataWithCirclingRelation() throws Exception { - ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation()); + @Test + public void testUpdateRuleChainMetaDataWithCirclingRelation() { + Assertions.assertThrows(DataValidationException.class, () -> { + ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation()); + }); } - @Test(expected = DataValidationException.class) - public void testUpdateRuleChainMetaDataWithCirclingRelation2() throws Exception { - ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation2()); + @Test + public void testUpdateRuleChainMetaDataWithCirclingRelation2() { + Assertions.assertThrows(DataValidationException.class, () -> { + ruleChainService.saveRuleChainMetaData(tenantId, createRuleChainMetadataWithCirclingRelation2()); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java index a21a8e5845..926f680e40 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantProfileServiceTest.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.node.NullNode; import org.junit.After; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.Tenant; @@ -154,30 +155,36 @@ public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest { Assert.assertEquals(savedTenantProfile2.getId(), defaultTenantProfile.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTenantProfileWithEmptyName() { TenantProfile tenantProfile = new TenantProfile(); - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTenantProfileWithSameName() { TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); TenantProfile tenantProfile2 = this.createTenantProfile("Tenant Profile"); - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile2); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile2); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveSameTenantProfileWithDifferentIsolatedTbRuleEngine() { TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); savedTenantProfile.setIsolatedTbRuleEngine(true); addMainQueueConfig(savedTenantProfile); - tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile); + }); } - @Test(expected = DataValidationException.class) + @Test public void testDeleteTenantProfileWithExistingTenant() { TenantProfile tenantProfile = this.createTenantProfile("Tenant Profile"); TenantProfile savedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); @@ -186,7 +193,9 @@ public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest { tenant.setTenantProfileId(savedTenantProfile.getId()); tenant = tenantService.saveTenant(tenant); try { - tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantProfileService.deleteTenantProfile(TenantId.SYS_TENANT_ID, savedTenantProfile.getId()); + }); } finally { tenantService.deleteTenant(tenant.getId()); } @@ -294,7 +303,7 @@ public abstract class BaseTenantProfileServiceTest extends AbstractServiceTest { return tenantProfile; } - private void addMainQueueConfig(TenantProfile tenantProfile) { + public static void addMainQueueConfig(TenantProfile tenantProfile) { TenantProfileQueueConfiguration mainQueueConfiguration = new TenantProfileQueueConfiguration(); mainQueueConfiguration.setName(DataConstants.MAIN_QUEUE_NAME); mainQueueConfiguration.setTopic(DataConstants.MAIN_QUEUE_TOPIC); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java index 4e4fc44c56..3f8c33c5e7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseTenantServiceTest.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.service; import org.junit.Assert; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.SpyBean; @@ -52,8 +53,6 @@ import org.thingsboard.server.common.data.rpc.RpcStatus; import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.common.data.rule.RuleChainType; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration; -import org.thingsboard.server.common.data.tenant.profile.TenantProfileData; import org.thingsboard.server.common.data.widget.WidgetsBundle; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.tenant.TenantDao; @@ -121,18 +120,22 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { tenantService.deleteTenant(savedTenant.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTenantWithEmptyTitle() { Tenant tenant = new Tenant(); - tenantService.saveTenant(tenant); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantService.saveTenant(tenant); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveTenantWithInvalidEmail() { Tenant tenant = new Tenant(); tenant.setTitle("My tenant"); tenant.setEmail("invalid@mail"); - tenantService.saveTenant(tenant); + Assertions.assertThrows(DataValidationException.class, () -> { + tenantService.saveTenant(tenant); + }); } @Test @@ -301,23 +304,6 @@ public abstract class BaseTenantServiceTest extends AbstractServiceTest { } - @Test(expected = DataValidationException.class) - public void testSaveTenantWithIsolatedProfileInMonolithSetup() { - TenantProfile tenantProfile = new TenantProfile(); - tenantProfile.setName("Isolated Tenant Profile"); - TenantProfileData profileData = new TenantProfileData(); - profileData.setConfiguration(new DefaultTenantProfileConfiguration()); - tenantProfile.setProfileData(profileData); - tenantProfile.setDefault(false); - tenantProfile.setIsolatedTbRuleEngine(true); - TenantProfile isolatedTenantProfile = tenantProfileService.saveTenantProfile(TenantId.SYS_TENANT_ID, tenantProfile); - - Tenant tenant = new Tenant(); - tenant.setTitle("Tenant"); - tenant.setTenantProfileId(isolatedTenantProfile.getId()); - tenantService.saveTenant(tenant); - } - @Test public void testGettingTenantAddingItToCache() { Tenant tenant = new Tenant(); diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java index d7885a1cb2..49ececc8e4 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseUserServiceTest.java @@ -19,6 +19,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.StringUtils; @@ -142,32 +143,40 @@ public abstract class BaseUserServiceTest extends AbstractServiceTest { userService.deleteUser(tenantId, savedUser.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveUserWithSameEmail() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail("sysadmin@thingsboard.org"); - userService.saveUser(tenantAdminUser); + Assertions.assertThrows(DataValidationException.class, () -> { + userService.saveUser(tenantAdminUser); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveUserWithInvalidEmail() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail("tenant_thingsboard.org"); - userService.saveUser(tenantAdminUser); + Assertions.assertThrows(DataValidationException.class, () -> { + userService.saveUser(tenantAdminUser); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveUserWithEmptyEmail() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setEmail(null); - userService.saveUser(tenantAdminUser); + Assertions.assertThrows(DataValidationException.class, () -> { + userService.saveUser(tenantAdminUser); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveUserWithoutTenant() { User tenantAdminUser = userService.findUserByEmail(tenantId, "tenant@thingsboard.org"); tenantAdminUser.setTenantId(null); - userService.saveUser(tenantAdminUser); + Assertions.assertThrows(DataValidationException.class, () -> { + userService.saveUser(tenantAdminUser); + }); } @Test diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java index 404e6fe2e1..04cc1d8412 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetTypeServiceTest.java @@ -22,6 +22,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.widget.WidgetType; @@ -88,7 +89,7 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithEmptyName() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -100,22 +101,26 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); try { - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithEmptyBundleAlias() throws IOException { WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithEmptyDescriptor() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -128,13 +133,15 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetType.setBundleAlias(savedWidgetsBundle.getAlias()); widgetType.setDescriptor(new ObjectMapper().readValue("{}", JsonNode.class)); try { - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithInvalidTenant() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -147,23 +154,27 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); try { - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetTypeWithInvalidBundleAlias() throws IOException { WidgetTypeDetails widgetType = new WidgetTypeDetails(); widgetType.setTenantId(tenantId); widgetType.setBundleAlias("some_alias"); widgetType.setName("Widget Type"); widgetType.setDescriptor(new ObjectMapper().readValue("{ \"someKey\": \"someValue\" }", JsonNode.class)); - widgetTypeService.saveWidgetType(widgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(widgetType); + }); } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetTypeTenant() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -178,13 +189,15 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); savedWidgetType.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); try { - widgetTypeService.saveWidgetType(savedWidgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(savedWidgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetTypeBundleAlias() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -199,13 +212,15 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); savedWidgetType.setBundleAlias("some_alias"); try { - widgetTypeService.saveWidgetType(savedWidgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(savedWidgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetTypeAlias() throws IOException { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); @@ -220,7 +235,9 @@ public abstract class BaseWidgetTypeServiceTest extends AbstractServiceTest { WidgetTypeDetails savedWidgetType = widgetTypeService.saveWidgetType(widgetType); savedWidgetType.setAlias("some_alias"); try { - widgetTypeService.saveWidgetType(savedWidgetType); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetTypeService.saveWidgetType(savedWidgetType); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java index ac86572cdb..c28f5face7 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseWidgetsBundleServiceTest.java @@ -20,6 +20,7 @@ import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.page.PageData; @@ -77,22 +78,26 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetsBundleWithEmptyTitle() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTenantId(tenantId); - widgetsBundleService.saveWidgetsBundle(widgetsBundle); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetsBundleService.saveWidgetsBundle(widgetsBundle); + }); } - @Test(expected = DataValidationException.class) + @Test public void testSaveWidgetsBundleWithInvalidTenant() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("My widgets bundle"); widgetsBundle.setTenantId(TenantId.fromUUID(Uuids.timeBased())); - widgetsBundleService.saveWidgetsBundle(widgetsBundle); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetsBundleService.saveWidgetsBundle(widgetsBundle); + }); } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetsBundleTenant() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("My widgets bundle"); @@ -100,13 +105,15 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); savedWidgetsBundle.setTenantId(TenantId.fromUUID(ModelConstants.NULL_UUID)); try { - widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } } - @Test(expected = DataValidationException.class) + @Test public void testUpdateWidgetsBundleAlias() { WidgetsBundle widgetsBundle = new WidgetsBundle(); widgetsBundle.setTitle("My widgets bundle"); @@ -114,7 +121,9 @@ public abstract class BaseWidgetsBundleServiceTest extends AbstractServiceTest { WidgetsBundle savedWidgetsBundle = widgetsBundleService.saveWidgetsBundle(widgetsBundle); savedWidgetsBundle.setAlias("new_alias"); try { - widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); + Assertions.assertThrows(DataValidationException.class, () -> { + widgetsBundleService.saveWidgetsBundle(savedWidgetsBundle); + }); } finally { widgetsBundleService.deleteWidgetsBundle(tenantId, savedWidgetsBundle.getId()); } diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java index 6e195b266c..40262ac2b8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/DataValidatorTest.java @@ -15,7 +15,8 @@ */ package org.thingsboard.server.dao.service; -import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import org.thingsboard.server.dao.exception.DataValidationException; public class DataValidatorTest { @@ -26,38 +27,50 @@ public class DataValidatorTest { DataValidator.validateEmail(email); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail1() { String email = "test:1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail2() { String email = "test()1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail3() { String email = "test[]1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail4() { String email = "test\\1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail5() { String email = "test\"1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } - @Test(expected = DataValidationException.class) + @Test public void validateInvalidEmail6() { String email = "test<>1@mail.io"; - DataValidator.validateEmail(email); + Assertions.assertThrows(DataValidationException.class, () -> { + DataValidator.validateEmail(email); + }); } -} \ No newline at end of file +} diff --git a/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java index fa1caca815..f2618da6b8 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/NoXssValidatorTest.java @@ -16,7 +16,7 @@ package org.thingsboard.server.dao.service; import com.fasterxml.jackson.databind.node.TextNode; -import org.junit.Test; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.thingsboard.common.util.JacksonUtil; diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java index ef1d8fe737..ba3fe61f8a 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmCommentDaoTest.java @@ -76,7 +76,6 @@ public class JpaAlarmCommentDaoTest extends AbstractJpaDaoTest { alarm.setPropagate(true); alarm.setStartTs(System.currentTimeMillis()); alarm.setEndTs(System.currentTimeMillis()); - alarm.setStatus(AlarmStatus.ACTIVE_UNACK); alarmDao.save(TenantId.fromUUID(tenantId), alarm); } private void saveAlarmComment(UUID id, UUID alarmId, UUID userId, AlarmCommentType type) { diff --git a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java index 45af699f67..5fbc2fd627 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/sql/alarm/JpaAlarmDaoTest.java @@ -19,21 +19,31 @@ import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmSeverity; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.AlarmId; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.dao.AbstractJpaDaoTest; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmDao; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; /** * Created by Valerii Sosliuk on 5/21/2017. @@ -48,28 +58,235 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { @Test public void testFindLatestByOriginatorAndType() throws ExecutionException, InterruptedException, TimeoutException { log.info("Current system time in millis = {}", System.currentTimeMillis()); - UUID tenantId = UUID.fromString("d4b68f40-3e96-11e7-a884-898080180d6b"); + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); UUID originator2Id = UUID.fromString("d4b68f42-3e96-11e7-a884-898080180d6b"); UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); UUID alarm2Id = UUID.fromString("d4b68f44-3e96-11e7-a884-898080180d6b"); UUID alarm3Id = UUID.fromString("d4b68f45-3e96-11e7-a884-898080180d6b"); - int alarmCountBeforeSave = alarmDao.find(TenantId.fromUUID(tenantId)).size(); - saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + // The find method does not filter by tenant. It is just using the tenantId for rate limits if any. + var alarmsBeforeSave = alarmDao.find(tenantId).stream().filter(a -> a.getTenantId().equals(tenantId)).collect(Collectors.toList()); + int alarmCountBeforeSave = alarmsBeforeSave.size(); + saveAlarm(alarm1Id, tenantId.getId(), originator1Id, "TEST_ALARM"); //The timestamp of the startTime should be different in order for test to always work Thread.sleep(1); - saveAlarm(alarm2Id, tenantId, originator1Id, "TEST_ALARM"); - saveAlarm(alarm3Id, tenantId, originator2Id, "TEST_ALARM"); - int alarmCountAfterSave = alarmDao.find(TenantId.fromUUID(tenantId)).size(); - assertEquals(3, alarmCountAfterSave - alarmCountBeforeSave); + saveAlarm(alarm2Id, tenantId.getId(), originator1Id, "TEST_ALARM"); + saveAlarm(alarm3Id, tenantId.getId(), originator2Id, "TEST_ALARM"); + var alarmsAfterSave = alarmDao.find(tenantId).stream().filter(a -> a.getTenantId().equals(tenantId)).collect(Collectors.toList()); + int alarmCountAfterSave = alarmsAfterSave.size(); + int diff = alarmCountAfterSave - alarmCountBeforeSave; + if (diff != 3) { + System.out.println("test"); + } + assertEquals(3, diff); ListenableFuture future = alarmDao - .findLatestByOriginatorAndTypeAsync(TenantId.fromUUID(tenantId), new DeviceId(originator1Id), "TEST_ALARM"); + .findLatestByOriginatorAndTypeAsync(tenantId, new DeviceId(originator1Id), "TEST_ALARM"); Alarm alarm = future.get(30, TimeUnit.SECONDS); assertNotNull(alarm); assertEquals(alarm2Id, alarm.getId().getId()); } - private void saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) { + @Test + public void createOrUpdateActiveAlarm() { + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + AlarmCreateOrUpdateActiveRequest request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.MAJOR) + .build(); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + UUID newAlarmId = result.getAlarm().getUuidId(); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(tenantId, newAlarmId); + assertEquals(afterSave, result.getAlarm()); + + request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertFalse(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(newAlarmId, result.getAlarm().getUuidId()); + afterSave = alarmDao.findAlarmInfoById(tenantId, newAlarmId); + assertEquals(afterSave, result.getAlarm()); + + alarmDao.clearAlarm(tenantId, result.getAlarm().getId(), System.currentTimeMillis(), result.getAlarm().getDetails()); + + request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertNotEquals(newAlarmId, result.getAlarm().getUuidId()); + afterSave = alarmDao.findAlarmInfoById(tenantId, result.getAlarm().getUuidId()); + assertEquals(afterSave, result.getAlarm()); + + alarmDao.clearAlarm(tenantId, result.getAlarm().getId(), System.currentTimeMillis(), result.getAlarm().getDetails()); + + request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE2") + .severity(AlarmSeverity.CRITICAL) + .build(); + result = alarmDao.createOrUpdateActiveAlarm(request, true); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCreated()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertNotEquals(newAlarmId, result.getAlarm().getUuidId()); + } + + @Test + public void testCantCreateAlarmIfCreateIsDisabled() { + TenantId tenantId = TenantId.fromUUID(UUID.randomUUID()); + DeviceId deviceId = new DeviceId(UUID.randomUUID()); + + AlarmCreateOrUpdateActiveRequest request = AlarmCreateOrUpdateActiveRequest.builder() + .tenantId(tenantId) + .originator(deviceId) + .type("ALARM_TYPE") + .severity(AlarmSeverity.MAJOR) + .build(); + AlarmApiCallResult result = alarmDao.createOrUpdateActiveAlarm(request, false); + assertFalse(result.isSuccessful()); + } + + @Test + public void testAckAlarmProcedure() { + UUID tenantId = UUID.randomUUID(); + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + long ackTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), ackTs); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(ackTs, result.getAlarm().getAckTs()); + assertTrue(result.getAlarm().isAcknowledged()); + result = alarmDao.acknowledgeAlarm(alarm.getTenantId(), alarm.getId(), ackTs + 1); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertEquals(ackTs, result.getAlarm().getAckTs()); + assertTrue(result.getAlarm().isAcknowledged()); + } + + @Test + public void testClearAlarmProcedure() { + UUID tenantId = UUID.randomUUID(); + ; + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarm1Id = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + Alarm alarm = saveAlarm(alarm1Id, tenantId, originator1Id, "TEST_ALARM"); + long clearTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs, null); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isCleared()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(clearTs, result.getAlarm().getClearTs()); + assertTrue(result.getAlarm().isCleared()); + result = alarmDao.clearAlarm(alarm.getTenantId(), alarm.getId(), clearTs + 1, JacksonUtil.newObjectNode()); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isCleared()); + assertEquals(clearTs, result.getAlarm().getClearTs()); + assertTrue(result.getAlarm().isCleared()); + } + + @Test + public void testAssignAlarmProcedure() { + UUID tenantId = UUID.randomUUID(); + ; + UUID originator1Id = UUID.fromString("d4b68f41-3e96-11e7-a884-898080180d6b"); + UUID alarmId = UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d6b"); + UserId userId1 = new UserId(UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d7b")); + UserId userId2 = new UserId(UUID.fromString("d4b68f43-3e96-11e7-a884-898080180d8b")); + Alarm alarm = saveAlarm(alarmId, tenantId, originator1Id, "TEST_ALARM"); + long assignTs = System.currentTimeMillis(); + AlarmApiCallResult result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId1, assignTs); + AlarmInfo afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(assignTs, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId1, result.getAlarm().getAssigneeId()); + result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId1, assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertEquals(assignTs, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId1, result.getAlarm().getAssigneeId()); + result = alarmDao.assignAlarm(alarm.getTenantId(), alarm.getId(), userId2, assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertEquals(assignTs + 1, result.getAlarm().getAssignTs()); + assertNotNull(result.getAlarm().getAssigneeId()); + assertEquals(userId2, result.getAlarm().getAssigneeId()); + + result = alarmDao.unassignAlarm(alarm.getTenantId(), alarm.getId(), assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertTrue(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertNull(result.getAlarm().getAssigneeId()); + + result = alarmDao.unassignAlarm(alarm.getTenantId(), alarm.getId(), assignTs + 1); + afterSave = alarmDao.findAlarmInfoById(alarm.getTenantId(), alarm.getUuidId()); + assertNotNull(result); + assertTrue(result.isSuccessful()); + assertFalse(result.isModified()); + assertNotNull(result.getAlarm()); + assertEquals(afterSave, result.getAlarm()); + assertNull(result.getAlarm().getAssigneeId()); + } + + private Alarm saveAlarm(UUID id, UUID tenantId, UUID deviceId, String type) { Alarm alarm = new Alarm(); alarm.setId(new AlarmId(id)); alarm.setTenantId(TenantId.fromUUID(tenantId)); @@ -78,7 +295,10 @@ public class JpaAlarmDaoTest extends AbstractJpaDaoTest { alarm.setPropagate(true); alarm.setStartTs(System.currentTimeMillis()); alarm.setEndTs(System.currentTimeMillis()); - alarm.setStatus(AlarmStatus.ACTIVE_UNACK); - alarmDao.save(TenantId.fromUUID(tenantId), alarm); + alarm.setAcknowledged(false); + alarm.setCleared(false); + alarm.setDetails(JacksonUtil.newObjectNode().put("a", UUID.randomUUID().toString()).set("b", JacksonUtil.newObjectNode().put("a", "[}/.`1321421!@@$$(%&&$"))); + return alarmDao.save(TenantId.fromUUID(tenantId), alarm); } + } diff --git a/dao/src/test/resources/application-test.properties b/dao/src/test/resources/application-test.properties index 15131011d8..a2a21abc11 100644 --- a/dao/src/test/resources/application-test.properties +++ b/dao/src/test/resources/application-test.properties @@ -7,10 +7,9 @@ updates.enabled=false audit-log.enabled=true audit-log.sink.type=none -cache.type=caffeine +#cache.type=caffeine # will be injected redis by RedisContainer or will be default (caffeine) cache.maximumPoolSize=16 cache.attributes.enabled=true -#cache.type=redis cache.specs.relations.timeToLiveInMinutes=1440 cache.specs.relations.maxSize=100000 diff --git a/dao/src/test/resources/cassandra-test.properties b/dao/src/test/resources/cassandra-test.properties index 5765153a6f..5876582ee6 100644 --- a/dao/src/test/resources/cassandra-test.properties +++ b/dao/src/test/resources/cassandra-test.properties @@ -2,7 +2,7 @@ cassandra.cluster_name=Thingsboard Cluster cassandra.keyspace_name=thingsboard -cassandra.url=127.0.0.1:9142 +#cassandra.url=127.0.0.1:9142 # will be injected by NoSqlContainer cassandra.local_datacenter=datacenter1 diff --git a/dao/src/test/resources/logback.xml b/dao/src/test/resources/logback.xml index f74ba574b5..61397ec6f1 100644 --- a/dao/src/test/resources/logback.xml +++ b/dao/src/test/resources/logback.xml @@ -8,9 +8,7 @@ - - - + diff --git a/msa/black-box-tests/README.md b/msa/black-box-tests/README.md index ae0e8b79a0..5b63ba712a 100644 --- a/msa/black-box-tests/README.md +++ b/msa/black-box-tests/README.md @@ -18,7 +18,7 @@ As result, in REPOSITORY column, next images should be present: thingsboard/tb-web-ui thingsboard/tb-js-executor -- Run the black box tests in the [msa/black-box-tests](../black-box-tests) directory with Redis standalone: +- Run the black box tests (without ui tests) in the [msa/black-box-tests](../black-box-tests) directory with Redis standalone: mvn clean install -DblackBoxTests.skip=false @@ -34,11 +34,23 @@ As result, in REPOSITORY column, next images should be present: mvn clean install -DblackBoxTests.skip=false -DrunLocal=true -- To run ui smoke tests in the [msa/black-box-tests](../black-box-tests) directory specifying suite name: +- To run only ui tests in the [msa/black-box-tests](../black-box-tests) directory: mvn clean install -DblackBoxTests.skip=false -Dsuite=uiTests -- To run all tests in the [msa/black-box-tests](../black-box-tests) directory specifying suite name: +- To run only ui smoke rule chains tests in the [msa/black-box-tests](../black-box-tests) directory: + + mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesRuleChain + +- To run only ui smoke customers tests in the [msa/black-box-tests](../black-box-tests) directory: + + mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesCustomer + +- To run only ui smoke profiles tests in the [msa/black-box-tests](../black-box-tests) directory: + + mvn clean install -DblackBoxTests.skip=false -Dsuite=smokesPrifiles + +- To run all tests (black-box and ui) in the [msa/black-box-tests](../black-box-tests) directory: mvn clean install -DblackBoxTests.skip=false -Dsuite=all diff --git a/msa/black-box-tests/pom.xml b/msa/black-box-tests/pom.xml index cd0dd07543..ae0b12ead7 100644 --- a/msa/black-box-tests/pom.xml +++ b/msa/black-box-tests/pom.xml @@ -212,8 +212,7 @@ ${allure-maven.version} ${allure-testng.version} - https://repo.maven.apache.org/maven2/io/qameta/allure/allure-commandline/ - ${allure-testng.version}/allure-commandline-${allure-testng.version}.zip + src/test/resources/allure.properties diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java index ea145a97bc..bbcf6f32a3 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/TestListener.java @@ -19,11 +19,6 @@ import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.WebDriver; import org.testng.ITestListener; import org.testng.ITestResult; -import org.testng.internal.ConstructorOrMethod; -import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; - -import static org.testng.internal.Utils.log; -import static org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest.captureScreen; @Slf4j public class TestListener implements ITestListener { @@ -41,13 +36,7 @@ public class TestListener implements ITestListener { @Override public void onTestSuccess(ITestResult result) { log.info("<<<=== Test completed successfully: " + result.getName()); - ConstructorOrMethod consOrMethod = result.getMethod().getConstructorOrMethod(); - DisableUIListeners disable = consOrMethod.getMethod().getDeclaringClass().getAnnotation(DisableUIListeners.class); - if (disable != null) { - return; - } - driver = ((AbstractDriverBaseTest) result.getInstance()).getDriver(); - captureScreen(driver, "success"); + } /** @@ -56,13 +45,6 @@ public class TestListener implements ITestListener { @Override public void onTestFailure(ITestResult result) { log.info("<<<=== Test failed: " + result.getName()); - ConstructorOrMethod consOrMethod = result.getMethod().getConstructorOrMethod(); - DisableUIListeners disable = consOrMethod.getMethod().getDeclaringClass().getAnnotation(DisableUIListeners.class); - if (disable != null) { - return; - } - driver = ((AbstractDriverBaseTest) result.getInstance()).getDriver(); - captureScreen(driver, "failure"); } /** @@ -71,12 +53,5 @@ public class TestListener implements ITestListener { @Override public void onTestSkipped(ITestResult result) { log.info("<<<=== Test skipped: " + result.getName()); - ConstructorOrMethod consOrMethod = result.getMethod().getConstructorOrMethod(); - DisableUIListeners disable = consOrMethod.getMethod().getDeclaringClass().getAnnotation(DisableUIListeners.class); - if (disable != null) { - return; - } - driver = ((AbstractDriverBaseTest) result.getInstance()).getDriver(); - captureScreen(driver, "skipped"); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java index af3b25b924..29574a1942 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/base/AbstractDriverBaseTest.java @@ -15,12 +15,10 @@ */ package org.thingsboard.server.msa.ui.base; -import com.google.common.io.Files; import io.github.bonigarcia.wdm.WebDriverManager; -import io.qameta.allure.Attachment; -import lombok.SneakyThrows; +import io.qameta.allure.Allure; import lombok.extern.slf4j.Slf4j; -import org.apache.commons.io.FileUtils; +import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.JavascriptExecutor; import org.openqa.selenium.OutputType; @@ -34,7 +32,9 @@ import org.openqa.selenium.remote.LocalFileDetector; import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.WebDriverWait; +import org.testng.annotations.AfterClass; import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.DeviceProfile; @@ -44,9 +44,11 @@ import org.thingsboard.server.common.data.rule.RuleChain; import org.thingsboard.server.msa.AbstractContainerTest; import org.thingsboard.server.msa.ContainerTestSuite; -import java.io.File; +import java.io.ByteArrayInputStream; +import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import static org.thingsboard.server.msa.TestProperties.getBaseUiUrl; @@ -65,9 +67,8 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { private static final ContainerTestSuite instance = ContainerTestSuite.getInstance(); private JavascriptExecutor js; - @SneakyThrows - @BeforeMethod - public void openBrowser() { + @BeforeClass + public void startUp() throws MalformedURLException { log.info("===>>> Setup driver"); testRestClient.login(TENANT_EMAIL, TENANT_PASSWORD); ChromeOptions options = new ChromeOptions(); @@ -84,8 +85,18 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { openLocalhost(); } + @BeforeMethod + public void open() { + openHomePage(); + } + @AfterMethod - public void closeBrowser() { + public void addScreenshotToReport() { + captureScreen(driver, "After test page screenshot"); + } + + @AfterClass + public void teardown() { log.info("<<<=== Teardown"); driver.quit(); } @@ -94,6 +105,10 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { driver.get(getBaseUiUrl()); } + public void openHomePage() { + driver.get(getBaseUiUrl() + "/home"); + } + public String getUrl() { return driver.getCurrentUrl(); } @@ -157,11 +172,10 @@ abstract public class AbstractDriverBaseTest extends AbstractContainerTest { } } - @SneakyThrows - @Attachment(value = "Page screenshot", type = "image/png") - public static byte[] captureScreen(WebDriver driver, String dirPath) { - File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE); - FileUtils.copyFile(screenshot, new File("./target/allure-results/screenshots/" + dirPath + "//" + screenshot.getName())); - return Files.toByteArray(screenshot); + public void captureScreen(WebDriver driver, String screenshotName) { + if (driver instanceof TakesScreenshot) { + Allure.addAttachment(screenshotName, + new ByteArrayInputStream(((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES))); + } } -} \ No newline at end of file +} diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/LoginPageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/LoginPageElements.java index 6aeb20c87d..d87fd3c5bf 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/LoginPageElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/LoginPageElements.java @@ -27,6 +27,7 @@ public class LoginPageElements extends AbstractBasePage { private static final String EMAIL_FIELD = "//input[@id='username-input']"; private static final String PASSWORD_FIELD = "//input[@id='password-input']"; private static final String SUBMIT_BTN = "//button[@type='submit']"; + private static final String TITLE_LOGO = "//img[@class='tb-logo-title']"; public WebElement emailField() { return waitUntilElementToBeClickable(EMAIL_FIELD); @@ -40,4 +41,8 @@ public class LoginPageElements extends AbstractBasePage { return waitUntilElementToBeClickable(SUBMIT_BTN); } + public WebElement titleLogo() { + return waitUntilVisibilityOfElementLocated(TITLE_LOGO); + } + } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/LoginPageHelper.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/LoginPageHelper.java index a746a78416..38a4a50207 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/LoginPageHelper.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/LoginPageHelper.java @@ -16,6 +16,8 @@ package org.thingsboard.server.msa.ui.pages; import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.ExpectedConditions; import org.thingsboard.server.msa.ui.utils.Const; public class LoginPageHelper extends LoginPageElements { @@ -27,5 +29,6 @@ public class LoginPageHelper extends LoginPageElements { emailField().sendKeys(Const.TENANT_EMAIL); passwordField().sendKeys(Const.TENANT_PASSWORD); submitBtn().click(); + waitUntilUrlContainsText("/home"); } } diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java index 3b18df801f..99e737dba1 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/pages/OtherPageElements.java @@ -42,7 +42,7 @@ public class OtherPageElements extends AbstractBasePage { private static final String MARKS_CHECKBOX = "//mat-row[contains (@class,'mat-selected')]//mat-checkbox[contains(@class, 'checked')]"; private static final String SELECT_ALL_CHECKBOX = "//thead//mat-checkbox"; private static final String ALL_ENTITY = "//mat-row[@class='mat-row cdk-row mat-row-select ng-star-inserted']"; - private static final String EDIT_PENCIL_BTN = "//mat-icon[contains(text(),'edit')]/ancestor::button"; + private static final String EDIT_PENCIL_BTN = "//tb-details-panel//mat-icon[contains(text(),'edit')]/ancestor::button"; private static final String NAME_FIELD_EDIT_VIEW = "//input[@formcontrolname='name']"; private static final String HEADER_NAME_VIEW = "//header//div[@class='tb-details-title']/span"; private static final String DONE_BTN_EDIT_VIEW = "//mat-icon[contains(text(),'done')]/ancestor::button"; diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/AssetProfileEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/AssetProfileEditMenuTest.java index 5be2c2d7a6..0d02bd9434 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/AssetProfileEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/AssetProfileEditMenuTest.java @@ -19,7 +19,7 @@ import io.qameta.allure.Description; import org.openqa.selenium.Keys; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -39,7 +39,7 @@ public class AssetProfileEditMenuTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileImportTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileImportTest.java index 3e8c3ddeb7..275eb866ce 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileImportTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileImportTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.assetProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -40,7 +40,7 @@ public class CreateAssetProfileImportTest extends AbstractDriverBaseTest { private final String absolutePathToFileImportTxt = getClass().getClassLoader().getResource(IMPORT_TXT_FILE_NAME).getPath(); private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileTest.java index 5f06cd1f8d..cb7d70f33f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/CreateAssetProfileTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.assetProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -37,7 +37,7 @@ public class CreateAssetProfileTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteAssetProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteAssetProfileTest.java index 2e9c7503a7..1525849df7 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteAssetProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteAssetProfileTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.assetProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -33,7 +33,7 @@ public class DeleteAssetProfileTest extends AbstractDriverBaseTest { private SideBarMenuViewHelper sideBarMenuView; private ProfilesPageHelper profilesPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteSeveralAssetProfilesTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteSeveralAssetProfilesTest.java index e57d446d3d..40436c6ea0 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteSeveralAssetProfilesTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/DeleteSeveralAssetProfilesTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.assetProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -32,7 +32,7 @@ public class DeleteSeveralAssetProfilesTest extends AbstractDriverBaseTest { private SideBarMenuViewHelper sideBarMenuView; private ProfilesPageHelper profilesPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/MakeAssetProfileDefaultTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/MakeAssetProfileDefaultTest.java index f7d6052a68..761c390338 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/MakeAssetProfileDefaultTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/MakeAssetProfileDefaultTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.assetProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -34,7 +34,7 @@ public class MakeAssetProfileDefaultTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/SearchAssetProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/SearchAssetProfileTest.java index 902e20c104..16de1924e7 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/SearchAssetProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/SearchAssetProfileTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.assetProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -33,7 +33,7 @@ public class SearchAssetProfileTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/SortByNameTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/SortByNameTest.java index c8617f2c2f..6104e80e80 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/SortByNameTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/assetProfileSmoke/SortByNameTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.assetProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -33,7 +33,7 @@ public class SortByNameTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java index 4efb7ed861..b0659ac328 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CreateCustomerTest.java @@ -19,7 +19,7 @@ import io.qameta.allure.Description; import org.openqa.selenium.Keys; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -37,7 +37,7 @@ public class CreateCustomerTest extends AbstractDriverBaseTest { private CustomerPageHelper customerPage; private String customerName; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java index c67ed9d2cd..c47ae92468 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/CustomerEditMenuTest.java @@ -18,6 +18,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; @@ -38,16 +39,18 @@ import static org.thingsboard.server.msa.ui.utils.EntityPrototypes.defaultCustom public class CustomerEditMenuTest extends AbstractDriverBaseTest { private SideBarMenuViewElements sideBarMenuView; + private LoginPageHelper loginPage; private CustomerPageHelper customerPage; private DashboardPageHelper dashboardPage; private String customerName; - @BeforeMethod + @BeforeClass public void login() { - new LoginPageHelper(driver).authorizationTenant(); + loginPage = new LoginPageHelper(driver); sideBarMenuView = new SideBarMenuViewElements(driver); customerPage = new CustomerPageHelper(driver); dashboardPage = new DashboardPageHelper(driver); + loginPage.authorizationTenant(); } @AfterMethod @@ -58,6 +61,13 @@ public class CustomerEditMenuTest extends AbstractDriverBaseTest { } } + @BeforeMethod + public void reLogin() { + if (getUrl().contains("/login")) { + loginPage.authorizationTenant(); + } + } + @Test(priority = 10, groups = "smoke") @Description public void changeTitle() { diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteCustomerTest.java index 4372799f33..3f535ce113 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteCustomerTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -35,7 +35,7 @@ public class DeleteCustomerTest extends AbstractDriverBaseTest { private CustomerPageHelper customerPage; private RuleChainsPageHelper ruleChainsPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteSeveralCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteSeveralCustomerTest.java index a32a9304b2..635505b64a 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteSeveralCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/DeleteSeveralCustomerTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -33,7 +33,7 @@ public class DeleteSeveralCustomerTest extends AbstractDriverBaseTest { private SideBarMenuViewElements sideBarMenuView; private CustomerPageHelper customerPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersAssetsTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersAssetsTest.java index 90528f0d89..405a028a15 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersAssetsTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersAssetsTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -30,7 +30,7 @@ public class ManageCustomersAssetsTest extends AbstractDriverBaseTest { private CustomerPageHelper customerPage; private final String manage = "Assets"; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersDashboardsTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersDashboardsTest.java index 27b5ecb1aa..f1f27c96cf 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersDashboardsTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersDashboardsTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -29,7 +29,7 @@ public class ManageCustomersDashboardsTest extends AbstractDriverBaseTest { private CustomerPageHelper customerPage; private final String manage = "Dashboards"; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersDevicesTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersDevicesTest.java index f6f8ef15e4..fde5b87e52 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersDevicesTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersDevicesTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -29,7 +29,7 @@ public class ManageCustomersDevicesTest extends AbstractDriverBaseTest { private CustomerPageHelper customerPage; private final String manage = "Devices"; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersEdgesTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersEdgesTest.java index d4b9ee769a..184f506584 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersEdgesTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersEdgesTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -30,7 +30,7 @@ public class ManageCustomersEdgesTest extends AbstractDriverBaseTest { private CustomerPageHelper customerPage; private final String iconText = "Edge instances"; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersUsersTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersUsersTest.java index faa7cde0fb..9b8a7b8eef 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersUsersTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/ManageCustomersUsersTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -30,7 +30,7 @@ public class ManageCustomersUsersTest extends AbstractDriverBaseTest { private CustomerPageHelper customerPage; private final String iconText = "Customer Users"; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/SearchCustomerTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/SearchCustomerTest.java index 8b5a5bf1d2..2ff2a6fef9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/SearchCustomerTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/SearchCustomerTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -32,7 +32,7 @@ public class SearchCustomerTest extends AbstractDriverBaseTest { private SideBarMenuViewElements sideBarMenuView; private CustomerPageHelper customerPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/SortByNameTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/SortByNameTest.java index a819a31d8c..296ab2c847 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/SortByNameTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/customerSmoke/SortByNameTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.customerSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.CustomerPageHelper; @@ -33,7 +33,7 @@ public class SortByNameTest extends AbstractDriverBaseTest { private CustomerPageHelper customerPage; private String customerName; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java index a75d24ea91..1c530d33ad 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileImportTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.deviceProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -40,7 +40,7 @@ public class CreateDeviceProfileImportTest extends AbstractDriverBaseTest { private final String absolutePathToFileImportTxt = getClass().getClassLoader().getResource(IMPORT_TXT_FILE_NAME).getPath(); private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java index 9fc7e3ae77..b983b7fe5f 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/CreateDeviceProfileTest.java @@ -19,7 +19,7 @@ import io.qameta.allure.Description; import org.openqa.selenium.Keys; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -39,7 +39,7 @@ public class CreateDeviceProfileTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteDeviceProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteDeviceProfileTest.java index 04d85545d5..3dd6c2b28d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteDeviceProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteDeviceProfileTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.deviceProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -33,7 +33,7 @@ public class DeleteDeviceProfileTest extends AbstractDriverBaseTest { private SideBarMenuViewHelper sideBarMenuView; private ProfilesPageHelper profilesPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteSeveralDeviceProfilesTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteSeveralDeviceProfilesTest.java index 4e6d633da4..8459474c38 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteSeveralDeviceProfilesTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeleteSeveralDeviceProfilesTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.deviceProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -32,7 +32,7 @@ public class DeleteSeveralDeviceProfilesTest extends AbstractDriverBaseTest { private SideBarMenuViewHelper sideBarMenuView; private ProfilesPageHelper profilesPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java index 1ebf5d1597..9d889f2e54 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/DeviceProfileEditMenuTest.java @@ -19,7 +19,7 @@ import io.qameta.allure.Description; import org.openqa.selenium.Keys; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -39,7 +39,7 @@ public class DeviceProfileEditMenuTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/MakeDeviceProfileDefaultTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/MakeDeviceProfileDefaultTest.java index b8c0c4ead9..9ef711eee4 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/MakeDeviceProfileDefaultTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/MakeDeviceProfileDefaultTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.deviceProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -29,7 +29,7 @@ public class MakeDeviceProfileDefaultTest extends AbstractDriverBaseTest { private SideBarMenuViewHelper sideBarMenuView; private ProfilesPageHelper profilesPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java index dabdaeec29..bb6b82b3a9 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SearchDeviceProfileTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.deviceProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -34,7 +34,7 @@ public class SearchDeviceProfileTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java index 937145d37f..a6c1a7b0cf 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/deviceProfileSmoke/SortByNameTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.deviceProfileSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -33,7 +33,7 @@ public class SortByNameTest extends AbstractDriverBaseTest { private ProfilesPageHelper profilesPage; private String name; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewHelper(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainImportTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainImportTest.java index d30c7799dd..14d3a0a6cd 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainImportTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainImportTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -42,7 +42,7 @@ public class CreateRuleChainImportTest extends AbstractDriverBaseTest { private final String absolutePathToFileImportTxt = getClass().getClassLoader().getResource(IMPORT_TXT_FILE_NAME).getPath(); private String ruleChainName; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainTest.java index d250031b7e..4e1ec60f10 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/CreateRuleChainTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -38,7 +38,7 @@ public class CreateRuleChainTest extends AbstractDriverBaseTest { private RuleChainsPageHelper ruleChainsPage; private String ruleChainName; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteRuleChainTest.java index 8a1be66505..078a807185 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteRuleChainTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteRuleChainTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -34,7 +34,7 @@ public class DeleteRuleChainTest extends AbstractDriverBaseTest { private SideBarMenuViewElements sideBarMenuView; private RuleChainsPageHelper ruleChainsPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteSeveralRuleChainsTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteSeveralRuleChainsTest.java index d631ed05d5..612efee64c 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteSeveralRuleChainsTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/DeleteSeveralRuleChainsTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -34,7 +34,7 @@ public class DeleteSeveralRuleChainsTest extends AbstractDriverBaseTest { private SideBarMenuViewElements sideBarMenuView; private RuleChainsPageHelper ruleChainsPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/MakeRuleChainRootTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/MakeRuleChainRootTest.java index 99e9ff5d11..7ad3fad982 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/MakeRuleChainRootTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/MakeRuleChainRootTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -30,7 +30,7 @@ public class MakeRuleChainRootTest extends AbstractDriverBaseTest { private SideBarMenuViewElements sideBarMenuView; private RuleChainsPageHelper ruleChainsPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/OpenRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/OpenRuleChainTest.java index 6cf760451a..05c59b0d7d 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/OpenRuleChainTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/OpenRuleChainTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -37,7 +37,7 @@ public class OpenRuleChainTest extends AbstractDriverBaseTest { private OpenRuleChainPageHelper openRuleChainPage; private String ruleChainName; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java index f649fd7167..144ca0e836 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/RuleChainEditMenuTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -39,7 +39,7 @@ public class RuleChainEditMenuTest extends AbstractDriverBaseTest { private RuleChainsPageHelper ruleChainsPage; private String ruleChainName; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SearchRuleChainTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SearchRuleChainTest.java index ef7aafed25..29c5b21556 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SearchRuleChainTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SearchRuleChainTest.java @@ -17,7 +17,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -32,7 +32,7 @@ public class SearchRuleChainTest extends AbstractDriverBaseTest { private SideBarMenuViewElements sideBarMenuView; private RuleChainsPageHelper ruleChainsPage; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByNameTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByNameTest.java index 55c72ec6ad..84cd1028e8 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByNameTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByNameTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -34,7 +34,7 @@ public class SortByNameTest extends AbstractDriverBaseTest { private RuleChainsPageHelper ruleChainsPage; private String ruleChainName; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByTimeTest.java b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByTimeTest.java index 80d290166c..4654654fb5 100644 --- a/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByTimeTest.java +++ b/msa/black-box-tests/src/test/java/org/thingsboard/server/msa/ui/tests/ruleChainsSmoke/SortByTimeTest.java @@ -18,7 +18,7 @@ package org.thingsboard.server.msa.ui.tests.ruleChainsSmoke; import io.qameta.allure.Description; import org.testng.Assert; import org.testng.annotations.AfterMethod; -import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.thingsboard.server.msa.ui.base.AbstractDriverBaseTest; import org.thingsboard.server.msa.ui.pages.LoginPageHelper; @@ -34,7 +34,7 @@ public class SortByTimeTest extends AbstractDriverBaseTest { private RuleChainsPageHelper ruleChainsPage; private String ruleChainName; - @BeforeMethod + @BeforeClass public void login() { new LoginPageHelper(driver).authorizationTenant(); sideBarMenuView = new SideBarMenuViewElements(driver); diff --git a/pom.xml b/pom.xml index 7d6b59f10b..49f30df835 100755 --- a/pom.xml +++ b/pom.xml @@ -126,7 +126,6 @@ 2.8.5 4.1.0 - 4.3.1.0 2.7.2 1.5.2 5.8.2 @@ -147,8 +146,8 @@ 1.0.0 4.6.0 5.2.0 - 2.19.0 - 2.8 + 2.21.0 + 2.12.0 @@ -659,7 +658,7 @@ ${surefire.version} - --illegal-access=permit -Xss384k -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=20 + --illegal-access=permit -XX:+UseStringDeduplication -XX:MaxGCPauseMillis=20 @@ -1603,26 +1602,6 @@
- - org.cassandraunit - cassandra-unit - ${cassandra-unit.version} - test - - - junit - junit - - - org.hamcrest - hamcrest-core - - - org.hamcrest - hamcrest-library - - - org.apache.cassandra cassandra-all @@ -1746,6 +1725,12 @@ bcpkix-jdk15on ${bouncycastle.version} + + org.testcontainers + cassandra + ${testcontainers.version} + test + org.testcontainers postgresql diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index 058d9c08fd..c2c0a6d794 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -415,6 +415,14 @@ public class RestClient implements ClientHttpRequestInterceptor, Closeable { restTemplate.postForLocation(baseURL + "/api/alarm/{alarmId}/clear", null, alarmId.getId()); } + public void assignAlarm(AlarmId alarmId, UserId userId) { + restTemplate.postForLocation(baseURL + "/api/alarm/{alarmId}/assign/{userId}", null, alarmId.getId(), userId.getId()); + } + + public void unassignAlarm(AlarmId alarmId) { + restTemplate.delete(baseURL + "/api/alarm/{alarmId}/assign", alarmId.getId()); + } + public PageData getAlarms(EntityId entityId, AlarmSearchStatus searchStatus, AlarmStatus status, TimePageLink pageLink, Boolean fetchOriginator) { String urlSecondPart = "/api/alarm/{entityType}/{entityId}?fetchOriginator={fetchOriginator}"; Map params = new HashMap<>(); diff --git a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java index 2620ecd069..e88411d73a 100644 --- a/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java +++ b/rule-engine/rule-engine-api/src/main/java/org/thingsboard/rule/engine/api/RuleEngineAlarmService.java @@ -16,6 +16,7 @@ package org.thingsboard.rule.engine.api; import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmInfo; @@ -23,13 +24,17 @@ import org.thingsboard.server.common.data.alarm.AlarmQuery; import org.thingsboard.server.common.data.alarm.AlarmSearchStatus; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.query.AlarmData; import org.thingsboard.server.common.data.query.AlarmDataQuery; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.alarm.AlarmOperationResult; import java.util.Collection; @@ -39,29 +44,65 @@ import java.util.Collection; */ public interface RuleEngineAlarmService { - Alarm createOrUpdateAlarm(Alarm alarm); + /* + * New API, since 3.5. + */ - Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId); + /** + * Designed for atomic operations over active alarms. + * Only one active alarm may exist for the pair {originatorId, alarmType} + */ + AlarmApiCallResult createAlarm(AlarmCreateOrUpdateActiveRequest request); + /** + * Designed to update existing alarm. Accepts only part of the alarm fields. + */ + AlarmApiCallResult updateAlarm(AlarmUpdateRequest request); + + AlarmApiCallResult acknowledgeAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + + AlarmApiCallResult clearAlarm(TenantId tenantId, AlarmId alarmId, long clearTs, JsonNode details); + + AlarmApiCallResult assignAlarm(TenantId tenantId, AlarmId alarmId, UserId assigneeId, long assignTs); + + AlarmApiCallResult unassignAlarm(TenantId tenantId, AlarmId alarmId, long assignTs); + /* + * Legacy API, before 3.5. + */ + @Deprecated(since = "3.5", forRemoval = true) + Alarm createOrUpdateAlarm(Alarm alarm); + + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture ackAlarm(TenantId tenantId, AlarmId alarmId, long ackTs); + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture clearAlarm(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + @Deprecated(since = "3.5", forRemoval = true) ListenableFuture clearAlarmForResult(TenantId tenantId, AlarmId alarmId, JsonNode details, long clearTs); + // Other API + Boolean deleteAlarm(TenantId tenantId, AlarmId alarmId); + ListenableFuture findAlarmByIdAsync(TenantId tenantId, AlarmId alarmId); Alarm findAlarmById(TenantId tenantId, AlarmId alarmId); + Alarm findLatestActiveByOriginatorAndType(TenantId tenantId, EntityId originator, String type); + ListenableFuture findLatestByOriginatorAndType(TenantId tenantId, EntityId originator, String type); - ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId); + AlarmInfo findAlarmInfoById(TenantId tenantId, AlarmId alarmId); + + default ListenableFuture findAlarmInfoByIdAsync(TenantId tenantId, AlarmId alarmId) { + return Futures.immediateFuture(findAlarmInfoById(tenantId, alarmId)); + } ListenableFuture> findAlarms(TenantId tenantId, AlarmQuery query); ListenableFuture> findCustomerAlarms(TenantId tenantId, CustomerId customerId, AlarmQuery query); - AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus); + AlarmSeverity findHighestAlarmSeverity(TenantId tenantId, EntityId entityId, AlarmSearchStatus alarmSearchStatus, AlarmStatus alarmStatus, String assigneeId); PageData findAlarmDataByQueryForEntities(TenantId tenantId, AlarmDataQuery query, Collection orderedEntityIds); } diff --git a/rule-engine/rule-engine-components/pom.xml b/rule-engine/rule-engine-components/pom.xml index 56621162f7..e85f0094b5 100644 --- a/rule-engine/rule-engine-components/pom.xml +++ b/rule-engine/rule-engine-components/pom.xml @@ -150,22 +150,6 @@ mockserver-client-java test - - - org.cassandraunit - cassandra-unit - - - org.slf4j - slf4j-log4j12 - - - org.hibernate - hibernate-validator - - - test - com.jayway.jsonpath json-path diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java index b5658a3451..3641d78b7c 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbAlarmResult.java @@ -17,7 +17,9 @@ package org.thingsboard.rule.engine.action; import lombok.AllArgsConstructor; import lombok.Data; +import org.jetbrains.annotations.NotNull; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; @Data @AllArgsConstructor @@ -34,4 +36,8 @@ public class TbAlarmResult { this.isCleared = isCleared; this.alarm = alarm; } + + public static TbAlarmResult fromAlarmResult(AlarmApiCallResult result) { + return new TbAlarmResult(result.isCreated(), result.isModified(), result.isSeverityChanged(), result.isCleared(), result.getAlarm()); + } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java index f94b4c36e2..ee32ac1533 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbClearAlarmNode.java @@ -26,10 +26,10 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.alarm.Alarm; -import org.thingsboard.server.common.data.alarm.AlarmStatus; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; @Slf4j @RuleNode( @@ -57,38 +57,29 @@ public class TbClearAlarmNode extends TbAbstractAlarmNode processAlarm(TbContext ctx, TbMsg msg) { String alarmType = TbNodeUtils.processPattern(this.config.getAlarmType(), msg); - ListenableFuture alarmFuture; + Alarm alarm; if (msg.getOriginator().getEntityType().equals(EntityType.ALARM)) { - alarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); + alarm = ctx.getAlarmService().findAlarmById(ctx.getTenantId(), new AlarmId(msg.getOriginator().getId())); } else { - alarmFuture = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); + alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); } - return Futures.transformAsync(alarmFuture, a -> { - if (a != null && !a.getStatus().isCleared()) { - return clearAlarm(ctx, msg, a); - } - return Futures.immediateFuture(new TbAlarmResult(false, false, false, null)); - }, ctx.getDbCallbackExecutor()); + if (alarm != null && !alarm.getStatus().isCleared()) { + return clearAlarm(ctx, msg, alarm); + } + return Futures.immediateFuture(new TbAlarmResult(false, false, false, null)); } private ListenableFuture clearAlarm(TbContext ctx, TbMsg msg, Alarm alarm) { ctx.logJsEvalRequest(); ListenableFuture asyncDetails = buildAlarmDetails(ctx, msg, alarm.getDetails()); - return Futures.transformAsync(asyncDetails, details -> { + return Futures.transform(asyncDetails, details -> { ctx.logJsEvalResponse(); - ListenableFuture clearFuture = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), details, System.currentTimeMillis()); - return Futures.transformAsync(clearFuture, cleared -> { - ListenableFuture savedAlarmFuture = ctx.getAlarmService().findAlarmByIdAsync(ctx.getTenantId(), alarm.getId()); - return Futures.transformAsync(savedAlarmFuture, savedAlarm -> { - if (cleared && savedAlarm != null) { - alarm.setDetails(savedAlarm.getDetails()); - alarm.setEndTs(savedAlarm.getEndTs()); - alarm.setClearTs(savedAlarm.getClearTs()); - } - alarm.setStatus(alarm.getStatus().isAck() ? AlarmStatus.CLEARED_ACK : AlarmStatus.CLEARED_UNACK); - return Futures.immediateFuture(new TbAlarmResult(false, false, true, alarm)); - }, ctx.getDbCallbackExecutor()); - }, ctx.getDbCallbackExecutor()); + AlarmApiCallResult result = ctx.getAlarmService().clearAlarm(ctx.getTenantId(), alarm.getId(), System.currentTimeMillis(), details); + if (result.isSuccessful()) { + return new TbAlarmResult(false, false, result.isCleared(), result.getAlarm()); + } else { + return new TbAlarmResult(false, false, false, alarm); + } }, ctx.getDbCallbackExecutor()); } } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java index 104acadda7..333926864d 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/action/TbCreateAlarmNode.java @@ -17,13 +17,11 @@ package org.thingsboard.rule.engine.action; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Function; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.EnumUtils; -import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.RuleNode; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -31,10 +29,12 @@ import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.rule.engine.api.util.TbNodeUtils; import org.thingsboard.server.common.data.alarm.Alarm; import org.thingsboard.server.common.data.alarm.AlarmSeverity; -import org.thingsboard.server.common.data.alarm.AlarmStatus; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.plugin.ComponentType; import org.thingsboard.server.common.msg.TbMsg; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import java.io.IOException; import java.util.List; @@ -57,16 +57,16 @@ import java.util.List; ) public class TbCreateAlarmNode extends TbAbstractAlarmNode { - private static ObjectMapper mapper = new ObjectMapper(); + private static final ObjectMapper mapper = new ObjectMapper(); private List relationTypes; private AlarmSeverity notDynamicAlarmSeverity; @Override public void init(TbContext ctx, TbNodeConfiguration configuration) throws TbNodeException { super.init(ctx, configuration); - if(!this.config.isDynamicSeverity()) { + if (!this.config.isDynamicSeverity()) { this.notDynamicAlarmSeverity = EnumUtils.getEnum(AlarmSeverity.class, this.config.getSeverity()); - if(this.notDynamicAlarmSeverity == null) { + if (this.notDynamicAlarmSeverity == null) { throw new TbNodeException("Incorrect Alarm Severity value: " + this.config.getSeverity()); } } @@ -98,15 +98,12 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode latest = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); - return Futures.transformAsync(latest, existingAlarm -> { - if (existingAlarm == null || existingAlarm.getStatus().isCleared()) { - return createNewAlarm(ctx, msg, msgAlarm); - } else { - return updateAlarm(ctx, msg, existingAlarm, msgAlarm); - } - }, ctx.getDbCallbackExecutor()); - + Alarm existingAlarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), msg.getOriginator(), alarmType); + if (existingAlarm == null || existingAlarm.getStatus().isCleared()) { + return createNewAlarm(ctx, msg, msgAlarm); + } else { + return updateAlarm(ctx, msg, existingAlarm, msgAlarm); + } } private Alarm getAlarmFromMessage(TbContext ctx, TbMsg msg) throws IOException { @@ -116,9 +113,6 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncAlarm = Futures.transform(asyncDetails, details -> { + ListenableFuture asyncAlarm = Futures.transform(asyncDetails, details -> { if (buildDetails) { ctx.logJsEvalResponse(); } @@ -146,9 +140,9 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncCreated = Futures.transform(asyncAlarm, - alarm -> ctx.getAlarmService().createOrUpdateAlarm(alarm), ctx.getDbCallbackExecutor()); - return Futures.transform(asyncCreated, alarm -> new TbAlarmResult(true, false, false, alarm), MoreExecutors.directExecutor()); + ListenableFuture asyncCreated = Futures.transform(asyncAlarm, + alarm -> ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(alarm)), ctx.getDbCallbackExecutor()); + return Futures.transform(asyncCreated, TbAlarmResult::fromAlarmResult, MoreExecutors.directExecutor()); } private ListenableFuture updateAlarm(TbContext ctx, TbMsg msg, Alarm existingAlarm, Alarm msgAlarm) { @@ -160,7 +154,7 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode asyncUpdated = Futures.transform(asyncDetails, (Function) details -> { + ListenableFuture asyncUpdated = Futures.transform(asyncDetails, details -> { if (buildDetails) { ctx.logJsEvalResponse(); } @@ -184,10 +178,9 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode new TbAlarmResult(false, true, false, a), MoreExecutors.directExecutor()); + return Futures.transform(asyncUpdated, TbAlarmResult::fromAlarmResult, MoreExecutors.directExecutor()); } private Alarm buildAlarm(TbMsg msg, JsonNode details, TenantId tenantId) { @@ -195,7 +188,8 @@ public class TbCreateAlarmNode extends TbAbstractAlarmNode generate(TbContext ctx, TbMsg msg) { log.trace("generate, config {}", config); if (prevMsg == null) { - prevMsg = ctx.newMsg(null, "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); + prevMsg = ctx.newMsg(config.getQueueName(), "", originatorId, msg.getCustomerId(), new TbMsgMetaData(), "{}"); } if (initialized.get()) { ctx.logJsEvalRequest(); return Futures.transformAsync(scriptEngine.executeGenerateAsync(prevMsg), generated -> { log.trace("generate process response, generated {}, config {}", generated, config); ctx.logJsEvalResponse(); - prevMsg = ctx.newMsg(null, generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); + prevMsg = ctx.newMsg(config.getQueueName(), generated.getType(), originatorId, msg.getCustomerId(), generated.getMetaData(), generated.getData()); return Futures.immediateFuture(prevMsg); }, MoreExecutors.directExecutor()); //usually it runs on js-executor-remote-callback thread pool } diff --git a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java index 027f4aa708..5f12603832 100644 --- a/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java +++ b/rule-engine/rule-engine-components/src/main/java/org/thingsboard/rule/engine/debug/TbMsgGeneratorNodeConfiguration.java @@ -36,6 +36,7 @@ public class TbMsgGeneratorNodeConfiguration implements NodeConfiguration alarmClearOperationResult = ctx.getAlarmService().clearAlarmForResult( - ctx.getTenantId(), currentAlarm.getId(), createDetails(clearState), System.currentTimeMillis() + AlarmApiCallResult result = ctx.getAlarmService().clearAlarm( + ctx.getTenantId(), currentAlarm.getId(), System.currentTimeMillis(), createDetails(clearState) ); - DonAsynchron.withCallback(alarmClearOperationResult, - result -> { - pushMsg(ctx, msg, new TbAlarmResult(false, false, true, result.getAlarm()), clearState); - }, - throwable -> { - throw new RuntimeException(throwable); - }); + if (result.isCleared()) { + pushMsg(ctx, msg, new TbAlarmResult(false, false, true, result.getAlarm()), clearState); + } currentAlarm = null; } else if (AlarmEvalResult.FALSE.equals(evalResult)) { stateUpdate = clearAlarmState(stateUpdate, clearState); @@ -165,9 +159,9 @@ class AlarmState { return true; } - public void initCurrentAlarm(TbContext ctx) throws InterruptedException, ExecutionException { + public void initCurrentAlarm(TbContext ctx) { if (!initialFetchDone) { - Alarm alarm = ctx.getAlarmService().findLatestByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()).get(); + Alarm alarm = ctx.getAlarmService().findLatestActiveByOriginatorAndType(ctx.getTenantId(), originator, alarmDefinition.getAlarmType()); if (alarm != null && !alarm.getStatus().isCleared()) { currentAlarm = alarm; } @@ -241,21 +235,18 @@ class AlarmState { // Skip update if severity is decreased. if (severity.ordinal() <= oldSeverity.ordinal()) { currentAlarm.setDetails(createDetails(ruleState)); - if (!oldSeverity.equals(severity)) { - currentAlarm.setSeverity(severity); - currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); - return new TbAlarmResult(false, false, true, false, currentAlarm); - } else { - currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); - return new TbAlarmResult(false, true, false, false, currentAlarm); - } + currentAlarm.setSeverity(severity); + AlarmApiCallResult result = ctx.getAlarmService().updateAlarm(AlarmUpdateRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); } else { return null; } } else { currentAlarm = new Alarm(); currentAlarm.setType(alarmDefinition.getAlarmType()); - currentAlarm.setStatus(AlarmStatus.ACTIVE_UNACK); + currentAlarm.setAcknowledged(false); + currentAlarm.setCleared(false); currentAlarm.setSeverity(severity); long startTs = dataSnapshot.getTs(); if (startTs == 0L) { @@ -272,9 +263,9 @@ class AlarmState { if (alarmDefinition.getPropagateRelationTypes() != null) { currentAlarm.setPropagateRelationTypes(alarmDefinition.getPropagateRelationTypes()); } - currentAlarm = ctx.getAlarmService().createOrUpdateAlarm(currentAlarm); - boolean updated = currentAlarm.getStartTs() != currentAlarm.getEndTs(); - return new TbAlarmResult(!updated, updated, false, false, currentAlarm); + AlarmApiCallResult result = ctx.getAlarmService().createAlarm(AlarmCreateOrUpdateActiveRequest.fromAlarm(currentAlarm)); + currentAlarm = result.getAlarm(); + return TbAlarmResult.fromAlarmResult(result); } } @@ -342,7 +333,7 @@ class AlarmState { public void processAckAlarm(Alarm alarm) { if (currentAlarm != null && currentAlarm.getId().equals(alarm.getId())) { - currentAlarm.setStatus(alarm.getStatus()); + currentAlarm.setAcknowledged(alarm.isAcknowledged()); currentAlarm.setAckTs(alarm.getAckTs()); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java index 0b6a1d6ed9..c3a72392e1 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/action/TbAlarmNodeTest.java @@ -28,7 +28,6 @@ import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.mockito.stubbing.Answer; import org.thingsboard.common.util.ListeningExecutor; import org.thingsboard.rule.engine.api.RuleEngineAlarmService; import org.thingsboard.rule.engine.api.ScriptEngine; @@ -36,6 +35,9 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; import org.thingsboard.rule.engine.api.TbNodeException; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmInfo; +import org.thingsboard.server.common.data.alarm.AlarmUpdateRequest; import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.EntityId; @@ -46,6 +48,7 @@ import org.thingsboard.server.common.data.script.ScriptLanguage; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgDataType; import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import javax.script.ScriptException; import java.io.IOException; @@ -59,7 +62,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.atMost; -import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.same; import static org.mockito.Mockito.times; @@ -71,9 +73,6 @@ import static org.thingsboard.server.common.data.DataConstants.IS_EXISTING_ALARM import static org.thingsboard.server.common.data.DataConstants.IS_NEW_ALARM; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.CRITICAL; import static org.thingsboard.server.common.data.alarm.AlarmSeverity.WARNING; -import static org.thingsboard.server.common.data.alarm.AlarmStatus.ACTIVE_UNACK; -import static org.thingsboard.server.common.data.alarm.AlarmStatus.CLEARED_ACK; -import static org.thingsboard.server.common.data.alarm.AlarmStatus.CLEARED_UNACK; @RunWith(MockitoJUnitRunner.class) public class TbAlarmNodeTest { @@ -93,16 +92,16 @@ public class TbAlarmNodeTest { @Captor private ArgumentCaptor> failureCaptor; - private RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); - private RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); + private final RuleChainId ruleChainId = new RuleChainId(Uuids.timeBased()); + private final RuleNodeId ruleNodeId = new RuleNodeId(Uuids.timeBased()); private ListeningExecutor dbExecutor; - private EntityId originator = new DeviceId(Uuids.timeBased()); - private EntityId alarmOriginator = new AlarmId(Uuids.timeBased()); - private TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); - private TbMsgMetaData metaData = new TbMsgMetaData(); - private String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; + private final EntityId originator = new DeviceId(Uuids.timeBased()); + private final EntityId alarmOriginator = new AlarmId(Uuids.timeBased()); + private final TenantId tenantId = TenantId.fromUUID(Uuids.timeBased()); + private final TbMsgMetaData metaData = new TbMsgMetaData(); + private final String rawJson = "{\"name\": \"Vit\", \"passed\": 5}"; @Before public void before() { @@ -128,11 +127,26 @@ public class TbAlarmNodeTest { initWithCreateAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + long ts = msg.getTs(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); - long ts = msg.getTs(); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(CRITICAL) + .propagate(true) + .type("SomeType") + .details(null) + .build(); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); + node.onMsg(ctx, msg); verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); @@ -153,18 +167,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @@ -175,7 +177,7 @@ public class TbAlarmNodeTest { TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFailedFuture(new NotImplementedException("message"))); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); node.onMsg(ctx, msg); @@ -183,10 +185,10 @@ public class TbAlarmNodeTest { verify(ctx).createScriptEngine(ScriptLanguage.JS, "DETAILS"); verify(ctx).getAlarmService(); - verify(ctx, times(3)).getDbCallbackExecutor(); + verify(ctx, times(2)).getDbCallbackExecutor(); verify(ctx).logJsEvalRequest(); verify(ctx).getTenantId(); - verify(alarmService).findLatestByOriginatorAndType(tenantId, originator, "SomeType"); + verify(alarmService).findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType"); verifyNoMoreInteractions(ctx, alarmService); } @@ -197,12 +199,27 @@ public class TbAlarmNodeTest { metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); - Alarm clearedAlarm = Alarm.builder().status(CLEARED_ACK).build(); + Alarm clearedAlarm = Alarm.builder().cleared(true).acknowledged(true).build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(clearedAlarm)); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(clearedAlarm); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(CRITICAL) + .propagate(true) + .type("SomeType") + .details(null) + .build(); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -225,35 +242,36 @@ public class TbAlarmNodeTest { Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @Test - public void alarmCanBeUpdated() throws ScriptException, IOException { + public void alarmCanBeUpdated() throws IOException { initWithCreateAlarmScript(); metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); - - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(activeAlarm); + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(originator) + .severity(CRITICAL) + .propagate(true) + .type("SomeType") + .details(null) + .endTs(activeAlarm.getEndTs()) + .build(); + when(alarmService.updateAlarm(any(AlarmUpdateRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .modified(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); verify(ctx).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); @@ -274,18 +292,7 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - assertTrue(activeAlarm.getEndTs() > oldEndDate); - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(CRITICAL) - .propagate(true) - .type("SomeType") - .details(null) - .endTs(activeAlarm.getEndTs()) - .build(); - + assertTrue(activeAlarm.getEndTs() >= oldEndDate); assertEquals(expectedAlarm, actualAlarm); } @@ -293,17 +300,30 @@ public class TbAlarmNodeTest { public void alarmCanBeCleared() throws ScriptException, IOException { initWithClearAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg( "USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); + + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(originator) + .cleared(true) + .severity(WARNING) + .propagate(false) + .type("SomeType") + .details(null) + .endTs(oldEndDate) + .build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(activeAlarm)); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), nullable(JsonNode.class), anyLong())) - .thenReturn(Futures.immediateFuture( false)); - when(alarmService.findAlarmByIdAsync(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()))).thenReturn(Futures.immediateFuture(activeAlarm)); -// doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(activeAlarm); + when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) + .thenReturn(AlarmApiCallResult.builder() + .successful(true) + .cleared(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -325,17 +345,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .status(CLEARED_UNACK) - .severity(WARNING) - .propagate(false) - .type("SomeType") - .details(null) - .endTs(oldEndDate) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @@ -343,17 +352,33 @@ public class TbAlarmNodeTest { public void alarmCanBeClearedWithAlarmOriginator() throws ScriptException, IOException { initWithClearAlarmScript(); metaData.putValue("key", "value"); - TbMsg msg = TbMsg.newMsg( "USER", alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + TbMsg msg = TbMsg.newMsg("USER", alarmOriginator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long oldEndDate = System.currentTimeMillis(); AlarmId id = new AlarmId(alarmOriginator.getId()); - Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).status(ACTIVE_UNACK).severity(WARNING).endTs(oldEndDate).build(); + Alarm activeAlarm = Alarm.builder().type("SomeType").tenantId(tenantId).originator(originator).severity(WARNING).endTs(oldEndDate).build(); activeAlarm.setId(id); + Alarm expectedAlarm = Alarm.builder() + .tenantId(tenantId) + .originator(originator) + .cleared(true) + .severity(WARNING) + .propagate(false) + .type("SomeType") + .details(null) + .endTs(oldEndDate) + .build(); + expectedAlarm.setId(id); + when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findAlarmByIdAsync(tenantId, id)).thenReturn(Futures.immediateFuture(activeAlarm)); - when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), nullable(JsonNode.class), anyLong())).thenReturn(Futures.immediateFuture(true)); -// doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(activeAlarm); + when(alarmService.findAlarmById(tenantId, id)).thenReturn(activeAlarm); + when(alarmService.clearAlarm(eq(activeAlarm.getTenantId()), eq(activeAlarm.getId()), anyLong(), nullable(JsonNode.class))) + .thenReturn(AlarmApiCallResult.builder() + .successful(true) + .cleared(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -375,18 +400,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .tenantId(tenantId) - .originator(originator) - .status(CLEARED_UNACK) - .severity(WARNING) - .propagate(false) - .type("SomeType") - .details(null) - .endTs(oldEndDate) - .build(); - expectedAlarm.setId(id); - assertEquals(expectedAlarm, actualAlarm); } @@ -415,9 +428,25 @@ public class TbAlarmNodeTest { metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(WARNING) + .propagate(true) + .type("SomeType") + .details(null) + .build(); + when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -439,18 +468,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(WARNING) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @@ -478,10 +495,25 @@ public class TbAlarmNodeTest { metaData.putValue("alarmSeverity", "WARNING"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); long ts = msg.getTs(); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(WARNING) + .propagate(true) + .type("SomeType") + .details(null) + .build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(Futures.immediateFuture(null)); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType")).thenReturn(null); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); @@ -502,23 +534,11 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(WARNING) - .propagate(true) - .type("SomeType") - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } @Test - public void testCreateAlarmsWithPropagationToTenantWithDynamicTypes() throws Exception{ + public void testCreateAlarmsWithPropagationToTenantWithDynamicTypes() throws Exception { for (int i = 0; i < 10; i++) { var config = new TbCreateAlarmNodeConfiguration(); config.setPropagateToTenant(true); @@ -541,11 +561,26 @@ public class TbAlarmNodeTest { metaData.putValue("key", "value"); TbMsg msg = TbMsg.newMsg("USER", originator, metaData, TbMsgDataType.JSON, rawJson, ruleChainId, ruleNodeId); + long ts = msg.getTs(); + Alarm expectedAlarm = Alarm.builder() + .startTs(ts) + .endTs(ts) + .tenantId(tenantId) + .originator(originator) + .severity(CRITICAL) + .propagateToTenant(true) + .type("SomeType" + i) + .details(null) + .build(); when(detailsJs.executeJsonAsync(msg)).thenReturn(Futures.immediateFuture(null)); - when(alarmService.findLatestByOriginatorAndType(tenantId, originator, "SomeType" + i)).thenReturn(Futures.immediateFuture(null)); - doAnswer((Answer) invocationOnMock -> (Alarm) (invocationOnMock.getArguments())[0]).when(alarmService).createOrUpdateAlarm(any(Alarm.class)); - long ts = msg.getTs(); + when(alarmService.findLatestActiveByOriginatorAndType(tenantId, originator, "SomeType" + i)).thenReturn(null); + when(alarmService.createAlarm(any(AlarmCreateOrUpdateActiveRequest.class))).thenReturn( + AlarmApiCallResult.builder() + .successful(true) + .created(true) + .alarm(new AlarmInfo(expectedAlarm)) + .build()); node.onMsg(ctx, msg); verify(ctx, atMost(10)).enqueue(any(), successCaptor.capture(), failureCaptor.capture()); @@ -566,18 +601,6 @@ public class TbAlarmNodeTest { assertNotSame(metaData, metadataCaptor.getValue()); Alarm actualAlarm = new ObjectMapper().readValue(dataCaptor.getValue().getBytes(), Alarm.class); - Alarm expectedAlarm = Alarm.builder() - .startTs(ts) - .endTs(ts) - .tenantId(tenantId) - .originator(originator) - .status(ACTIVE_UNACK) - .severity(CRITICAL) - .propagateToTenant(true) - .type("SomeType" + i) - .details(null) - .build(); - assertEquals(expectedAlarm, actualAlarm); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java index 3d8652a7a0..bd761a084c 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/metadata/TbGetTelemetryNodeTest.java @@ -18,6 +18,7 @@ package org.thingsboard.rule.engine.metadata; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.Before; import org.junit.Test; +import org.junit.jupiter.api.Assertions; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.rule.engine.api.TbNodeConfiguration; @@ -70,14 +71,18 @@ public class TbGetTelemetryNodeTest { } } - @Test(expected = IllegalArgumentException.class) + @Test public void givenAggregationWhiteSpace_whenParseAggregation_thenException() { - node.parseAggregationConfig(" "); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + node.parseAggregationConfig(" "); + }); } - @Test(expected = IllegalArgumentException.class) + @Test public void givenAggregationIncorrect_whenParseAggregation_thenException() { - node.parseAggregationConfig("TOP"); + Assertions.assertThrows(IllegalArgumentException.class, () -> { + node.parseAggregationConfig("TOP"); + }); } } diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java index fd10e1212e..f64661bc5b 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/DeviceStateTest.java @@ -25,6 +25,8 @@ import org.thingsboard.rule.engine.api.TbContext; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmCreateOrUpdateActiveRequest; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; @@ -42,6 +44,7 @@ import org.thingsboard.server.common.data.query.FilterPredicateValue; import org.thingsboard.server.common.msg.TbMsg; import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.common.msg.session.SessionMsgType; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; @@ -77,11 +80,15 @@ public class DeviceStateTest { when(ctx.getAttributesService()).thenReturn(attributesService); RuleEngineAlarmService alarmService = mock(RuleEngineAlarmService.class); - when(alarmService.findLatestByOriginatorAndType(any(), any(), any())).thenReturn(Futures.immediateFuture(null)); - when(alarmService.createOrUpdateAlarm(any())).thenAnswer(invocationOnMock -> { - Alarm alarm = invocationOnMock.getArgument(0); - alarm.setId(new AlarmId(UUID.randomUUID())); - return alarm; + when(alarmService.findLatestActiveByOriginatorAndType(any(), any(), any())).thenReturn(null); + when(alarmService.createAlarm(any())).thenAnswer(invocationOnMock -> { + AlarmCreateOrUpdateActiveRequest request = invocationOnMock.getArgument(0); + return AlarmApiCallResult.builder() + .successful(true) + .created(true) + .modified(true) + .alarm(new AlarmInfo(new Alarm(new AlarmId(UUID.randomUUID())))) + .build(); }); when(ctx.getAlarmService()).thenReturn(alarmService); diff --git a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java index 5d9abc130e..fb3085f3f7 100644 --- a/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java +++ b/rule-engine/rule-engine-components/src/test/java/org/thingsboard/rule/engine/profile/TbDeviceProfileNodeTest.java @@ -21,7 +21,6 @@ import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.AdditionalAnswers; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; @@ -34,6 +33,8 @@ import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.alarm.Alarm; +import org.thingsboard.server.common.data.alarm.AlarmInfo; import org.thingsboard.server.common.data.alarm.AlarmSeverity; import org.thingsboard.server.common.data.device.profile.AlarmCondition; import org.thingsboard.server.common.data.device.profile.AlarmConditionFilter; @@ -46,6 +47,7 @@ import org.thingsboard.server.common.data.device.profile.DeviceProfileAlarm; import org.thingsboard.server.common.data.device.profile.DeviceProfileData; import org.thingsboard.server.common.data.device.profile.DurationAlarmConditionSpec; import org.thingsboard.server.common.data.device.profile.RepeatingAlarmConditionSpec; +import org.thingsboard.server.common.data.id.AlarmId; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.DeviceId; import org.thingsboard.server.common.data.id.DeviceProfileId; @@ -62,6 +64,7 @@ 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.msg.session.SessionMsgType; +import org.thingsboard.server.dao.alarm.AlarmApiCallResult; import org.thingsboard.server.dao.attributes.AttributesService; import org.thingsboard.server.dao.device.DeviceService; import org.thingsboard.server.dao.model.sql.AttributeKvCompositeKey; @@ -79,8 +82,10 @@ import java.util.TreeMap; import java.util.UUID; import java.util.concurrent.TimeUnit; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class TbDeviceProfileNodeTest { @@ -187,8 +192,8 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")).thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg); @@ -205,6 +210,8 @@ public class TbDeviceProfileNodeTest { TbMsg theMsg2 = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), "2"); Mockito.when(ctx.newMsg(Mockito.any(), Mockito.anyString(), Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString())).thenReturn(theMsg2); + registerCreateAlarmMock(alarmService.updateAlarm(any()), false); + TbMsg msg2 = TbMsg.newMsg(SessionMsgType.POST_TELEMETRY_REQUEST.name(), deviceId, new TbMsgMetaData(), TbMsgDataType.JSON, mapper.writeValueAsString(data), null, null); @@ -274,9 +281,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(attrListListenableFuture); @@ -357,9 +364,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "alarmEnabledAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(Futures.immediateFuture(Collections.emptyList())); @@ -430,9 +437,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); @@ -524,9 +531,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); @@ -642,9 +649,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString())) .thenReturn(optionalDurationAttribute); @@ -757,9 +764,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); @@ -867,9 +874,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), Mockito.anyString(), Mockito.anyString())) .thenReturn(optionalDurationAttribute); @@ -969,9 +976,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); @@ -1067,9 +1074,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFuture); @@ -1149,9 +1156,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureActiveSchedule); @@ -1246,8 +1253,8 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "highTemperatureAlarm")) + .thenReturn(null); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureInactiveSchedule); @@ -1319,9 +1326,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())).thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); @@ -1394,10 +1401,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) - .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(attributesService.find(eq(tenantId), eq(deviceId), Mockito.anyString(), Mockito.anySet())) .thenReturn(listListenableFutureWithLess); @@ -1474,10 +1480,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) - .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "lessTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); @@ -1485,7 +1490,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFutureWithLess); Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.anyString(), Mockito.anyString())) .thenReturn(emptyOptionalFuture); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); @@ -1560,10 +1565,9 @@ public class TbDeviceProfileNodeTest { Mockito.when(cache.get(tenantId, deviceId)).thenReturn(deviceProfile); Mockito.when(timeseriesService.findLatest(tenantId, deviceId, Collections.singleton("temperature"))) .thenReturn(Futures.immediateFuture(Collections.emptyList())); - Mockito.when(alarmService.findLatestByOriginatorAndType(tenantId, deviceId, "greaterTemperatureAlarm")) - .thenReturn(Futures.immediateFuture(null)); - Mockito.when(alarmService.createOrUpdateAlarm(Mockito.any())) - .thenAnswer(AdditionalAnswers.returnsFirstArg()); + Mockito.when(alarmService.findLatestActiveByOriginatorAndType(tenantId, deviceId, "greaterTemperatureAlarm")) + .thenReturn(null); + registerCreateAlarmMock(alarmService.createAlarm(any()), true); Mockito.when(ctx.getAttributesService()).thenReturn(attributesService); Mockito.when(ctx.getDeviceService().findDeviceById(tenantId, deviceId)) .thenReturn(device); @@ -1571,7 +1575,7 @@ public class TbDeviceProfileNodeTest { .thenReturn(listListenableFutureWithLess); Mockito.when(attributesService.find(eq(tenantId), eq(customerId), Mockito.anyString(), Mockito.anyString())) .thenReturn(emptyOptionalFuture); - Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) + Mockito.when(attributesService.find(eq(tenantId), eq(tenantId), eq(DataConstants.SERVER_SCOPE), Mockito.anyString())) .thenReturn(optionalListenableFutureWithLess); TbMsg theMsg = TbMsg.newMsg("ALARM", deviceId, new TbMsgMetaData(), ""); @@ -1601,4 +1605,18 @@ public class TbDeviceProfileNodeTest { node.init(ctx, nodeConfiguration); } + private void registerCreateAlarmMock(AlarmApiCallResult a, boolean created) { + when(a).thenAnswer(invocationOnMock -> { +// AlarmCreateOrUpdateActiveRequest request = invocationOnMock.getArgument(0); + AlarmInfo alarm = new AlarmInfo(new Alarm(new AlarmId(UUID.randomUUID()))); + alarm.setSeverity(AlarmSeverity.CRITICAL); + return AlarmApiCallResult.builder() + .successful(true) + .created(created) + .modified(true) + .alarm(alarm) + .build(); + }); + } + } diff --git a/ui-ngx/angular.json b/ui-ngx/angular.json index a875eed727..cc13556d4b 100644 --- a/ui-ngx/angular.json +++ b/ui-ngx/angular.json @@ -140,7 +140,8 @@ "ace", "ace-builds", "diff-match-patch", - "tv4" + "tv4", + "@messageformat/parser" ] }, "configurations": { diff --git a/ui-ngx/package.json b/ui-ngx/package.json index 0591bc9dc6..a8abb6fbeb 100644 --- a/ui-ngx/package.json +++ b/ui-ngx/package.json @@ -32,10 +32,11 @@ "@flowjs/ngx-flow": "~0.6.0", "@geoman-io/leaflet-geoman-free": "^2.13.0", "@juggle/resize-observer": "^3.3.1", - "@mat-datetimepicker/core": "~11.0.2", + "@mat-datetimepicker/core": "~11.0.3", "@material-ui/core": "4.12.3", "@material-ui/icons": "4.11.2", "@material-ui/pickers": "3.3.10", + "@messageformat/core": "^3.0.1", "@ngrx/effects": "^15.3.0", "@ngrx/store": "^15.3.0", "@ngrx/store-devtools": "^15.3.0", @@ -67,9 +68,8 @@ "leaflet.markercluster": "^1.5.3", "libphonenumber-js": "^1.10.4", "marked": "^4.0.17", - "messageformat": "^2.3.0", - "moment": "^2.29.1", - "moment-timezone": "^0.5.40", + "moment": "^2.29.4", + "moment-timezone": "^0.5.41", "ngx-clipboard": "^15.1.0", "ngx-color-picker": "^13.0.0", "ngx-daterangepicker-material": "^6.0.4", @@ -78,7 +78,7 @@ "ngx-hm-carousel": "^3.0.0", "ngx-markdown": "^15.1.1", "ngx-sharebuttons": "^11.0.0", - "ngx-translate-messageformat-compiler": "^5.1.0", + "ngx-translate-messageformat-compiler": "^6.2.0", "objectpath": "^2.0.0", "prettier": "^2.8.3", "prop-types": "^15.8.1", @@ -131,7 +131,6 @@ "@types/leaflet.markercluster": "^1.5.1", "@types/lodash": "^4.14.177", "@types/marked": "^4.0.3", - "@types/moment-timezone": "^0.5.30", "@types/mousetrap": "^1.6.0", "@types/node": "~15.14.9", "@types/raphael": "^2.3.2", @@ -169,4 +168,4 @@ "ace-builds": "1.4.13", "@date-io/core": "1.3.7" } -} \ No newline at end of file +} diff --git a/ui-ngx/src/app/core/api/widget-api.models.ts b/ui-ngx/src/app/core/api/widget-api.models.ts index 45ac09120e..c6e00ab882 100644 --- a/ui-ngx/src/app/core/api/widget-api.models.ts +++ b/ui-ngx/src/app/core/api/widget-api.models.ts @@ -56,6 +56,7 @@ import { AlarmDataService } from '@core/api/alarm-data.service'; import { IDashboardController } from '@home/components/dashboard-page/dashboard-page.models'; import { PopoverPlacement } from '@shared/components/popover.models'; import { PersistentRpc } from '@shared/models/rpc.models'; +import { EventEmitter } from '@angular/core'; export interface TimewindowFunctions { onUpdateTimewindow: (startTimeMs: number, endTimeMs: number, interval?: number) => void; @@ -338,6 +339,8 @@ export interface IWidgetSubscription { pageLink: EntityDataPageLink, keyFilters: KeyFilter[]): Observable; + paginatedDataSubscriptionUpdated: EventEmitter; + subscribeForAlarms(pageLink: AlarmDataPageLink, keyFilters: KeyFilter[]): void; diff --git a/ui-ngx/src/app/core/api/widget-subscription.ts b/ui-ngx/src/app/core/api/widget-subscription.ts index 5d3eae16e3..9d07c751a5 100644 --- a/ui-ngx/src/app/core/api/widget-subscription.ts +++ b/ui-ngx/src/app/core/api/widget-subscription.ts @@ -80,6 +80,7 @@ import { import { distinct, filter, map, switchMap, takeUntil } from 'rxjs/operators'; import { AlarmDataListener } from '@core/api/alarm-data.service'; import { RpcStatus } from '@shared/models/rpc.models'; +import { EventEmitter } from '@angular/core'; const moment = moment_; @@ -214,6 +215,8 @@ export class WidgetSubscription implements IWidgetSubscription { distinct() ); + paginatedDataSubscriptionUpdated = new EventEmitter(); + constructor(subscriptionContext: WidgetSubscriptionContext, public options: WidgetSubscriptionOptions) { const subscriptionSubject = new ReplaySubject(); this.init$ = subscriptionSubject.asObservable(); @@ -939,6 +942,13 @@ export class WidgetSubscription implements IWidgetSubscription { } } + stopSubscription(datasourceIndex: number) { + const entityDataListener = this.entityDataListeners[datasourceIndex]; + if (entityDataListener) { + this.ctx.entityDataService.stopSubscription(entityDataListener); + } + } + subscribeForPaginatedData(datasourceIndex: number, pageLink: EntityDataPageLink, keyFilters: KeyFilter[]): Observable { @@ -1193,6 +1203,10 @@ export class WidgetSubscription implements IWidgetSubscription { } private updatePaginatedDataSubscriptions() { + for (let datasourceIndex = 0; datasourceIndex < this.entityDataListeners.length; datasourceIndex++) { + this.stopSubscription(datasourceIndex); + } + this.paginatedDataSubscriptionUpdated.emit(); for (let datasourceIndex = 0; datasourceIndex < this.entityDataListeners.length; datasourceIndex++) { const entityDataListener = this.entityDataListeners[datasourceIndex]; if (entityDataListener) { diff --git a/ui-ngx/src/app/core/http/alarm-comment.service.ts b/ui-ngx/src/app/core/http/alarm-comment.service.ts new file mode 100644 index 0000000000..ffb8fc4528 --- /dev/null +++ b/ui-ngx/src/app/core/http/alarm-comment.service.ts @@ -0,0 +1,46 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Injectable } from '@angular/core'; +import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; +import { Observable } from 'rxjs'; +import { HttpClient } from '@angular/common/http'; +import { PageLink } from '@shared/models/page/page-link'; +import { PageData } from '@shared/models/page/page-data'; +import { AlarmComment, AlarmCommentInfo } from '@shared/models/alarm.models'; + +@Injectable({ + providedIn: 'root' +}) +export class AlarmCommentService { + + constructor( + private http: HttpClient + ) { } + + public saveAlarmComment(alarmId: string, alarmComment: AlarmComment, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/comment`, alarmComment, defaultHttpOptionsFromConfig(config)); + } + + public getAlarmComments(alarmId: string, pageLink: PageLink, config?: RequestConfig): Observable> { + return this.http.get>(`/api/alarm/${alarmId}/comment${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + + public deleteAlarmComments(alarmId: string, commentId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/${alarmId}/comment/${commentId}`, defaultHttpOptionsFromConfig(config)); + } + +} diff --git a/ui-ngx/src/app/core/http/alarm.service.ts b/ui-ngx/src/app/core/http/alarm.service.ts index 802045b684..076f590199 100644 --- a/ui-ngx/src/app/core/http/alarm.service.ts +++ b/ui-ngx/src/app/core/http/alarm.service.ts @@ -60,6 +60,14 @@ export class AlarmService { return this.http.post(`/api/alarm/${alarmId}/clear`, null, defaultHttpOptionsFromConfig(config)); } + public assignAlarm(alarmId: string, assigneeId: string, config?: RequestConfig): Observable { + return this.http.post(`/api/alarm/${alarmId}/assign/${assigneeId}`, null, defaultHttpOptionsFromConfig(config)); + } + + public unassignAlarm(alarmId: string, config?: RequestConfig): Observable { + return this.http.delete(`/api/alarm/${alarmId}/assign`, defaultHttpOptionsFromConfig(config)); + } + public deleteAlarm(alarmId: string, config?: RequestConfig): Observable { return this.http.delete(`/api/alarm/${alarmId}`, defaultHttpOptionsFromConfig(config)); } diff --git a/ui-ngx/src/app/core/http/user.service.ts b/ui-ngx/src/app/core/http/user.service.ts index 0a7d2054e4..9a6b70ce29 100644 --- a/ui-ngx/src/app/core/http/user.service.ts +++ b/ui-ngx/src/app/core/http/user.service.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { defaultHttpOptionsFromConfig, RequestConfig } from './http-utils'; -import { User } from '@shared/models/user.model'; +import { User, UserEmailInfo } from '@shared/models/user.model'; import { Observable } from 'rxjs'; import { HttpClient, HttpParams } from '@angular/common/http'; import { PageLink } from '@shared/models/page/page-link'; @@ -84,4 +84,8 @@ export class UserService { return this.http.post(url, null, defaultHttpOptionsFromConfig(config)); } + public findUsersByQuery(pageLink: PageLink, config?: RequestConfig) : Observable> { + return this.http.get>(`/api/users/info${pageLink.toQuery()}`, defaultHttpOptionsFromConfig(config)); + } + } diff --git a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts index b4afeae6c3..84134f04a9 100644 --- a/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts +++ b/ui-ngx/src/app/core/services/dynamic-component-factory.service.ts @@ -59,10 +59,11 @@ export class DynamicComponentFactoryService { template: string, modules?: Type[], preserveWhitespaces?: boolean, - compileAttempt = 1): Observable> { + compileAttempt = 1, + styles?: string[]): Observable> { return from(import('@angular/compiler')).pipe( mergeMap(() => { - const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces); + const comp = this.createDynamicComponent(componentType, template, preserveWhitespaces, styles); let moduleImports: Type[] = [CommonModule]; if (modules) { moduleImports = [...moduleImports, ...modules]; @@ -91,7 +92,7 @@ export class DynamicComponentFactoryService { catchError((error) => { if (compileAttempt === 1) { ɵresetCompiledComponents(); - return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt); + return this.createDynamicComponentFactory(componentType, template, modules, preserveWhitespaces, ++compileAttempt, styles); } else { throw error; } @@ -110,11 +111,12 @@ export class DynamicComponentFactoryService { } } - private createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean): Type { + private createDynamicComponent(componentType: Type, template: string, preserveWhitespaces?: boolean, styles?: string[]): Type { // noinspection AngularMissingOrInvalidDeclarationInModule return Component({ template, - preserveWhitespaces + preserveWhitespaces, + styles })(componentType); } diff --git a/ui-ngx/src/app/core/services/utils.service.ts b/ui-ngx/src/app/core/services/utils.service.ts index ce6cf32dbd..ea4c85eb64 100644 --- a/ui-ngx/src/app/core/services/utils.service.ts +++ b/ui-ngx/src/app/core/services/utils.service.ts @@ -25,7 +25,7 @@ import { createLabelFromDatasource, deepClone, deleteNullProperties, - guid, + guid, hashCode, isDefined, isDefinedAndNotNull, isString, @@ -405,6 +405,13 @@ export class UtilsService { }); } + public stringToHslColor(str: string, saturationPercentage: number, lightnessPercentage: number): string { + if (str && str.length) { + let hue = hashCode(str) % 360; + return `hsl(${hue}, ${saturationPercentage}%, ${lightnessPercentage}%)`; + } + } + public currentPerfTime(): number { return this.window.performance && this.window.performance.now ? this.window.performance.now() : Date.now(); diff --git a/ui-ngx/src/app/core/translate/translate-default-compiler.ts b/ui-ngx/src/app/core/translate/translate-default-compiler.ts index 8bc590fb2a..739621eaca 100644 --- a/ui-ngx/src/app/core/translate/translate-default-compiler.ts +++ b/ui-ngx/src/app/core/translate/translate-default-compiler.ts @@ -20,7 +20,7 @@ import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler'; import { Inject, Injectable, Optional } from '@angular/core'; -import messageFormatParser from 'messageformat-parser'; +import { parse } from '@messageformat/parser'; @Injectable({ providedIn: 'root' }) export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler { @@ -44,7 +44,12 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler { private defaultCompile(src: any, lang: string): any { if (typeof src !== 'object') { if (this.checkIsPlural(src)) { - return super.compile(src, lang); + try { + return super.compile(src, lang.replace('_', '-')); + } catch (e) { + console.warn('Failed compile translate:', src, e); + return src; + } } else { return src; } @@ -60,7 +65,7 @@ export class TranslateDefaultCompiler extends TranslateMessageFormatCompiler { private checkIsPlural(src: string): boolean { let tokens: any[]; try { - tokens = messageFormatParser.parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'), + tokens = parse(src.replace(/\{\{/g, '{').replace(/\}\}/g, '}'), {cardinal: [], ordinal: []}); } catch (e) { console.warn(`Failed to parse source: ${src}`); diff --git a/ui-ngx/src/app/core/utils.ts b/ui-ngx/src/app/core/utils.ts index 992ddec310..4018fd491f 100644 --- a/ui-ngx/src/app/core/utils.ts +++ b/ui-ngx/src/app/core/utils.ts @@ -304,7 +304,7 @@ export function deepClone(target: T, ignoreFields?: string[]): T { (target as any[]).forEach((v) => { cp.push(v); }); return cp.map((n: any) => deepClone(n)) as any; } - if (typeof target === 'object' && Object.keys(target).length) { + if (typeof target === 'object') { const cp = {...(target as { [key: string]: any })} as { [key: string]: any }; Object.keys(cp).forEach(k => { if (!ignoreFields || ignoreFields.indexOf(k) === -1) { diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html new file mode 100644 index 0000000000..f6ba02e7a0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.html @@ -0,0 +1,52 @@ + + + + search + + + account_circle + alarm.unassigned + + + + +
+ + +
+
+ + + {{ translate.get('user.no-users-matching', {entity: searchText}) | async }} + + +
+
+ diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss new file mode 100644 index 0000000000..a10848cc70 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.scss @@ -0,0 +1,89 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + width: 100%; + overflow: auto; + background: #fff; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3), 0px 2px 6px 2px rgba(0, 0, 0, 0.15); + border-radius: 4px; +} + +::ng-deep { + mat-form-field.search-users { + padding: 8px; + height: 340px; + font-size: 14px; + background-color: #fff; + } + + .mat-form-field-appearance-outline .mdc-notched-outline__trailing{ + color: rgba(0, 0, 0, 0.12) !important; + } + + .tb-assignee-autocomplete { + &.tb-assignee-autocomplete.mat-mdc-autocomplete-panel { + position: relative; + left: -8px; + margin-top: 8px; + box-shadow: none !important; + } + .mat-mdc-option { + font-size: 14px; + border: none; + height: 52px !important; + .unassigned-icon { + color: rgba(0, 0, 0, 0.38); + font-size: 28px; + width: 28px; + height: 28px; + margin-right: 8px; + } + .user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + margin-right: 8px; + border-radius: 50%; + background-color: #5cb445; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: #fff; + font-size: 13px; + font-weight: 700 + } + .user-display-name { + max-width: 180px; + overflow: hidden; + span { + overflow: hidden; + text-overflow: ellipsis; + } + span + span { + color: rgba(0, 0, 0, 0.38); + } + } + .mdc-list-item__primary-text { + display: flex; + justify-content: start; + align-items: center; + line-height: normal; + } + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts new file mode 100644 index 0000000000..bd50beef31 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee-panel.component.ts @@ -0,0 +1,227 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + AfterViewInit, + Component, + ElementRef, + Inject, + InjectionToken, OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { FormBuilder, FormGroup } from '@angular/forms'; +import { Observable, of, Subject } from 'rxjs'; +import { + catchError, + debounceTime, + distinctUntilChanged, + map, + share, + switchMap, + takeUntil, +} from 'rxjs/operators'; +import { User, UserEmailInfo } from '@shared/models/user.model'; +import { TranslateService } from '@ngx-translate/core'; +import { UserService } from '@core/http/user.service'; +import { PageLink } from '@shared/models/page/page-link'; +import { Direction } from '@shared/models/page/sort-order'; +import { emptyPageData } from '@shared/models/page/page-data'; +import { AlarmService } from '@core/http/alarm.service'; +import { OverlayRef } from '@angular/cdk/overlay'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { UtilsService } from '@core/services/utils.service'; + +export const ALARM_ASSIGNEE_PANEL_DATA = new InjectionToken('AlarmAssigneePanelData'); + +export interface AlarmAssigneePanelData { + alarmId: string; + assigneeId: string; +} + +@Component({ + selector: 'tb-alarm-assignee-panel', + templateUrl: './alarm-assignee-panel.component.html', + styleUrls: ['./alarm-assignee-panel.component.scss'] +}) +export class AlarmAssigneePanelComponent implements OnInit, AfterViewInit, OnDestroy { + + private dirty = false; + + alarmId: string; + + assigneeId?: string; + + selectUserFormGroup: FormGroup; + + @ViewChild('userInput', {static: true}) userInput: ElementRef; + + filteredUsers: Observable>; + + searchText = ''; + + private destroy$ = new Subject(); + + constructor(@Inject(ALARM_ASSIGNEE_PANEL_DATA) public data: AlarmAssigneePanelData, + public overlayRef: OverlayRef, + public translate: TranslateService, + private userService: UserService, + private alarmService: AlarmService, + private fb: FormBuilder, + private utilsService: UtilsService) { + this.alarmId = data.alarmId; + this.assigneeId = data.assigneeId; + this.selectUserFormGroup = this.fb.group({ + user: [null] + }); + } + + ngOnInit() { + this.filteredUsers = this.selectUserFormGroup.get('user').valueChanges + .pipe( + debounceTime(150), + map(value => { + return value ? (typeof value === 'string' ? value : '') : '' + }), + distinctUntilChanged(), + switchMap(name => this.fetchUsers(name)), + share(), + takeUntil(this.destroy$) + ); + } + + ngAfterViewInit() { + setTimeout(() => { + this.userInput.nativeElement.focus(); + }, 0) + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } + + displayUserFn(user?: User): string | undefined { + return user ? user.email : undefined; + } + + selected(event: MatAutocompleteSelectedEvent): void { + this.clear(); + const user: User = event.option.value; + if (user) { + this.assign(user); + } else { + this.unassign(); + } + } + + assign(user: User): void { + this.alarmService.assignAlarm(this.alarmId, user.id.id, {ignoreLoading: true}).subscribe( + () => this.overlayRef.dispose()); + } + + unassign(): void { + this.alarmService.unassignAlarm(this.alarmId, {ignoreLoading: true}).subscribe( + () => this.overlayRef.dispose()); + } + + fetchUsers(searchText?: string): Observable> { + this.searchText = searchText; + const pageLink = new PageLink(50, 0, searchText, { + property: 'email', + direction: Direction.ASC + }); + return this.userService.findUsersByQuery(pageLink, {ignoreLoading: true}) + .pipe( + catchError(() => of(emptyPageData())), + map(pageData => { + return pageData.data; + }) + ); + } + + onFocus(): void { + if (!this.dirty) { + this.selectUserFormGroup.get('user').updateValueAndValidity({onlySelf: true}); + this.dirty = true; + } + } + + clear() { + this.selectUserFormGroup.get('user').patchValue('', {emitEvent: true}); + setTimeout(() => { + this.userInput.nativeElement.blur(); + this.userInput.nativeElement.focus(); + }, 0); + } + + getUserDisplayName(entity: User) { + let displayName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + displayName += entity.firstName; + } + if (entity.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.lastName; + } + } else { + displayName = entity.email; + } + return displayName; + } + + getUserInitials(entity: User): string { + let initials = ''; + if (entity.firstName && entity.firstName.length || + entity.lastName && entity.lastName.length) { + if (entity.firstName) { + initials += entity.firstName.charAt(0); + } + if (entity.lastName) { + initials += entity.lastName.charAt(0); + } + } else { + initials += entity.email.charAt(0); + } + return initials.toUpperCase(); + } + + getFullName(entity: User): string { + let fullName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + fullName += entity.firstName; + } + if (entity.lastName) { + if (fullName.length > 0) { + fullName += ' '; + } + fullName += entity.lastName; + } + } + return fullName; + } + + getAvatarBgColor(entity: User) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html new file mode 100644 index 0000000000..52066ff3ac --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.html @@ -0,0 +1,43 @@ + + +
+ + + {{ getUserInitials(alarm.assignee) }} + + + {{ getUserDisplayName(alarm.assignee) }} + + + + account_circle + alarm.unassigned + + +
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss new file mode 100644 index 0000000000..935ac2f32d --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.scss @@ -0,0 +1,49 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +:host { + .tb-assignee { + cursor: pointer; + max-width: 273px; + + .assigned-container { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + .user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + border-radius: 50%; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: white; + font-size: 13px; + font-weight: 700; + } + } + .material-icons.unassigned-icon { + width: 28px; + height: 28px; + font-size: 28px; + color: rgba(0, 0, 0, 0.38); + overflow: visible; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts new file mode 100644 index 0000000000..5d7f29d55b --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-assignee.component.ts @@ -0,0 +1,141 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { + Component, EventEmitter, Injector, Input, Output, StaticProvider, ViewContainerRef +} from '@angular/core'; +import { UtilsService } from '@core/services/utils.service'; +import { AlarmAssignee, AlarmInfo } from '@shared/models/alarm.models'; +import { + ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent, + AlarmAssigneePanelData +} from '@home/components/alarm/alarm-assignee-panel.component'; +import { ConnectedPosition, Overlay, OverlayConfig, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; + +@Component({ + selector: 'tb-alarm-assignee', + templateUrl: './alarm-assignee.component.html', + styleUrls: ['./alarm-assignee.component.scss'] +}) +export class AlarmAssigneeComponent { + @Input() + alarm: AlarmInfo; + + @Output() + alarmReassigned = new EventEmitter(); + + constructor(private utilsService: UtilsService, + private overlay: Overlay, + private viewContainerRef: ViewContainerRef) { + } + + getUserDisplayName(entity: AlarmAssignee) { + let displayName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + displayName += entity.firstName; + } + if (entity.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.lastName; + } + } else { + displayName = entity.email; + } + return displayName; + } + + getUserInitials(entity: AlarmAssignee): string { + let initials = ''; + if (entity.firstName && entity.firstName.length || + entity.lastName && entity.lastName.length) { + if (entity.firstName) { + initials += entity.firstName.charAt(0); + } + if (entity.lastName) { + initials += entity.lastName.charAt(0); + } + } else { + initials += entity.email.charAt(0); + } + return initials.toUpperCase(); + } + + getFullName(entity: AlarmAssignee): string { + let fullName = ''; + if ((entity.firstName && entity.firstName.length > 0) || + (entity.lastName && entity.lastName.length > 0)) { + if (entity.firstName) { + fullName += entity.firstName; + } + if (entity.lastName) { + if (fullName.length > 0) { + fullName += ' '; + } + fullName += entity.lastName; + } + } + return fullName; + } + + getAvatarBgColor(entity: AlarmAssignee) { + return this.utilsService.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + + openAlarmAssigneePanel($event: Event, alarm: AlarmInfo) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + config.minWidth = '260px'; + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: ALARM_ASSIGNEE_PANEL_DATA, + useValue: { + alarmId: alarm.id.id, + assigneeId: alarm.assigneeId?.id + } as AlarmAssigneePanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent, + this.viewContainerRef, injector)).onDestroy(() => this.alarmReassigned.emit(true)); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html new file mode 100644 index 0000000000..94f8f47eb1 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.html @@ -0,0 +1,44 @@ + +
+ +

{{ 'alarm.comments' | translate }}

+ + +
+ + +
+
+ + +
+
+ +
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts new file mode 100644 index 0000000000..026ebae8c0 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment-dialog.component.ts @@ -0,0 +1,54 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Inject } from '@angular/core'; +import { DialogComponent } from '@shared/components/dialog.component'; +import { Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { Router } from '@angular/router'; +import { AlarmInfo } from '@shared/models/alarm.models'; + +export interface AlarmCommentDialogData { + alarmId?: string; + alarm?: AlarmInfo; + commentsHeaderEnabled: boolean; +} + +@Component({ + selector: 'tb-alarm-comment-dialog', + templateUrl: './alarm-comment-dialog.component.html', + styleUrls: [] +}) +export class AlarmCommentDialogComponent extends DialogComponent { + + alarmId: string; + + commentsHeaderEnabled: boolean = false; + + constructor(protected store: Store, + protected router: Router, + @Inject(MAT_DIALOG_DATA) public data: AlarmCommentDialogData, + public dialogRef: MatDialogRef) { + super(store, router, dialogRef); + this.commentsHeaderEnabled = this.data.commentsHeaderEnabled + this.alarmId = this.data.alarmId; + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html new file mode 100644 index 0000000000..91b4871d86 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.html @@ -0,0 +1,162 @@ + + +
+
+ + {{ 'alarm-comment.comments' | translate }} + +
+ + +
+
+
+ + + +
+
+ + {{ displayDataElement.commentText }} + + + {{ displayDataElement.createdDateAgo }} + +
+ +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+
+
+ {{ displayDataElement.displayName }} + + edited {{ displayDataElement.editedDateAgo }} + + + {{ displayDataElement.createdDateAgo }} + +
+ {{ displayDataElement.commentText }} +
+
+ + +
+
+ +
+
+ {{ getUserInitials(displayDataElement.displayName) }} +
+ + +
+ + +
+
+
+
+
+
+ + + +
+ +
+
+ {{ getUserInitials(userDisplayName) }} +
+ + + + +
+
+
diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss new file mode 100644 index 0000000000..3cd97767a2 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.scss @@ -0,0 +1,107 @@ +/** + * Copyright © 2016-2023 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + .tb-alarm-comments { + padding: 16px 24px 24px 24px; + background-color: #fafafa; + max-width: 600px; + + &-header { + background-color: #fafafa; + position: sticky; + top: -25px; + z-index: 1; + margin-bottom: 10px; + + &-title { + color: rgba(0, 0, 0, 0.76); + letter-spacing: 0.25px; + font-weight: 500; + } + + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + + &-user-avatar { + width: 28px; + min-width: 28px; + height: 28px; + min-height: 28px; + border-radius: 50%; + font-weight: 700; + color: #FFFFFF; + font-size: 13px; + } + + &-user-name { + font-size: 16px; + color: rgba(0, 0, 0, 0.76); + font-weight: 500; + letter-spacing: 0.25px; + } + + &-time { + font-size: 14px; + font-weight: 400; + color: rgba(0, 0, 0, 0.38); + letter-spacing: 0.2px + } + + &-system-text { + color: rgba(0, 0, 0, 0.38); + font-weight: 500; + letter-spacing: 0.25px; + } + + &-text { + white-space: pre-line; + word-break: break-word; + color: rgba(0, 0, 0, 0.54); + letter-spacing: 0.15px; + } + + &-action-buttons { + visibility: hidden; + .mat-icon { + color: rgba(0, 0, 0, 0.38); + } + } + + .show-buttons { + visibility: visible; + } + + .green-button { + color: #00695C; + } + + .red-button { + color: #D12730; + } + + .mat-form-field { + font-size: 16px; + letter-spacing: 0.15px; + color: rgba(0, 0, 0, 0.76); + } + + textarea { + letter-spacing: 0.15px; + } + } +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts new file mode 100644 index 0000000000..6e32979a14 --- /dev/null +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-comment.component.ts @@ -0,0 +1,295 @@ +/// +/// Copyright © 2016-2023 The Thingsboard Authors +/// +/// Licensed under the Apache License, Version 2.0 (the "License"); +/// you may not use this file except in compliance with the License. +/// You may obtain a copy of the License at +/// +/// http://www.apache.org/licenses/LICENSE-2.0 +/// +/// Unless required by applicable law or agreed to in writing, software +/// distributed under the License is distributed on an "AS IS" BASIS, +/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +/// See the License for the specific language governing permissions and +/// limitations under the License. +/// + +import { Component, Input, OnInit } from '@angular/core'; +import { select, Store } from '@ngrx/store'; +import { AppState } from '@core/core.state'; +import { TranslateService } from '@ngx-translate/core'; +import { AlarmCommentService } from '@core/http/alarm-comment.service'; +import { AbstractControl, FormBuilder, FormGroup } from '@angular/forms'; +import { DialogService } from '@core/services/dialog.service'; +import { AuthUser, User } from '@shared/models/user.model'; +import { getCurrentAuthUser, selectUserDetails } from '@core/auth/auth.selectors'; +import { Direction, SortOrder } from '@shared/models/page/sort-order'; +import { MAX_SAFE_PAGE_SIZE, PageLink } from '@shared/models/page/page-link'; +import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; +import { map } from 'rxjs/operators'; +import { AlarmComment, AlarmCommentInfo, AlarmCommentType } from '@shared/models/alarm.models'; +import { UtilsService } from '@core/services/utils.service'; +import { EntityType } from '@shared/models/entity-type.models'; + +interface AlarmCommentsDisplayData { + commentId?: string, + displayName?: string, + createdDateAgo?: string, + edit?: boolean, + isEdited?: boolean, + editedDateAgo?: string, + showActions?: boolean, + commentText?: string, + isSystemComment?: boolean, + avatarBgColor?: string +} + +@Component({ + selector: 'tb-alarm-comment', + templateUrl: './alarm-comment.component.html', + styleUrls: ['./alarm-comment.component.scss'] +}) +export class AlarmCommentComponent implements OnInit { + @Input() + alarmId: string; + + @Input() + commentsHeaderEnabled: boolean = true; + + authUser: AuthUser; + + alarmCommentFormGroup: FormGroup; + + alarmComments: Array; + + displayData: Array = new Array(); + + alarmCommentSortOrder: SortOrder = { + property: 'createdTime', + direction: Direction.DESC + }; + + editMode: boolean = false; + + userDisplayName$ = this.store.pipe( + select(selectUserDetails), + map((user) => this.getUserDisplayName(user)) + ); + + currentUserDisplayName: string; + currentUserAvatarColor: string; + + constructor(protected store: Store, + private translate: TranslateService, + private alarmCommentService: AlarmCommentService, + public fb: FormBuilder, + private dialogService: DialogService, + public dateAgoPipe: DateAgoPipe, + private utilsService: UtilsService) { + + this.authUser = getCurrentAuthUser(store); + + this.alarmCommentFormGroup = this.fb.group( + { + alarmCommentEdit: [''], + alarmComment: [''] + } + ); + } + + ngOnInit() { + this.loadAlarmComments(); + this.currentUserAvatarColor = this.utilsService.stringToHslColor(this.currentUserDisplayName, + 60, 40); + } + + loadAlarmComments(): void { + this.alarmCommentService.getAlarmComments(this.alarmId, new PageLink(MAX_SAFE_PAGE_SIZE, 0, null, + this.alarmCommentSortOrder), {ignoreLoading: true}).subscribe( + (pagedData) => { + this.alarmComments = pagedData.data; + this.displayData.length = 0; + for (let alarmComment of pagedData.data) { + let displayDataElement: AlarmCommentsDisplayData = {}; + displayDataElement.createdDateAgo = this.dateAgoPipe.transform(alarmComment.createdTime); + displayDataElement.commentText = alarmComment.comment.text; + displayDataElement.isSystemComment = alarmComment.type === AlarmCommentType.SYSTEM; + if (alarmComment.type === AlarmCommentType.OTHER) { + displayDataElement.commentId = alarmComment.id.id; + displayDataElement.displayName = this.getUserDisplayName(alarmComment); + displayDataElement.edit = false; + displayDataElement.isEdited = alarmComment.comment.edited; + displayDataElement.editedDateAgo = this.dateAgoPipe.transform(alarmComment.comment.editedOn).toLowerCase(); + displayDataElement.showActions = false; + displayDataElement.isSystemComment = false; + displayDataElement.avatarBgColor = this.utilsService.stringToHslColor(displayDataElement.displayName, + 40, 60); + } + this.displayData.push(displayDataElement); + } + } + ) + } + + changeSortDirection() { + let currentDirection = this.alarmCommentSortOrder.direction; + this.alarmCommentSortOrder.direction = currentDirection === Direction.DESC ? Direction.ASC : Direction.DESC; + this.loadAlarmComments(); + } + + saveComment(): void { + const commentInputValue: string = this.getAlarmCommentFormControl().value; + if (commentInputValue) { + const comment: AlarmComment = { + alarmId: { + id: this.alarmId, + entityType: EntityType.ALARM + }, + type: AlarmCommentType.OTHER, + comment: { + text: commentInputValue + } + } + this.doSave(comment); + this.clearCommentInput(); + } + } + + saveEditedComment(commentId: string): void { + const commentEditInputValue: string = this.getAlarmCommentEditFormControl().value; + if (commentEditInputValue) { + const editedComment: AlarmComment = this.getAlarmCommentById(commentId); + editedComment.comment.text = commentEditInputValue; + this.doSave(editedComment); + this.clearCommentEditInput(); + this.editMode = false; + this.getAlarmCommentFormControl().enable({emitEvent: false}); + } + } + + private doSave(comment: AlarmComment): void { + this.alarmCommentService.saveAlarmComment(this.alarmId, comment, {ignoreLoading: true}).subscribe( + () => { + this.loadAlarmComments(); + } + ) + } + + editComment(commentId: string): void { + const commentDisplayData = this.getDataElementByCommentId(commentId); + commentDisplayData.edit = true; + this.editMode = true; + this.getAlarmCommentEditFormControl().patchValue(commentDisplayData.commentText); + this.getAlarmCommentFormControl().disable({emitEvent: false}); + } + + cancelEdit(commentId: string): void { + const commentDisplayData = this.getDataElementByCommentId(commentId); + commentDisplayData.edit = false; + this.editMode = false; + this.getAlarmCommentFormControl().enable({emitEvent: false}); + } + + deleteComment(commentId: string): void { + const alarmCommentInfo: AlarmComment = this.getAlarmCommentById(commentId); + const commentText: string = alarmCommentInfo.comment.text; + this.dialogService.confirm( + this.translate.instant('alarm-comment.delete-alarm-comment'), + commentText, + this.translate.instant('action.cancel'), + this.translate.instant('action.delete')).subscribe( + (result) => { + if (result) { + this.alarmCommentService.deleteAlarmComments(this.alarmId, commentId, {ignoreLoading: true}) + .subscribe(() => { + this.loadAlarmComments(); + } + ) + } + } + ) + } + + getSortDirectionIcon() { + return this.alarmCommentSortOrder.direction === Direction.DESC ? 'arrow_downward' : 'arrow_upward' + } + + isDirectionAscending() { + return this.alarmCommentSortOrder.direction === Direction.ASC; + } + + isDirectionDescending() { + return this.alarmCommentSortOrder.direction === Direction.DESC; + } + + onCommentMouseEnter(commentId: string, displayDataIndex: number): void { + if (!this.editMode) { + const alarmUserId = this.getAlarmCommentById(commentId).userId.id; + if (this.authUser.userId === alarmUserId) { + this.displayData[displayDataIndex].showActions = true; + } + } + } + + onCommentMouseLeave(displayDataIndex: number): void { + this.displayData[displayDataIndex].showActions = false; + } + + getUserInitials(userName: string): string { + let initials = ''; + const userNameSplit = userName.split(' '); + initials += userNameSplit[0].charAt(0).toUpperCase(); + if (userNameSplit.length > 1) { + initials += userNameSplit[userNameSplit.length - 1].charAt(0).toUpperCase(); + } + return initials; + } + + getCurrentUserBgColor(userDisplayName: string) { + return this.utilsService.stringToHslColor(userDisplayName, 40, 60); + } + + private getUserDisplayName(alarmCommentInfo: AlarmCommentInfo | User): string { + let name = ''; + if ((alarmCommentInfo.firstName && alarmCommentInfo.firstName.length > 0) || + (alarmCommentInfo.lastName && alarmCommentInfo.lastName.length > 0)) { + if (alarmCommentInfo.firstName) { + name += alarmCommentInfo.firstName; + } + if (alarmCommentInfo.lastName) { + if (name.length > 0) { + name += ' '; + } + name += alarmCommentInfo.lastName; + } + } else { + name = alarmCommentInfo.email; + } + return name; + } + + getAlarmCommentFormControl(): AbstractControl { + return this.alarmCommentFormGroup.get('alarmComment'); + } + + getAlarmCommentEditFormControl(): AbstractControl { + return this.alarmCommentFormGroup.get('alarmCommentEdit'); + } + + private clearCommentInput(): void { + this.getAlarmCommentFormControl().patchValue(''); + } + + private clearCommentEditInput(): void { + this.getAlarmCommentEditFormControl().patchValue(''); + } + + private getAlarmCommentById(id: string): AlarmComment { + return this.alarmComments.find(comment => comment.id.id === id); + } + + private getDataElementByCommentId(commentId: string): AlarmCommentsDisplayData { + return this.displayData.find(commentDisplayData => commentDisplayData.commentId === commentId); + } + +} diff --git a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html index 3bdb10db57..e2af9dcf24 100644 --- a/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html +++ b/ui-ngx/src/app/modules/home/components/alarm/alarm-details-dialog.component.html @@ -28,41 +28,17 @@
-
-
+
+
- - alarm.created-time - - alarm.originator -
-
- - alarm.start-time - - - - alarm.end-time - - - -
-
- - alarm.ack-time - - - - alarm.clear-time - + + alarm.created-time + -
@@ -74,18 +50,60 @@ +
+
alarm.status + +
- - + + + + + + alarm.advanced-info + + + +
+ + alarm.start-time + + + + alarm.end-time + + + +
+
+ + alarm.ack-time + + + + alarm.clear-time + + + +
+ + +
+
+
+ + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss index cb4a293072..2a77861e21 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.scss @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { +:host::ng-deep { width: 100%; height: 100%; display: block; @@ -26,6 +26,42 @@ &.invisible { visibility: hidden; } + .mat-mdc-cell { + .assignee-cell { + .assigned-container { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + .user-avatar { + display: inline-flex; + justify-content: center; + align-items: center; + margin-right: 8px; + border-radius: 50%; + width: 28px; + height: 28px; + min-width: 28px; + min-height: 28px; + color: white; + font-size: 13px; + font-weight: 700; + } + } + .unassigned-container { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + .material-icons.unassigned-icon { + width: 28px; + height: 28px; + font-size: 28px; + margin-right: 8px; + color: rgba(0, 0, 0, 0.38); + overflow: visible; + } + } + } + } } } span.no-data-found { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts index 35392e4f9e..5b233ba2d1 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/alarms-table-widget.component.ts @@ -93,7 +93,7 @@ import { } from '@home/components/widget/lib/display-columns-panel.component'; import { AlarmDataInfo, - alarmFields, + alarmFields, AlarmInfo, AlarmSearchStatus, alarmSeverityColors, alarmSeverityTranslations, @@ -127,6 +127,14 @@ import { entityFields } from '@shared/models/entity.models'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { ResizeObserver } from '@juggle/resize-observer'; import { hidePageSizePixelValue } from '@shared/models/constants'; +import { + ALARM_ASSIGNEE_PANEL_DATA, AlarmAssigneePanelComponent, + AlarmAssigneePanelData +} from '@home/components/alarm/alarm-assignee-panel.component'; +import { + AlarmCommentDialogComponent, + AlarmCommentDialogData +} from '@home/components/alarm/alarm-comment-dialog.component'; interface AlarmsTableWidgetSettings extends TableWidgetSettings { alarmsTitle: string; @@ -135,15 +143,18 @@ interface AlarmsTableWidgetSettings extends TableWidgetSettings { enableSelection: boolean; enableStatusFilter?: boolean; enableFilter: boolean; + displayComments: boolean; displayDetails: boolean; allowAcknowledgment: boolean; allowClear: boolean; + allowAssign: boolean; } interface AlarmWidgetActionDescriptor extends TableCellButtonActionDescriptor { details?: boolean; acknowledge?: boolean; clear?: boolean; + comments?: boolean; } @Component({ @@ -186,9 +197,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private alarmsTitlePattern: string; + private displayComments = false; private displayDetails = true; public allowAcknowledgment = true; private allowClear = true; + public allowAssign = true; private defaultPageSize = 10; private defaultSortOrder = '-' + alarmFields.createdTime.value; @@ -322,9 +335,11 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, private initializeConfig() { this.ctx.widgetActions = [this.searchAction, this.alarmFilterAction, this.columnDisplayAction]; + this.displayComments = isDefined(this.settings.displayComments) ? this.settings.displayComments : false; this.displayDetails = isDefined(this.settings.displayDetails) ? this.settings.displayDetails : true; this.allowAcknowledgment = isDefined(this.settings.allowAcknowledgment) ? this.settings.allowAcknowledgment : true; this.allowClear = isDefined(this.settings.allowClear) ? this.settings.allowClear : true; + this.allowAssign = isDefined(this.settings.allowAssign) ? this.settings.allowAssign : true; if (this.settings.alarmsTitle && this.settings.alarmsTitle.length) { this.alarmsTitlePattern = this.utils.customTranslation(this.settings.alarmsTitle, this.settings.alarmsTitle); @@ -418,6 +433,9 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, if (alarmField && alarmField.time) { keySettings.columnWidth = '120px'; } + if (alarmField && alarmField.keyName === alarmFields.assignee.keyName) { + keySettings.columnWidth = '120px' + } } this.stylesInfo[dataKey.def] = getCellStyleInfo(keySettings, 'value, alarm, ctx'); this.contentsInfo[dataKey.def] = getCellContentInfo(keySettings, 'value, alarm, ctx'); @@ -446,6 +464,16 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.sortOrderProperty = sortColumn ? sortColumn.def : null; const actionCellDescriptors: AlarmWidgetActionDescriptor[] = []; + if (this.displayComments) { + actionCellDescriptors.push( + { + displayName: this.translate.instant('alarm-comment.comments'), + icon: 'comment', + comments: true + } as AlarmWidgetActionDescriptor + ); + } + if (this.displayDetails) { actionCellDescriptors.push( { @@ -791,6 +819,8 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.ackAlarm($event, alarm); } else if (actionDescriptor.clear) { this.clearAlarm($event, alarm); + } else if (actionDescriptor.comments) { + this.openAlarmComments($event, alarm); } else { if ($event) { $event.stopPropagation(); @@ -954,6 +984,24 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, } } + private openAlarmComments($event: Event, alarm: AlarmDataInfo) { + if ($event) { + $event.stopPropagation(); + } + if (alarm && alarm.id && alarm.id.id !== NULL_UUID) { + this.dialog.open + (AlarmCommentDialogComponent, + { + disableClose: true, + panelClass: ['tb-dialog', 'tb-fullscreen-dialog'], + data: { + alarmId: alarm.id.id, + commentsHeaderEnabled: false + } + }).afterClosed() + } + } + private defaultContent(key: EntityColumn, contentInfo: CellContentInfo, value: any): any { if (isDefined(value)) { const alarmField = alarmFields[key.name]; @@ -966,7 +1014,10 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, return alarmStatusTranslations.get(value) ? this.translate.instant(alarmStatusTranslations.get(value)) : value; } else if (alarmField.value === alarmFields.originatorType.value) { return this.translate.instant(entityTypeTranslations.get(value).type); - } else { + } else if (alarmField.value === alarmFields.assignee.value) { + return ''; + } + else { return value; } } @@ -1013,6 +1064,84 @@ export class AlarmsTableWidgetComponent extends PageComponent implements OnInit, this.cellStyleCache.length = 0; this.rowStyleCache.length = 0; } + + getUserDisplayName(entity: AlarmInfo) { + let displayName = ''; + if ((entity.assignee.firstName && entity.assignee.firstName.length > 0) || + (entity.assignee.lastName && entity.assignee.lastName.length > 0)) { + if (entity.assignee.firstName) { + displayName += entity.assignee.firstName; + } + if (entity.assignee.lastName) { + if (displayName.length > 0) { + displayName += ' '; + } + displayName += entity.assignee.lastName; + } + } else { + displayName = entity.assignee.email; + } + return displayName; + } + + getUserInitials(entity: AlarmInfo): string { + let initials = ''; + if (entity.assignee.firstName && entity.assignee.firstName.length || + entity.assignee.lastName && entity.assignee.lastName.length) { + if (entity.assignee.firstName) { + initials += entity.assignee.firstName.charAt(0); + } + if (entity.assignee.lastName) { + initials += entity.assignee.lastName.charAt(0); + } + } else { + initials += entity.assignee.email.charAt(0); + } + return initials.toUpperCase(); + } + + getAvatarBgColor(entity: AlarmInfo) { + return this.utils.stringToHslColor(this.getUserDisplayName(entity), 40, 60); + } + + openAlarmAssigneePanel($event: Event, entity: AlarmInfo) { + if ($event) { + $event.stopPropagation(); + } + const target = $event.target || $event.srcElement || $event.currentTarget; + const config = new OverlayConfig(); + config.backdropClass = 'cdk-overlay-transparent-backdrop'; + config.hasBackdrop = true; + const connectedPosition: ConnectedPosition = { + originX: 'end', + originY: 'bottom', + overlayX: 'end', + overlayY: 'top' + }; + config.positionStrategy = this.overlay.position().flexibleConnectedTo(target as HTMLElement) + .withPositions([connectedPosition]); + config.minWidth = '260px'; + const overlayRef = this.overlay.create(config); + overlayRef.backdropClick().subscribe(() => { + overlayRef.dispose(); + }); + const providers: StaticProvider[] = [ + { + provide: ALARM_ASSIGNEE_PANEL_DATA, + useValue: { + alarmId: entity.id.id, + assigneeId: entity.assigneeId?.id + } as AlarmAssigneePanelData + }, + { + provide: OverlayRef, + useValue: overlayRef + } + ]; + const injector = Injector.create({parent: this.viewContainerRef.injector, providers}); + overlayRef.attach(new ComponentPortal(AlarmAssigneePanelComponent, + this.viewContainerRef, injector)); + } } class AlarmsDatasource implements DataSource { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts index c0e5aa4212..aae7d2c37a 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/flot-widget.ts @@ -341,6 +341,7 @@ export class TbFlot { private init($element: JQuery, subscription: IWidgetSubscription) { this.$element = $element; + this.$element.css('letter-spacing', 'normal'); this.subscription = subscription; this.comparisonEnabled = this.subscription ? this.subscription.comparisonEnabled : this.settings.comparisonEnabled; if (this.comparisonEnabled) { diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts index e2dfff11ea..a78dd27431 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/leaflet-map.ts @@ -520,6 +520,13 @@ export default abstract class LeafletMap { this.createdControlButtonTooltip(); } + resetState() { + if (this.options.initDragMode) { + this.initDragModeIgnoreUpdateBoundsSet = false; + this.ignoreUpdateBounds = false; + } + } + private createdControlButtonTooltip() { import('tooltipster').then(() => { if ($.tooltipster) { @@ -841,7 +848,7 @@ export default abstract class LeafletMap { this.fitBounds(bounds); } } - if (this.options.initDragMode && !this.initDragModeIgnoreUpdateBoundsSet) { + if (this.options.initDragMode && !this.initDragModeIgnoreUpdateBoundsSet && bounds.isValid()) { this.initDragModeIgnoreUpdateBoundsSet = true; this.ignoreUpdateBounds = true; } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts index 143c106baa..f69c4dcc76 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/maps/map-widget2.ts @@ -83,6 +83,9 @@ export class MapWidgetController implements MapWidgetInterface { dynamic: true }; this.map.setLoading(true); + this.ctx.defaultSubscription.paginatedDataSubscriptionUpdated.subscribe(() => { + this.map.resetState(); + }); this.ctx.defaultSubscription.subscribeAllForPaginatedData(this.pageLink, null); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html index d182c26dbe..1c3df367c4 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.html @@ -16,4 +16,7 @@ --> diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts index c71affe5c1..98c196f393 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/markdown-widget.component.ts @@ -14,7 +14,7 @@ /// limitations under the License. /// -import { ChangeDetectorRef, Component, HostBinding, Inject, Input, OnInit, Type } from '@angular/core'; +import { ChangeDetectorRef, Component, Inject, Input, OnInit, Type } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { WidgetContext } from '@home/models/widget-component.models'; import { Store } from '@ngrx/store'; @@ -25,7 +25,8 @@ import { createLabelFromPattern, flatDataWithoutOverride, formattedDataFormDatasourceData, - hashCode, isDefinedAndNotNull, + hashCode, + isDefinedAndNotNull, isNotEmptyStr, parseFunction, safeExecute @@ -39,13 +40,14 @@ interface MarkdownWidgetSettings { markdownTextPattern: string; useMarkdownTextFunction: boolean; markdownTextFunction: string; + applyDefaultMarkdownStyle: boolean; markdownCss: string; } type MarkdownTextFunction = (data: FormattedData[], ctx: WidgetContext) => string; @Component({ - selector: 'tb-markdown-widget ', + selector: 'tb-markdown-widget', templateUrl: './markdown-widget.component.html' }) export class MarkdownWidgetComponent extends PageComponent implements OnInit { @@ -53,7 +55,6 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { settings: MarkdownWidgetSettings; markdownTextFunction: MarkdownTextFunction; - @HostBinding('class') markdownClass: string; @Input() @@ -61,6 +62,9 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { markdownText: string; + additionalStyles: string[]; + + applyDefaultMarkdownStyle = true; constructor(protected store: Store, private utils: UtilsService, @@ -74,14 +78,18 @@ export class MarkdownWidgetComponent extends PageComponent implements OnInit { this.settings = this.ctx.settings; this.markdownTextFunction = this.settings.useMarkdownTextFunction ? parseFunction(this.settings.markdownTextFunction, ['data', 'ctx']) : null; - this.markdownClass = 'markdown-widget'; - const cssString = this.settings.markdownCss; + let cssString = this.settings.markdownCss; if (isNotEmptyStr(cssString)) { const cssParser = new cssjs(); - cssParser.testMode = false; - this.markdownClass += '-' + hashCode(cssString); + this.markdownClass = 'markdown-widget-' + hashCode(cssString); cssParser.cssPreviewNamespace = this.markdownClass; - cssParser.createStyleElement(this.markdownClass, cssString); + cssParser.testMode = false; + cssString = cssParser.applyNamespacing(cssString); + cssString = cssParser.getCSSForEditor(cssString); + this.additionalStyles = [cssString]; + } + if (isDefinedAndNotNull(this.settings.applyDefaultMarkdownStyle)) { + this.applyDefaultMarkdownStyle = this.settings.applyDefaultMarkdownStyle; } const pageSize = isDefinedAndNotNull(this.ctx.widgetConfig.pageSize) && this.ctx.widgetConfig.pageSize > 0 ? this.ctx.widgetConfig.pageSize : 16384; diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss index 07dc221e6f..d0dd324e52 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/lib/multiple-input-widget.component.scss @@ -66,13 +66,17 @@ :host ::ng-deep { .tb-multiple-input { - .mat-mdc-slide-toggle .mdc-form-field > label { - display: flex; - align-items: center; - - .mat-icon { - margin-left: 8px; - margin-right: 8px; + .mat-mdc-slide-toggle .mdc-form-field { + width: 100%; + & > label { + display: flex; + align-items: center; + width: 100%; + margin: 0; + .mat-icon { + margin-left: 8px; + margin-right: 8px; + } } } } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html index f2ce2642bd..8820225e6c 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.html @@ -58,6 +58,9 @@
+ + {{ 'widgets.table.display-alarm-comments' | translate }} + {{ 'widgets.table.display-alarm-details' | translate }} @@ -67,6 +70,9 @@ {{ 'widgets.table.allow-alarms-clear' | translate }} + + {{ 'widgets.table.allow-alarms-assign' | translate }} +
{{ 'widgets.table.display-pagination' | translate }} diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts index 953bde001c..655f915a4d 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/alarm/alarms-table-widget-settings.component.ts @@ -51,6 +51,7 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent displayDetails: true, allowAcknowledgment: true, allowClear: true, + allowAssign: true, displayPagination: true, defaultPageSize: 10, defaultSortOrder: '-createdTime', @@ -72,6 +73,8 @@ export class AlarmsTableWidgetSettingsComponent extends WidgetSettingsComponent displayDetails: [settings.displayDetails, []], allowAcknowledgment: [settings.allowAcknowledgment, []], allowClear: [settings.allowClear, []], + allowAssign: [settings.allowAssign, []], + displayComments: [settings.displayComments, []], displayPagination: [settings.displayPagination, []], defaultPageSize: [settings.defaultPageSize, [Validators.min(1)]], defaultSortOrder: [settings.defaultSortOrder, []], diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.html b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.html index 97dcf167f2..cf5415afe0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.html +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.html @@ -31,6 +31,9 @@ label="{{ 'widgets.markdown.markdown-text-pattern' | translate }}" helpId="widget/editor/widget_js_markdown_pattern"> + + {{ 'widgets.markdown.apply-default-markdown-style' | translate }} + diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts index 0c24a38361..9aac7efb20 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/settings/cards/markdown-widget-settings.component.ts @@ -43,6 +43,7 @@ export class MarkdownWidgetSettingsComponent extends WidgetSettingsComponent { useMarkdownTextFunction: false, markdownTextPattern: '# Markdown/HTML card \\n - **Current entity**: **${entityName}**. \\n - **Current value**: **${Random}**.', markdownTextFunction: 'return \'# Some title\\\\n - Entity name: \' + data[0][\'entityName\'];', + applyDefaultMarkdownStyle: true, markdownCss: '' }; } @@ -52,6 +53,7 @@ export class MarkdownWidgetSettingsComponent extends WidgetSettingsComponent { useMarkdownTextFunction: [settings.useMarkdownTextFunction, []], markdownTextPattern: [settings.markdownTextPattern, []], markdownTextFunction: [settings.markdownTextFunction, []], + applyDefaultMarkdownStyle: [settings.applyDefaultMarkdownStyle, []], markdownCss: [settings.markdownCss, []] }); } diff --git a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts index 3ef074c91a..68e89ef8d0 100644 --- a/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts +++ b/ui-ngx/src/app/modules/home/components/widget/lib/table-widget.models.ts @@ -457,6 +457,7 @@ export function constructTableCssString(widgetConfig: WidgetConfig): string { '}\n' + '.mat-mdc-table .mat-mdc-cell {\n' + 'color: ' + mdDark + ';\n' + + 'background-color: rgba(0, 0, 0, 0);\n' + '}\n' + '.mat-mdc-table .mat-mdc-cell button.mat-mdc-icon-button mat-icon {\n' + 'color: ' + mdDarkSecondary + ';\n' + diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss index b2b14dcc3e..4c7264ce29 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.scss @@ -13,9 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -:host { - -} tb-widget.tb-widget { position: relative; @@ -92,10 +89,20 @@ div.tb-widget { min-height: 20px; font-size: 20px; line-height: 20px; + svg { + width: 20px; + height: 20px; + } } } } + .tb-widget-title, .tb-widget-actions { + .mat-icon { + vertical-align: bottom; + } + } + .tb-widget-content { &.tb-no-interaction { pointer-events: none; diff --git a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts index dd558f2386..bf7042d233 100644 --- a/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts +++ b/ui-ngx/src/app/modules/home/components/widget/widget-container.component.ts @@ -17,18 +17,25 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, - Component, ElementRef, - EventEmitter, HostBinding, Inject, - Input, OnDestroy, + Component, + ElementRef, + EventEmitter, + HostBinding, + Inject, + Input, + OnDestroy, OnInit, - Output, Renderer2, ViewChild + Output, + Renderer2, + ViewChild, + ViewEncapsulation } from '@angular/core'; import { PageComponent } from '@shared/components/page.component'; import { DashboardWidget, DashboardWidgets } from '@home/models/dashboard-component.models'; import { Store } from '@ngrx/store'; import { AppState } from '@core/core.state'; import { SafeStyle } from '@angular/platform-browser'; -import { guid, hashCode, isNotEmptyStr } from '@core/utils'; +import { guid, isNotEmptyStr } from '@core/utils'; import cssjs from '@core/css/css'; import { DOCUMENT } from '@angular/common'; import { GridsterItemComponent } from 'angular-gridster2'; @@ -52,6 +59,7 @@ export class WidgetComponentAction { selector: 'tb-widget-container', templateUrl: './widget-container.component.html', styleUrls: ['./widget-container.component.scss'], + encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush }) export class WidgetContainerComponent extends PageComponent implements OnInit, OnDestroy { diff --git a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss index 1a19de8b06..85c5dfa42e 100644 --- a/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss +++ b/ui-ngx/src/app/modules/home/menu/menu-toggle.component.scss @@ -18,6 +18,7 @@ .tb-toggle-icon { display: inline-block; width: 15px; + min-width: 15px; margin: auto 0 auto auto; background-size: 100% auto; transition: transform .3s ease-in-out; diff --git a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss index e0b8b24110..54cbf11c53 100644 --- a/ui-ngx/src/app/modules/home/menu/side-menu.component.scss +++ b/ui-ngx/src/app/modules/home/menu/side-menu.component.scss @@ -53,12 +53,19 @@ mat-icon { margin-right: 8px; margin-left: 0; + min-width: 1.125rem; + min-height: 1.125rem; } span.mdc-button__label { width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + display: inline-flex; + & > span { + overflow: hidden; + text-overflow: ellipsis; + } .tb-toggle-icon { padding-top: 12px; padding-bottom: 12px; diff --git a/ui-ngx/src/app/modules/home/models/widget-component.models.ts b/ui-ngx/src/app/modules/home/models/widget-component.models.ts index 6d8b710b85..09949a29b3 100644 --- a/ui-ngx/src/app/modules/home/models/widget-component.models.ts +++ b/ui-ngx/src/app/modules/home/models/widget-component.models.ts @@ -89,6 +89,7 @@ import { TbPopoverComponent } from '@shared/components/popover.component'; import { EntityId } from '@shared/models/id/entity-id'; import { AlarmQuery, AlarmSearchStatus, AlarmStatus} from '@app/shared/models/alarm.models'; import { MillisecondsToTimeStringPipe, TelemetrySubscriber } from '@app/shared/public-api'; +import { UserId } from '@shared/models/id/user-id'; export interface IWidgetAction { name: string; @@ -415,8 +416,8 @@ export class WidgetContext { return new TimePageLink(pageSize, page, textSearch, sortOrder, startTime, endTime); } - alarmQuery(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, fetchOriginator: boolean) { - return new AlarmQuery(entityId, pageLink, searchStatus, status, fetchOriginator); + alarmQuery(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, fetchOriginator: boolean, assigneeId: UserId) { + return new AlarmQuery(entityId, pageLink, searchStatus, status, fetchOriginator, assigneeId); } } diff --git a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html index 981d83d5be..813b63d3f0 100644 --- a/ui-ngx/src/app/modules/login/pages/login/create-password.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/create-password.component.html @@ -16,7 +16,7 @@ -->
- + login.create-password @@ -47,7 +47,7 @@ - diff --git a/ui-ngx/src/app/modules/login/pages/login/login.component.html b/ui-ngx/src/app/modules/login/pages/login/login.component.html index 3d04afffe9..f5797ae574 100644 --- a/ui-ngx/src/app/modules/login/pages/login/login.component.html +++ b/ui-ngx/src/app/modules/login/pages/login/login.component.html @@ -16,10 +16,10 @@ -->