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 e54c6a9e7e..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, @@ -31,11 +95,255 @@ CREATE TABLE IF NOT EXISTS user_settings ( CONSTRAINT fk_user_id FOREIGN KEY (user_id) REFERENCES tb_user(id) ON DELETE CASCADE ); -ALTER TABLE user_credentials - ADD COLUMN IF NOT EXISTS additional_info varchar NOT NULL DEFAULT '{}'; +-- ALARM COMMENTS END -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'; +-- 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 +$$; -UPDATE tb_user SET additional_info = tb_user.additional_info::jsonb - 'userPasswordHistory'; \ No newline at end of file +-- 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/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/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 8c8a8cdfb8..a060335713 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/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(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/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/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 4afb12e638..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; @@ -363,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(); } } @@ -830,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/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..303e91ee80 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); @@ -688,7 +690,7 @@ abstract public class BaseDeviceEdgeTest extends AbstractEdgeTest { edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(5, TimeUnit.SECONDS)); + Assert.assertTrue(onUpdateCallback.getSubscribeLatch().await(30, TimeUnit.SECONDS)); Assert.assertEquals(JacksonUtil.OBJECT_MAPPER.createObjectNode().put(attrKey, attrValue), JacksonUtil.fromBytes(onUpdateCallback.getPayloadBytes())); 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..e2c5985553 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 @@ -19,9 +19,8 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.Assertions; import org.springframework.beans.factory.annotation.Autowired; import org.thingsboard.server.common.data.EntityInfo; import org.thingsboard.server.common.data.ResourceType; @@ -45,6 +44,7 @@ import java.util.Base64; import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @DaoSqlTest @@ -115,10 +115,6 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { .andExpect(status().isOk()); } - @SuppressWarnings("deprecation") - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Test public void testSaveResourceWithMaxSumDataSizeOutOfLimit() throws Exception { loginSysAdmin(); @@ -137,9 +133,9 @@ public class BaseTbResourceServiceTest extends AbstractControllerTest { Assert.assertEquals(1, resourceService.sumDataSizeByTenantId(tenantId)); try { - thrown.expect(DataValidationException.class); - thrown.expectMessage(String.format("Failed to create the tb resource, files size limit is exhausted %d bytes!", limit)); - createResource("test1", 1 + DEFAULT_FILE_NAME); + assertThatThrownBy(() -> createResource("test1", 1 + DEFAULT_FILE_NAME)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Failed to create the tb resource, files size limit is exhausted %d bytes!", limit); } finally { defaultTenantProfile.getProfileData().setConfiguration(DefaultTenantProfileConfiguration.builder().maxResourcesInBytes(0).build()); loginSysAdmin(); @@ -237,7 +233,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 +252,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 +280,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/lwm2m/AbstractLwM2MIntegrationTest.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java index ef00a3050e..c92369e3c2 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/AbstractLwM2MIntegrationTest.java @@ -70,7 +70,7 @@ import org.thingsboard.server.service.telemetry.cmd.v2.LatestValueCmd; import org.thingsboard.server.transport.AbstractTransportIntegrationTest; import org.thingsboard.server.transport.lwm2m.client.LwM2MTestClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.io.IOException; import java.net.ServerSocket; @@ -108,7 +108,7 @@ import static org.thingsboard.server.transport.lwm2m.Lwm2mTestHelper.LwM2MProfil public abstract class AbstractLwM2MIntegrationTest extends AbstractTransportIntegrationTest { @SpyBean - DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; + LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; @Autowired private LwM2mClientContext clientContextTest; diff --git a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java index 7cb5f88733..449ac4c2ae 100644 --- a/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java +++ b/application/src/test/java/org/thingsboard/server/transport/lwm2m/client/LwM2MTestClient.java @@ -45,7 +45,7 @@ import org.junit.Assert; import org.mockito.Mockito; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClient; import org.thingsboard.server.transport.lwm2m.server.client.LwM2mClientContext; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import java.io.IOException; @@ -109,12 +109,12 @@ public class LwM2MTestClient { private LwM2MLocationParams locationParams; private LwM2mTemperatureSensor lwM2MTemperatureSensor; private Set clientStates; - private DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; + private LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandlerTest; private LwM2mClientContext clientContext; public void init(Security security, Configuration coapConfig, int port, boolean isRpc, boolean isBootstrap, int shortServerId, int shortServerIdBs, Security securityBs, - DefaultLwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, + LwM2mUplinkMsgHandler defaultLwM2mUplinkMsgHandler, LwM2mClientContext clientContext) throws InvalidDDFFileException, IOException { Assert.assertNull("client already initialized", leshanClient); this.defaultLwM2mUplinkMsgHandlerTest = defaultLwM2mUplinkMsgHandler; 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/logback-test.xml b/application/src/test/resources/logback-test.xml index 23e6d8c2f6..953b7094a4 100644 --- a/application/src/test/resources/logback-test.xml +++ b/application/src/test/resources/logback-test.xml @@ -21,6 +21,13 @@ + + + + + + + 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/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/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/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java index f4b52a0f5f..3eb096cd38 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/DefaultLwM2mTransportService.java @@ -25,6 +25,7 @@ import org.eclipse.leshan.core.node.codec.DefaultLwM2mEncoder; import org.eclipse.leshan.server.californium.LeshanServer; import org.eclipse.leshan.server.californium.LeshanServerBuilder; import org.eclipse.leshan.server.californium.registration.CaliforniumRegistrationStore; +import org.springframework.context.annotation.DependsOn; import org.springframework.stereotype.Component; import org.thingsboard.server.cache.ota.OtaPackageDataCache; import org.thingsboard.server.common.data.DataConstants; @@ -35,7 +36,7 @@ import org.thingsboard.server.transport.lwm2m.config.LwM2MTransportServerConfig; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MAuthorizer; import org.thingsboard.server.transport.lwm2m.secure.TbLwM2MDtlsCertificateVerifier; import org.thingsboard.server.transport.lwm2m.server.store.TbSecurityStore; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2mValueConverterImpl; import javax.annotation.PreDestroy; @@ -56,6 +57,7 @@ import static org.thingsboard.server.transport.lwm2m.server.ota.DefaultLwM2MOtaU @Slf4j @Component +@DependsOn({"lwM2mDownlinkMsgHandler", "lwM2mUplinkMsgHandler"}) @TbLwM2mTransportComponent @RequiredArgsConstructor public class DefaultLwM2mTransportService implements LwM2MTransportService { @@ -66,7 +68,7 @@ public class DefaultLwM2mTransportService implements LwM2MTransportService { private final LwM2mTransportContext context; private final LwM2MTransportServerConfig config; private final OtaPackageDataCache otaPackageDataCache; - private final DefaultLwM2mUplinkMsgHandler handler; + private final LwM2mUplinkMsgHandler handler; private final CaliforniumRegistrationStore registrationStore; private final TbSecurityStore securityStore; private final TbLwM2MDtlsCertificateVerifier certificateVerifier; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java index 49a77d1eea..d624badc1a 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/client/LwM2mClientContextImpl.java @@ -42,7 +42,7 @@ import org.thingsboard.server.transport.lwm2m.server.ota.LwM2MOtaUpdateService; import org.thingsboard.server.transport.lwm2m.server.session.LwM2MSessionManager; import org.thingsboard.server.transport.lwm2m.server.store.TbLwM2MClientStore; import org.thingsboard.server.transport.lwm2m.server.store.TbMainSecurityStore; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil; import java.util.Arrays; @@ -75,7 +75,7 @@ public class LwM2mClientContextImpl implements LwM2mClientContext { @Autowired @Lazy - private DefaultLwM2mUplinkMsgHandler defaultLwM2MUplinkMsgHandler; + private LwM2mUplinkMsgHandler defaultLwM2MUplinkMsgHandler; @Autowired @Lazy private LwM2MOtaUpdateService otaUpdateService; diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java index 735fa36e1e..ecc57a7458 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/downlink/DefaultLwM2mDownlinkMsgHandler.java @@ -85,6 +85,7 @@ import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.RejectedExecutionException; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -103,7 +104,7 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.ge import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.validateVersionedId; @Slf4j -@Service +@Service("lwM2mDownlinkMsgHandler") @TbLwM2mTransportComponent @RequiredArgsConstructor public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService implements LwM2mDownlinkMsgHandler { @@ -124,6 +125,7 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im @PreDestroy public void destroy() { + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } @@ -521,16 +523,22 @@ public class DefaultLwM2mDownlinkMsgHandler extends LwM2MExecutorAwareService im private , T extends LwM2mResponse> void handleDownlinkError(LwM2mClient client, R request, DownlinkRequestCallback callback, Exception e) { log.trace("[{}] Received downlink error: {}.", client.getEndpoint(), e); - client.updateLastUplinkTime(); - executor.submit(() -> { - if (e instanceof TimeoutException || e instanceof ClientSleepingException) { - log.trace("[{}] Received {}, client is probably sleeping", client.getEndpoint(), e.getClass().getSimpleName()); - clientContext.asleep(client); - } else { - log.trace("[{}] Received {}", client.getEndpoint(), e.getClass().getSimpleName()); - } - callback.onError(toString(request), e); - }); + try { + client.updateLastUplinkTime(); + executor.submit(() -> { + if (e instanceof TimeoutException || e instanceof ClientSleepingException) { + log.trace("[{}] Received {}, client is probably sleeping", client.getEndpoint(), e.getClass().getSimpleName()); + clientContext.asleep(client); + } else { + log.trace("[{}] Received {}", client.getEndpoint(), e.getClass().getSimpleName()); + } + callback.onError(toString(request), e); + }); + } catch (RejectedExecutionException ree) { + log.warn("[{}] Can not handle downlink error. Executor already down", client.getEndpoint(), ree); + } catch (Exception exception) { + log.warn("[{}] Can not handle downlink error", client.getEndpoint(), exception); + } } private WriteRequest getWriteRequestSingleResource(ResourceModel.Type type, ContentFormat contentFormat, int objectId, int instanceId, int resourceId, Object value) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java index 153e3894fb..f59643cbeb 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/ota/DefaultLwM2MOtaUpdateService.java @@ -146,6 +146,7 @@ public class DefaultLwM2MOtaUpdateService extends LwM2MExecutorAwareService impl @PreDestroy public void destroy() { + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java index c790d2495c..25e3a6b591 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/DefaultLwM2mUplinkMsgHandler.java @@ -20,6 +20,8 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; +import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.eclipse.leshan.core.ResponseCode; import org.eclipse.leshan.core.model.ObjectModel; @@ -32,6 +34,7 @@ import org.eclipse.leshan.core.node.LwM2mPath; import org.eclipse.leshan.core.node.LwM2mResource; import org.eclipse.leshan.core.node.LwM2mResourceInstance; import org.eclipse.leshan.core.node.LwM2mSingleResource; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.*; import org.eclipse.leshan.core.request.WriteRequest.Mode; @@ -122,69 +125,41 @@ import static org.thingsboard.server.transport.lwm2m.utils.LwM2MTransportUtil.fr @Slf4j -@Service +@Service("lwM2mUplinkMsgHandler") @TbLwM2mTransportComponent +@RequiredArgsConstructor public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService implements LwM2mUplinkMsgHandler { - public LwM2mValueConverterImpl converter; + @Getter + private final LwM2mValueConverter converter = LwM2mValueConverterImpl.getInstance();; private final TransportService transportService; private final LwM2mTransportContext context; + @Lazy private final LwM2MAttributesService attributesService; private final LwM2MSessionManager sessionManager; + @Lazy private final LwM2MOtaUpdateService otaService; private final LwM2MTransportServerConfig config; private final LwM2MTelemetryLogService logService; private final LwM2mTransportServerHelper helper; private final TbLwM2MDtlsSessionStore sessionStore; private final LwM2mClientContext clientContext; - private final LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler; + private final LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler; //Do not use Lazy because we need live executor to handle msgs private final LwM2mVersionedModelProvider modelProvider; private final RegistrationStore registrationStore; private final TbLwM2mSecurityStore securityStore; private final LwM2MModelConfigService modelConfigService; - public DefaultLwM2mUplinkMsgHandler(TransportService transportService, - LwM2MTransportServerConfig config, - LwM2mTransportServerHelper helper, - LwM2mClientContext clientContext, - LwM2MTelemetryLogService logService, - LwM2MSessionManager sessionManager, - @Lazy LwM2MOtaUpdateService otaService, - @Lazy LwM2MAttributesService attributesService, - @Lazy LwM2mDownlinkMsgHandler defaultLwM2MDownlinkMsgHandler, - LwM2mTransportContext context, - TbLwM2MDtlsSessionStore sessionStore, - LwM2mVersionedModelProvider modelProvider, - RegistrationStore registrationStore, - TbLwM2mSecurityStore securityStore, - LwM2MModelConfigService modelConfigService) { - this.transportService = transportService; - this.sessionManager = sessionManager; - this.attributesService = attributesService; - this.otaService = otaService; - this.config = config; - this.helper = helper; - this.clientContext = clientContext; - this.logService = logService; - this.defaultLwM2MDownlinkMsgHandler = defaultLwM2MDownlinkMsgHandler; - this.context = context; - this.sessionStore = sessionStore; - this.modelProvider = modelProvider; - this.registrationStore = registrationStore; - this.securityStore = securityStore; - this.modelConfigService = modelConfigService; - } - @PostConstruct public void init() { super.init(); this.context.getScheduler().scheduleAtFixedRate(this::reportActivity, new Random().nextInt((int) config.getSessionReportTimeout()), config.getSessionReportTimeout(), TimeUnit.MILLISECONDS); - this.converter = LwM2mValueConverterImpl.getInstance(); } @PreDestroy public void destroy() { + log.trace("Destroying {}", getClass().getSimpleName()); super.destroy(); } @@ -955,6 +930,7 @@ public class DefaultLwM2mUplinkMsgHandler extends LwM2MExecutorAwareService impl * * @param lwM2MClient - LwM2M Client */ + @Override public void initAttributes(LwM2mClient lwM2MClient, boolean logFailedUpdateOfNonChangedValue) { Map keyNamesMap = this.getNamesFromProfileForSharedAttributes(lwM2MClient); if (!keyNamesMap.isEmpty()) { diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java index 0b2d819fa8..9beebe5279 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/uplink/LwM2mUplinkMsgHandler.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.transport.lwm2m.server.uplink; +import org.eclipse.leshan.core.node.codec.LwM2mValueConverter; import org.eclipse.leshan.core.observation.Observation; import org.eclipse.leshan.core.request.CreateRequest; import org.eclipse.leshan.core.request.SendRequest; @@ -69,5 +70,10 @@ public interface LwM2mUplinkMsgHandler { void onToTransportUpdateCredentials(TransportProtos.SessionInfoProto sessionInfo, TransportProtos.ToTransportUpdateCredentialsProto updateCredentials); + void initAttributes(LwM2mClient lwM2MClient, boolean logFailedUpdateOfNonChangedValue); + LwM2MTransportServerConfig getConfig(); + + LwM2mValueConverter getConverter(); + } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java index f207e7d05a..861261d951 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/utils/LwM2MTransportUtil.java @@ -52,7 +52,7 @@ import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdate import org.thingsboard.server.transport.lwm2m.server.ota.firmware.FirmwareUpdateState; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateResult; import org.thingsboard.server.transport.lwm2m.server.ota.software.SoftwareUpdateState; -import org.thingsboard.server.transport.lwm2m.server.uplink.DefaultLwM2mUplinkMsgHandler; +import org.thingsboard.server.transport.lwm2m.server.uplink.LwM2mUplinkMsgHandler; import java.util.ArrayList; import java.util.Arrays; @@ -289,12 +289,12 @@ public class LwM2MTransportUtil { * Attribute pmax = new Attribute(MAXIMUM_PERIOD, "60"); * Attribute [] attrs = {gt, st}; */ - public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, DefaultLwM2mUplinkMsgHandler serviceImpl) { + public static SimpleDownlinkRequest createWriteAttributeRequest(String target, Object params, LwM2mUplinkMsgHandler serviceImpl) { AttributeSet attrSet = new AttributeSet(createWriteAttributes(params, serviceImpl, target)); return attrSet.getAttributes().size() > 0 ? new WriteAttributesRequest(target, attrSet) : null; } - private static Attribute[] createWriteAttributes(Object params, DefaultLwM2mUplinkMsgHandler serviceImpl, String target) { + private static Attribute[] createWriteAttributes(Object params, LwM2mUplinkMsgHandler serviceImpl, String target) { List attributeLists = new ArrayList<>(); Map map = JacksonUtil.convertValue(params, new TypeReference<>() { }); @@ -366,19 +366,19 @@ public class LwM2MTransportUtil { return newValues; } - public static Object convertWriteAttributes(String type, Object value, DefaultLwM2mUplinkMsgHandler serviceImpl, String target) { + public static Object convertWriteAttributes(String type, Object value, LwM2mUplinkMsgHandler serviceImpl, String target) { switch (type) { /** Integer [0:255]; */ case DIMENSION: - Long dim = (Long) serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); + Long dim = (Long) serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); return dim >= 0 && dim <= 255 ? dim : null; /**String;*/ case OBJECT_VERSION: - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), STRING, new LwM2mPath(target)); /**INTEGER */ case MINIMUM_PERIOD: case MAXIMUM_PERIOD: - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), INTEGER, new LwM2mPath(target)); /**Float; */ case GREATER_THAN: case LESSER_THAN: @@ -386,7 +386,7 @@ public class LwM2MTransportUtil { if (value.getClass().getSimpleName().equals("String")) { value = Double.valueOf((String) value); } - return serviceImpl.converter.convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); + return serviceImpl.getConverter().convertValue(value, equalsResourceTypeGetSimpleName(value), FLOAT, new LwM2mPath(target)); default: return null; } 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/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/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 526876adba..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"; @@ -287,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/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/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 6cdf431ce0..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, @@ -796,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/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/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..900eea8a6d 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 @@ -19,9 +19,8 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; +import org.junit.jupiter.api.Assertions; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceInfo; @@ -46,6 +45,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; @@ -71,10 +71,6 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { tenantProfileService.deleteTenantProfiles(anotherTenantId); } - @SuppressWarnings("deprecation") - @Rule - public ExpectedException thrown = ExpectedException.none(); - @Test public void testSaveDevicesWithoutMaxDeviceLimit() { Device device = this.saveDevice(tenantId, "My device"); @@ -91,7 +87,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 +98,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 @@ -241,68 +239,78 @@ public abstract class BaseDeviceServiceTest extends AbstractServiceTest { savedDevice.setFirmwareId(savedFirmware.getId()); - thrown.expect(DataValidationException.class); - thrown.expectMessage("Can't assign firmware with different deviceProfile!"); - deviceService.saveDevice(savedDevice); + assertThatThrownBy(() -> deviceService.saveDevice(savedDevice)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Can't assign firmware with different deviceProfile!"); } - @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/BaseOtaPackageServiceTest.java b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java index c89e7c4b69..c5c217a486 100644 --- a/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java +++ b/dao/src/test/java/org/thingsboard/server/dao/service/BaseOtaPackageServiceTest.java @@ -19,9 +19,7 @@ import com.datastax.oss.driver.api.core.uuid.Uuids; import org.junit.After; import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Device; import org.thingsboard.server.common.data.DeviceProfile; @@ -44,6 +42,7 @@ import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.thingsboard.server.common.data.ota.OtaPackageType.FIRMWARE; public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @@ -78,10 +77,6 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { deviceProfileId = savedDeviceProfile.getId(); } - @SuppressWarnings("deprecation") - @Rule - public ExpectedException thrown = ExpectedException.none(); - @After public void after() { tenantService.deleteTenant(tenantId); @@ -99,9 +94,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { createAndSaveFirmware(tenantId, "1"); Assert.assertEquals(1, otaPackageService.sumDataSizeByTenantId(tenantId)); - thrown.expect(DataValidationException.class); - thrown.expectMessage(String.format("Failed to create the ota package, files size limit is exhausted %d bytes!", DATA_SIZE)); - createAndSaveFirmware(tenantId, "2"); + assertThatThrownBy(() -> createAndSaveFirmware(tenantId, "2")) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Failed to create the ota package, files size limit is exhausted %d bytes!", DATA_SIZE); } @Test @@ -243,9 +238,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage should be assigned to tenant!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage should be assigned to tenant!"); } @Test @@ -261,9 +256,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("Type should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Type should be specified!"); } @Test @@ -279,9 +274,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage title should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage title should be specified!"); } @Test @@ -297,9 +292,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage file name should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage file name should be specified!"); } @Test @@ -315,9 +310,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage content type should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage content type should be specified!"); } @Test @@ -333,9 +328,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); firmware.setChecksum(CHECKSUM); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage data should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage data should be specified!"); } @Test @@ -352,9 +347,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage is referencing to non-existent tenant!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage is referencing to non-existent tenant!"); } @Test @@ -371,9 +366,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksum(CHECKSUM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage is referencing to non-existent device profile!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage is referencing to non-existent device profile!"); } @Test @@ -389,9 +384,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmware.setChecksumAlgorithm(CHECKSUM_ALGORITHM); firmware.setData(DATA); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage checksum should be specified!"); - otaPackageService.saveOtaPackage(firmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(firmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage checksum should be specified!"); } @Test @@ -411,17 +406,17 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { newFirmwareInfo.setTitle(TITLE); newFirmwareInfo.setVersion(VERSION); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage with such title and version already exists!"); - otaPackageService.saveOtaPackageInfo(newFirmwareInfo, false); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(newFirmwareInfo, false)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage with such title and version already exists!"); } @Test public void testSaveFirmwareWithExistingTitleAndVersion() { createAndSaveFirmware(tenantId, VERSION); - thrown.expect(DataValidationException.class); - thrown.expectMessage("OtaPackage with such title and version already exists!"); - createAndSaveFirmware(tenantId, VERSION); + assertThatThrownBy(() -> createAndSaveFirmware(tenantId, VERSION)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("OtaPackage with such title and version already exists!"); } @Test @@ -436,9 +431,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { Device savedDevice = deviceService.saveDevice(device); try { - thrown.expect(DataValidationException.class); - thrown.expectMessage("The otaPackage referenced by the devices cannot be deleted!"); - otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId()); + assertThatThrownBy(() -> otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId())) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("The otaPackage referenced by the devices cannot be deleted!"); } finally { deviceService.deleteDevice(tenantId, savedDevice.getId()); otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId()); @@ -448,12 +443,12 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { @Test public void testUpdateDeviceProfileId() { OtaPackage savedFirmware = createAndSaveFirmware(tenantId, VERSION); + savedFirmware.setDeviceProfileId(null); try { - thrown.expect(DataValidationException.class); - thrown.expectMessage("Updating otaPackage deviceProfile is prohibited!"); - savedFirmware.setDeviceProfileId(null); - otaPackageService.saveOtaPackage(savedFirmware); + assertThatThrownBy(() -> otaPackageService.saveOtaPackage(savedFirmware)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Updating otaPackage deviceProfile is prohibited!"); } finally { otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId()); } @@ -482,9 +477,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { deviceProfileService.saveDeviceProfile(savedDeviceProfile); try { - thrown.expect(DataValidationException.class); - thrown.expectMessage("The otaPackage referenced by the device profile cannot be deleted!"); - otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId()); + assertThatThrownBy(() -> otaPackageService.deleteOtaPackage(tenantId, savedFirmware.getId())) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("The otaPackage referenced by the device profile cannot be deleted!"); } finally { deviceProfileService.deleteDeviceProfile(tenantId, savedDeviceProfile.getId()); } @@ -636,12 +631,18 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setType(FIRMWARE); firmwareInfo.setTitle(TITLE); firmwareInfo.setVersion(VERSION); + firmwareInfo.setUrl(" "); - thrown.expect(DataValidationException.class); - thrown.expectMessage("Ota package URL should be specified!"); - otaPackageService.saveOtaPackageInfo(firmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(firmwareInfo, true)) + .as("firmwareInfo url set whitespaces") + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Ota package URL should be specified!"); + firmwareInfo.setUrl(""); - otaPackageService.saveOtaPackageInfo(firmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(firmwareInfo, true)) + .as("firmwareInfo url is empty") + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Ota package URL should be specified!"); } @Test @@ -655,12 +656,10 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setTenantId(tenantId); OtaPackageInfo savedFirmwareInfo = otaPackageService.saveOtaPackageInfo(firmwareInfo, true); - - thrown.expect(DataValidationException.class); - thrown.expectMessage("Updating otaPackage URL is prohibited!"); - savedFirmwareInfo.setUrl("https://newurl.com"); - otaPackageService.saveOtaPackageInfo(savedFirmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(savedFirmwareInfo, true)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("Updating otaPackage URL is prohibited!"); } @Test @@ -673,10 +672,9 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setUrl(URL); firmwareInfo.setTenantId(tenantId); - thrown.expect(DataValidationException.class); - thrown.expectMessage("title length must be equal or less than 255"); - - otaPackageService.saveOtaPackageInfo(firmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(firmwareInfo, true)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("title length must be equal or less than 255"); } @Test @@ -687,11 +685,11 @@ public abstract class BaseOtaPackageServiceTest extends AbstractServiceTest { firmwareInfo.setUrl(URL); firmwareInfo.setTenantId(tenantId); firmwareInfo.setTitle(TITLE); - firmwareInfo.setVersion(StringUtils.random(257)); - thrown.expectMessage("version length must be equal or less than 255"); - otaPackageService.saveOtaPackageInfo(firmwareInfo, true); + assertThatThrownBy(() -> otaPackageService.saveOtaPackageInfo(firmwareInfo, true)) + .isInstanceOf(DataValidationException.class) + .hasMessageContaining("version length must be equal or less than 255"); } private OtaPackage createAndSaveFirmware(TenantId tenantId, String version) { 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/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/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 77aeb81498..49f30df835 100755 --- a/pom.xml +++ b/pom.xml @@ -146,8 +146,8 @@ 1.0.0 4.6.0 5.2.0 - 2.19.0 - 2.8 + 2.21.0 + 2.12.0 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 7573b61b65..81703d24a0 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 @@ -418,6 +418,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/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 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/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/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 4a922b3f71..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(); + this.markdownClass = 'markdown-widget-' + hashCode(cssString); + cssParser.cssPreviewNamespace = this.markdownClass; cssParser.testMode = false; - this.markdownClass += '-' + hashCode(cssString); - cssParser.cssPreviewNamespace = 'tb-default .' + this.markdownClass; - cssParser.createStyleElement(this.markdownClass, cssString); + 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/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/shared/components/markdown.component.scss b/ui-ngx/src/app/shared/components/markdown.component.scss new file mode 100644 index 0000000000..757a26c587 --- /dev/null +++ b/ui-ngx/src/app/shared/components/markdown.component.scss @@ -0,0 +1,406 @@ +/** + * 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-markdown-view { + display: block; + + $headings: h1, h2, h3, h4, h5, h6; + + h1 { + font-size: 32px; + padding-right: 60px; + } + + @each $heading in $headings { + #{$heading} { + color: #0F161D; + line-height: normal; + font-weight: 500; + border-bottom: none; + margin: 0; + } + & > #{$heading} { + padding: 30px 32px 10px; + } + } + + p { + font-size: 16px; + font-weight: 400; + line-height: 1.25em; + margin: 0; + } + + p + p { + margin-top: 10px; + } + + p, div { + color: rgba(15, 22, 29, 0.8); + line-height: 1.5em; + } + + & > p, & > div { + padding-right: 32px; + padding-left: 32px; + } + + ul { + padding-left: 62px; + padding-right: 32px; + color: rgba(15, 22, 29, 0.8); + margin-top: 16px; + margin-bottom: 16px; + } + + ul { + @each $heading in $headings { + & + #{$heading} { + padding-top: 14px; + } + } + } + + li { + padding-bottom: .75em; + line-height: 1.5em; + + ul { + margin-bottom: 0; + } + + p { + padding-left: 0; + } + } + + a { + font-weight: 500; + color: #2a7dec; + text-decoration: none; + border: none; + + &:hover { + color: #2a7dec; + text-decoration: underline; + border: none; + } + } + + & > table { + margin-left: 32px; + width: calc(100% - 64px); + border: 1px solid rgba(42, 125, 236, .2); + border-radius: 4px; + border-collapse: unset; + border-spacing: 0; + margin-top: 30px; + margin-bottom: 30px; + overflow: hidden; + table-layout: fixed; + + &.auto { + table-layout: auto; + } + + & > thead { + background-color: #f9fbff; + color: rgba(33, 37, 41, .6); + + & > tr { + & > th { + border-bottom: 1px solid rgba(42, 125, 236, .2); + font-size: 16px; + padding: 12px 16px; + text-align: left; + margin: 0; + @media screen and (max-width: 400px) { + font-size: 12px; + padding: 12px 4px; + code:not([class*=language-]) { + font-size: 12px; + } + } + } + } + } + + & > tbody { + & > tr:not(:last-child) { + & > td { + border-bottom: 1px solid rgba(42, 125, 236, .2); + } + } + + & > tr { + & > td { + font-size: 16px; + padding: 12px 16px; + text-align: left; + margin: 0; + color: rgba(15, 22, 29, 0.8); + @media screen and (max-width: 400px) { + font-size: 12px; + padding: 12px 4px; + code:not([class*=language-]) { + font-size: 12px; + } + } + } + } + } + + th, td { + font-size: .85em; + padding: 8px; + margin: 0; + text-align: left; + } + + td[align=center], th[align=center] { + text-align: center; + } + + td[align=right], th[align=right] { + text-align: right; + } + + tr td div { + padding-left: 0; + padding-right: 0; + } + } + + + div.divider { + padding-top: 32px; + border-bottom: 1px solid rgba(15, 22, 29, 0.1); + } + + ul + div.divider { + padding-top: 16px; + } + + img { + max-width: 100%; + } + + button.tb-button { + cursor: pointer; + display: inline-block; + border-radius: 4px; + border: none; + padding: 10px 20px; + line-height: 24px; + color: #fff; + background-color: #305680; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + text-decoration: none; + font-size: 16px; + font-weight: 500; + transition: background-color .4s cubic-bezier(.25, .8, .25, 1); + + &:hover { + background-color: #264363; + color: #fff; + text-decoration: none; + } + } + + #video { + width: 100%; + margin: 0; + position: relative; + + #video_wrapper { + position: relative; + width: 100%; + padding-bottom: 56.25%; + padding-left: 0; + padding-right: 0; + + iframe { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + } + } + } + + @media screen and (min-width: 750px) { + #video { + width: 100%; + display: block; + } + } + + @media screen and (min-width: 1025px) { + #video { + width: 50%; + position: relative; + } + } + + code:not([class*=language-]) { + color: #eb5757; + font-family: monospace; + font-size: 16px; + } + + div.code-wrapper { + position: relative; + + button.clipboard-btn { + pointer-events: none; + outline: none; + position: absolute; + width: 206px; + height: 42px; + top: 0; + right: 32px; + background: 0 0; + border: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + + &.multiline { + right: 44px; + } + + p { + padding: 8px; + top: 1px; + transition: .2s; + color: #2a7dec; + background: rgba(255, 255, 255, .85); + backdrop-filter: blur(4px); + opacity: 0; + font-weight: 500; + right: 32px; + position: absolute; + } + + div { + background-color: #fff; + position: absolute; + width: 38px; + height: 38px; + top: 3px; + right: 3px; + padding: 10px; + + img { + position: initial; + width: 18px; + height: 18px; + filter: invert(51%) sepia(6%) saturate(172%) hue-rotate(177deg) brightness(94%) contrast(92%); + } + } + } + + &:hover { + cursor: pointer; + + pre[class*="language-"] { + border: solid 1px #2a7dec; + } + + button.clipboard-btn { + p { + opacity: 1; + } + + div { + img { + filter: invert(49%) sepia(97%) saturate(3730%) hue-rotate(200deg) brightness(95%) contrast(95%); + } + } + } + } + } + + th, td { + div.code-wrapper { + display: inline-block; + width: 100%; + + button.clipboard-btn { + top: -10px; + right: 0; + padding: 0 3px; + } + } + } + + pre[class*="language-"] { + font-size: 16px; + border: 1px solid rgba(42, 125, 236, .2); + border-radius: 4px; + background: 0 0; + padding: 8px 16px; + color: #212529; + + .token.atrule, .token.attr-value, .token.keyword { + color: #2a7dec; + } + + .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { + color: #eb5757; + } + + .token.punctuation { + color: #212529; + } + + &.line-numbers { + padding-left: 66px; + + & > code { + span.line-numbers-rows { + top: -12px; + bottom: -12px; + left: -66px; + width: 50px; + border: none; + padding: 8px 12px 8px 18px; + text-align: right; + background: #f9fbff; + + & > span:before { + color: rgba(33, 37, 41, .6); + padding-right: 0; + } + } + } + + &.no-line-numbers { + padding-left: 16px; + + & > code { + span.line-numbers-rows { + display: none; + } + } + } + } + } + } +} diff --git a/ui-ngx/src/app/shared/components/markdown.component.ts b/ui-ngx/src/app/shared/components/markdown.component.ts index 95c0ccd23b..4ed77b66d2 100644 --- a/ui-ngx/src/app/shared/components/markdown.component.ts +++ b/ui-ngx/src/app/shared/components/markdown.component.ts @@ -18,14 +18,18 @@ import { ChangeDetectorRef, Component, ComponentFactory, - ComponentRef, ElementRef, + ComponentRef, + ElementRef, EventEmitter, Inject, Injector, - Input, OnChanges, + Input, + OnChanges, Output, + Renderer2, SimpleChanges, - Type, ViewChild, + Type, + ViewChild, ViewContainerRef } from '@angular/core'; import { HelpService } from '@core/services/help.service'; @@ -33,12 +37,15 @@ import { MarkdownService, PrismPlugin } from 'ngx-markdown'; import { DynamicComponentFactoryService } from '@core/services/dynamic-component-factory.service'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { SHARED_MODULE_TOKEN } from '@shared/components/tokens'; -import { isDefinedAndNotNull } from '@core/utils'; +import { deepClone, guid, isDefinedAndNotNull } from '@core/utils'; import { Observable, of, ReplaySubject } from 'rxjs'; +let defaultMarkdownStyle; + @Component({ selector: 'tb-markdown', - templateUrl: './markdown.component.html' + templateUrl: './markdown.component.html', + styleUrls: ['./markdown.component.scss'] }) export class TbMarkdownComponent implements OnChanges { @@ -53,8 +60,14 @@ export class TbMarkdownComponent implements OnChanges { @Input() markdownClass: string | undefined; + @Input() containerClass: string | undefined; + @Input() style: { [klass: string]: any } = {}; + @Input() applyDefaultMarkdownStyle = true; + + @Input() additionalStyles: string[]; + @Input() get lineNumbers(): boolean { return this.lineNumbersValue; } set lineNumbers(value: boolean) { this.lineNumbersValue = coerceBooleanProperty(value); } @@ -79,7 +92,8 @@ export class TbMarkdownComponent implements OnChanges { private cd: ChangeDetectorRef, public markdownService: MarkdownService, @Inject(SHARED_MODULE_TOKEN) private sharedModule: Type, - private dynamicComponentFactoryService: DynamicComponentFactoryService) {} + private dynamicComponentFactoryService: DynamicComponentFactoryService, + private renderer: Renderer2) {} ngOnChanges(changes: SimpleChanges): void { if (isDefinedAndNotNull(this.data)) { @@ -89,12 +103,25 @@ export class TbMarkdownComponent implements OnChanges { private render(markdown: string) { const compiled = this.markdownService.parse(markdown, { decodeHtml: false }); - let template = this.sanitizeCurlyBraces(compiled); let markdownClass = 'tb-markdown-view'; if (this.markdownClass) { markdownClass += ` ${this.markdownClass}`; } - template = `
${template}
`; + let template = `
${compiled}
`; + if (this.containerClass) { + template = `
${template}
`; + } + const element: HTMLDivElement = this.renderer.createElement('div'); + element.innerHTML = template; + this.handlePlugins(element); + this.markdownService.highlight(element); + const preElements = element.querySelectorAll('pre'); + const matches = Array.from(template.matchAll(/)<\/pre>/g)); + for (let i = 0; i < preElements.length; i++) { + const preHtml = preElements.item(i).outerHTML.replace('ngnonbindable=""', 'ngNonBindable'); + template = template.replace(matches[i][0], preHtml); + } + template = this.sanitizeCurlyBraces(template); this.markdownContainer.clear(); const parent = this; let readyObservable: Observable; @@ -102,6 +129,17 @@ export class TbMarkdownComponent implements OnChanges { if (this.additionalCompileModules) { compileModules = compileModules.concat(this.additionalCompileModules); } + let styles: string[] = []; + if (this.applyDefaultMarkdownStyle) { + if (!defaultMarkdownStyle) { + defaultMarkdownStyle = deepClone(TbMarkdownComponent['ɵcmp'].styles)[0].replace(/\[_nghost\-%COMP%\]/g, '') + .replace(/\[_ngcontent\-%COMP%\]/g, ''); + } + styles.push(defaultMarkdownStyle); + } + if (this.additionalStyles) { + styles = styles.concat(this.additionalStyles); + } this.dynamicComponentFactoryService.createDynamicComponentFactory( class TbMarkdownInstance { ngOnDestroy(): void { @@ -110,7 +148,7 @@ export class TbMarkdownComponent implements OnChanges { }, template, compileModules, - true + true, 1, styles ).subscribe((factory) => { this.tbMarkdownInstanceComponentFactory = factory; const injector: Injector = Injector.create({providers: [], parent: this.markdownContainer.injector}); @@ -123,20 +161,18 @@ export class TbMarkdownComponent implements OnChanges { } } this.tbMarkdownInstanceComponentRef.instance.style = this.style; - this.handlePlugins(this.tbMarkdownInstanceComponentRef.location.nativeElement); - this.markdownService.highlight(this.tbMarkdownInstanceComponentRef.location.nativeElement); readyObservable = this.handleImages(this.tbMarkdownInstanceComponentRef.location.nativeElement); this.cd.detectChanges(); this.error = null; } catch (error) { - readyObservable = this.handleError(compiled, error); + readyObservable = this.handleError(template, error, styles); } readyObservable.subscribe(() => { this.ready.emit(); }); }, (error) => { - readyObservable = this.handleError(compiled, error); + readyObservable = this.handleError(template, error, styles); this.cd.detectChanges(); readyObservable.subscribe(() => { this.ready.emit(); @@ -144,14 +180,24 @@ export class TbMarkdownComponent implements OnChanges { }); } - private handleError(template: string, error): Observable { + private handleError(template: string, error, styles?: string[]): Observable { this.error = (error ? error + '' : 'Failed to render markdown!').replace(/\n/g, '
'); this.markdownContainer.clear(); if (this.fallbackToPlainMarkdownValue) { const element = this.fallbackElement.nativeElement; + let styleElement; + if (styles?.length) { + const markdownClass = 'tb-markdown-view-' + guid(); + let innerStyle = styles.join('\n'); + innerStyle = innerStyle.replace(/\.tb-markdown-view/g, '.' + markdownClass); + template = template.replace(/tb-markdown-view/g, markdownClass); + styleElement = this.renderer.createElement('style'); + styleElement.innerHTML = innerStyle; + } element.innerHTML = template; - this.handlePlugins(element); - this.markdownService.highlight(element); + if (styleElement) { + this.renderer.appendChild(element, styleElement); + } return this.handleImages(element); } else { return of(null); diff --git a/ui-ngx/src/app/shared/models/alarm.models.ts b/ui-ngx/src/app/shared/models/alarm.models.ts index ed962b2fc1..5f0f7f37ef 100644 --- a/ui-ngx/src/app/shared/models/alarm.models.ts +++ b/ui-ngx/src/app/shared/models/alarm.models.ts @@ -23,6 +23,8 @@ import { NULL_UUID } from '@shared/models/id/has-uuid'; import { EntityType } from '@shared/models/entity-type.models'; import { CustomerId } from '@shared/models/id/customer-id'; import { TableCellButtonActionDescriptor } from '@home/components/widget/lib/table-widget.models'; +import { AlarmCommentId } from '@shared/models/id/alarm-comment-id'; +import { UserId } from '@shared/models/id/user-id'; export enum AlarmSeverity { CRITICAL = 'CRITICAL', @@ -89,6 +91,7 @@ export const alarmSeverityColors = new Map( export interface Alarm extends BaseData { tenantId: TenantId; customerId: CustomerId; + assigneeId: UserId; type: string; originator: EntityId; severity: AlarmSeverity; @@ -97,12 +100,43 @@ export interface Alarm extends BaseData { endTs: number; ackTs: number; clearTs: number; + assignTs: number; propagate: boolean; details?: any; } +export enum AlarmCommentType { + SYSTEM = 'SYSTEM', + OTHER = 'OTHER' +} + +export interface AlarmComment extends BaseData { + alarmId: AlarmId; + userId?: UserId; + type: AlarmCommentType; + comment: { + text: string; + edited?: boolean; + editedOn?: number; + } +} + +export interface AlarmCommentInfo extends AlarmComment { + firstName?: string; + lastName?: string; + email?: string; +} + export interface AlarmInfo extends Alarm { originatorName: string; + originatorLabel: string; + assignee: AlarmAssignee; +} + +export interface AlarmAssignee { + firstName: string; + lastName: string; + email: string; } export interface AlarmDataInfo extends AlarmInfo { @@ -115,12 +149,20 @@ export const simulatedAlarm: AlarmInfo = { id: new AlarmId(NULL_UUID), tenantId: new TenantId(NULL_UUID), customerId: new CustomerId(NULL_UUID), + assigneeId: new UserId(NULL_UUID), createdTime: new Date().getTime(), startTs: new Date().getTime(), endTs: 0, ackTs: 0, clearTs: 0, + assignTs: 0, originatorName: 'Simulated', + originatorLabel: 'Simulated', + assignee: { + firstName: "", + lastName: "", + email: "test@example.com", + }, originator: { entityType: EntityType.DEVICE, id: '1' @@ -172,11 +214,22 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { name: 'alarm.clear-time', time: true }, + assignTime: { + keyName: 'assignTime', + value: 'assignTs', + name: 'alarm.assign-time', + time: true + }, originator: { keyName: 'originator', value: 'originatorName', name: 'alarm.originator' }, + originatorLabel: { + keyName: 'originatorLabel', + value: 'originatorLabel', + name: 'alarm.originator-label' + }, originatorType: { keyName: 'originatorType', value: 'originator.entityType', @@ -196,6 +249,11 @@ export const alarmFields: {[fieldName: string]: AlarmField} = { keyName: 'status', value: 'status', name: 'alarm.status' + }, + assignee: { + keyName: 'assignee', + value: 'assignee', + name: 'alarm.assignee' } }; @@ -206,15 +264,17 @@ export class AlarmQuery { searchStatus: AlarmSearchStatus; status: AlarmStatus; fetchOriginator: boolean; + assigneeId?: UserId; constructor(entityId: EntityId, pageLink: TimePageLink, searchStatus: AlarmSearchStatus, status: AlarmStatus, - fetchOriginator: boolean) { + fetchOriginator: boolean, assigneeId?: UserId) { this.affectedEntityId = entityId; this.pageLink = pageLink; this.searchStatus = searchStatus; this.status = status; this.fetchOriginator = fetchOriginator; + this.assigneeId = assigneeId; } public toQuery(): string { @@ -228,6 +288,9 @@ export class AlarmQuery { if (typeof this.fetchOriginator !== 'undefined' && this.fetchOriginator !== null) { query += `&fetchOriginator=${this.fetchOriginator}`; } + if (typeof this.assigneeId !== 'undefined' && this.assigneeId !== null) { + query += `&assigneeId=${this.assigneeId.id}`; + } return query; } diff --git a/ui-ngx/src/app/shared/models/audit-log.models.ts b/ui-ngx/src/app/shared/models/audit-log.models.ts index 096e13a47c..8f4e26171e 100644 --- a/ui-ngx/src/app/shared/models/audit-log.models.ts +++ b/ui-ngx/src/app/shared/models/audit-log.models.ts @@ -47,6 +47,8 @@ export enum ActionType { RELATIONS_DELETED = 'RELATIONS_DELETED', ALARM_ACK = 'ALARM_ACK', ALARM_CLEAR = 'ALARM_CLEAR', + ALARM_ASSIGN = 'ALARM_ASSIGN', + ALARM_UNASSIGN = 'ALARM_UNASSIGN', ADDED_COMMENT = 'ADDED_COMMENT', UPDATED_COMMENT = 'UPDATED_COMMENT', DELETED_COMMENT = 'DELETED_COMMENT', @@ -88,6 +90,8 @@ export const actionTypeTranslations = new Map( [ActionType.RELATIONS_DELETED, 'audit-log.type-relations-delete'], [ActionType.ALARM_ACK, 'audit-log.type-alarm-ack'], [ActionType.ALARM_CLEAR, 'audit-log.type-alarm-clear'], + [ActionType.ALARM_ASSIGN, 'audit-log.type-alarm-assign'], + [ActionType.ALARM_UNASSIGN, 'audit-log.type-alarm-unassign'], [ActionType.ADDED_COMMENT, 'audit-log.type-added-comment'], [ActionType.UPDATED_COMMENT, 'audit-log.type-updated-comment'], [ActionType.DELETED_COMMENT, 'audit-log.type-deleted-comment'], diff --git a/ui-ngx/src/app/shared/models/device.models.ts b/ui-ngx/src/app/shared/models/device.models.ts index e36aa99117..df7ba701c7 100644 --- a/ui-ngx/src/app/shared/models/device.models.ts +++ b/ui-ngx/src/app/shared/models/device.models.ts @@ -242,6 +242,7 @@ export interface DefaultDeviceProfileTransportConfiguration { export interface MqttDeviceProfileTransportConfiguration { deviceTelemetryTopic?: string; deviceAttributesTopic?: string; + sparkplug?: boolean; sendAckOnValidationException?: boolean; transportPayloadTypeConfiguration?: { transportPayloadType?: TransportPayloadType; @@ -359,6 +360,7 @@ export function createDeviceProfileTransportConfiguration(type: DeviceTransportT const mqttTransportConfiguration: MqttDeviceProfileTransportConfiguration = { deviceTelemetryTopic: 'v1/devices/me/telemetry', deviceAttributesTopic: 'v1/devices/me/attributes', + sparkplug: false, sendAckOnValidationException: false, transportPayloadTypeConfiguration: { transportPayloadType: TransportPayloadType.JSON, diff --git a/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts b/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts new file mode 100644 index 0000000000..c161e3095c --- /dev/null +++ b/ui-ngx/src/app/shared/models/id/alarm-comment-id.ts @@ -0,0 +1,24 @@ +/// +/// 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 { HasUUID } from '@shared/models/id/has-uuid'; + +export class AlarmCommentId implements HasUUID { + id: string; + constructor(id: string) { + this.id = id; + } +} diff --git a/ui-ngx/src/app/shared/models/time/time.models.ts b/ui-ngx/src/app/shared/models/time/time.models.ts index 6c04aef946..06cf0ffd5f 100644 --- a/ui-ngx/src/app/shared/models/time/time.models.ts +++ b/ui-ngx/src/app/shared/models/time/time.models.ts @@ -237,7 +237,7 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO model.hideAggInterval = value.hideAggInterval; model.hideTimezone = value.hideTimezone; model.selectedTab = getTimewindowType(value); - if (model.selectedTab === TimewindowType.REALTIME) { + if (isDefined(value.realtime)) { if (isDefined(value.realtime.interval)) { model.realtime.interval = value.realtime.interval; } @@ -250,12 +250,14 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO } else { model.realtime.realtimeType = value.realtime.realtimeType; } - if (model.realtime.realtimeType === RealtimeWindowType.INTERVAL) { + if (isDefined(value.realtime.quickInterval)) { model.realtime.quickInterval = value.realtime.quickInterval; - } else { + } + if (isDefined(value.realtime.timewindowMs)) { model.realtime.timewindowMs = value.realtime.timewindowMs; } - } else { + } + if (isDefined(value.history)) { if (isDefined(value.history.interval)) { model.history.interval = value.history.interval; } @@ -270,13 +272,19 @@ export function initModelFromDefaultTimewindow(value: Timewindow, quickIntervalO } else { model.history.historyType = value.history.historyType; } - if (model.history.historyType === HistoryWindowType.LAST_INTERVAL) { + if (isDefined(value.history.timewindowMs)) { model.history.timewindowMs = value.history.timewindowMs; - } else if (model.history.historyType === HistoryWindowType.INTERVAL) { + } + if (isDefined(value.history.quickInterval)) { model.history.quickInterval = value.history.quickInterval; - } else { - model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; - model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; + } + if (isDefined(value.history.fixedTimewindow)) { + if (isDefined(value.history.fixedTimewindow.startTimeMs)) { + model.history.fixedTimewindow.startTimeMs = value.history.fixedTimewindow.startTimeMs; + } + if (isDefined(value.history.fixedTimewindow.endTimeMs)) { + model.history.fixedTimewindow.endTimeMs = value.history.fixedTimewindow.endTimeMs; + } } } if (value.aggregation) { diff --git a/ui-ngx/src/app/shared/models/user.model.ts b/ui-ngx/src/app/shared/models/user.model.ts index 73fe32f1c9..2080a155c9 100644 --- a/ui-ngx/src/app/shared/models/user.model.ts +++ b/ui-ngx/src/app/shared/models/user.model.ts @@ -54,3 +54,10 @@ export interface AuthUser { isPublic: boolean; authority: Authority; } + +export interface UserEmailInfo { + id: UserId; + email: string; + firstName: string; + lastName: string; +} diff --git a/ui-ngx/src/app/shared/pipe/date-ago.pipe.ts b/ui-ngx/src/app/shared/pipe/date-ago.pipe.ts new file mode 100644 index 0000000000..c0f4b5c026 --- /dev/null +++ b/ui-ngx/src/app/shared/pipe/date-ago.pipe.ts @@ -0,0 +1,58 @@ +/// +/// 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 { Inject, Pipe, PipeTransform } from '@angular/core'; +import { DAY, HOUR, MINUTE, SECOND, WEEK, YEAR } from '@shared/models/time/time.models'; +import { TranslateService } from '@ngx-translate/core'; + +const intervals = { + years: YEAR, + months: DAY * 30, + weeks: WEEK, + days: DAY, + hr: HOUR, + min: MINUTE, + sec: SECOND +}; + +@Pipe({ + name: 'dateAgo' +}) +export class DateAgoPipe implements PipeTransform { + + constructor(@Inject(TranslateService) private translate: TranslateService) { + + } + + transform(value: number): string { + if (value) { + const ms = Math.floor((+new Date() - +new Date(value))); + if (ms < 29 * SECOND) { // less than 30 seconds ago will show as 'Just now' + return this.translate.instant('timewindow.just-now'); + } + let counter; + // tslint:disable-next-line:forin + for (const i in intervals) { + counter = Math.floor(ms / intervals[i]); + if (counter > 0) { + return this.translate.instant(`timewindow.${i}`, {[i]: counter}); + } + } + } + return ''; + } + +} diff --git a/ui-ngx/src/app/shared/shared.module.ts b/ui-ngx/src/app/shared/shared.module.ts index ea30938e8e..950532adab 100644 --- a/ui-ngx/src/app/shared/shared.module.ts +++ b/ui-ngx/src/app/shared/shared.module.ts @@ -168,6 +168,7 @@ import { PhoneInputComponent } from '@shared/components/phone-input.component'; import { CustomDateAdapter } from '@shared/adapter/custom-datatime-adapter'; import { CustomPaginatorIntl } from '@shared/services/custom-paginator-intl'; import { TbScriptLangComponent } from '@shared/components/script-lang.component'; +import { DateAgoPipe } from '@shared/pipe/date-ago.pipe'; export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) { return markedOptionsService; @@ -183,6 +184,7 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) TbJsonPipe, FileSizePipe, SafePipe, + DateAgoPipe, { provide: FlowInjectionToken, useValue: Flow @@ -306,7 +308,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ProtobufContentComponent, BranchAutocompleteComponent, PhoneInputComponent, - TbScriptLangComponent + TbScriptLangComponent, + DateAgoPipe ], imports: [ CommonModule, @@ -510,7 +513,8 @@ export function MarkedOptionsFactory(markedOptionsService: MarkedOptionsService) ProtobufContentComponent, BranchAutocompleteComponent, PhoneInputComponent, - TbScriptLangComponent + TbScriptLangComponent, + DateAgoPipe ] }) export class SharedModule { } diff --git a/ui-ngx/src/assets/locale/locale.constant-en_US.json b/ui-ngx/src/assets/locale/locale.constant-en_US.json index 370432ca42..7f0b1badeb 100644 --- a/ui-ngx/src/assets/locale/locale.constant-en_US.json +++ b/ui-ngx/src/assets/locale/locale.constant-en_US.json @@ -435,9 +435,19 @@ "originator": "Originator", "originator-type": "Originator type", "details": "Details", + "originator-label": "Originator label", + "assign": "Assign", + "assignments": "Assignments", + "assignee": "Assignee", + "assignee-id": "Assignee id", + "assignee-first-name": "Assignee first name", + "assignee-last-name": "Assignee last name", + "assignee-email": "Assignee email", + "unassigned": "Unassigned", "status": "Status", "alarm-details": "Alarm details", "start-time": "Start time", + "assign-time": "Assign time", "end-time": "End time", "ack-time": "Acknowledged time", "clear-time": "Cleared time", @@ -474,7 +484,17 @@ "fetch-size-error-min": "Minimum value is 10.", "alarm-type-list": "Alarm type list", "any-type": "Any type", - "search-propagated-alarms": "Search propagated alarms" + "search-propagated-alarms": "Search propagated alarms", + "comments": "Alarm comments", + "advanced-info": "Advanced info" + }, + "alarm-comment": { + "add": "Add a comment...", + "alarm-comment": "Alarm comment", + "comments": "Comments", + "delete-alarm-comment": "Do you want to delete this comment?", + "refresh": "Refresh", + "sort-direction": "Sort direction" }, "alias": { "add": "Add alias", @@ -728,6 +748,8 @@ "type-relations-delete": "All relation deleted", "type-alarm-ack": "Acknowledged", "type-alarm-clear": "Cleared", + "type-alarm-assign": "Assigned", + "type-alarm-unassign": "Unassigned", "type-added-comment": "Added comment", "type-updated-comment": "Updated comment", "type-deleted-comment": "Deleted comment", @@ -1360,6 +1382,10 @@ "create-new-device-profile": "Create a new one!", "mqtt-device-topic-filters": "MQTT device topic filters", "mqtt-device-topic-filters-unique": "MQTT device topic filters need to be unique.", + "mqtt-device-topic-filters-spark-plug": "MQTT device topic filters SparkPlug.", + "mqtt-device-topic-filters-spark-plug-hint": "Default - telemetry. Example: namespace/group_id/message_type/edge_node_id/[device_id].", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names": "SparkPlug attributes metric names", + "mqtt-device-topic-filters-spark-plug-attribute-metric-names-hint": "Names of SparkPlug metrics that will be stored as device attributes. All other metrics will be stored as device telemetry", "mqtt-device-payload-type": "MQTT device payload", "mqtt-device-payload-type-json": "JSON", "mqtt-device-payload-type-proto": "Protobuf", @@ -3383,10 +3409,16 @@ "days": "Days" }, "timewindow": { + "years": "{ years, plural, =1 { year } other {# years } }", + "months": "{ months, plural, =1 { month } other {# months } }", + "weeks": "{ weeks, plural, =1 { week } other {# weeks } }", "days": "{ days, plural, =1 { day } other {# days } }", "hours": "{ hours, plural, =0 { hour } =1 {1 hour } other {# hours } }", + "hr": "{{ hr }} hr", "minutes": "{ minutes, plural, =0 { minute } =1 {1 minute } other {# minutes } }", + "min": "{{ min }} min", "seconds": "{ seconds, plural, =0 { second } =1 {1 second } other {# seconds } }", + "sec": "{{ sec }} sec", "short": { "days": "{ days, plural, =1 {1 day } other {# days } }", "hours": "{ hours, plural, =1 {1 hour } other {# hours } }", @@ -3402,7 +3434,8 @@ "last": "Last", "time-period": "Time period", "hide": "Hide", - "interval": "Interval" + "interval": "Interval", + "just-now": "Just now" }, "user": { "user": "User", @@ -4678,6 +4711,7 @@ "use-markdown-text-function": "Use markdown/HTML value function", "markdown-text-function": "Markdown/HTML value function", "markdown-text-pattern": "Markdown/HTML pattern (markdown or HTML with variables, for ex. '${entityName} or ${keyName} - some text.')", + "apply-default-markdown-style": "Apply default markdown style", "markdown-css": "Markdown/HTML CSS" }, "simple-card": { @@ -4733,7 +4767,9 @@ "enable-alarm-filter": "Enable alarm filter", "display-alarm-details": "Display alarm details", "allow-alarms-ack": "Allow alarms acknowledgment", - "allow-alarms-clear": "Allow alarms clear" + "allow-alarms-clear": "Allow alarms clear", + "display-alarm-comments": "Display alarm comments", + "allow-alarms-assign": "Allow alarms assignment" }, "value-source": { "value-source": "Value source", diff --git a/ui-ngx/src/styles.scss b/ui-ngx/src/styles.scss index 883ccaf72f..c975ece53a 100644 --- a/ui-ngx/src/styles.scss +++ b/ui-ngx/src/styles.scss @@ -424,373 +424,6 @@ mat-label { } } -.tb-default .tb-markdown-view, .tb-dark .tb-markdown-view, .tb-markdown-view { - display: block; - - $headings: h1, h2, h3, h4, h5, h6; - - h1 { - font-size: 32px; - padding-right: 60px; - } - - @each $heading in $headings { - #{$heading} { - color: #0F161D; - line-height: normal; - font-weight: 500; - border-bottom: none; - margin: 0; - } - & > #{$heading} { - padding: 30px 32px 10px; - } - } - - p { - font-size: 16px; - font-weight: 400; - line-height: 1.25em; - margin: 0; - } - - p + p { - margin-top: 10px; - } - - p, div { - color: rgba(15, 22, 29, 0.8); - line-height: 1.5em; - } - - & > p, & > div { - padding-right: 32px; - padding-left: 32px; - } - - ul { - padding-left: 62px; - padding-right: 32px; - color: rgba(15, 22, 29, 0.8); - margin-top: 16px; - margin-bottom: 16px; - } - - ul { - @each $heading in $headings { - & + #{$heading} { - padding-top: 14px; - } - } - } - - li { - padding-bottom: .75em; - line-height: 1.5em; - ul { - margin-bottom: 0; - } - p { - padding-left: 0; - } - } - - a { - font-weight: 500; - color: #2a7dec; - text-decoration: none; - border: none; - &:hover { - color: #2a7dec; - text-decoration: underline; - border: none; - } - } - - & > table { - margin-left: 32px; - width: calc(100% - 64px); - border: 1px solid rgba(42,125,236,.2); - border-radius: 4px; - border-collapse: unset; - border-spacing: 0; - margin-top: 30px; - margin-bottom: 30px; - overflow: hidden; - table-layout: fixed; - &.auto { - table-layout: auto; - } - - & > thead { - background-color: #f9fbff; - color: rgba(33,37,41,.6); - - & > tr { - & > th { - border-bottom: 1px solid rgba(42,125,236,.2); - font-size: 16px; - padding: 12px 16px; - text-align: left; - margin: 0; - @media screen and (max-width: 400px) { - font-size: 12px; - padding: 12px 4px; - code:not([class*=language-]) { - font-size: 12px; - } - } - } - } - } - - & > tbody { - & > tr:not(:last-child) { - & > td { - border-bottom: 1px solid rgba(42,125,236,.2); - } - } - & > tr { - & > td { - font-size: 16px; - padding: 12px 16px; - text-align: left; - margin: 0; - color: rgba(15, 22, 29, 0.8); - @media screen and (max-width: 400px) { - font-size: 12px; - padding: 12px 4px; - code:not([class*=language-]) { - font-size: 12px; - } - } - } - } - } - th, td { - font-size: .85em; - padding: 8px; - margin: 0; - text-align: left; - } - td[align=center], th[align=center] { - text-align: center; - } - - td[align=right], th[align=right] { - text-align: right; - } - - tr td div { - padding-left: 0; - padding-right: 0; - } - } - - - div.divider { - padding-top: 32px; - border-bottom: 1px solid rgba(15, 22, 29, 0.1); - } - - ul + div.divider { - padding-top: 16px; - } - - img { - max-width: 100%; - } - - button.tb-button { - cursor: pointer; - display: inline-block; - border-radius: 4px; - border: none; - padding: 10px 20px; - line-height: 24px; - color: #fff; - background-color: #305680; - box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 2px 2px rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2); - text-decoration: none; - font-size: 16px; - font-weight: 500; - transition: background-color .4s cubic-bezier(.25,.8,.25,1); - } - - button.tb-button:hover { - background-color: #264363; - color: #fff; - text-decoration: none; - } - - #video { - width: 100%; - margin: 0; - position: relative; - } - - - #video #video_wrapper { - position: relative; - width: 100%; - padding-bottom: 56.25%; - padding-left: 0; - padding-right: 0; - } - - #video #video_wrapper iframe { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - } - - @media screen and (min-width: 750px) { - #video { - width: 100%; - display: block; - } - } - - @media screen and (min-width: 1025px) { - #video { - width: 50%; - position: relative; - } - } - - code:not([class*=language-]) { - color: #eb5757; - font-family: monospace; - font-size: 16px; - } - - div.code-wrapper { - position: relative; - button.clipboard-btn { - pointer-events: none; - outline: none; - position: absolute; - width: 206px; - height: 42px; - top: 0; - right: 32px; - background: 0 0; - border: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - &.multiline { - right: 44px; - } - p { - padding: 8px; - top: 1px; - transition: .2s; - color: #2a7dec; - background: rgba(255,255,255,.85); - backdrop-filter: blur(4px); - opacity: 0; - font-weight: 500; - right: 32px; - position: absolute; - } - div { - background-color: #fff; - position: absolute; - width: 38px; - height: 38px; - top: 3px; - right: 3px; - padding: 10px; - img { - position: initial; - width: 18px; - height: 18px; - filter: invert(51%) sepia(6%) saturate(172%) hue-rotate(177deg) brightness(94%) contrast(92%); - } - } - } - - &:hover { - cursor: pointer; - pre[class*="language-"] { - border: solid 1px #2a7dec; - } - button.clipboard-btn { - p { - opacity: 1; - } - div { - img { - filter: invert(49%) sepia(97%) saturate(3730%) hue-rotate(200deg) brightness(95%) contrast(95%); - } - } - } - } - } - - th, td { - div.code-wrapper { - display: inline-block; - width: 100%; - - button.clipboard-btn { - top: -10px; - right: 0; - padding: 0 3px; - } - } - } - - pre[class*="language-"] { - font-size: 16px; - border: 1px solid rgba(42,125,236,.2); - border-radius: 4px; - background: 0 0; - padding: 8px 16px; - color: #212529; - .token.atrule, .token.attr-value, .token.keyword { - color: #2a7dec; - } - .token.selector, .token.attr-name, .token.string, .token.char, .token.builtin, .token.inserted { - color: #eb5757; - } - .token.punctuation { - color: #212529; - } - &.line-numbers { - padding-left: 66px; - & > code { - span.line-numbers-rows { - top: -12px; - bottom: -12px; - left: -66px; - width: 50px; - border: none; - padding: 8px 12px 8px 18px; - text-align: right; - background: #f9fbff; - & > span:before { - color: rgba(33,37,41,.6); - padding-right: 0; - } - } - } - &.no-line-numbers { - padding-left: 16px; - & > code { - span.line-numbers-rows { - display: none; - } - } - } - } - } -} - // Tooltipster .tooltipster-sidetip.tooltipster-tb {